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

View File

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

View File

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

View File

@ -6,7 +6,8 @@ const initialState =
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
toolbarsVisible : true toolbarsVisible : true,
mode : 'democratic'
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -65,6 +66,10 @@ const room = (state = initialState, action) =>
return { ...state, toolbarsVisible }; return { ...state, toolbarsVisible };
} }
case 'SET_DISPLAY_MODE':
return { ...state, mode: action.payload.mode };
default: default:
return state; 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) => export const setAudioOnlyState = (enabled) =>
{ {
return { 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,6 +22,7 @@
> .view-container { > .view-container {
position: relative; position: relative;
flex-grow: 1;
&.webcam { &.webcam {
order: 2; order: 2;
@ -29,6 +30,7 @@
&.screen { &.screen {
order: 1; order: 1;
max-width: 50%;
} }
> .controls { > .controls {

View File

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

View File

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