diff --git a/app/lib/components/Filmstrip.jsx b/app/lib/components/Filmstrip.jsx
new file mode 100644
index 0000000..54a46e1
--- /dev/null
+++ b/app/lib/components/Filmstrip.jsx
@@ -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 (
+
+
+ {peers[activePeerName] && (
+
+ )}
+
+
+
+
+ {Object.keys(peers).map((peerName) => (
+
this.handleSelectPeer(peerName)}
+ className={classnames('film', {
+ selected : this.state.selectedPeerName === peerName,
+ active : this.state.lastSpeaker === peerName
+ })}
+ >
+
+
+ ))}
+
+
+
+ );
+ }
+}
+
+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);
diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx
index fccebb8..de8cbbc 100644
--- a/app/lib/components/Peers.jsx
+++ b/app/lib/components/Peers.jsx
@@ -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 (
-
+
{
peers.map((peer) =>
{
@@ -108,7 +115,6 @@ class Peers extends React.Component
advancedMode={advancedMode}
name={peer.name}
style={style}
- toolAreaOpen={toolAreaOpen}
/>
@@ -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
};
};
diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx
index 7202c90..6aeb26e 100644
--- a/app/lib/components/Room.jsx
+++ b/app/lib/components/Room.jsx
@@ -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 (
@@ -121,9 +127,7 @@ class Room extends React.Component
-
+
{
- 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
+
+
- Advanced mode
+
+
+
);
@@ -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(
diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js
index e92196b..4a787de 100644
--- a/app/lib/redux/reducers/room.js
+++ b/app/lib/redux/reducers/room.js
@@ -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;
}
diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js
index 9dcbb41..2c51b02 100644
--- a/app/lib/redux/stateActions.js
+++ b/app/lib/redux/stateActions.js
@@ -93,6 +93,12 @@ export const toggleAdvancedMode = () =>
};
};
+export const setDisplayMode = (mode) =>
+ ({
+ type : 'SET_DISPLAY_MODE',
+ payload : { mode }
+ });
+
export const setAudioOnlyState = (enabled) =>
{
return {
diff --git a/app/stylus/components/Filmstrip.styl b/app/stylus/components/Filmstrip.styl
new file mode 100644
index 0000000..65b94ef
--- /dev/null
+++ b/app/stylus/components/Filmstrip.styl
@@ -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;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl
index 5a9e762..7d26715 100644
--- a/app/stylus/components/Peer.styl
+++ b/app/stylus/components/Peer.styl
@@ -22,13 +22,15 @@
> .view-container {
position: relative;
-
+ flex-grow: 1;
+
&.webcam {
order: 2;
}
&.screen {
order: 1;
+ max-width: 50%;
}
> .controls {
diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl
index 55dbe71..dcc62d5 100644
--- a/app/stylus/components/Room.styl
+++ b/app/stylus/components/Room.styl
@@ -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);
}
diff --git a/app/stylus/index.styl b/app/stylus/index.styl
index 94e08b8..5e67e46 100644
--- a/app/stylus/index.styl
+++ b/app/stylus/index.styl
@@ -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