Merge pull request #26 from torjusti/feat/filmstrip

Filmstrip view
master
Stefan Otto 2018-07-23 09:42:05 +02:00 committed by GitHub
commit 4431a40297
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 351 additions and 44 deletions

View File

@ -0,0 +1,172 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux';
import classnames from 'classnames';
import Peer from './Peer';
class Filmstrip extends Component
{
constructor(props)
{
super(props);
this.activePeerContainer = React.createRef();
}
state = {
selectedPeerName : null,
lastSpeaker : null,
width : 400
};
// Find the name of the peer which is currently speaking. This is either
// the latest active speaker, or the manually selected peer.
getActivePeerName = () =>
this.state.selectedPeerName || this.state.lastSpeaker;
isSharingCamera = (peerName) => this.props.peers[peerName] &&
this.props.peers[peerName].consumers.some((consumer) =>
this.props.consumers[consumer].source === 'screen');
getRatio = () =>
{
let ratio = 4 / 3;
if (this.isSharingCamera(this.getActivePeerName()))
{
ratio *= 2;
}
return ratio;
};
updateDimensions = () =>
{
const container = this.activePeerContainer.current;
if (container)
{
const ratio = this.getRatio();
let width = container.clientWidth;
if (width / ratio > container.clientHeight)
{
width = container.clientHeight * ratio;
}
this.setState({
width
});
}
};
componentDidMount()
{
window.addEventListener('resize', this.updateDimensions);
const observer = new ResizeObserver(this.updateDimensions);
observer.observe(this.activePeerContainer.current);
this.updateDimensions();
}
componentWillUnmount()
{
window.removeEventListener('resize', this.updateDimensions);
}
componentDidUpdate(prevProps)
{
if (prevProps !== this.props)
{
this.updateDimensions();
if (this.props.activeSpeakerName !== this.props.myName)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : this.props.activeSpeakerName
});
}
}
}
handleSelectPeer = (selectedPeerName) =>
{
this.setState((oldState) => ({
selectedPeerName : oldState.selectedPeerName === selectedPeerName ?
null : selectedPeerName
}));
};
render()
{
const { peers, advancedMode } = this.props;
const activePeerName = this.getActivePeerName();
return (
<div data-component='Filmstrip'>
<div className='active-peer-container' ref={this.activePeerContainer}>
{peers[activePeerName] && (
<div
className='active-peer'
style={{
width : this.state.width,
height : this.state.width / this.getRatio()
}}
>
<Peer
advancedMode={advancedMode}
name={activePeerName}
/>
</div>
)}
</div>
<div className='filmstrip'>
<div className='filmstrip-content'>
{Object.keys(peers).map((peerName) => (
<div
key={peerName}
onClick={() => this.handleSelectPeer(peerName)}
className={classnames('film', {
selected : this.state.selectedPeerName === peerName,
active : this.state.lastSpeaker === peerName
})}
>
<div className='film-content'>
<Peer
advancedMode={advancedMode}
name={peerName}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
}
}
Filmstrip.propTypes = {
activeSpeakerName : PropTypes.string,
advancedMode : PropTypes.bool,
peers : PropTypes.object.isRequired,
consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired
};
const mapStateToProps = (state) =>
({
activeSpeakerName : state.room.activeSpeakerName,
peers : state.peers,
consumers : state.consumers,
myName : state.me.name
});
export default connect(
mapStateToProps
)(Filmstrip);

View File

@ -14,14 +14,22 @@ class Peers extends React.Component
constructor(props)
{
super(props);
this.state = {
peerWidth : 400,
peerHeight : 300
};
this.peersRef = React.createRef();
}
updateDimensions = () =>
{
if (!this.peersRef.current)
{
return;
}
const n = this.props.boxes;
if (n === 0)
@ -29,8 +37,8 @@ class Peers extends React.Component
return;
}
const width = this.refs.peers.clientWidth;
const height = this.refs.peers.clientHeight;
const width = this.peersRef.current.clientWidth;
const height = this.peersRef.current.clientHeight;
let x, y, space;
@ -64,7 +72,7 @@ class Peers extends React.Component
window.addEventListener('resize', this.updateDimensions);
const observer = new ResizeObserver(this.updateDimensions);
observer.observe(this.refs.peers);
observer.observe(this.peersRef.current);
}
componentWillUnmount()
@ -82,8 +90,7 @@ class Peers extends React.Component
const {
advancedMode,
activeSpeakerName,
peers,
toolAreaOpen
peers
} = this.props;
const style =
@ -93,7 +100,7 @@ class Peers extends React.Component
};
return (
<div data-component='Peers' ref='peers'>
<div data-component='Peers' ref={this.peersRef}>
{
peers.map((peer) =>
{
@ -108,7 +115,6 @@ class Peers extends React.Component
advancedMode={advancedMode}
name={peer.name}
style={style}
toolAreaOpen={toolAreaOpen}
/>
</div>
</Appear>
@ -125,8 +131,7 @@ Peers.propTypes =
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
boxes : PropTypes.number,
activeSpeakerName : PropTypes.string,
toolAreaOpen : PropTypes.bool
activeSpeakerName : PropTypes.string
};
const mapStateToProps = (state) =>
@ -139,8 +144,7 @@ const mapStateToProps = (state) =>
return {
peers,
boxes,
activeSpeakerName : state.room.activeSpeakerName,
toolAreaOpen : state.toolarea.toolAreaOpen
activeSpeakerName : state.room.activeSpeakerName
};
};

View File

@ -17,6 +17,7 @@ import FullScreenView from './FullScreenView';
import Draggable from 'react-draggable';
import { idle } from '../utils';
import Sidebar from './Sidebar';
import Filmstrip from './Filmstrip';
// Hide toolbars after 10 seconds of inactivity.
const TIMEOUT = 10 * 1000;
@ -64,6 +65,11 @@ class Room extends React.Component
onRoomLinkCopy
} = this.props;
const View = {
filmstrip : Filmstrip,
democratic : Peers
}[room.mode];
return (
<Appear duration={300}>
<div data-component='Room'>
@ -121,9 +127,7 @@ class Room extends React.Component
</div>
</div>
<Peers
advancedMode={room.advancedMode}
/>
<View advancedMode={room.advancedMode} />
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
<div

View File

@ -6,20 +6,54 @@ import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types';
import Dropdown from 'react-dropdown';
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ];
class Settings extends React.Component
{
constructor(props)
state = {
selectedCamera : null,
selectedAudioDevice : null,
selectedMode : modes[0]
};
handleChangeWebcam = (webcam) =>
{
super(props);
this.props.handleChangeWebcam(webcam.value);
this.setState({
selectedCamera : webcam
});
}
render()
handleChangeAudioDevice = (device) =>
{
this.props.handleChangeAudioDevice(device.value);
this.setState({
selectedAudioDevice : device
});
}
handleChangeMode = (mode) =>
{
this.setState({
selectedMode : mode
});
this.props.handleChangeMode(mode.value);
};
render()
{
const {
room,
me,
handleChangeWebcam,
handleChangeAudioDevice,
onToggleAdvancedMode
} = this.props;
@ -55,21 +89,32 @@ class Settings extends React.Component
<Dropdown
disabled={!me.canChangeWebcam}
options={webcams}
onChange={handleChangeWebcam}
value={this.state.selectedCamera}
onChange={this.handleChangeWebcam}
placeholder={webcamText}
/>
<Dropdown
disabled={!me.canChangeAudioDevice}
options={audioDevices}
onChange={handleChangeAudioDevice}
value={this.state.selectedAudioDevice}
onChange={this.handleChangeAudioDevice}
placeholder={audioDevicesText}
/>
<input
id='room-mode'
type='checkbox'
defaultChecked={room.advancedMode}
checked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<span>Advanced mode</span>
<label htmlFor='room-mode'>Advanced mode</label>
<Dropdown
options={modes}
value={this.state.selectedMode}
onChange={this.handleChangeMode}
/>
</div>
</div>
);
@ -82,7 +127,8 @@ Settings.propTypes =
room : appPropTypes.Room.isRequired,
handleChangeWebcam : PropTypes.func.isRequired,
handleChangeAudioDevice : PropTypes.func.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired
onToggleAdvancedMode : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
@ -93,22 +139,11 @@ const mapStateToProps = (state) =>
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
handleChangeWebcam : (device) =>
{
dispatch(requestActions.changeWebcam(device.value));
},
handleChangeAudioDevice : (device) =>
{
dispatch(requestActions.changeAudioDevice(device.value));
},
onToggleAdvancedMode : () =>
{
dispatch(stateActions.toggleAdvancedMode());
}
};
const mapDispatchToProps = {
handleChangeWebcam : requestActions.changeWebcam,
handleChangeAudioDevice : requestActions.changeAudioDevice,
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode
};
const SettingsContainer = connect(

View File

@ -6,7 +6,8 @@ const initialState =
showSettings : false,
advancedMode : false,
fullScreenConsumer : null, // ConsumerID
toolbarsVisible : true
toolbarsVisible : true,
mode : 'democratic'
};
const room = (state = initialState, action) =>
@ -65,6 +66,10 @@ const room = (state = initialState, action) =>
return { ...state, toolbarsVisible };
}
case 'SET_DISPLAY_MODE':
return { ...state, mode: action.payload.mode };
default:
return state;
}

View File

@ -93,6 +93,12 @@ export const toggleAdvancedMode = () =>
};
};
export const setDisplayMode = (mode) =>
({
type : 'SET_DISPLAY_MODE',
payload : { mode }
});
export const setAudioOnlyState = (enabled) =>
{
return {

View File

@ -0,0 +1,78 @@
[data-component='Filmstrip'] {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
> .active-peer-container {
width: 100%;
height: 80vh;
display: flex;
justify-content: center;
align-items: center;
> .active-peer {
width: 100%;
padding: 1vmin;
> [data-component='Peer'] {
border: 5px solid rgba(255, 255, 255, 0.15);
box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
}
}
}
> .filmstrip {
display: flex;
background: rgba(0, 0, 0 , 0.5);
width: 100%;
overflow-x: auto;
height: 20vh;
align-items: center;
> .filmstrip-content {
margin: 0 auto;
display: flex;
height: 100%;
align-items: center;
> .film {
height: 18vh;
flex-shrink: 0;
padding-left: 1vh;
&:last-child {
padding-right: 1vh;
}
> .film-content {
height: 100%;
width: 100%;
border: 1px solid rgba(255,255,255,0.15);
> [data-component='Peer'] {
max-width: 18vh * (4 / 3);
cursor: pointer;
&.screen {
max-width: 18vh * (2 * 4 / 3);
border: 0;
}
}
}
&.active {
> .film-content {
border-color: #FFF;
}
}
&.selected {
> .film-content {
border-color: #377EFF;
}
}
}
}
}
}

View File

@ -22,13 +22,15 @@
> .view-container {
position: relative;
flex-grow: 1;
&.webcam {
order: 2;
}
&.screen {
order: 1;
max-width: 50%;
}
> .controls {

View File

@ -153,13 +153,13 @@
}
+desktop() {
bottom: 20px;
top: 20px;
left: 20px;
border: 1px solid rgba(#fff, 0.15);
}
+mobile() {
bottom: 10px;
top: 10px;
left: 10px;
border: 1px solid rgba(#fff, 0.25);
}

View File

@ -50,6 +50,7 @@ body {
@import './components/ParticipantList';
@import './components/FullScreenView';
@import './components/FullView';
@import './components/Filmstrip';
}
// Hack to detect in JS the current media query