From edd10bf0347a68fb5bc5b6756b2f91fd147ff7be Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Fri, 16 Mar 2018 11:20:48 +0100 Subject: [PATCH 01/43] Raise hand first step (no signalling yet) --- app/lib/RoomClient.js | 29 +++++++++++++++++++++++++++ app/lib/components/Room.jsx | 19 ++++++++++++++++++ app/lib/redux/reducers/me.js | 16 +++++++++++++++ app/lib/redux/requestActions.js | 14 +++++++++++++ app/lib/redux/roomClientMiddleware.js | 14 +++++++++++++ app/lib/redux/stateActions.js | 16 +++++++++++++++ app/stylus/components/Room.styl | 4 ++++ 7 files changed, 112 insertions(+) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index c9ebcf7..833ef3e 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -486,6 +486,35 @@ export default class RoomClient }); } + raiseHand() + { + logger.debug('raiseHand()'); + + this._dispatch( + stateActions.setRaiseHandInProgress(true)); + + this._dispatch( + stateActions.setRaiseHandState(true)); + + this._dispatch( + stateActions.setRaiseHandInProgress(false)); + + } + + lowerHand() + { + logger.debug('lowerHand()'); + + this._dispatch( + stateActions.setRaiseHandInProgress(true)); + + this._dispatch( + stateActions.setRaiseHandState(false)); + + this._dispatch( + stateActions.setRaiseHandInProgress(false)); + } + restartIce() { logger.debug('restartIce()'); diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 74284bc..ca0663b 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -23,6 +23,7 @@ class Room extends React.Component onRoomLinkCopy, onSetAudioMode, onRestartIce, + onToggleHand, onLeaveMeeting } = this.props; @@ -98,6 +99,16 @@ class Room extends React.Component onClick={() => onRestartIce()} /> +
onToggleHand(!me.raiseHand)} + /> +
{ dispatch(requestActions.restartIce()); }, + onToggleHand : (enable) => + { + if (enable) + dispatch(requestActions.raiseHand()); + else + dispatch(requestActions.lowerHand()); + }, onLeaveMeeting : () => { dispatch(requestActions.leaveRoom()); diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index b692b1d..f50d082 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -10,6 +10,8 @@ const initialState = webcamInProgress : false, audioOnly : false, audioOnlyInProgress : false, + raiseHand : false, + raiseHandInProgress : false, restartIceInProgress : false }; @@ -70,6 +72,20 @@ const me = (state = initialState, action) => return { ...state, audioOnlyInProgress: flag }; } + case 'SET_RAISE_HAND_STATE': + { + const { enabled } = action.payload; + + return { ...state, raiseHand: enabled }; + } + + case 'SET_RAISE_HAND_STATE_IN_PROGRESS': + { + const { flag } = action.payload; + + return { ...state, raiseHandInProgress: flag }; + } + case 'SET_RESTART_ICE_IN_PROGRESS': { const { flag } = action.payload; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index 14210ef..074d315 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -78,6 +78,20 @@ export const disableAudioOnly = () => }; }; +export const raiseHand = () => +{ + return { + type : 'RAISE_HAND' + }; +}; + +export const lowerHand = () => +{ + return { + type : 'LOWER_HAND' + }; +}; + export const restartIce = () => { return { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 271d700..caab57f 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -102,6 +102,20 @@ export default ({ dispatch, getState }) => (next) => break; } + case 'RAISE_HAND': + { + client.raiseHand(); + + break; + } + + case 'LOWER_HAND': + { + client.lowerHand(); + + break; + } + case 'RESTART_ICE': { client.restartIce(); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index bd19225..48fd0d1 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -70,6 +70,22 @@ export const setAudioOnlyInProgress = (flag) => }; }; +export const setRaiseHandState = (enabled) => +{ + return { + type : 'SET_RAISE_HAND_STATE', + payload : { enabled } + }; +}; + +export const setRaiseHandInProgress = (flag) => +{ + return { + type : 'SET_RAISE_HAND_STATE_IN_PROGRESS', + payload : { flag } + }; +}; + export const setRestartIceInProgress = (flag) => { return { diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 5cd6b26..717df68 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -232,6 +232,10 @@ } } + &.raise-hand { + background-image: url('/resources/images/leave-meeting.svg'); + } + &.leave-meeting { background-image: url('/resources/images/leave-meeting.svg'); } From bd30a54958e099d3308da6892218670ea67ca1cf Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 10 Apr 2018 10:51:12 +0200 Subject: [PATCH 02/43] raise-hand works local... --- app/stylus/components/Room.styl | 2 +- server/lib/Room.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 717df68..6acc3ba 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -233,7 +233,7 @@ } &.raise-hand { - background-image: url('/resources/images/leave-meeting.svg'); +// background-image: url('/resources/images/leave-meeting.svg'); } &.leave-meeting { diff --git a/server/lib/Room.js b/server/lib/Room.js index a2bad3f..ec15ab5 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -260,6 +260,24 @@ class Room extends EventEmitter break; } + case 'raisehand-message': + { + accept(); + + const { raiseHandMessage } = request.data; + + // Spread to others via protoo. + this._protooRoom.spread( + 'raisehand-message-receive', + { + peerName : protooPeer.id, + raiseHandMessage : raiseHandMessage + }, + [ protooPeer ]); + + break; + } + default: { logger.error('unknown request.method "%s"', request.method); From 6541b8bb7f4cfa7ba6f47b54e0f1a9027ccccf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 16 Apr 2018 15:04:55 +0200 Subject: [PATCH 03/43] Fix for safari and edge screen sharing --- app/lib/ScreenShare.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/lib/ScreenShare.js b/app/lib/ScreenShare.js index 36d2760..7c71b52 100644 --- a/app/lib/ScreenShare.js +++ b/app/lib/ScreenShare.js @@ -189,6 +189,19 @@ class FirefoxScreenShare } } +class DefaultScreenShare +{ + isScreenShareAvailable() + { + return false; + } + + needExtension() + { + return false; + } +} + export default class ScreenShare { static create() @@ -205,7 +218,7 @@ export default class ScreenShare } default: { - return null; + return new DefaultScreenShare(); } } } From ad131d0510bc2e2aa02c0319bc105eb05a47545d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 9 Apr 2018 09:43:48 +0200 Subject: [PATCH 04/43] Add the needed modification to support redux chrome extension --- app/lib/index.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/lib/index.jsx b/app/lib/index.jsx index dbd91c4..e4f9cd2 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -5,7 +5,8 @@ import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { applyMiddleware as applyReduxMiddleware, - createStore as createReduxStore + createStore as createReduxStore, + compose as composeRedux } from 'redux'; import thunk from 'redux-thunk'; import { createLogger as createReduxLogger } from 'redux-logger'; @@ -40,10 +41,22 @@ if (process.env.NODE_ENV === 'development') reduxMiddlewares.push(reduxLogger); } +const composeEnhancers = +typeof window === 'object' && +window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ + // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... + }) : composeRedux; + +const enhancer = composeEnhancers( + applyReduxMiddleware(...reduxMiddlewares) + // other store enhancers if any +); + const store = createReduxStore( reducers, undefined, - applyReduxMiddleware(...reduxMiddlewares) + enhancer ); domready(() => From 8a8d9938e92470634e0c1bdb265ef8ebf273c143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 23 Apr 2018 15:31:08 +0200 Subject: [PATCH 05/43] fix lint error key-spacing --- server/lib/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/Room.js b/server/lib/Room.js index a2bad3f..e1ded90 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -254,7 +254,7 @@ class Room extends EventEmitter protooPeer.send( 'chat-history-receive', - { chatHistory : this._chatHistory } + { chatHistory: this._chatHistory } ); break; From 63b48ebd71c387fe6396dad1e809140feeaa2f78 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 24 Apr 2018 10:05:54 +0200 Subject: [PATCH 06/43] Raise hand over network - still no state on server (new participants don't get the raised hand state of others until the others change their raised hand state) --- app/lib/RoomClient.js | 59 ++++++++++++------- app/lib/components/Peer.jsx | 4 ++ app/lib/redux/STATE.md | 9 +-- app/lib/redux/reducers/me.js | 8 +-- app/lib/redux/reducers/peers.js | 13 ++++ app/lib/redux/roomClientMiddleware.js | 4 +- app/lib/redux/stateActions.js | 18 ++++-- app/resources/images/icon-hand-black.svg | 26 ++++++++ app/resources/images/icon-hand-white.svg | 26 ++++++++ .../images/icon_remote_raise_hand.svg | 26 ++++++++ app/stylus/components/Peer.styl | 4 ++ app/stylus/components/Room.styl | 6 +- server/lib/Room.js | 9 ++- 13 files changed, 173 insertions(+), 39 deletions(-) create mode 100644 app/resources/images/icon-hand-black.svg create mode 100644 app/resources/images/icon-hand-white.svg create mode 100644 app/resources/images/icon_remote_raise_hand.svg diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 833ef3e..4a15805 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -486,33 +486,41 @@ export default class RoomClient }); } - raiseHand() + sendRaiseHandState(state) { - logger.debug('raiseHand()'); + logger.debug('sendRaiseHandState: ', state); this._dispatch( - stateActions.setRaiseHandInProgress(true)); + stateActions.setMyRaiseHandStateInProgress(true)); - this._dispatch( - stateActions.setRaiseHandState(true)); + return this._protoo.send('raisehand-message', { raiseHandState: state }) + .then(() => + { + this._dispatch( + stateActions.setMyRaiseHandState(state)); - this._dispatch( - stateActions.setRaiseHandInProgress(false)); + this._dispatch(requestActions.notify( + { + text : 'raiseHand state changed' + })); + this._dispatch( + stateActions.setMyRaiseHandStateInProgress(false)); + }) + .catch((error) => + { + logger.error('sendRaiseHandState() | failed: %o', error); - } + this._dispatch(requestActions.notify( + { + type : 'error', + text : `Could not change raise hand state: ${error}` + })); - lowerHand() - { - logger.debug('lowerHand()'); - - this._dispatch( - stateActions.setRaiseHandInProgress(true)); - - this._dispatch( - stateActions.setRaiseHandState(false)); - - this._dispatch( - stateActions.setRaiseHandInProgress(false)); + // We need to refresh the component for it to render changed state + this._dispatch(stateActions.setMyRaiseHandState(!state)); + this._dispatch( + stateActions.setMyRaiseHandStateInProgress(false)); + }); } restartIce() @@ -641,6 +649,17 @@ export default class RoomClient break; } + case 'raisehand-message': + { + accept(); + const { peerName, raiseHandState } = request.data; + + logger.debug('Got raiseHandState from "%s"', peerName); + + this._dispatch( + stateActions.setPeerRaiseHandState(peerName, raiseHandState)); + break; + } case 'chat-message-receive': { diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index 61ed633..389d1d1 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -31,6 +31,10 @@ const Peer = (props) => return (
+ {peer.raiseHandState ? +
+ :null + } {!micEnabled ?
:null diff --git a/app/lib/redux/STATE.md b/app/lib/redux/STATE.md index 3171af9..26b0773 100644 --- a/app/lib/redux/STATE.md +++ b/app/lib/redux/STATE.md @@ -49,10 +49,11 @@ { 'alice' : { - name : 'alice', - displayName : 'Alice Thomsom', - device : { flag: 'chrome', name: 'Chrome', version: '58' }, - consumers : [ 5551, 5552 ] + name : 'alice', + displayName : 'Alice Thomsom', + raiseHandState : false, + device : { flag: 'chrome', name: 'Chrome', version: '58' }, + consumers : [ 5551, 5552 ] } }, consumers : diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index f50d082..3e009ad 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -72,14 +72,14 @@ const me = (state = initialState, action) => return { ...state, audioOnlyInProgress: flag }; } - case 'SET_RAISE_HAND_STATE': + case 'SET_MY_RAISE_HAND_STATE': { - const { enabled } = action.payload; + const { flag } = action.payload; - return { ...state, raiseHand: enabled }; + return { ...state, raiseHand: flag }; } - case 'SET_RAISE_HAND_STATE_IN_PROGRESS': + case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS': { const { flag } = action.payload; diff --git a/app/lib/redux/reducers/peers.js b/app/lib/redux/reducers/peers.js index 59761e2..8ff36bd 100644 --- a/app/lib/redux/reducers/peers.js +++ b/app/lib/redux/reducers/peers.js @@ -34,6 +34,19 @@ const peers = (state = initialState, action) => return { ...state, [newPeer.name]: newPeer }; } + case 'SET_PEER_RAISE_HAND_STATE': + { + const { peerName, raiseHandState } = action.payload; + const peer = state[peerName]; + + if (!peer) + throw new Error('no Peer found'); + + const newPeer = { ...peer, raiseHandState }; + + return { ...state, [newPeer.name]: newPeer }; + } + case 'ADD_CONSUMER': { const { consumer, peerName } = action.payload; diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index caab57f..3e491e8 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -104,14 +104,14 @@ export default ({ dispatch, getState }) => (next) => case 'RAISE_HAND': { - client.raiseHand(); + client.sendRaiseHandState(true); break; } case 'LOWER_HAND': { - client.lowerHand(); + client.sendRaiseHandState(false); break; } diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 48fd0d1..0829645 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -70,22 +70,30 @@ export const setAudioOnlyInProgress = (flag) => }; }; -export const setRaiseHandState = (enabled) => +export const setMyRaiseHandState = (flag) => { return { - type : 'SET_RAISE_HAND_STATE', - payload : { enabled } + type : 'SET_MY_RAISE_HAND_STATE', + payload : { flag } }; }; -export const setRaiseHandInProgress = (flag) => +export const setMyRaiseHandStateInProgress = (flag) => { return { - type : 'SET_RAISE_HAND_STATE_IN_PROGRESS', + type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS', payload : { flag } }; }; +export const setPeerRaiseHandState = (peerName, raiseHandState) => +{ + return { + type : 'SET_PEER_RAISE_HAND_STATE', + payload : { peerName, raiseHandState } + }; +}; + export const setRestartIceInProgress = (flag) => { return { diff --git a/app/resources/images/icon-hand-black.svg b/app/resources/images/icon-hand-black.svg new file mode 100644 index 0000000..8f0f065 --- /dev/null +++ b/app/resources/images/icon-hand-black.svg @@ -0,0 +1,26 @@ + + + + image/svg+xml + + + diff --git a/app/resources/images/icon-hand-white.svg b/app/resources/images/icon-hand-white.svg new file mode 100644 index 0000000..0e2f05f --- /dev/null +++ b/app/resources/images/icon-hand-white.svg @@ -0,0 +1,26 @@ + + + + image/svg+xml + + + diff --git a/app/resources/images/icon_remote_raise_hand.svg b/app/resources/images/icon_remote_raise_hand.svg new file mode 100644 index 0000000..0e2f05f --- /dev/null +++ b/app/resources/images/icon_remote_raise_hand.svg @@ -0,0 +1,26 @@ + + + + image/svg+xml + + + diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 8426b0f..3c32d99 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -38,6 +38,10 @@ opacity: 0.85; } + &.raise-hand { + background-image: url('/resources/images/icon_remote_raise_hand.svg'); + } + &.mic-off { background-image: url('/resources/images/icon_remote_mic_white_off.svg'); } diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 6acc3ba..061af50 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -233,7 +233,11 @@ } &.raise-hand { -// background-image: url('/resources/images/leave-meeting.svg'); + background-image: url('/resources/images/icon-hand-white.svg'); + + &.on { + background-image: url('/resources/images/icon-hand-black.svg'); + } } &.leave-meeting { diff --git a/server/lib/Room.js b/server/lib/Room.js index ec15ab5..4a72188 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -264,14 +264,17 @@ class Room extends EventEmitter { accept(); - const { raiseHandMessage } = request.data; + const { raiseHandState } = request.data; + const { mediaPeer } = protooPeer.data; + + mediaPeer.appData.raiseHand = request.data.raiseHandState; // Spread to others via protoo. this._protooRoom.spread( - 'raisehand-message-receive', + 'raisehand-message', { peerName : protooPeer.id, - raiseHandMessage : raiseHandMessage + raiseHandState : raiseHandState }, [ protooPeer ]); From cc7e491a90b0f48204d708bab9c7126f733e7daa Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 24 Apr 2018 14:51:37 +0200 Subject: [PATCH 07/43] Fix for last not correctly working merge - sorry for the mess! --- app/lib/components/Peer.jsx | 49 ++++++++++--------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index f094977..b797b4d 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -40,42 +40,19 @@ const Peer = (props) => if (screenConsumer) screenProfile = screenConsumer.profile; - if (screenVisible && screenConsumer.supported) - { - return ( -
- -
- ); - } - else - { - return ( -
-
- {peer.raiseHandState ? -
- :null - } - {!micEnabled ? -
- :null - } - {!videoVisible ? -
- :null - } -
- - {videoVisible && !webcamConsumer.supported ? -
-

incompatible video

-
+ return ( +
+
+ {peer.raiseHandState ? +
+ :null + } + {!micEnabled ? +
+ :null + } + {!videoVisible ? +
:null }
From a52a96b13f822f613cc2a4217436d54c62b57129 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 24 Apr 2018 18:28:57 +0200 Subject: [PATCH 08/43] Switch off simulcast as default until this works better --- app/lib/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/index.jsx b/app/lib/index.jsx index e4f9cd2..f6e291c 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -78,7 +78,7 @@ function run() const produce = urlParser.query.produce !== 'false'; let displayName = urlParser.query.displayName; const isSipEndpoint = urlParser.query.sipEndpoint === 'true'; - const useSimulcast = urlParser.query.simulcast !== 'false'; + const useSimulcast = urlParser.query.simulcast === 'true'; if (!roomId) { From 84ac401d139a4f88ba349b1d1642a14e00b35cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 26 Apr 2018 09:27:14 +0200 Subject: [PATCH 09/43] Change to URL handling. Join by /roomname. --- app/lib/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/index.jsx b/app/lib/index.jsx index e4f9cd2..c5ca035 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -74,7 +74,8 @@ function run() const peerName = randomString({ length: 8 }).toLowerCase(); const urlParser = new UrlParse(window.location.href, true); - let roomId = urlParser.query.roomId; + let roomId = (urlParser.pathname).substr(1) + ? (urlParser.pathname).substr(1) : urlParser.query.roomId; const produce = urlParser.query.produce !== 'false'; let displayName = urlParser.query.displayName; const isSipEndpoint = urlParser.query.sipEndpoint === 'true'; From d3883906aba09cc76231d821e25bae479835c032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 26 Apr 2018 09:34:10 +0200 Subject: [PATCH 10/43] Deactivate simulcast by default. --- app/lib/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/index.jsx b/app/lib/index.jsx index c5ca035..6987482 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -79,7 +79,7 @@ function run() const produce = urlParser.query.produce !== 'false'; let displayName = urlParser.query.displayName; const isSipEndpoint = urlParser.query.sipEndpoint === 'true'; - const useSimulcast = urlParser.query.simulcast !== 'false'; + const useSimulcast = urlParser.query.simulcast === 'true'; if (!roomId) { From ce062fb7e9f14c0e16787bbdab6b3ebff88ac105 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 5 Jun 2018 17:25:02 +0200 Subject: [PATCH 11/43] adding service file for systemd for automatical upstart of the service: this makes sense in future when the server hosts his own static files by himself --- multiparty-meeting.service | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 multiparty-meeting.service diff --git a/multiparty-meeting.service b/multiparty-meeting.service new file mode 100644 index 0000000..39fd30d --- /dev/null +++ b/multiparty-meeting.service @@ -0,0 +1,15 @@ +[Unit] +Description=multiparty-meeting is a audio / video meeting service running in the browser and powered by webRTC +After=network.target + +[Service] +ExecStart=/usr/local/src/multiparty-meeting/server.js +Restart=always +User=nobody +Group=nogroup +Environment=PATH=/usr/bin:/usr/local/bin +Environment=NODE_ENV=production +WorkingDirectory=/usr/local/src/multiparty-meeting + +[Install] +WantedBy=multi-user.target From 6ae1a097ae365156a1fad10033f803a1389e8a31 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Fri, 8 Jun 2018 13:47:58 +0200 Subject: [PATCH 12/43] adding some 4:3 aspect fixes, adding httpserver for serving own static files, adding first working of oauth2.0 / openID Connect --- app/gulpfile.js | 2 + app/lib/RoomClient.js | 51 +++++++- app/lib/components/Peers.jsx | 29 ++--- app/stylus/components/PeerView.styl | 2 +- server/config.example.js | 15 +++ server/http-helpers.js | 31 +++++ server/lib/Room.js | 1 - server/router.js | 194 ++++++++++++++++++++++++++++ server/server.js | 29 +++-- server/util.js | 18 +++ 10 files changed, 343 insertions(+), 29 deletions(-) create mode 100644 server/http-helpers.js create mode 100644 server/router.js create mode 100644 server/util.js diff --git a/app/gulpfile.js b/app/gulpfile.js index 3eaa076..a214167 100644 --- a/app/gulpfile.js +++ b/app/gulpfile.js @@ -206,6 +206,7 @@ gulp.task('livebrowser', (done) => { open : 'external', host : config.domain, + port : 3000, server : { baseDir : OUTPUT_DIR @@ -226,6 +227,7 @@ gulp.task('browser', (done) => { open : 'external', host : config.domain, + port : 3000, server : { baseDir : OUTPUT_DIR diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 464c2c2..a3ff903 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -20,9 +20,9 @@ const ROOM_OPTIONS = const VIDEO_CONSTRAINS = { - qvga : { width: { ideal: 320 }, height: { ideal: 240 } }, - vga : { width: { ideal: 640 }, height: { ideal: 480 } }, - hd : { width: { ideal: 1280 }, height: { ideal: 720 } } + qvga : { width: { ideal: 320 }, height: { ideal: 240 }, aspectRatio: 1.334 }, + vga : { width: { ideal: 640 }, height: { ideal: 480 }, aspectRatio: 1.334 }, + hd : { width: { ideal: 800 }, height: { ideal: 600 }, aspectRatio: 1.334 } }; export default class RoomClient @@ -37,6 +37,9 @@ export default class RoomClient const protooUrl = getProtooUrl(peerName, roomId); const protooTransport = new protooClient.WebSocketTransport(protooUrl); + // window element to external login site + this._loginWindow; + // Closed flag. this._closed = false; @@ -60,6 +63,7 @@ export default class RoomClient // mediasoup-client Room instance. this._room = new mediasoupClient.Room(ROOM_OPTIONS); + this._room.roomId = roomId; // Transport for sending. this._sendTransport = null; @@ -111,6 +115,18 @@ export default class RoomClient this._dispatch(stateActions.setRoomState('closed')); } + login() + { + const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; + + this._loginWindow = window.open(url, 'loginWindow'); + } + + closeLoginWindow() + { + this._loginWindow.close(); + } + changeDisplayName(displayName) { logger.debug('changeDisplayName() [displayName:"%s"]', displayName); @@ -750,6 +766,35 @@ export default class RoomClient break; } + + // This means: server wants to change MY displayName + case 'auth': + { + logger.debug('got auth event from server', request.data); + accept(); + + if (request.data.verified == true) + { + this.changeDisplayName(request.data.name); + this._dispatch(requestActions.notify( + { + text : `Authenticated successfully: ${request.data}` + } + )); + } + else + { + this._dispatch(requestActions.notify( + { + text : `Authentication failed: ${request.data}` + } + )); + } + this.closeLoginWindow(); + break; + + } + case 'raisehand-message': { accept(); diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index fdd4a92..abb8d62 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -13,24 +13,23 @@ class Peers extends React.Component { super(); this.state = { - ratio : 4 / 3 + ratio : 1.334 }; - } updateDimensions() { const n = this.props.peers.length; - if (n == 0) + if (n == 0) { return; } const width = this.refs.peers.clientWidth; const height = this.refs.peers.clientHeight; - + let x, y, space; - + for (let rows = 1; rows < 100; rows = rows + 1) { x = width / Math.ceil(n / rows); @@ -43,8 +42,8 @@ class Peers extends React.Component } space = height - (y * (rows)); if (space < y) - { - break; + { + break; } } if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x)) @@ -52,17 +51,17 @@ class Peers extends React.Component this.props.onComponentResize(0.9 * x, 0.9 * y); } } - + componentDidMount() { window.addEventListener('resize', this.updateDimensions.bind(this)); } - - componentWillUnmount() + + componentWillUnmount() { window.removeEventListener('resize', this.updateDimensions.bind(this)); } - + render() { const { @@ -71,15 +70,15 @@ class Peers extends React.Component peerWidth, peerHeight } = this.props; - - const style = + + const style = { 'width' : peerWidth, 'height' : peerHeight }; this.updateDimensions(); - + return (
{ @@ -127,7 +126,7 @@ const mapStateToProps = (state) => // TODO: This is not OK since it's creating a new array every time, so triggering a // component rendering. const peersArray = Object.values(state.peers); - + return { peers : peersArray, activeSpeakerName : state.room.activeSpeakerName, diff --git a/app/stylus/components/PeerView.styl b/app/stylus/components/PeerView.styl index 1230284..f5dbdc0 100644 --- a/app/stylus/components/PeerView.styl +++ b/app/stylus/components/PeerView.styl @@ -170,7 +170,7 @@ flex: 100 100 auto; height: 100%; width: 100%; - object-fit: cover; + object-fit: contain; user-select: none; transition-property: opacity; transition-duration: .15s; diff --git a/server/config.example.js b/server/config.example.js index 43695cb..8ad774b 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -1,5 +1,18 @@ module.exports = { + // oAuth2 conf + oauth2 : + { + client_id : '', + client_secret : '', + providerID : '', + redirect_uri : 'https://mYDomainName:port/auth-callback', + authorization_endpoint : '', + userinfo_endpoint : '', + token_endpoint : '', + scopes : { request : [ 'openid', 'userid','profile'] }, + response_type : 'code' + }, // Listening hostname for `gulp live|open`. domain : 'localhost', tls : @@ -7,6 +20,8 @@ module.exports = cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`, key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem` }, + // Listening port for https server. + listeningPort : 3443, mediasoup : { // mediasoup Server settings. diff --git a/server/http-helpers.js b/server/http-helpers.js new file mode 100644 index 0000000..de66801 --- /dev/null +++ b/server/http-helpers.js @@ -0,0 +1,31 @@ +'use strict'; + +const headers = { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "content-type, accept", + "access-control-max-age": 10, + "Content-Type": "application/json" +}; + +exports.prepareResponse = function(req, cb) { + var data = ""; + req.on('data', function(chunk) { data += chunk; }); + req.on('end', function() { cb(data); }); +}; + +exports.respond = function(res, data, status) { + status = status || 200; + res.writeHead(status, headers); + res.end(data); +}; + +exports.send404 = function(res) { + exports.respond(res, 'Not Found', 404); +}; + +exports.redirector = function(res, loc, status) { + status = status || 302; + res.writeHead(status, { Location: loc }); + res.end(); +}; diff --git a/server/lib/Room.js b/server/lib/Room.js index dbe067e..c6b5089 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -268,7 +268,6 @@ class Room extends EventEmitter const { mediaPeer } = protooPeer.data; mediaPeer.appData.raiseHand = request.data.raiseHandState; - // Spread to others via protoo. this._protooRoom.spread( 'raisehand-message', diff --git a/server/router.js b/server/router.js new file mode 100644 index 0000000..97da297 --- /dev/null +++ b/server/router.js @@ -0,0 +1,194 @@ +'use strict'; + +const EventEmitter = require( 'events' ); +const eventEmitter = new EventEmitter(); +const path = require('path'); +const url = require('url'); +const httpHelpers = require('./http-helpers'); +const fs = require('fs'); +const config = require('./config'); +const utils = require('./util'); +const querystring = require('querystring'); +const https = require('https') +const Logger = require('./lib/Logger'); + +const logger = new Logger(); + +let authRequests = {}; // ongoing auth requests : +/* +{ + state: + { + peerName:'peerName' + code:'oauth2 code', + roomId: 'romid', + } +} +*/ +const actions = { + 'GET': function(req, res) { + var parsedUrl = url.parse(req.url,true); + if ( parsedUrl.pathname === '/auth-callback' ) + { + if ( typeof(authRequests[parsedUrl.query.state]) != 'undefined' ) + { + console.log('got authorization code for access token: ',parsedUrl.query,authRequests[parsedUrl.query.state]); + const auth = "Basic " + new Buffer(config.oauth2.client_id + ":" + config.oauth2.client_secret).toString("base64"); + const postUrl = url.parse(config.oauth2.token_endpoint); + let postData = querystring.stringify({ + "grant_type":"authorization_code", + "code":parsedUrl.query.code, + "redirect_uri":config.oauth2.redirect_uri + }); + + let request = https.request( { + host : postUrl.hostname, + path : postUrl.pathname, + port : postUrl.port, + method : 'POST', + headers : + { + 'Content-Type' : 'application/x-www-form-urlencoded', + 'Authorization' : auth, + 'Content-Length': Buffer.byteLength(postData) + } + }, function(res) + { + res.setEncoding("utf8"); + let body = ""; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + if ( res.statusCode == 200 ) + { + console.log('We\'ve got an access token!', body); + body = JSON.parse(body); + authRequests[parsedUrl.query.state].access_token = + body.access_token; + const auth = "Bearer " + body.access_token; + const getUrl = url.parse(config.oauth2.userinfo_endpoint); + let request = https.request( { + host : getUrl.hostname, + path : getUrl.pathname, + port : getUrl.port, + method : 'GET', + headers : + { + 'Authorization' : auth, + } + }, function(res) + { + res.setEncoding("utf8"); + let body = ''; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + // we don't need this any longer: + delete authRequests[parsedUrl.query.state].access_token; + + body = JSON.parse(body); + console.log(body); + if ( res.statusCode == 200 ) + { + authRequests[parsedUrl.query.state].verified = true; + if ( typeof(body.sub) != 'undefined') + { + authRequests[parsedUrl.query.state].sub = body.sub; + } + if ( typeof(body.name) != 'undefined') + { + authRequests[parsedUrl.query.state].name = body.name; + } + if ( typeof(body.picture) != 'undefined') + { + authRequests[parsedUrl.query.state].picture = body.picture; + } + } else { + { + authRequests[parsedUrl.query.state].verified = false; + } + } + eventEmitter.emit('auth', + authRequests[parsedUrl.query.state]); + + delete authRequests[parsedUrl.query.state]; + }); + }); + request.write(' '); + request.end; + } + else + { + console.log('access_token denied',body); + authRequests[parsedUrl.query.state].verified = false; + delete authRequests[parsedUrl.query.state].access_token; + eventEmitter.emit('auth', + authRequests[parsedUrl.query.state]); + } + }); + }); + request.write(postData); + request.end; + } + else + { + logger.warn('Got authorization_code for unseen state:', parsedUrl) + } + } + else if (parsedUrl.pathname === '/login') { + const state = utils.random(10); + httpHelpers.redirector(res, config.oauth2.authorization_endpoint + + '?client_id=' + config.oauth2.client_id + + '&redirect_uri=' + config.oauth2.redirect_uri + + '&state=' + state + + '&scopes=' + config.oauth2.scopes.request.join('+') + + '&response_type=' + config.oauth2.response_type); + authRequests[state] = + { + 'roomId' : parsedUrl.query.roomId, + 'peerName' : parsedUrl.query.peerName + }; + console.log('Started authorization process: ', parsedUrl.query); + } + else + { + console.log('requested url:', parsedUrl.pathname); + var resolvedBase = path.resolve('./public'); + var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); + var fileLoc = path.join(resolvedBase, safeSuffix); + + var stream = fs.createReadStream(fileLoc); + + // Handle non-existent file -> delivering index.html + stream.on('error', function(error) { + stream = fs.createReadStream(path.resolve('./public/index.html')); + res.statusCode = 200; + stream.pipe(res); + }); + + // File exists, stream it to user + res.statusCode = 200; + stream.pipe(res); + } + }, + + 'POST': function(req, res) { + httpHelpers.prepareResponse(req, function(data) { + // Do something with the data that was just collected by the helper + // e.g., validate and save to db + // either redirect or respond + // should be based on result of the operation performed in response to the POST request intent + // e.g., if user wants to save, and save fails, throw error + httpHelpers.redirector(res, /* redirect path , optional status code - defaults to 302 */); + }); + } +}; + +module.exports = eventEmitter; + +module.exports.handleRequest = function(req, res) { + var action = actions[req.method]; + action ? action(req, res) : httpHelpers.send404(res); +}; diff --git a/server/server.js b/server/server.js index 07a9664..514b089 100755 --- a/server/server.js +++ b/server/server.js @@ -14,7 +14,9 @@ console.log('- config.mediasoup.logTags:', config.mediasoup.logTags); const fs = require('fs'); const https = require('https'); +const router = require('./router'); const url = require('url'); +const path = require('path'); const protooServer = require('protoo-server'); const mediasoup = require('mediasoup'); const readline = require('readline'); @@ -77,25 +79,34 @@ mediaServer.on('newroom', (room) => }); }); -// HTTPS server for the protoo WebSocket server. +// HTTPS server const tls = { cert : fs.readFileSync(config.tls.cert), key : fs.readFileSync(config.tls.key) }; -const httpsServer = https.createServer(tls, (req, res) => +const httpsServer = https.createServer(tls, router.handleRequest); +httpsServer.listen(config.listeningPort, '0.0.0.0', () => { - res.writeHead(404, 'Not Here'); - res.end(); + logger.info('Server running, port: ',config.listeningPort); }); -httpsServer.listen(3443, '0.0.0.0', () => -{ - logger.info('protoo WebSocket server running'); -}); +router.on('auth',function(event){ + console.log('router: Got an event: ',event) + if ( rooms.has(event.roomId) ) + { + const room = rooms.get(event.roomId)._protooRoom; + if ( room.hasPeer(event.peerName) ) + { + const peer = room.getPeer(event.peerName); + peer.send('auth', event) + } + } +}) -// Protoo WebSocket server. +// Protoo WebSocket server listens to same webserver so everythink is available +// via same port const webSocketServer = new protooServer.WebSocketServer(httpsServer, { maxReceivedFrameSize : 960000, // 960 KBytes. diff --git a/server/util.js b/server/util.js new file mode 100644 index 0000000..cfa4548 --- /dev/null +++ b/server/util.js @@ -0,0 +1,18 @@ +'use strict'; + +var crypto = require('crypto'); + +exports.random = function (howMany, chars) { + chars = chars + || "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789"; + var rnd = crypto.randomBytes(howMany) + , value = new Array(howMany) + , len = len = Math.min(256, chars.length) + , d = 256 / len + + for (var i = 0; i < howMany; i++) { + value[i] = chars[Math.floor(rnd[i] / d)] + }; + + return value.join(''); +} From c18d982c1b95dacd98f48a239a06d4891106921e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 15 Jun 2018 09:29:47 +0200 Subject: [PATCH 13/43] Added support for muting audio and video from participants --- app/lib/RoomClient.js | 144 ++++++++++++++++++++++++++ app/lib/components/Peer.jsx | 89 ++++++++++++---- app/lib/components/PeerView.jsx | 2 +- app/lib/components/Peers.jsx | 12 ++- app/lib/redux/reducers/peers.js | 26 +++++ app/lib/redux/requestActions.js | 32 ++++++ app/lib/redux/roomClientMiddleware.js | 36 +++++++ app/lib/redux/stateActions.js | 16 +++ app/stylus/components/Peer.styl | 122 ++++++++++++++-------- 9 files changed, 414 insertions(+), 65 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index a3ff903..119cf1b 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -523,6 +523,150 @@ export default class RoomClient }); } + mutePeerAudio(peerName) + { + logger.debug('mutePeerAudio() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'audio') + continue; + + consumer.pause('mute-audio'); + } + } + } + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('mutePeerAudio() failed: %o', error); + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, false)); + }); + } + + unmutePeerAudio(peerName) + { + logger.debug('unmutePeerAudio() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'audio' || !consumer.supported) + continue; + + consumer.resume(); + } + } + } + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('unmutePeerAudio() failed: %o', error); + + this._dispatch( + stateActions.setPeerAudioInProgress(peerName, false)); + }); + } + + pausePeerVideo(peerName) + { + logger.debug('pausePeerVideo() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'video') + continue; + + consumer.pause('pause-video'); + } + } + } + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('pausePeerVideo() failed: %o', error); + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, false)); + }); + } + + resumePeerVideo(peerName) + { + logger.debug('resumePeerVideo() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'video' || !consumer.supported) + continue; + + consumer.resume(); + } + } + } + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('resumePeerVideo() failed: %o', error); + + this._dispatch( + stateActions.setPeerVideoInProgress(peerName, false)); + }); + } + enableAudioOnly() { logger.debug('enableAudioOnly()'); diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index b797b4d..51d323f 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -1,6 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; import * as appPropTypes from './appPropTypes'; +import * as requestActions from '../redux/requestActions'; import PeerView from './PeerView'; const Peer = (props) => @@ -9,7 +12,11 @@ const Peer = (props) => peer, micConsumer, webcamConsumer, - screenConsumer + screenConsumer, + onMuteMic, + onUnmuteMic, + onDisableWebcam, + onEnableWebcam } = props; const micEnabled = ( @@ -42,19 +49,33 @@ const Peer = (props) => return (
-
- {peer.raiseHandState ? -
- :null - } - {!micEnabled ? -
- :null - } - {!videoVisible ? -
- :null - } +
+
+ { + e.stopPropagation(); + micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name); + }} + /> + +
+ { + e.stopPropagation(); + videoVisible ? + onDisableWebcam(peer.name) : onEnableWebcam(peer.name); + }} + />
{videoVisible && !webcamConsumer.supported ? @@ -83,10 +104,14 @@ const Peer = (props) => Peer.propTypes = { - peer : appPropTypes.Peer.isRequired, - micConsumer : appPropTypes.Consumer, - webcamConsumer : appPropTypes.Consumer, - screenConsumer : appPropTypes.Consumer + peer : appPropTypes.Peer.isRequired, + micConsumer : appPropTypes.Consumer, + webcamConsumer : appPropTypes.Consumer, + screenConsumer : appPropTypes.Consumer, + onMuteMic : PropTypes.func.isRequired, + onUnmuteMic : PropTypes.func.isRequired, + onEnableWebcam : PropTypes.func.isRequired, + onDisableWebcam : PropTypes.func.isRequired }; const mapStateToProps = (state, { name }) => @@ -109,6 +134,32 @@ const mapStateToProps = (state, { name }) => }; }; -const PeerContainer = connect(mapStateToProps)(Peer); +const mapDispatchToProps = (dispatch) => +{ + return { + onMuteMic : (peerName) => + { + dispatch(requestActions.mutePeerAudio(peerName)); + }, + onUnmuteMic : (peerName) => + { + dispatch(requestActions.unmutePeerAudio(peerName)); + }, + onEnableWebcam : (peerName) => + { + + dispatch(requestActions.resumePeerVideo(peerName)); + }, + onDisableWebcam : (peerName) => + { + dispatch(requestActions.pausePeerVideo(peerName)); + } + }; +}; + +const PeerContainer = connect( + mapStateToProps, + mapDispatchToProps +)(Peer); export default PeerContainer; diff --git a/app/lib/components/PeerView.jsx b/app/lib/components/PeerView.jsx index e4f4572..cda3d47 100644 --- a/app/lib/components/PeerView.jsx +++ b/app/lib/components/PeerView.jsx @@ -315,7 +315,7 @@ PeerView.propTypes = screenTrack : PropTypes.any, videoVisible : PropTypes.bool.isRequired, videoProfile : PropTypes.string, - screenVisible : PropTypes.bool.isRequired, + screenVisible : PropTypes.bool, screenProfile : PropTypes.string, audioCodec : PropTypes.string, videoCodec : PropTypes.string, diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index abb8d62..39dd406 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -16,9 +16,10 @@ class Peers extends React.Component ratio : 1.334 }; } - updateDimensions() + + updateDimensions(nextProps = null) { - const n = this.props.peers.length; + const n = nextProps ? nextProps.peers.length : this.props.peers.length; if (n == 0) { @@ -62,6 +63,11 @@ class Peers extends React.Component window.removeEventListener('resize', this.updateDimensions.bind(this)); } + componentWillReceiveProps(nextProps) + { + this.updateDimensions(nextProps); + } + render() { const { @@ -77,8 +83,6 @@ class Peers extends React.Component 'height' : peerHeight }; - this.updateDimensions(); - return (
{ diff --git a/app/lib/redux/reducers/peers.js b/app/lib/redux/reducers/peers.js index 8ff36bd..b7001b9 100644 --- a/app/lib/redux/reducers/peers.js +++ b/app/lib/redux/reducers/peers.js @@ -34,6 +34,32 @@ const peers = (state = initialState, action) => return { ...state, [newPeer.name]: newPeer }; } + case 'SET_PEER_VIDEO_IN_PROGRESS': + { + const { peerName, flag } = action.payload; + const peer = state[peerName]; + + if (!peer) + throw new Error('no Peer found'); + + const newPeer = { ...peer, peerVideoInProgress: flag }; + + return { ...state, [newPeer.name]: newPeer }; + } + + case 'SET_PEER_AUDIO_IN_PROGRESS': + { + const { peerName, flag } = action.payload; + const peer = state[peerName]; + + if (!peer) + throw new Error('no Peer found'); + + const newPeer = { ...peer, peerAudioInProgress: flag }; + + return { ...state, [newPeer.name]: newPeer }; + } + case 'SET_PEER_RAISE_HAND_STATE': { const { peerName, raiseHandState } = action.payload; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index e13203b..f8d2178 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -78,6 +78,38 @@ export const disableAudioOnly = () => }; }; +export const mutePeerAudio = (peerName) => +{ + return { + type : 'MUTE_PEER_AUDIO', + payload : { peerName } + }; +}; + +export const unmutePeerAudio = (peerName) => +{ + return { + type : 'UNMUTE_PEER_AUDIO', + payload : { peerName } + }; +}; + +export const pausePeerVideo = (peerName) => +{ + return { + type : 'PAUSE_PEER_VIDEO', + payload : { peerName } + }; +}; + +export const resumePeerVideo = (peerName) => +{ + return { + type : 'RESUME_PEER_VIDEO', + payload : { peerName } + }; +}; + export const raiseHand = () => { return { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 971985a..628cba3 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -102,6 +102,42 @@ export default ({ dispatch, getState }) => (next) => break; } + case 'MUTE_PEER_AUDIO': + { + const { peerName } = action.payload; + + client.mutePeerAudio(peerName); + + break; + } + + case 'UNMUTE_PEER_AUDIO': + { + const { peerName } = action.payload; + + client.unmutePeerAudio(peerName); + + break; + } + + case 'PAUSE_PEER_VIDEO': + { + const { peerName } = action.payload; + + client.pausePeerVideo(peerName); + + break; + } + + case 'RESUME_PEER_VIDEO': + { + const { peerName } = action.payload; + + client.resumePeerVideo(peerName); + + break; + } + case 'RAISE_HAND': { client.sendRaiseHandState(true); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 21bb7c3..4c6b61d 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -86,6 +86,22 @@ export const setAudioOnlyInProgress = (flag) => }; }; +export const setPeerVideoInProgress = (peerName, flag) => +{ + return { + type : 'SET_PEER_VIDEO_IN_PROGRESS', + payload : { peerName, flag } + }; +}; + +export const setPeerAudioInProgress = (peerName, flag) => +{ + return { + type : 'SET_PEER_AUDIO_IN_PROGRESS', + payload : { peerName, flag } + }; +}; + export const setMyRaiseHandState = (flag) => { return { diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 3c32d99..daa2baa 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -4,6 +4,87 @@ height: 100%; width: 100%; + > .controls { + position: absolute; + z-index: 10; + right: 0; + top: 0; + display: flex; + flex-direction:; row; + justify-content: flex-start; + align-items: center; + + > .button { + flex: 0 0 auto; + margin: 4px; + border-radius: 2px; + background-position: center; + background-size: 75%; + background-repeat: no-repeat; + background-color: rgba(#000, 0.5); + cursor: pointer; + transition-property: opacity, background-color; + transition-duration: 0.15s; + + +desktop() { + width: 24px; + height: 24px; + opacity: 0.85; + + &:hover { + opacity: 1; + } + } + + +mobile() { + width: 22px; + height: 22px; + } + + &.unsupported { + pointer-events: none; + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + + &.on { + background-color: rgba(#fff, 0.7); + } + + &.mic { + &.on { + background-image: url('/resources/images/icon_mic_black_on.svg'); + } + + &.off { + background-image: url('/resources/images/icon_mic_white_off.svg'); + background-color: rgba(#d42241, 0.7); + } + + &.unsupported { + background-image: url('/resources/images/icon_mic_white_unsupported.svg'); + } + } + + &.webcam { + &.on { + background-image: url('/resources/images/icon_webcam_black_on.svg'); + } + + &.off { + background-image: url('/resources/images/icon_webcam_white_on.svg'); + } + + &.unsupported { + background-image: url('/resources/images/icon_webcam_white_unsupported.svg'); + } + } + } + } + +mobile() { display: flex; flex-direction: column; @@ -11,47 +92,6 @@ align-items: center; } - > .indicators { - position: absolute; - z-index: 10 - top: 0; - left: 0; - right: 0; - display: flex; - flex-direction:; row; - justify-content: flex-end; - align-items: center; - - > .icon { - flex: 0 0 auto; - margin: 4px; - margin-left: 0; - width: 32px; - height: 32px; - background-position: center; - background-size: 75%; - background-repeat: no-repeat; - transition-property: opacity; - transition-duration: 0.15s; - - +desktop() { - opacity: 0.85; - } - - &.raise-hand { - background-image: url('/resources/images/icon_remote_raise_hand.svg'); - } - - &.mic-off { - background-image: url('/resources/images/icon_remote_mic_white_off.svg'); - } - - &.webcam-off { - background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); - } - } - } - .incompatible-video { position: absolute; z-index: 2 From 7e1b391fe1d0c88be167af6924d0cb1e070afc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 15 Jun 2018 14:58:01 +0200 Subject: [PATCH 14/43] Started work on settings dialog. Possible to switch camera. --- app/lib/RoomClient.js | 52 +-- app/lib/components/Me.jsx | 23 +- app/lib/components/Room.jsx | 46 ++- app/lib/components/Settings.jsx | 89 ++++++ app/lib/redux/reducers/me.js | 8 + app/lib/redux/reducers/room.js | 10 +- app/lib/redux/requestActions.js | 5 +- app/lib/redux/roomClientMiddleware.js | 4 +- app/lib/redux/stateActions.js | 15 + app/package-lock.json | 296 +++++++++++++++++- app/package.json | 2 + .../images/icon_audio_only_white.svg | 2 +- .../images/icon_restart_ice_white.svg | 2 +- app/resources/images/icon_settings_black.svg | 4 + app/resources/images/icon_settings_white.svg | 3 + app/stylus/components/Room.styl | 10 + app/stylus/components/Settings.styl | 149 +++++++++ app/stylus/index.styl | 1 + 18 files changed, 651 insertions(+), 70 deletions(-) create mode 100644 app/lib/components/Settings.jsx create mode 100644 app/resources/images/icon_settings_black.svg create mode 100644 app/resources/images/icon_settings_white.svg create mode 100644 app/stylus/components/Settings.styl diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 119cf1b..8c5d0fe 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -366,9 +366,9 @@ export default class RoomClient }); } - changeWebcam() + changeWebcam(deviceId) { - logger.debug('changeWebcam()'); + logger.debug('changeWebcam() [deviceId: %s]', deviceId); this._dispatch( stateActions.setWebcamInProgress(true)); @@ -376,22 +376,20 @@ export default class RoomClient return Promise.resolve() .then(() => { - return this._updateWebcams(); + logger.debug('changeWebcam() | calling enumerateDevices()'); + + return navigator.mediaDevices.enumerateDevices(); }) - .then(() => + .then((devices) => { - const array = Array.from(this._webcams.keys()); - const len = array.length; - const deviceId = - this._webcam.device ? this._webcam.device.deviceId : undefined; - let idx = array.indexOf(deviceId); + for (const device of devices) + { + if (device.kind !== 'videoinput') + continue; - if (idx < len - 1) - idx++; - else - idx = 0; - - this._webcam.device = this._webcams.get(array[idx]); + if (device.deviceId == deviceId) + this._webcam.device = device; + } logger.debug( 'changeWebcam() | new selected webcam [device:%o]', @@ -1487,7 +1485,7 @@ export default class RoomClient logger.debug('_updateWebcams()'); // Reset the list. - this._webcams = new Map(); + this._webcams = {}; return Promise.resolve() .then(() => @@ -1503,25 +1501,33 @@ export default class RoomClient if (device.kind !== 'videoinput') continue; - this._webcams.set(device.deviceId, device); + this._webcams[device.deviceId] = { + value : device.deviceId, + label : device.label + }; } }) .then(() => { - const array = Array.from(this._webcams.values()); - const len = array.length; const currentWebcamId = this._webcam.device ? this._webcam.device.deviceId : undefined; - logger.debug('_updateWebcams() [webcams:%o]', array); + logger.debug('_updateWebcams() [webcams:%o]', this._webcams); + + const len = Object.keys(this._webcams).length; if (len === 0) this._webcam.device = null; - else if (!this._webcams.has(currentWebcamId)) - this._webcam.device = array[0]; + else if (!this._webcams[currentWebcamId]) + for (this._webcam.device in this._webcams) + if (this._webcams.hasOwnProperty(this._webcam.device)) + break; this._dispatch( - stateActions.setCanChangeWebcam(this._webcams.size >= 2)); + stateActions.setCanChangeWebcam(len >= 2)); + if (len >= 1) + this._dispatch( + stateActions.setWebcamDevices(this._webcams)); }); } diff --git a/app/lib/components/Me.jsx b/app/lib/components/Me.jsx index 36251c8..e8286b1 100644 --- a/app/lib/components/Me.jsx +++ b/app/lib/components/Me.jsx @@ -35,8 +35,7 @@ class Me extends React.Component onMuteMic, onUnmuteMic, onEnableWebcam, - onDisableWebcam, - onChangeWebcam + onDisableWebcam } = this.props; let micState; @@ -59,13 +58,6 @@ class Me extends React.Component else webcamState = 'off'; - let changeWebcamState; - - if (Boolean(webcamProducer) && me.canChangeWebcam) - changeWebcamState = 'on'; - else - changeWebcamState = 'unsupported'; - const videoVisible = ( Boolean(webcamProducer) && !webcamProducer.locallyPaused && @@ -104,13 +96,6 @@ class Me extends React.Component webcamState === 'on' ? onDisableWebcam() : onEnableWebcam(); }} /> - -
onChangeWebcam()} - />
:null } @@ -179,8 +164,7 @@ Me.propTypes = onMuteMic : PropTypes.func.isRequired, onUnmuteMic : PropTypes.func.isRequired, onEnableWebcam : PropTypes.func.isRequired, - onDisableWebcam : PropTypes.func.isRequired, - onChangeWebcam : PropTypes.func.isRequired + onDisableWebcam : PropTypes.func.isRequired }; const mapStateToProps = (state) => @@ -209,8 +193,7 @@ const mapDispatchToProps = (dispatch) => onMuteMic : () => dispatch(requestActions.muteMic()), onUnmuteMic : () => dispatch(requestActions.unmuteMic()), onEnableWebcam : () => dispatch(requestActions.enableWebcam()), - onDisableWebcam : () => dispatch(requestActions.disableWebcam()), - onChangeWebcam : () => dispatch(requestActions.changeWebcam()) + onDisableWebcam : () => dispatch(requestActions.disableWebcam()) }; }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 80f030b..89b5077 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -5,12 +5,14 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import ClipboardButton from 'react-clipboard.js'; import * as appPropTypes from './appPropTypes'; +import * as stateActions from '../redux/stateActions'; import * as requestActions from '../redux/requestActions'; import { Appear } from './transitions'; import Me from './Me'; import Peers from './Peers'; import Notifications from './Notifications'; import ChatWidget from './ChatWidget'; +import Settings from './Settings'; class Room extends React.Component { @@ -24,6 +26,7 @@ class Room extends React.Component onRoomLinkCopy, onSetAudioMode, onRestartIce, + onToggleSettings, onShareScreen, onUnShareScreen, onNeedExtension, @@ -158,6 +161,16 @@ class Room extends React.Component onClick={() => onRestartIce()} /> +
onToggleSettings()} + /> +
+ + @@ -238,6 +256,10 @@ const mapDispatchToProps = (dispatch) => { dispatch(requestActions.restartIce()); }, + onToggleSettings : () => + { + dispatch(stateActions.toggleSettings()); + }, onToggleHand : (enable) => { if (enable) diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx new file mode 100644 index 0000000..d8e4802 --- /dev/null +++ b/app/lib/components/Settings.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import * as appPropTypes from './appPropTypes'; +import * as requestActions from '../redux/requestActions'; +import PropTypes from 'prop-types'; +import Dropdown from 'react-dropdown'; + +class Settings extends React.Component +{ + constructor(props) + { + super(props); + } + + render() + { + const { + room, + me, + handleChangeWebcam, + onToggleSettings + } = this.props; + + if (!room.showSettings) + return null; + + const webcams = Object.values(me.webcamDevices); + + return ( +
+
+
+ Settings +
+
+ +
+
+ onToggleSettings()} + > + Close + +
+
+
+ ); + } +} + +Settings.propTypes = +{ + me : appPropTypes.Me.isRequired, + room : appPropTypes.Room.isRequired, + onToggleSettings : PropTypes.func.isRequired, + handleChangeWebcam : PropTypes.func.isRequired +}; + +const mapStateToProps = (state) => +{ + return { + me : state.me, + room : state.room + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + handleChangeWebcam : (device) => + { + dispatch(requestActions.changeWebcam(device.value)); + } + }; +}; + +const SettingsContainer = connect( + mapStateToProps, + mapDispatchToProps +)(Settings); + +export default SettingsContainer; diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index 8f15038..785acd2 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -9,6 +9,7 @@ const initialState = canShareScreen : false, needExtension : false, canChangeWebcam : false, + webcamDevices : null, webcamInProgress : false, screenShareInProgress : false, audioOnly : false, @@ -50,6 +51,13 @@ const me = (state = initialState, action) => return { ...state, canChangeWebcam }; } + case 'SET_WEBCAM_DEVICES': + { + const devices = action.payload; + + return { ...state, webcamDevices: devices }; + } + case 'SET_WEBCAM_IN_PROGRESS': { const { flag } = action.payload; diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index 622ba71..1122f82 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -4,7 +4,8 @@ const initialState = state : 'new', // new/connecting/connected/disconnected/closed, activeSpeakerName : null, peerHeight : 300, - peerWidth : 400 + peerWidth : 400, + showSettings : false }; const room = (state = initialState, action) => @@ -42,6 +43,13 @@ const room = (state = initialState, action) => return { ...state, peerWidth: peerWidth, peerHeight: peerHeight }; } + case 'TOGGLE_SETTINGS': + { + const showSettings = !state.showSettings; + + return { ...state, showSettings }; + } + default: return state; } diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index f8d2178..073464b 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -57,10 +57,11 @@ export const disableWebcam = () => }; }; -export const changeWebcam = () => +export const changeWebcam = (deviceId) => { return { - type : 'CHANGE_WEBCAM' + type : 'CHANGE_WEBCAM', + payload : { deviceId } }; }; diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 628cba3..09df186 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -83,7 +83,9 @@ export default ({ dispatch, getState }) => (next) => case 'CHANGE_WEBCAM': { - client.changeWebcam(); + const { deviceId } = action.payload; + + client.changeWebcam(deviceId); break; } diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 4c6b61d..1106dac 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -62,6 +62,14 @@ export const setCanChangeWebcam = (flag) => }; }; +export const setWebcamDevices = (devices) => +{ + return { + type : 'SET_WEBCAM_DEVICES', + payload : devices + }; +}; + export const setDisplayName = (displayName) => { return { @@ -110,6 +118,13 @@ export const setMyRaiseHandState = (flag) => }; }; +export const toggleSettings = () => +{ + return { + type : 'TOGGLE_SETTINGS' + }; +}; + export const setMyRaiseHandStateInProgress = (flag) => { return { diff --git a/app/package-lock.json b/app/package-lock.json index 1dfaf80..920397c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -591,6 +591,28 @@ } } }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, "babel-helper-builder-react-jsx": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", @@ -626,6 +648,29 @@ "lodash": "4.17.4" } }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", @@ -680,6 +725,19 @@ "lodash": "4.17.4" } }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, "babel-helper-replace-supers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", @@ -722,12 +780,72 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, + "babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", + "dev": true + }, "babel-plugin-syntax-flow": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", "dev": true }, + "babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", + "dev": true + }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -740,6 +858,80 @@ "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", "dev": true }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-generators": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "dev": true, + "requires": { + "babel-plugin-syntax-class-constructor-call": "6.18.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "dev": true, + "requires": { + "babel-helper-explode-class": "6.24.1", + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", + "dev": true, + "requires": { + "babel-plugin-syntax-do-expressions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -974,6 +1166,27 @@ "regexpu-core": "2.0.0" } }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "dev": true, + "requires": { + "babel-plugin-syntax-export-extensions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-flow-strip-types": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", @@ -984,6 +1197,16 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", + "dev": true, + "requires": { + "babel-plugin-syntax-function-bind": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-object-assign": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz", @@ -1126,6 +1349,53 @@ "babel-preset-flow": "6.23.0" } }, + "babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", + "dev": true, + "requires": { + "babel-plugin-transform-do-expressions": "6.22.0", + "babel-plugin-transform-function-bind": "6.22.0", + "babel-preset-stage-1": "6.24.1" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "dev": true, + "requires": { + "babel-plugin-transform-class-constructor-call": "6.24.1", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-preset-stage-2": "6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-decorators": "6.24.1", + "babel-preset-stage-3": "6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-generator-functions": "6.24.1", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.26.0" + } + }, "babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", @@ -3850,8 +4120,8 @@ "dev": true, "optional": true, "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "ansi-regex": { @@ -3924,7 +4194,7 @@ "bundled": true, "dev": true, "requires": { - "inherits": "2.0.3" + "inherits": "~2.0.0" } }, "boom": { @@ -3932,7 +4202,7 @@ "bundled": true, "dev": true, "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "brace-expansion": { @@ -3994,7 +4264,7 @@ "bundled": true, "dev": true, "requires": { - "boom": "2.10.1" + "boom": "2.x.x" } }, "dashdash": { @@ -4052,7 +4322,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "extend": { @@ -4132,7 +4402,7 @@ "dev": true, "optional": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" }, "dependencies": { "assert-plus": { @@ -4261,7 +4531,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "jsbn": { @@ -4282,7 +4552,7 @@ "dev": true, "optional": true, "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { @@ -8579,6 +8849,14 @@ "prop-types": "15.6.0" } }, + "react-dropdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-dropdown/-/react-dropdown-1.5.0.tgz", + "integrity": "sha512-rRv3a7NiP++yC1rzdjzkviC5ujq759i4SRa0M3C0Cr7loYT4Z3+JhSPekv1/04JiZNXX46cV3/g6A9kS7rkI4Q==", + "requires": { + "classnames": "2.2.5" + } + }, "react-redux": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", diff --git a/app/package.json b/app/package.json index 8c5a1c4..85f4bc4 100644 --- a/app/package.json +++ b/app/package.json @@ -21,6 +21,7 @@ "react": "^16.2.0", "react-clipboard.js": "^1.1.3", "react-dom": "^16.2.0", + "react-dropdown": "^1.5.0", "react-redux": "^5.0.6", "react-spinner": "^0.2.7", "react-tooltip": "^3.4.0", @@ -38,6 +39,7 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", "babelify": "^8.0.0", "browser-sync": "^2.23.6", "browserify": "^16.1.0", diff --git a/app/resources/images/icon_audio_only_white.svg b/app/resources/images/icon_audio_only_white.svg index 12a0389..fd5a889 100644 --- a/app/resources/images/icon_audio_only_white.svg +++ b/app/resources/images/icon_audio_only_white.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_restart_ice_white.svg b/app/resources/images/icon_restart_ice_white.svg index 2190d8c..6be67f1 100644 --- a/app/resources/images/icon_restart_ice_white.svg +++ b/app/resources/images/icon_restart_ice_white.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_settings_black.svg b/app/resources/images/icon_settings_black.svg new file mode 100644 index 0000000..9ca9954 --- /dev/null +++ b/app/resources/images/icon_settings_black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/resources/images/icon_settings_white.svg b/app/resources/images/icon_settings_white.svg new file mode 100644 index 0000000..250a969 --- /dev/null +++ b/app/resources/images/icon_settings_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 6685afd..cb51f02 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -232,6 +232,16 @@ } } + &.settings { + &.off { + background-image: url('/resources/images/icon_settings_white.svg'); + } + + &.on { + background-image: url('/resources/images/icon_settings_black.svg'); + } + } + &.screen { &.on { background-image: url('/resources/images/no-share-screen-black.svg'); diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl new file mode 100644 index 0000000..f1eab49 --- /dev/null +++ b/app/stylus/components/Settings.styl @@ -0,0 +1,149 @@ +[data-component='Settings'] { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 19999; + background-color: rgba(000, 000, 000, 0.5); + + > .dialog { + position: absolute; + height: 50vmin; + width: 60vmin; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: #fff; + border-radius: 4px; + box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); + padding: 1vmin; + + > .header { + > span { + font-size: 2vmin; + font-weight: 400; + } + } + + > .settings { + height: 100%; + width: 100%; + + .Dropdown-root { + position: relative; + } + + .Dropdown-control { + position: relative; + overflow: hidden; + background-color: white; + border: 1px solid #ccc; + border-radius: 2px; + box-sizing: border-box; + color: #333; + cursor: default; + outline: none; + padding: 8px 52px 8px 10px; + transition: all 200ms ease; + } + + .Dropdown-control:hover { + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); + } + + .Dropdown-arrow { + border-color: #999 transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + content: ' '; + display: block; + height: 0; + margin-top: -ceil(2.5); + position: absolute; + right: 10px; + top: 14px; + width: 0 + } + + .is-open .Dropdown-arrow { + border-color: transparent transparent #999; + border-width: 0 5px 5px; + } + + .Dropdown-menu { + background-color: white; + border: 1px solid #ccc; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); + box-sizing: border-box; + margin-top: -1px; + max-height: 200px; + overflow-y: auto; + position: absolute; + top: 100%; + width: 100%; + z-index: 1000; + -webkit-overflow-scrolling: touch; + } + + .Dropdown-menu .Dropdown-group > .Dropdown-title { + padding: 8px 10px; + color: rgba(51, 51, 51, 1.2); + font-weight: bold; + text-transform: capitalize; + } + + .Dropdown-option { + box-sizing: border-box; + color: rgba(51, 51, 51, 0.8); + cursor: pointer; + display: block; + padding: 8px 10px; + } + + .Dropdown-option:last-child { + border-bottom-right-radius: 2px; + border-bottom-left-radius: 2px; + } + + .Dropdown-option:hover { + background-color: #f2f9fc; + color: #333; + } + + .Dropdown-option.is-selected { + background-color: #f2f9fc; + color: #333; + } + + .Dropdown-noresults { + box-sizing: border-box; + color: #ccc; + cursor: default; + display: block; + padding: 8px 10px; + } + } + + > .footer { + position: absolute; + bottom: 0; + right: 0; + left: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + + > .button { + flex: 0 0 auto; + margin: 1vmin; + background-color: rgba(#000, 0.8); + color: #fff; + cursor: pointer; + border-radius: 4px; + padding: 0.5vmin; + } + } + } +} diff --git a/app/stylus/index.styl b/app/stylus/index.styl index f73898b..238808c 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -42,6 +42,7 @@ body { @import './components/PeerView'; @import './components/Notifications'; @import './components/Chat'; + @import './components/Settings'; } // Hack to detect in JS the current media query From fcb15e706ddc17e134a1d5337871b6b42a1e4bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 15 Jun 2018 23:19:26 +0200 Subject: [PATCH 15/43] Added support for setting audio input device, not working on linux at the moment. Updated webcam selection. --- app/lib/RoomClient.js | 156 +++++++++++++++++++++++--- app/lib/components/Me.jsx | 4 +- app/lib/components/Settings.jsx | 92 ++++++++++----- app/lib/redux/reducers/me.js | 26 ++++- app/lib/redux/requestActions.js | 8 ++ app/lib/redux/roomClientMiddleware.js | 9 ++ app/lib/redux/stateActions.js | 26 ++++- app/stylus/components/Settings.styl | 4 + 8 files changed, 278 insertions(+), 47 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 8c5d0fe..8e824ba 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -79,7 +79,9 @@ export default class RoomClient // Map of webcam MediaDeviceInfos indexed by deviceId. // @type {Map} - this._webcams = new Map(); + this._webcams = {}; + + this._audioDevices = {}; // Local Webcam. Object with: // - {MediaDeviceInfo} [device] @@ -89,6 +91,10 @@ export default class RoomClient resolution : 'hd' }; + this._audioDevice = { + device : null + }; + this._screenSharing = ScreenShare.create(); this._screenSharingProducer = null; @@ -366,6 +372,72 @@ export default class RoomClient }); } + changeAudioDevice(deviceId) + { + logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); + + this._dispatch( + stateActions.setAudioInProgress(true)); + + return Promise.resolve() + .then(() => + { + this._audioDevice.device = this._audioDevices[deviceId]; + + logger.debug( + 'changeAudioDevice() | new selected webcam [device:%o]', + this._audioDevice.device); + }) + .then(() => + { + const { device } = this._audioDevice; + + if (!device) + throw new Error('no audio devices'); + + logger.debug('changeAudioDevice() | calling getUserMedia()'); + + return navigator.mediaDevices.getUserMedia( + { + audio : + { + deviceId : { exact: device.deviceId } + } + }); + }) + .then((stream) => + { + const track = stream.getAudioTracks()[0]; + + return this._micProducer.replaceTrack(track) + .then((newTrack) => + { + track.stop(); + + return newTrack; + }); + }) + .then((newTrack) => + { + this._dispatch( + stateActions.setProducerTrack(this._micProducer.id, newTrack)); + + return this._updateAudioDevices(); + }) + .then(() => + { + this._dispatch( + stateActions.setAudioInProgress(false)); + }) + .catch((error) => + { + logger.error('changeAudioDevice() failed: %o', error); + + this._dispatch( + stateActions.setAudioInProgress(false)); + }); + } + changeWebcam(deviceId) { logger.debug('changeWebcam() [deviceId: %s]', deviceId); @@ -376,20 +448,7 @@ export default class RoomClient return Promise.resolve() .then(() => { - logger.debug('changeWebcam() | calling enumerateDevices()'); - - return navigator.mediaDevices.enumerateDevices(); - }) - .then((devices) => - { - for (const device of devices) - { - if (device.kind !== 'videoinput') - continue; - - if (device.deviceId == deviceId) - this._webcam.device = device; - } + this._webcam.device = this._webcams[deviceId]; logger.debug( 'changeWebcam() | new selected webcam [device:%o]', @@ -433,6 +492,10 @@ export default class RoomClient this._dispatch( stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); + return this._updateWebcams(); + }) + .then(() => + { this._dispatch( stateActions.setWebcamInProgress(false)); }) @@ -1161,6 +1224,12 @@ export default class RoomClient let producer; return Promise.resolve() + .then(() => + { + logger.debug('_setMicProducer() | calling _updateAudioDevices()'); + + return this._updateAudioDevices(); + }) .then(() => { logger.debug('_setMicProducer() | calling getUserMedia()'); @@ -1480,6 +1549,58 @@ export default class RoomClient }); } + _updateAudioDevices() + { + logger.debug('_updateAudioDevices()'); + + // Reset the list. + this._audioDevices = {}; + + return Promise.resolve() + .then(() => + { + logger.debug('_updateAudioDevices() | calling enumerateDevices()'); + + return navigator.mediaDevices.enumerateDevices(); + }) + .then((devices) => + { + for (const device of devices) + { + if (device.kind !== 'audioinput') + continue; + + this._audioDevices[device.deviceId] = { + value : device.deviceId, + label : device.label, + deviceId : device.deviceId + }; + } + }) + .then(() => + { + const currentAudioDeviceId = + this._audioDevice.device ? this._audioDevice.device.deviceId : undefined; + + logger.debug('_updateAudioDevices() [audiodevices:%o]', this._audioDevices); + + const len = Object.keys(this._audioDevices).length; + + if (len === 0) + this._audioDevice.device = null; + else if (!this._audioDevices[currentAudioDeviceId]) + for (this._audioDevice.device in this._audioDevices) + if (this._audioDevices.hasOwnProperty(this._audioDevice.device)) + break; + + this._dispatch( + stateActions.setCanChangeAudioDevice(len >= 2)); + if (len >= 1) + this._dispatch( + stateActions.setAudioDevices(this._audioDevices)); + }); + } + _updateWebcams() { logger.debug('_updateWebcams()'); @@ -1502,8 +1623,9 @@ export default class RoomClient continue; this._webcams[device.deviceId] = { - value : device.deviceId, - label : device.label + value : device.deviceId, + label : device.label, + deviceId : device.deviceId }; } }) diff --git a/app/lib/components/Me.jsx b/app/lib/components/Me.jsx index e8286b1..41b6231 100644 --- a/app/lib/components/Me.jsx +++ b/app/lib/components/Me.jsx @@ -80,7 +80,9 @@ class Me extends React.Component {connected ?
{ micState === 'on' ? onMuteMic() : onUnmuteMic(); diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index d8e4802..9e130a1 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; import PropTypes from 'prop-types'; +import { Appear } from './transitions'; import Dropdown from 'react-dropdown'; class Settings extends React.Component @@ -18,49 +19,82 @@ class Settings extends React.Component room, me, handleChangeWebcam, + handleChangeAudioDevice, onToggleSettings } = this.props; if (!room.showSettings) return null; - const webcams = Object.values(me.webcamDevices); + let webcams; + let webcamText; + + if (me.canChangeWebcam) + webcamText = 'Select camera'; + else + webcamText = 'Unable to select camera'; + + if (me.webcamDevices) + webcams = Object.values(me.webcamDevices); + else + webcams = []; + + let audioDevices; + let audioDevicesText; + + if (me.canChangeAudioDevice) + audioDevicesText = 'Select audio input device'; + else + audioDevicesText = 'Unable to select audio input device'; + + if (me.audioDevices) + audioDevices = Object.values(me.audioDevices); + else + audioDevices = []; return ( -
-
-
- Settings -
-
- -
-
- onToggleSettings()} - > - Close - + +
+
+
+ Settings +
+
+ + +
+
+ onToggleSettings()} + > + Close + +
-
+ ); } } Settings.propTypes = { - me : appPropTypes.Me.isRequired, - room : appPropTypes.Room.isRequired, - onToggleSettings : PropTypes.func.isRequired, - handleChangeWebcam : PropTypes.func.isRequired + me : appPropTypes.Me.isRequired, + room : appPropTypes.Room.isRequired, + onToggleSettings : PropTypes.func.isRequired, + handleChangeWebcam : PropTypes.func.isRequired, + handleChangeAudioDevice : PropTypes.func.isRequired }; const mapStateToProps = (state) => @@ -77,6 +111,10 @@ const mapDispatchToProps = (dispatch) => handleChangeWebcam : (device) => { dispatch(requestActions.changeWebcam(device.value)); + }, + handleChangeAudioDevice : (device) => + { + dispatch(requestActions.changeAudioDevice(device.value)); } }; }; diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index 785acd2..ca47a7e 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -8,9 +8,12 @@ const initialState = canSendWebcam : false, canShareScreen : false, needExtension : false, + canChangeAudioDevice : false, + audioDevices : null, canChangeWebcam : false, webcamDevices : null, webcamInProgress : false, + audioInProgress : false, screenShareInProgress : false, audioOnly : false, audioOnlyInProgress : false, @@ -44,6 +47,20 @@ const me = (state = initialState, action) => return { ...state, canShareScreen, needExtension }; } + case 'SET_CAN_CHANGE_AUDIO_DEVICE': + { + const canChangeAudioDevice = action.payload; + + return { ...state, canChangeAudioDevice }; + } + + case 'SET_AUDIO_DEVICES': + { + const { devices } = action.payload; + + return { ...state, audioDevices: devices }; + } + case 'SET_CAN_CHANGE_WEBCAM': { const canChangeWebcam = action.payload; @@ -53,11 +70,18 @@ const me = (state = initialState, action) => case 'SET_WEBCAM_DEVICES': { - const devices = action.payload; + const { devices } = action.payload; return { ...state, webcamDevices: devices }; } + case 'SET_AUDIO_IN_PROGRESS': + { + const { flag } = action.payload; + + return { ...state, audioInProgress: flag }; + } + case 'SET_WEBCAM_IN_PROGRESS': { const { flag } = action.payload; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index 073464b..59ffb7a 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -65,6 +65,14 @@ export const changeWebcam = (deviceId) => }; }; +export const changeAudioDevice = (deviceId) => +{ + return { + type : 'CHANGE_AUDIO_DEVICE', + payload : { deviceId } + }; +}; + export const enableAudioOnly = () => { return { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 09df186..60a8502 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -90,6 +90,15 @@ export default ({ dispatch, getState }) => (next) => break; } + case 'CHANGE_AUDIO_DEVICE': + { + const { deviceId } = action.payload; + + client.changeAudioDevice(deviceId); + + break; + } + case 'ENABLE_AUDIO_ONLY': { client.enableAudioOnly(); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 1106dac..72809f6 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -54,6 +54,22 @@ export const setScreenCapabilities = ({ canShareScreen, needExtension }) => }; }; +export const setCanChangeAudioDevice = (flag) => +{ + return { + type : 'SET_CAN_CHANGE_AUDIO_DEVICE', + payload : flag + }; +}; + +export const setAudioDevices = (devices) => +{ + return { + type : 'SET_AUDIO_DEVICES', + payload : { devices } + }; +}; + export const setCanChangeWebcam = (flag) => { return { @@ -66,7 +82,7 @@ export const setWebcamDevices = (devices) => { return { type : 'SET_WEBCAM_DEVICES', - payload : devices + payload : { devices } }; }; @@ -189,6 +205,14 @@ export const setProducerTrack = (producerId, track) => }; }; +export const setAudioInProgress = (flag) => +{ + return { + type : 'SET_AUDIO_IN_PROGRESS', + payload : { flag } + }; +}; + export const setWebcamInProgress = (flag) => { return { diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl index f1eab49..ef21430 100644 --- a/app/stylus/components/Settings.styl +++ b/app/stylus/components/Settings.styl @@ -7,6 +7,8 @@ z-index: 19999; background-color: rgba(000, 000, 000, 0.5); + AppearFadeIn(500ms); + > .dialog { position: absolute; height: 50vmin; @@ -29,6 +31,8 @@ > .settings { height: 100%; width: 100%; + padding-top: 1vmin; + padding-bottom: 1vmin; .Dropdown-root { position: relative; From 18c45ec9f2ed664122c4b8184d19bd075d676c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 16 Jun 2018 20:56:41 +0200 Subject: [PATCH 16/43] Updated styling of settings dialog. --- app/stylus/components/Settings.styl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl index ef21430..3e4e902 100644 --- a/app/stylus/components/Settings.styl +++ b/app/stylus/components/Settings.styl @@ -11,8 +11,7 @@ > .dialog { position: absolute; - height: 50vmin; - width: 60vmin; + width: 40vmin; left: 50%; top: 50%; transform: translate(-50%, -50%); @@ -29,13 +28,9 @@ } > .settings { - height: 100%; - width: 100%; - padding-top: 1vmin; - padding-bottom: 1vmin; - .Dropdown-root { position: relative; + padding: 0.3vmin; } .Dropdown-control { @@ -130,7 +125,6 @@ } > .footer { - position: absolute; bottom: 0; right: 0; left: 0; From 0696316bf1be7ac17858f538181b143ee78b6db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 16 Jun 2018 21:00:52 +0200 Subject: [PATCH 17/43] Moved some css to global. --- app/stylus/components/Room.styl | 95 +++++++++++++++++++++++++++++ app/stylus/components/Settings.styl | 94 ---------------------------- 2 files changed, 95 insertions(+), 94 deletions(-) diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index cb51f02..9186743 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -275,6 +275,101 @@ } } +.Dropdown-root { + position: relative; + padding: 0.3vmin; +} + +.Dropdown-control { + position: relative; + overflow: hidden; + background-color: white; + border: 1px solid #ccc; + border-radius: 2px; + box-sizing: border-box; + color: #333; + cursor: default; + outline: none; + padding: 8px 52px 8px 10px; + transition: all 200ms ease; +} + +.Dropdown-control:hover { + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); +} + +.Dropdown-arrow { + border-color: #999 transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + content: ' '; + display: block; + height: 0; + margin-top: -ceil(2.5); + position: absolute; + right: 10px; + top: 14px; + width: 0 +} + +.is-open .Dropdown-arrow { + border-color: transparent transparent #999; + border-width: 0 5px 5px; +} + +.Dropdown-menu { + background-color: white; + border: 1px solid #ccc; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); + box-sizing: border-box; + margin-top: -1px; + max-height: 200px; + overflow-y: auto; + position: absolute; + top: 100%; + width: 100%; + z-index: 1000; + -webkit-overflow-scrolling: touch; +} + +.Dropdown-menu .Dropdown-group > .Dropdown-title { + padding: 8px 10px; + color: rgba(51, 51, 51, 1.2); + font-weight: bold; + text-transform: capitalize; +} + +.Dropdown-option { + box-sizing: border-box; + color: rgba(51, 51, 51, 0.8); + cursor: pointer; + display: block; + padding: 8px 10px; +} + +.Dropdown-option:last-child { + border-bottom-right-radius: 2px; + border-bottom-left-radius: 2px; +} + +.Dropdown-option:hover { + background-color: #f2f9fc; + color: #333; +} + +.Dropdown-option.is-selected { + background-color: #f2f9fc; + color: #333; +} + +.Dropdown-noresults { + box-sizing: border-box; + color: #ccc; + cursor: default; + display: block; + padding: 8px 10px; +} + @keyframes Room-info-state-connecting { 50% { background-color: rgba(orange, 0.75); } } diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl index 3e4e902..a58d04a 100644 --- a/app/stylus/components/Settings.styl +++ b/app/stylus/components/Settings.styl @@ -28,100 +28,6 @@ } > .settings { - .Dropdown-root { - position: relative; - padding: 0.3vmin; - } - - .Dropdown-control { - position: relative; - overflow: hidden; - background-color: white; - border: 1px solid #ccc; - border-radius: 2px; - box-sizing: border-box; - color: #333; - cursor: default; - outline: none; - padding: 8px 52px 8px 10px; - transition: all 200ms ease; - } - - .Dropdown-control:hover { - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); - } - - .Dropdown-arrow { - border-color: #999 transparent transparent; - border-style: solid; - border-width: 5px 5px 0; - content: ' '; - display: block; - height: 0; - margin-top: -ceil(2.5); - position: absolute; - right: 10px; - top: 14px; - width: 0 - } - - .is-open .Dropdown-arrow { - border-color: transparent transparent #999; - border-width: 0 5px 5px; - } - - .Dropdown-menu { - background-color: white; - border: 1px solid #ccc; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); - box-sizing: border-box; - margin-top: -1px; - max-height: 200px; - overflow-y: auto; - position: absolute; - top: 100%; - width: 100%; - z-index: 1000; - -webkit-overflow-scrolling: touch; - } - - .Dropdown-menu .Dropdown-group > .Dropdown-title { - padding: 8px 10px; - color: rgba(51, 51, 51, 1.2); - font-weight: bold; - text-transform: capitalize; - } - - .Dropdown-option { - box-sizing: border-box; - color: rgba(51, 51, 51, 0.8); - cursor: pointer; - display: block; - padding: 8px 10px; - } - - .Dropdown-option:last-child { - border-bottom-right-radius: 2px; - border-bottom-left-radius: 2px; - } - - .Dropdown-option:hover { - background-color: #f2f9fc; - color: #333; - } - - .Dropdown-option.is-selected { - background-color: #f2f9fc; - color: #333; - } - - .Dropdown-noresults { - box-sizing: border-box; - color: #ccc; - cursor: default; - display: block; - padding: 8px 10px; - } } > .footer { From ac922092ea0939227fc29f5c677fa77fd869fcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 16 Jun 2018 22:09:03 +0200 Subject: [PATCH 18/43] Changed from using state.peers as array. --- app/lib/components/Peers.jsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index 39dd406..d13745a 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -86,16 +86,16 @@ class Peers extends React.Component return (
{ - peers.map((peer) => + Object.keys(peers).map(function(key) { return ( - +
- +
); @@ -108,7 +108,7 @@ class Peers extends React.Component Peers.propTypes = { - peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, + peers : PropTypes.object.isRequired, activeSpeakerName : PropTypes.string, peerHeight : PropTypes.number, peerWidth : PropTypes.number, @@ -127,12 +127,8 @@ const mapDispatchToProps = (dispatch) => const mapStateToProps = (state) => { - // TODO: This is not OK since it's creating a new array every time, so triggering a - // component rendering. - const peersArray = Object.values(state.peers); - return { - peers : peersArray, + peers : state.peers, activeSpeakerName : state.room.activeSpeakerName, peerHeight : state.room.peerHeight, peerWidth : state.room.peerWidth From bca6db0e5ef753b591b2c49e7b3e6e766cc8b931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sun, 17 Jun 2018 22:52:05 +0200 Subject: [PATCH 19/43] Testing some resolution changes. --- app/lib/RoomClient.js | 58 +++++++++++++++++++++++++++-------- app/lib/components/Peers.jsx | 59 +++++++++++++++--------------------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 8e824ba..3797b49 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -18,13 +18,6 @@ const ROOM_OPTIONS = } }; -const VIDEO_CONSTRAINS = -{ - qvga : { width: { ideal: 320 }, height: { ideal: 240 }, aspectRatio: 1.334 }, - vga : { width: { ideal: 640 }, height: { ideal: 480 }, aspectRatio: 1.334 }, - hd : { width: { ideal: 800 }, height: { ideal: 600 }, aspectRatio: 1.334 } -}; - export default class RoomClient { constructor( @@ -459,7 +452,7 @@ export default class RoomClient }) .then(() => { - const { device, resolution } = this._webcam; + const { device } = this._webcam; if (!device) throw new Error('no webcam devices'); @@ -471,7 +464,20 @@ export default class RoomClient video : { deviceId : { exact: device.deviceId }, - ...VIDEO_CONSTRAINS[resolution] + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] } }); }) @@ -540,7 +546,7 @@ export default class RoomClient }) .then(() => { - const { device, resolution } = this._webcam; + const { device } = this._webcam; logger.debug('changeWebcamResolution() | calling getUserMedia()'); @@ -549,7 +555,20 @@ export default class RoomClient video : { deviceId : { exact: device.deviceId }, - ...VIDEO_CONSTRAINS[resolution] + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] } }); }) @@ -1447,7 +1466,7 @@ export default class RoomClient return Promise.resolve() .then(() => { - const { device, resolution } = this._webcam; + const { device } = this._webcam; if (!device) throw new Error('no webcam devices'); @@ -1459,7 +1478,20 @@ export default class RoomClient video : { deviceId : { exact: device.deviceId }, - ...VIDEO_CONSTRAINS[resolution] + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] } }); }) diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index d13745a..229991e 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import * as appPropTypes from './appPropTypes'; -import * as stateActions from '../redux/stateActions'; import { Appear } from './transitions'; import Peer from './Peer'; @@ -13,12 +12,17 @@ class Peers extends React.Component { super(); this.state = { - ratio : 1.334 + peerWidth : 400, + peerHeight : 300, + ratio : 1.334 }; } updateDimensions(nextProps = null) { + if (!nextProps.peers && !this.props.peers) + return; + const n = nextProps ? nextProps.peers.length : this.props.peers.length; if (n == 0) @@ -47,9 +51,12 @@ class Peers extends React.Component break; } } - if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x)) + if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x)) { - this.props.onComponentResize(0.9 * x, 0.9 * y); + this.setState({ + peerWidth : 0.9 * x, + peerHeight : 0.9 * y + }); } } @@ -72,30 +79,28 @@ class Peers extends React.Component { const { activeSpeakerName, - peers, - peerWidth, - peerHeight + peers } = this.props; const style = { - 'width' : peerWidth, - 'height' : peerHeight + 'width' : this.state.peerWidth, + 'height' : this.state.peerHeight }; return (
{ - Object.keys(peers).map(function(key) + peers.map((peer) => { return ( - +
- +
); @@ -108,36 +113,22 @@ class Peers extends React.Component Peers.propTypes = { - peers : PropTypes.object.isRequired, - activeSpeakerName : PropTypes.string, - peerHeight : PropTypes.number, - peerWidth : PropTypes.number, - onComponentResize : PropTypes.func.isRequired -}; - -const mapDispatchToProps = (dispatch) => -{ - return { - onComponentResize : (peerWidth, peerHeight) => - { - dispatch(stateActions.onComponentResize(peerWidth, peerHeight)); - } - }; + peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, + activeSpeakerName : PropTypes.string }; const mapStateToProps = (state) => { + const peersArray = Object.values(state.peers); + return { - peers : state.peers, - activeSpeakerName : state.room.activeSpeakerName, - peerHeight : state.room.peerHeight, - peerWidth : state.room.peerWidth + peers : peersArray, + activeSpeakerName : state.room.activeSpeakerName }; }; const PeersContainer = connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps )(Peers); export default PeersContainer; From 7857ae27ddbd965b433ce0e6dbfa2722152d9d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sun, 17 Jun 2018 23:14:42 +0200 Subject: [PATCH 20/43] Updated resolution handling. --- app/lib/RoomClient.js | 153 +++++++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 3797b49..108f193 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -48,6 +48,9 @@ export default class RoomClient // Redux store getState function. this._getState = getState; + // This device + this._device = device; + // My peer name. this._peerName = peerName; @@ -459,27 +462,37 @@ export default class RoomClient logger.debug('changeWebcam() | calling getUserMedia()'); - return navigator.mediaDevices.getUserMedia( - { - video : + if (this._device.flag !== 'chrome') + return navigator.mediaDevices.getUserMedia( { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + video : + { + deviceId : { exact: device.deviceId }, + width : { ideal: 3840 } + } + }); + else + return navigator.mediaDevices.getUserMedia( + { + video : + { + deviceId : { exact: device.deviceId }, + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] + } + }); }) .then((stream) => { @@ -550,27 +563,37 @@ export default class RoomClient logger.debug('changeWebcamResolution() | calling getUserMedia()'); - return navigator.mediaDevices.getUserMedia( - { - video : + if (this._device.flag !== 'chrome') + return navigator.mediaDevices.getUserMedia( { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + video : + { + deviceId : { exact: device.deviceId }, + width : { ideal: 3840 } + } + }); + else + return navigator.mediaDevices.getUserMedia( + { + video : + { + deviceId : { exact: device.deviceId }, + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] + } + }); }) .then((stream) => { @@ -1473,27 +1496,37 @@ export default class RoomClient logger.debug('_setWebcamProducer() | calling getUserMedia()'); - return navigator.mediaDevices.getUserMedia( - { - video : + if (this._device.flag !== 'chrome') + return navigator.mediaDevices.getUserMedia( { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + video : + { + deviceId : { exact: device.deviceId }, + width : { ideal: 3840 } + } + }); + else + return navigator.mediaDevices.getUserMedia( + { + video : + { + deviceId : { exact: device.deviceId }, + optional : [ + { minWidth: 160 }, + { minWidth: 176 }, + { minWidth: 320 }, + { minWidth: 352 }, + { minWidth: 640 }, + { minWidth: 800 }, + { minWidth: 1024 }, + { minWidth: 1280 }, + { minWidth: 1600 }, + { minWidth: 1920 }, + { minWidth: 2560 }, + { minWidth: 3840 } + ] + } + }); }) .then((stream) => { From 24c5929e5baf8c8930f17f20ba463c21ac879f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 10:18:31 +0200 Subject: [PATCH 21/43] More resolution tests. --- app/lib/RoomClient.js | 116 ++++++++++-------------------------------- app/stylus/index.styl | 4 +- 2 files changed, 28 insertions(+), 92 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 108f193..a791893 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -18,6 +18,11 @@ const ROOM_OPTIONS = } }; +const VIDEO_CONSTRAINS = +{ + width : { ideal: 3840 } +}; + export default class RoomClient { constructor( @@ -462,37 +467,14 @@ export default class RoomClient logger.debug('changeWebcam() | calling getUserMedia()'); - if (this._device.flag !== 'chrome') - return navigator.mediaDevices.getUserMedia( + return navigator.mediaDevices.getUserMedia( + { + video : { - video : - { - deviceId : { exact: device.deviceId }, - width : { ideal: 3840 } - } - }); - else - return navigator.mediaDevices.getUserMedia( - { - video : - { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + deviceId : { exact: device.deviceId }, + ...VIDEO_CONSTRAINS + } + }); }) .then((stream) => { @@ -563,37 +545,14 @@ export default class RoomClient logger.debug('changeWebcamResolution() | calling getUserMedia()'); - if (this._device.flag !== 'chrome') - return navigator.mediaDevices.getUserMedia( + return navigator.mediaDevices.getUserMedia( + { + video : { - video : - { - deviceId : { exact: device.deviceId }, - width : { ideal: 3840 } - } - }); - else - return navigator.mediaDevices.getUserMedia( - { - video : - { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + deviceId : { exact: device.deviceId }, + ...VIDEO_CONSTRAINS + } + }); }) .then((stream) => { @@ -1496,37 +1455,14 @@ export default class RoomClient logger.debug('_setWebcamProducer() | calling getUserMedia()'); - if (this._device.flag !== 'chrome') - return navigator.mediaDevices.getUserMedia( + return navigator.mediaDevices.getUserMedia( + { + video : { - video : - { - deviceId : { exact: device.deviceId }, - width : { ideal: 3840 } - } - }); - else - return navigator.mediaDevices.getUserMedia( - { - video : - { - deviceId : { exact: device.deviceId }, - optional : [ - { minWidth: 160 }, - { minWidth: 176 }, - { minWidth: 320 }, - { minWidth: 352 }, - { minWidth: 640 }, - { minWidth: 800 }, - { minWidth: 1024 }, - { minWidth: 1280 }, - { minWidth: 1600 }, - { minWidth: 1920 }, - { minWidth: 2560 }, - { minWidth: 3840 } - ] - } - }); + deviceId : { exact: device.deviceId }, + ...VIDEO_CONSTRAINS + } + }); }) .then((stream) => { diff --git a/app/stylus/index.styl b/app/stylus/index.styl index 238808c..9f9fec5 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -47,9 +47,9 @@ body { // Hack to detect in JS the current media query #multiparty-meeting-media-query-detector { - position: relative; + position: absolute; z-index: -1000; - bottom: 1px; + bottom: 0; left: 0; height: 1px; width: 1px; From cac2caa5c0ea9ff4e13857bdc8e37212aebfd989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 11:06:39 +0200 Subject: [PATCH 22/43] Some small fixes to resolution and scaling. --- app/lib/RoomClient.js | 3 ++- app/lib/components/Peers.jsx | 17 ++++++++++------- app/stylus/components/PeerView.styl | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index a791893..14a899c 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -20,7 +20,8 @@ const ROOM_OPTIONS = const VIDEO_CONSTRAINS = { - width : { ideal: 3840 } + width : { ideal: 1280 }, + aspectRatio : 1.334 }; export default class RoomClient diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index 229991e..33d74dc 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -18,12 +18,14 @@ class Peers extends React.Component }; } - updateDimensions(nextProps = null) + resizeUpdate() { - if (!nextProps.peers && !this.props.peers) - return; + this.updateDimensions(); + } - const n = nextProps ? nextProps.peers.length : this.props.peers.length; + updateDimensions(props = this.props) + { + const n = props.peers ? props.peers.length : 0; if (n == 0) { @@ -62,17 +64,18 @@ class Peers extends React.Component componentDidMount() { - window.addEventListener('resize', this.updateDimensions.bind(this)); + window.addEventListener('resize', this.resizeUpdate.bind(this)); } componentWillUnmount() { - window.removeEventListener('resize', this.updateDimensions.bind(this)); + window.removeEventListener('resize', this.resizeUpdate.bind(this)); } componentWillReceiveProps(nextProps) { - this.updateDimensions(nextProps); + if (nextProps.peers) + this.updateDimensions(nextProps); } render() diff --git a/app/stylus/components/PeerView.styl b/app/stylus/components/PeerView.styl index f5dbdc0..1230284 100644 --- a/app/stylus/components/PeerView.styl +++ b/app/stylus/components/PeerView.styl @@ -170,7 +170,7 @@ flex: 100 100 auto; height: 100%; width: 100%; - object-fit: contain; + object-fit: cover; user-select: none; transition-property: opacity; transition-duration: .15s; From 6d46a777c08e46ebcec34e63b2a13e6d85328aff Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Mon, 18 Jun 2018 12:08:18 +0200 Subject: [PATCH 23/43] fix centered settings-icon --- app/resources/images/icon_settings_black.svg | 2 +- app/resources/images/icon_settings_white.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/resources/images/icon_settings_black.svg b/app/resources/images/icon_settings_black.svg index 9ca9954..131f14f 100644 --- a/app/resources/images/icon_settings_black.svg +++ b/app/resources/images/icon_settings_black.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_settings_white.svg b/app/resources/images/icon_settings_white.svg index 250a969..0c17069 100644 --- a/app/resources/images/icon_settings_white.svg +++ b/app/resources/images/icon_settings_white.svg @@ -1,3 +1,3 @@ - + From cee74f9b29f73095f912320dc93b3364e68cf901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 13:33:56 +0200 Subject: [PATCH 24/43] Added login button. --- app/lib/RoomClient.js | 4 ++ app/lib/components/Room.jsx | 51 +++++++---------------- app/lib/redux/reducers/me.js | 8 ++++ app/lib/redux/requestActions.js | 7 ++++ app/lib/redux/roomClientMiddleware.js | 7 ++++ app/lib/redux/stateActions.js | 8 ++++ app/resources/images/icon_login_black.svg | 3 ++ app/resources/images/icon_login_white.svg | 3 ++ app/stylus/components/Room.styl | 16 ++----- 9 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 app/resources/images/icon_login_black.svg create mode 100644 app/resources/images/icon_login_white.svg diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 14a899c..8a474cc 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -125,6 +125,8 @@ export default class RoomClient login() { + this._dispatch(stateActions.setLoginInProgress(true)); + const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; this._loginWindow = window.open(url, 'loginWindow'); @@ -998,6 +1000,8 @@ export default class RoomClient )); } this.closeLoginWindow(); + + this._dispatch(stateActions.setLoginInProgress(false)); break; } diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 89b5077..f6f8acf 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -24,9 +24,8 @@ class Room extends React.Component amActiveSpeaker, screenProducer, onRoomLinkCopy, - onSetAudioMode, - onRestartIce, onToggleSettings, + onLogin, onShareScreen, onUnShareScreen, onNeedExtension, @@ -142,25 +141,6 @@ class Room extends React.Component }} /> -
onSetAudioMode(!me.audioOnly)} - /> - -
onRestartIce()} - /> -
onToggleSettings()} /> +
onLogin()} + /> +
@@ -245,17 +233,6 @@ const mapDispatchToProps = (dispatch) => text : 'Room link copied to the clipboard' })); }, - onSetAudioMode : (enable) => - { - if (enable) - dispatch(requestActions.enableAudioOnly()); - else - dispatch(requestActions.disableAudioOnly()); - }, - onRestartIce : () => - { - dispatch(requestActions.restartIce()); - }, onToggleSettings : () => { dispatch(stateActions.toggleSettings()); @@ -282,6 +259,10 @@ const mapDispatchToProps = (dispatch) => onNeedExtension : () => { dispatch(requestActions.installExtension()); + }, + onLogin : () => + { + dispatch(requestActions.userLogin()); } }; }; diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index ca47a7e..752ef6d 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -15,6 +15,7 @@ const initialState = webcamInProgress : false, audioInProgress : false, screenShareInProgress : false, + loginInProgress : false, audioOnly : false, audioOnlyInProgress : false, raiseHand : false, @@ -96,6 +97,13 @@ const me = (state = initialState, action) => return { ...state, screenShareInProgress: flag }; } + case 'SET_LOGIN_IN_PROGRESS': + { + const { flag } = action.payload; + + return { ...state, loginInProgress: flag }; + } + case 'SET_DISPLAY_NAME': { let { displayName } = action.payload; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index 59ffb7a..ff865fa 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -119,6 +119,13 @@ export const resumePeerVideo = (peerName) => }; }; +export const userLogin = () => +{ + return { + type : 'USER_LOGIN' + }; +}; + export const raiseHand = () => { return { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 60a8502..64a1059 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -156,6 +156,13 @@ export default ({ dispatch, getState }) => (next) => break; } + case 'USER_LOGIN': + { + client.login(); + + break; + } + case 'LOWER_HAND': { client.sendRaiseHandState(false); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 72809f6..d6bf721 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -134,6 +134,14 @@ export const setMyRaiseHandState = (flag) => }; }; +export const setLoginInProgress = (flag) => +{ + return { + type : 'SET_LOGIN_IN_PROGRESS', + payload : { flag } + }; +}; + export const toggleSettings = () => { return { diff --git a/app/resources/images/icon_login_black.svg b/app/resources/images/icon_login_black.svg new file mode 100644 index 0000000..6f366fe --- /dev/null +++ b/app/resources/images/icon_login_black.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/resources/images/icon_login_white.svg b/app/resources/images/icon_login_white.svg new file mode 100644 index 0000000..c7c190e --- /dev/null +++ b/app/resources/images/icon_login_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 9186743..ec3e43c 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -216,19 +216,9 @@ opacity: 0.5; } - &.audio-only { - background-image: url('/resources/images/icon_audio_only_white.svg'); - - &.on { - background-image: url('/resources/images/icon_audio_only_black.svg'); - } - } - - &.restart-ice { - background-image: url('/resources/images/icon_restart_ice_white.svg'); - - &.on { - background-image: url('/resources/images/icon_restart_ice__black.svg'); + &.login { + &.off { + background-image: url('/resources/images/icon_login_white.svg'); } } From 894009aa6a9dead3092e34e5f92bcdf11262a55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 14:28:52 +0200 Subject: [PATCH 25/43] Added toggle for advanced mode. Show more info and diagnostics. --- app/lib/components/Me.jsx | 3 + app/lib/components/Peer.jsx | 3 + app/lib/components/PeerView.jsx | 92 ++++++++++++++++------------- app/lib/components/Peers.jsx | 11 +++- app/lib/components/Room.jsx | 8 ++- app/lib/components/Settings.jsx | 17 +++++- app/lib/redux/reducers/room.js | 19 +++--- app/lib/redux/stateActions.js | 15 +++-- app/stylus/components/PeerView.styl | 16 +++-- 9 files changed, 111 insertions(+), 73 deletions(-) diff --git a/app/lib/components/Me.jsx b/app/lib/components/Me.jsx index 41b6231..66226d1 100644 --- a/app/lib/components/Me.jsx +++ b/app/lib/components/Me.jsx @@ -29,6 +29,7 @@ class Me extends React.Component const { connected, me, + advancedMode, micProducer, webcamProducer, onChangeDisplayName, @@ -104,6 +105,7 @@ class Me extends React.Component { const { + advancedMode, peer, micConsumer, webcamConsumer, @@ -86,6 +87,7 @@ const Peer = (props) => } Peer.propTypes = { + advancedMode : PropTypes.bool, peer : appPropTypes.Peer.isRequired, micConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer, diff --git a/app/lib/components/PeerView.jsx b/app/lib/components/PeerView.jsx index cda3d47..8a6ef57 100644 --- a/app/lib/components/PeerView.jsx +++ b/app/lib/components/PeerView.jsx @@ -45,6 +45,7 @@ export default class PeerView extends React.Component { const { isMe, + advancedMode, peer, videoVisible, videoProfile, @@ -67,42 +68,45 @@ export default class PeerView extends React.Component return (
-
- {screenVisible ? -
- {audioCodec ? -

{audioCodec}

- :null - } + {advancedMode ? +
+ {screenVisible ? +
+ {audioCodec ? +

{audioCodec}

+ :null + } - {screenCodec ? -

{screenCodec} {screenProfile}

- :null - } + {screenCodec ? +

{screenCodec} {screenProfile}

+ :null + } - {(screenVisible && screenWidth !== null) ? -

{screenWidth}x{screenHeight}

- :null - } -
- :
- {audioCodec ? -

{audioCodec}

- :null - } + {(screenVisible && screenWidth !== null) ? +

{screenWidth}x{screenHeight}

+ :null + } +
+ :
+ {audioCodec ? +

{audioCodec}

+ :null + } - {videoCodec ? -

{videoCodec} {videoProfile}

- :null - } + {videoCodec ? +

{videoCodec} {videoProfile}

+ :null + } - {(videoVisible && videoWidth !== null) ? -

{videoWidth}x{videoHeight}

- :null - } -
- } -
+ {(videoVisible && videoWidth !== null) ? +

{videoWidth}x{videoHeight}

+ :null + } +
+ } +
+ :null + }
{isMe ? @@ -126,14 +130,17 @@ export default class PeerView extends React.Component } -
- - - {peer.device.name} {Math.floor(peer.device.version) || null} - -
+ {advancedMode ? +
+ + + {peer.device.name} {Math.floor(peer.device.version) || null} + +
+ :null + }
@@ -307,8 +314,9 @@ export default class PeerView extends React.Component PeerView.propTypes = { - isMe : PropTypes.bool, - peer : PropTypes.oneOfType( + isMe : PropTypes.bool, + advancedMode : PropTypes.bool, + peer : PropTypes.oneOfType( [ appPropTypes.Me, appPropTypes.Peer ]).isRequired, audioTrack : PropTypes.any, videoTrack : PropTypes.any, diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index 33d74dc..ae01ed1 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -8,9 +8,9 @@ import Peer from './Peer'; class Peers extends React.Component { - constructor() + constructor(props) { - super(); + super(props); this.state = { peerWidth : 400, peerHeight : 300, @@ -81,6 +81,7 @@ class Peers extends React.Component render() { const { + advancedMode, activeSpeakerName, peers } = this.props; @@ -103,7 +104,10 @@ class Peers extends React.Component 'active-speaker' : peer.name === activeSpeakerName })} style={style} > - +
); @@ -116,6 +120,7 @@ class Peers extends React.Component Peers.propTypes = { + advancedMode : PropTypes.bool, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, activeSpeakerName : PropTypes.string }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index f6f8acf..4675277 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -99,14 +99,18 @@ class Room extends React.Component
- +
- +
diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index 9e130a1..c41d65c 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; +import * as stateActions from '../redux/stateActions'; import PropTypes from 'prop-types'; import { Appear } from './transitions'; import Dropdown from 'react-dropdown'; @@ -20,7 +21,8 @@ class Settings extends React.Component me, handleChangeWebcam, handleChangeAudioDevice, - onToggleSettings + onToggleSettings, + onToggleAdvancedMode } = this.props; if (!room.showSettings) @@ -72,6 +74,12 @@ class Settings extends React.Component onChange={handleChangeAudioDevice} placeholder={audioDevicesText} /> + + Advanced mode
@@ -115,6 +124,10 @@ const mapDispatchToProps = (dispatch) => handleChangeAudioDevice : (device) => { dispatch(requestActions.changeAudioDevice(device.value)); + }, + onToggleAdvancedMode : () => + { + dispatch(stateActions.toggleAdvancedMode()); } }; }; diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index 1122f82..0ae277b 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -3,9 +3,8 @@ const initialState = url : null, state : 'new', // new/connecting/connected/disconnected/closed, activeSpeakerName : null, - peerHeight : 300, - peerWidth : 400, - showSettings : false + showSettings : false, + advancedMode : false }; const room = (state = initialState, action) => @@ -36,13 +35,6 @@ const room = (state = initialState, action) => return { ...state, activeSpeakerName: peerName }; } - case 'SET_COMPONENT_SIZE': - { - const { peerWidth, peerHeight } = action.payload; - - return { ...state, peerWidth: peerWidth, peerHeight: peerHeight }; - } - case 'TOGGLE_SETTINGS': { const showSettings = !state.showSettings; @@ -50,6 +42,13 @@ const room = (state = initialState, action) => return { ...state, showSettings }; } + case 'TOGGLE_ADVANCED_MODE': + { + const advancedMode = !state.advancedMode; + + return { ...state, advancedMode }; + } + default: return state; } diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index d6bf721..4fb47fe 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -22,14 +22,6 @@ export const setRoomActiveSpeaker = (peerName) => }; }; -export const onComponentResize = (peerWidth, peerHeight) => -{ - return { - type : 'SET_COMPONENT_SIZE', - payload : { peerWidth, peerHeight } - }; -}; - export const setMe = ({ peerName, displayName, displayNameSet, device }) => { return { @@ -94,6 +86,13 @@ export const setDisplayName = (displayName) => }; }; +export const toggleAdvancedMode = () => +{ + return { + type : 'TOGGLE_ADVANCED_MODE' + }; +}; + export const setAudioOnlyState = (enabled) => { return { diff --git a/app/stylus/components/PeerView.styl b/app/stylus/components/PeerView.styl index 1230284..1d8f964 100644 --- a/app/stylus/components/PeerView.styl +++ b/app/stylus/components/PeerView.styl @@ -17,9 +17,9 @@ position: absolute; z-index: 5 - top: 0; + top: 0.6vmin; + left: 0.6vmin; bottom: 0; - left: 0; right: 0; display: flex; flex-direction: column; @@ -31,8 +31,7 @@ flex-direction: row; > .box { - margin: 4px; - padding: 2px 4px; + padding: 0.4vmin; border-radius: 2px; background-color: rgba(#000, 0.25); @@ -55,15 +54,20 @@ display: flex; flex-direction: column; justify-content: flex-end; + position: absolute; + bottom: 0.6vmin; + left: 0; + border-radius: 2px; + background-color: rgba(#000, 0.25); +desktop() { &.is-me { - padding: 10px; + padding: 1vmin; align-items: flex-start; } &:not(.is-me) { - padding: 20px; + padding: 1vmin; align-items: flex-start; } } From 45cf120c1dce8c6fa9a5f80b695be27d26ee75f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 14:49:21 +0200 Subject: [PATCH 26/43] Small changes to css. --- app/stylus/components/PeerView.styl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/stylus/components/PeerView.styl b/app/stylus/components/PeerView.styl index 1d8f964..75c709f 100644 --- a/app/stylus/components/PeerView.styl +++ b/app/stylus/components/PeerView.styl @@ -74,12 +74,12 @@ +mobile() { &.is-me { - padding: 10px; + padding: 1vmin; align-items: flex-start; } &:not(.is-me) { - padding: 10px; + padding: 1vmin; align-items: flex-end; } } @@ -118,15 +118,15 @@ } > .row { - margin-top: 4px; + margin-top: 0.4vmin; display: flex; flex-direction: row; justify-content: flex-start; align-items: flex-end; > .device-icon { - height: 18px; - width: 18px; + height: 12px; + width: 12px; margin-right: 3px; user-select: none; pointer-events: none; From fd0083506ce0d58f8df8d1ff89fa23de43654988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 14:53:49 +0200 Subject: [PATCH 27/43] Changed remote media icons. --- app/stylus/components/Peer.styl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index daa2baa..6b443d3 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -60,7 +60,7 @@ } &.off { - background-image: url('/resources/images/icon_mic_white_off.svg'); + background-image: url('/resources/images/icon_remote_mic_white_off.svg'); background-color: rgba(#d42241, 0.7); } @@ -75,7 +75,8 @@ } &.off { - background-image: url('/resources/images/icon_webcam_white_on.svg'); + background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); + background-color: rgba(#d42241, 0.7); } &.unsupported { From 11bd9d3b52b467ad649bfec6b334e809b4560f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 15:05:40 +0200 Subject: [PATCH 28/43] Fixed some CSS. --- app/stylus/components/Me.styl | 25 ++++++++----------------- app/stylus/components/Peer.styl | 3 ++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/stylus/components/Me.styl b/app/stylus/components/Me.styl index 18f95d3..aefbd0d 100644 --- a/app/stylus/components/Me.styl +++ b/app/stylus/components/Me.styl @@ -13,11 +13,11 @@ flex-direction:; row; justify-content: flex-end; align-items: center; + padding: 0.4vmin; > .button { flex: 0 0 auto; - margin: 4px; - margin-left: 0; + margin: 0.2vmin; border-radius: 2px; background-position: center; background-size: 75%; @@ -28,8 +28,8 @@ transition-duration: 0.15s; +desktop() { - width: 28px; - height: 28px; + width: 24px; + height: 24px; opacity: 0.85; &:hover { @@ -38,8 +38,8 @@ } +mobile() { - width: 26px; - height: 26px; + width: 22px; + height: 22px; } &.unsupported { @@ -76,23 +76,14 @@ } &.off { - background-image: url('/resources/images/icon_webcam_white_on.svg'); + background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); + background-color: rgba(#d42241, 0.7); } &.unsupported { background-image: url('/resources/images/icon_webcam_white_unsupported.svg'); } } - - &.change-webcam { - &.on { - background-image: url('/resources/images/icon_change_webcam_black.svg'); - } - - &.unsupported { - background-image: url('/resources/images/icon_change_webcam_white_unsupported.svg'); - } - } } } } diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 6b443d3..4dc3036 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -13,10 +13,11 @@ flex-direction:; row; justify-content: flex-start; align-items: center; + padding: 0.4vmin; > .button { flex: 0 0 auto; - margin: 4px; + margin: 0.2vmin; border-radius: 2px; background-position: center; background-size: 75%; From d2496eefe59b4aeff337a61f4b6f47b40ad83f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 18 Jun 2018 15:11:02 +0200 Subject: [PATCH 29/43] Fixed icons. --- app/resources/images/icon_mic_white_off.svg | 2 +- app/resources/images/icon_remote_mic_white_off.svg | 2 +- app/resources/images/icon_remote_webcam_white_off.svg | 2 +- app/resources/images/icon_webcam_white_on.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/resources/images/icon_mic_white_off.svg b/app/resources/images/icon_mic_white_off.svg index 47cc5da..e5dc60c 100644 --- a/app/resources/images/icon_mic_white_off.svg +++ b/app/resources/images/icon_mic_white_off.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_remote_mic_white_off.svg b/app/resources/images/icon_remote_mic_white_off.svg index 2a4cbc0..e5dc60c 100644 --- a/app/resources/images/icon_remote_mic_white_off.svg +++ b/app/resources/images/icon_remote_mic_white_off.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_remote_webcam_white_off.svg b/app/resources/images/icon_remote_webcam_white_off.svg index a2379b3..1b7935e 100644 --- a/app/resources/images/icon_remote_webcam_white_off.svg +++ b/app/resources/images/icon_remote_webcam_white_off.svg @@ -1,4 +1,4 @@ - + diff --git a/app/resources/images/icon_webcam_white_on.svg b/app/resources/images/icon_webcam_white_on.svg index beec50b..ec150fe 100644 --- a/app/resources/images/icon_webcam_white_on.svg +++ b/app/resources/images/icon_webcam_white_on.svg @@ -1,4 +1,4 @@ - + From 85750d305f9ec78f9a895b4caf9e7dc302d9fde5 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 19 Jun 2018 16:25:27 +0200 Subject: [PATCH 30/43] Added chat badge displaying unread messages --- app/lib/components/ChatWidget.jsx | 23 ++++++++++++++++++----- app/lib/redux/reducers/chatbehavior.js | 7 ++++++- app/lib/redux/stateActions.js | 7 +++++++ app/stylus/components/Chat.styl | 19 +++++++++++++++---- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/app/lib/components/ChatWidget.jsx b/app/lib/components/ChatWidget.jsx index b7329a5..654b7ec 100644 --- a/app/lib/components/ChatWidget.jsx +++ b/app/lib/components/ChatWidget.jsx @@ -7,6 +7,13 @@ import MessageList from './Chat/MessageList'; class ChatWidget extends Component { + componentWillReceiveProps(nextProps) + { + if (nextProps.chatmessages.length !== this.props.chatmessages.length) + if (!this.props.showChat) + this.props.increaseBadge(); + } + render() { const { @@ -68,15 +75,15 @@ ChatWidget.propTypes = disabledInput : PropTypes.bool, badge : PropTypes.number, autofocus : PropTypes.bool, - displayName : PropTypes.string + displayName : PropTypes.string, + chatmessages : PropTypes.arrayOf(PropTypes.object), + increaseBadge : PropTypes.func }; ChatWidget.defaultProps = { senderPlaceHolder : 'Type a message...', - badge : 0, - autofocus : true, - displayName : null + autofocus : true }; const mapStateToProps = (state) => @@ -84,7 +91,9 @@ const mapStateToProps = (state) => return { showChat : state.chatbehavior.showChat, disabledInput : state.chatbehavior.disabledInput, - displayName : state.me.displayName + displayName : state.me.displayName, + badge : state.chatbehavior.badge, + chatmessages : state.chatmessages }; }; @@ -106,6 +115,10 @@ const mapDispatchToProps = (dispatch) => dispatch(requestActions.sendChatMessage(userInput, displayName)); } event.target.message.value = ''; + }, + increaseBadge : () => + { + dispatch(stateActions.increaseBadge()); } }; }; diff --git a/app/lib/redux/reducers/chatbehavior.js b/app/lib/redux/reducers/chatbehavior.js index bee0d85..12e3e51 100644 --- a/app/lib/redux/reducers/chatbehavior.js +++ b/app/lib/redux/reducers/chatbehavior.js @@ -12,8 +12,9 @@ const chatbehavior = (state = initialState, action) => case 'TOGGLE_CHAT': { const showChat = !state.showChat; + const badge = 0; - return { ...state, showChat }; + return { ...state, showChat, badge }; } case 'TOGGLE_INPUT_DISABLED': @@ -23,6 +24,10 @@ const chatbehavior = (state = initialState, action) => return { ...state, disabledInput }; } + case 'INCREASE_BADGE': + { + return { ...state, badge: state.badge + (state.showChat ? 0 : 1) }; + } default: return state; } diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 4fb47fe..dfc3e28 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -338,6 +338,13 @@ export const toggleChat = () => }; }; +export const increaseBadge = () => +{ + return { + type : 'INCREASE_BADGE' + }; +}; + export const toggleInputDisabled = () => { return { diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl index 45f5b63..4ae0c0b 100644 --- a/app/stylus/components/Chat.styl +++ b/app/stylus/components/Chat.styl @@ -8,7 +8,7 @@ right: 0; width: 90vw; z-index: 9999; - + > .launcher { align-self: flex-end; margin-top: 10px; @@ -23,11 +23,12 @@ border-radius: 100%; height: 45px; width: 45px; + position: relative; &.focus { outline: none; } - + &.on { background-color: rgba(#fff, 0.7); } @@ -36,6 +37,17 @@ pointer-events: none; opacity: 0.5; } + > .badge{ + border-radius: 50%; + padding: 0.7vmin; + top: -1vmin; + font-size: 1.5vmin; + left: -1vmin; + background: rgba(255,0,0,0.9); + color: #fff; + font-weight: bold; + position: absolute; + } } } @@ -105,7 +117,7 @@ height: 35px; padding: 5px; border-radius: 0 0 5px 5px; - + > .new-message { width: 100%; border: 0; @@ -121,4 +133,3 @@ } } } - From 5168feefe0dc3237b0d6fe7a6d7bab58dfc605b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 20 Jun 2018 14:23:47 +0200 Subject: [PATCH 31/43] Initial work on tool area. Moved chat, settings and participant list into tool area. --- app/lib/RoomClient.js | 72 +++ app/lib/components/Chat/Chat.jsx | 89 ++++ .../components/ParticipantList/ListPeer.jsx | 166 ++++++ .../ParticipantList/ParticipantList.jsx | 80 +++ app/lib/components/Room.jsx | 274 +++++----- app/lib/components/Settings.jsx | 63 +-- app/lib/components/ToolArea/ToolArea.jsx | 125 +++++ .../components/ToolArea/ToolAreaButton.jsx | 60 +++ app/lib/redux/reducers/index.js | 4 +- app/lib/redux/reducers/peers.js | 13 + app/lib/redux/reducers/toolarea.js | 30 ++ app/lib/redux/requestActions.js | 16 + app/lib/redux/roomClientMiddleware.js | 18 + app/lib/redux/stateActions.js | 23 + app/resources/images/icon_tool_area_black.svg | 4 + app/resources/images/icon_tool_area_white.svg | 4 + app/stylus/components/Chat.styl | 15 +- app/stylus/components/Notifications.styl | 4 +- app/stylus/components/ParticipantList.styl | 132 +++++ app/stylus/components/Room.styl | 499 ++++++++++-------- app/stylus/components/Settings.styl | 50 -- app/stylus/components/ToolArea.styl | 99 ++++ app/stylus/index.styl | 2 + 23 files changed, 1390 insertions(+), 452 deletions(-) create mode 100644 app/lib/components/Chat/Chat.jsx create mode 100644 app/lib/components/ParticipantList/ListPeer.jsx create mode 100644 app/lib/components/ParticipantList/ParticipantList.jsx create mode 100644 app/lib/components/ToolArea/ToolArea.jsx create mode 100644 app/lib/components/ToolArea/ToolAreaButton.jsx create mode 100644 app/lib/redux/reducers/toolarea.js create mode 100644 app/resources/images/icon_tool_area_black.svg create mode 100644 app/resources/images/icon_tool_area_white.svg create mode 100644 app/stylus/components/ParticipantList.styl create mode 100644 app/stylus/components/ToolArea.styl diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 8a474cc..1c35374 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -732,6 +732,78 @@ export default class RoomClient }); } + pausePeerScreen(peerName) + { + logger.debug('pausePeerScreen() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.appData.source !== 'screen') + continue; + + consumer.pause('pause-screen'); + } + } + } + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('pausePeerScreen() failed: %o', error); + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, false)); + }); + } + + resumePeerScreen(peerName) + { + logger.debug('resumePeerScreen() [peerName:"%s"]', peerName); + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, true)); + + return Promise.resolve() + .then(() => + { + for (const peer of this._room.peers) + { + if (peer.name === peerName) + { + for (const consumer of peer.consumers) + { + if (consumer.appData.source !== 'screen' || !consumer.supported) + continue; + + consumer.resume(); + } + } + } + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, false)); + }) + .catch((error) => + { + logger.error('resumePeerScreen() failed: %o', error); + + this._dispatch( + stateActions.setPeerScreenInProgress(peerName, false)); + }); + } + enableAudioOnly() { logger.debug('enableAudioOnly()'); diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx new file mode 100644 index 0000000..e678a3d --- /dev/null +++ b/app/lib/components/Chat/Chat.jsx @@ -0,0 +1,89 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import * as stateActions from '../../redux/stateActions'; +import * as requestActions from '../../redux/requestActions'; +import MessageList from './MessageList'; + +class Chat extends Component +{ + render() + { + const { + senderPlaceHolder, + onSendMessage, + disabledInput, + autofocus, + displayName + } = this.props; + + return ( +
+ +
{ onSendMessage(e, displayName); }} + > + +
+
+ ); + } +} + +Chat.propTypes = +{ + senderPlaceHolder : PropTypes.string, + onSendMessage : PropTypes.func, + disabledInput : PropTypes.bool, + autofocus : PropTypes.bool, + displayName : PropTypes.string +}; + +Chat.defaultProps = +{ + senderPlaceHolder : 'Type a message...', + autofocus : true, + displayName : null +}; + +const mapStateToProps = (state) => +{ + return { + disabledInput : state.chatbehavior.disabledInput, + displayName : state.me.displayName + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + onSendMessage : (event, displayName) => + { + event.preventDefault(); + const userInput = event.target.message.value; + + if (userInput) + { + dispatch(stateActions.addUserMessage(userInput)); + dispatch(requestActions.sendChatMessage(userInput, displayName)); + } + event.target.message.value = ''; + } + }; +}; + +const ChatContainer = connect( + mapStateToProps, + mapDispatchToProps +)(Chat); + +export default ChatContainer; diff --git a/app/lib/components/ParticipantList/ListPeer.jsx b/app/lib/components/ParticipantList/ListPeer.jsx new file mode 100644 index 0000000..2fb666c --- /dev/null +++ b/app/lib/components/ParticipantList/ListPeer.jsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import * as appPropTypes from '../appPropTypes'; +import * as requestActions from '../../redux/requestActions'; + +const ListPeer = (props) => +{ + const { + peer, + micConsumer, + webcamConsumer, + screenConsumer, + onMuteMic, + onUnmuteMic, + onDisableWebcam, + onEnableWebcam, + onDisableScreen, + onEnableScreen + } = props; + + const micEnabled = ( + Boolean(micConsumer) && + !micConsumer.locallyPaused && + !micConsumer.remotelyPaused + ); + + const videoVisible = ( + Boolean(webcamConsumer) && + !webcamConsumer.locallyPaused && + !webcamConsumer.remotelyPaused + ); + + const screenVisible = ( + Boolean(screenConsumer) && + !screenConsumer.locallyPaused && + !screenConsumer.remotelyPaused + ); + + return ( +
+ +
+ {peer.displayName} +
+
+ { screenConsumer ? +
+ { + e.stopPropagation(); + screenVisible ? + onDisableScreen(peer.name) : onEnableScreen(peer.name); + }} + /> + :null + } +
+ { + e.stopPropagation(); + micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name); + }} + /> + +
+ { + e.stopPropagation(); + videoVisible ? + onDisableWebcam(peer.name) : onEnableWebcam(peer.name); + }} + /> +
+
+ ); +}; + +ListPeer.propTypes = +{ + advancedMode : PropTypes.bool, + peer : appPropTypes.Peer.isRequired, + micConsumer : appPropTypes.Consumer, + webcamConsumer : appPropTypes.Consumer, + screenConsumer : appPropTypes.Consumer, + onMuteMic : PropTypes.func.isRequired, + onUnmuteMic : PropTypes.func.isRequired, + onEnableWebcam : PropTypes.func.isRequired, + onDisableWebcam : PropTypes.func.isRequired, + onEnableScreen : PropTypes.func.isRequired, + onDisableScreen : PropTypes.func.isRequired +}; + +const mapStateToProps = (state, { name }) => +{ + const peer = state.peers[name]; + const consumersArray = peer.consumers + .map((consumerId) => state.consumers[consumerId]); + const micConsumer = + consumersArray.find((consumer) => consumer.source === 'mic'); + const webcamConsumer = + consumersArray.find((consumer) => consumer.source === 'webcam'); + const screenConsumer = + consumersArray.find((consumer) => consumer.source === 'screen'); + + return { + peer, + micConsumer, + webcamConsumer, + screenConsumer + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + onMuteMic : (peerName) => + { + dispatch(requestActions.mutePeerAudio(peerName)); + }, + onUnmuteMic : (peerName) => + { + dispatch(requestActions.unmutePeerAudio(peerName)); + }, + onEnableWebcam : (peerName) => + { + + dispatch(requestActions.resumePeerVideo(peerName)); + }, + onDisableWebcam : (peerName) => + { + dispatch(requestActions.pausePeerVideo(peerName)); + }, + onEnableScreen : (peerName) => + { + dispatch(requestActions.resumePeerScreen(peerName)); + }, + onDisableScreen : (peerName) => + { + dispatch(requestActions.pausePeerScreen(peerName)); + } + }; +}; + +const ListPeerContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ListPeer); + +export default ListPeerContainer; diff --git a/app/lib/components/ParticipantList/ParticipantList.jsx b/app/lib/components/ParticipantList/ParticipantList.jsx new file mode 100644 index 0000000..a212509 --- /dev/null +++ b/app/lib/components/ParticipantList/ParticipantList.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import * as appPropTypes from '../appPropTypes'; +import * as requestActions from '../../redux/requestActions'; +import * as stateActions from '../../redux/stateActions'; +import PropTypes from 'prop-types'; +import ListPeer from './ListPeer'; + +class ParticipantList extends React.Component +{ + constructor(props) + { + super(props); + } + + render() + { + const { + advancedMode, + peers + } = this.props; + + return ( +
+
    + { + peers.map((peer) => + { + return ( +
  • + +
  • + ); + }) + } +
+
+ ); + } +} + +ParticipantList.propTypes = +{ + advancedMode : PropTypes.bool, + peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired +}; + +const mapStateToProps = (state) => +{ + const peersArray = Object.values(state.peers); + + return { + peers : peersArray + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + handleChangeWebcam : (device) => + { + dispatch(requestActions.changeWebcam(device.value)); + }, + handleChangeAudioDevice : (device) => + { + dispatch(requestActions.changeAudioDevice(device.value)); + }, + onToggleAdvancedMode : () => + { + dispatch(stateActions.toggleAdvancedMode()); + } + }; +}; + +const ParticipantListContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ParticipantList); + +export default ParticipantListContainer; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 4675277..8ae0258 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -5,14 +5,13 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import ClipboardButton from 'react-clipboard.js'; import * as appPropTypes from './appPropTypes'; -import * as stateActions from '../redux/stateActions'; import * as requestActions from '../redux/requestActions'; import { Appear } from './transitions'; import Me from './Me'; import Peers from './Peers'; import Notifications from './Notifications'; -import ChatWidget from './ChatWidget'; -import Settings from './Settings'; +import ToolAreaButton from './ToolArea/ToolAreaButton'; +import ToolArea from './ToolArea/ToolArea'; class Room extends React.Component { @@ -21,10 +20,10 @@ class Room extends React.Component const { room, me, + toolAreaOpen, amActiveSpeaker, screenProducer, onRoomLinkCopy, - onToggleSettings, onLogin, onShareScreen, onUnShareScreen, @@ -60,137 +59,143 @@ class Room extends React.Component return (
- - - -
-
-

{room.state}

-
- -
-
- - { - // If this is a 'Open in new window/tab' don't prevent - // click default action. - if ( - event.ctrlKey || event.shiftKey || event.metaKey || - // Middle click (IE > 9 and everyone else). - (event.button && event.button === 1) - ) - { - return; - } - - event.preventDefault(); - }} - > - invitation link - -
-
- - -
- + + +
+
+

{room.state}

+
+ +
+
+ + { + // If this is a 'Open in new window/tab' don't prevent + // click default action. + if ( + event.ctrlKey || event.shiftKey || event.metaKey || + // Middle click (IE > 9 and everyone else). + (event.button && event.button === 1) + ) + { + return; + } + + event.preventDefault(); + }} + > + invitation link + +
+
+ + -
-
- { - switch (screenState) + className={classnames('me-container', { + 'active-speaker' : amActiveSpeaker + })} + > + +
+ +
+
{ - case 'on': + switch (screenState) { - onUnShareScreen(); - break; + case 'on': + { + onUnShareScreen(); + break; + } + case 'off': + { + onShareScreen(); + break; + } + case 'need-extension': + { + onNeedExtension(); + break; + } + default: + { + break; + } } - case 'off': - { - onShareScreen(); - break; - } - case 'need-extension': - { - onNeedExtension(); - break; - } - default: - { - break; - } - } - }} - /> + }} + /> -
onToggleSettings()} - /> +
onLogin()} + /> -
onLogin()} - /> +
onToggleHand(!me.raiseHand)} + /> -
onToggleHand(!me.raiseHand)} - /> +
onLeaveMeeting()} + /> +
-
onLeaveMeeting()} +
- - - - +
+ {toolAreaOpen ? + + :null + } +
); @@ -199,18 +204,18 @@ class Room extends React.Component Room.propTypes = { - room : appPropTypes.Room.isRequired, - me : appPropTypes.Me.isRequired, - amActiveSpeaker : PropTypes.bool.isRequired, - screenProducer : appPropTypes.Producer, - onRoomLinkCopy : PropTypes.func.isRequired, - onShareScreen : PropTypes.func.isRequired, - onUnShareScreen : PropTypes.func.isRequired, - onNeedExtension : PropTypes.func.isRequired, - onToggleSettings : PropTypes.func.isRequired, - onToggleHand : PropTypes.func.isRequired, - onLeaveMeeting : PropTypes.func.isRequired, - onLogin : PropTypes.func.isRequired + room : appPropTypes.Room.isRequired, + me : appPropTypes.Me.isRequired, + amActiveSpeaker : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, + screenProducer : appPropTypes.Producer, + onRoomLinkCopy : PropTypes.func.isRequired, + onShareScreen : PropTypes.func.isRequired, + onUnShareScreen : PropTypes.func.isRequired, + onNeedExtension : PropTypes.func.isRequired, + onToggleHand : PropTypes.func.isRequired, + onLeaveMeeting : PropTypes.func.isRequired, + onLogin : PropTypes.func.isRequired }; const mapStateToProps = (state) => @@ -222,6 +227,7 @@ const mapStateToProps = (state) => return { room : state.room, me : state.me, + toolAreaOpen : state.toolarea.toolAreaOpen, amActiveSpeaker : state.me.name === state.room.activeSpeakerName, screenProducer : screenProducer }; @@ -237,10 +243,6 @@ const mapDispatchToProps = (dispatch) => text : 'Room link copied to the clipboard' })); }, - onToggleSettings : () => - { - dispatch(stateActions.toggleSettings()); - }, onToggleHand : (enable) => { if (enable) diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index c41d65c..e4bacb0 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -4,7 +4,6 @@ import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; import * as stateActions from '../redux/stateActions'; import PropTypes from 'prop-types'; -import { Appear } from './transitions'; import Dropdown from 'react-dropdown'; class Settings extends React.Component @@ -21,13 +20,9 @@ class Settings extends React.Component me, handleChangeWebcam, handleChangeAudioDevice, - onToggleSettings, onToggleAdvancedMode } = this.props; - if (!room.showSettings) - return null; - let webcams; let webcamText; @@ -55,43 +50,28 @@ class Settings extends React.Component audioDevices = []; return ( - -
-
-
- Settings -
-
- - - - Advanced mode -
-
- onToggleSettings()} - > - Close - -
-
+
+
+ + + + Advanced mode
- +
); } } @@ -100,7 +80,6 @@ Settings.propTypes = { me : appPropTypes.Me.isRequired, room : appPropTypes.Room.isRequired, - onToggleSettings : PropTypes.func.isRequired, handleChangeWebcam : PropTypes.func.isRequired, handleChangeAudioDevice : PropTypes.func.isRequired, onToggleAdvancedMode : PropTypes.func.isRequired diff --git a/app/lib/components/ToolArea/ToolArea.jsx b/app/lib/components/ToolArea/ToolArea.jsx new file mode 100644 index 0000000..3ec7cf7 --- /dev/null +++ b/app/lib/components/ToolArea/ToolArea.jsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import * as stateActions from '../../redux/stateActions'; +import ParticipantList from '../ParticipantList/ParticipantList'; +import Chat from '../Chat/Chat'; +import Settings from '../Settings'; + +class ToolArea extends React.Component +{ + constructor(props) + { + super(props); + } + + render() + { + const { + toolarea, + setToolTab + } = this.props; + + return ( +
+
+ + { + setToolTab('chat'); + }} + checked={toolarea.currentToolTab === 'chat'} + /> + + +
+ +
+ + + { + setToolTab('users'); + }} + checked={toolarea.currentToolTab === 'users'} + /> + + +
+ +
+ + + { + setToolTab('settings'); + }} + checked={toolarea.currentToolTab === 'settings'} + /> + + +
+ +
+ + + { + setToolTab('layout'); + }} + checked={toolarea.currentToolTab === 'layout'} + /> + + +
+

Tab Three Content

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

+
+
+
+ ); + } +} + +ToolArea.propTypes = +{ + advancedMode : PropTypes.bool, + toolarea : PropTypes.object.isRequired, + setToolTab : PropTypes.func.isRequired +}; + +const mapStateToProps = (state) => +{ + return { + toolarea : state.toolarea + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + setToolTab : (toolTab) => + { + dispatch(stateActions.setToolTab(toolTab)); + } + }; +}; + +const ToolAreaContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ToolArea); + +export default ToolAreaContainer; diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx new file mode 100644 index 0000000..b158ba4 --- /dev/null +++ b/app/lib/components/ToolArea/ToolAreaButton.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import * as stateActions from '../../redux/stateActions'; + +class ToolAreaButton extends React.Component +{ + render() + { + const { + toolAreaOpen, + toggleToolArea + } = this.props; + + return ( +
+
toggleToolArea()} + /> +
+ ); + } +} + +ToolAreaButton.propTypes = +{ + toolAreaOpen : PropTypes.bool.isRequired, + toggleToolArea : PropTypes.func.isRequired +}; + +const mapStateToProps = (state) => +{ + return { + toolAreaOpen : state.toolarea.toolAreaOpen + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + toggleToolArea : () => + { + dispatch(stateActions.toggleToolArea()); + } + }; +}; + +const ToolAreaButtonContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ToolAreaButton); + +export default ToolAreaButtonContainer; diff --git a/app/lib/redux/reducers/index.js b/app/lib/redux/reducers/index.js index 779f775..e5c89a3 100644 --- a/app/lib/redux/reducers/index.js +++ b/app/lib/redux/reducers/index.js @@ -7,6 +7,7 @@ import consumers from './consumers'; import notifications from './notifications'; import chatmessages from './chatmessages'; import chatbehavior from './chatbehavior'; +import toolarea from './toolarea'; const reducers = combineReducers( { @@ -17,7 +18,8 @@ const reducers = combineReducers( consumers, notifications, chatmessages, - chatbehavior + chatbehavior, + toolarea }); export default reducers; diff --git a/app/lib/redux/reducers/peers.js b/app/lib/redux/reducers/peers.js index b7001b9..d7174d5 100644 --- a/app/lib/redux/reducers/peers.js +++ b/app/lib/redux/reducers/peers.js @@ -60,6 +60,19 @@ const peers = (state = initialState, action) => return { ...state, [newPeer.name]: newPeer }; } + case 'SET_PEER_SCREEN_IN_PROGRESS': + { + const { peerName, flag } = action.payload; + const peer = state[peerName]; + + if (!peer) + throw new Error('no Peer found'); + + const newPeer = { ...peer, peerScreenInProgress: flag }; + + return { ...state, [newPeer.name]: newPeer }; + } + case 'SET_PEER_RAISE_HAND_STATE': { const { peerName, raiseHandState } = action.payload; diff --git a/app/lib/redux/reducers/toolarea.js b/app/lib/redux/reducers/toolarea.js new file mode 100644 index 0000000..00ad64a --- /dev/null +++ b/app/lib/redux/reducers/toolarea.js @@ -0,0 +1,30 @@ +const initialState = +{ + toolAreaOpen : false, + currentToolTab : 'chat' // chat, settings, layout, users +}; + +const toolarea = (state = initialState, action) => +{ + switch (action.type) + { + case 'TOGGLE_TOOL_AREA': + { + const toolAreaOpen = !state.toolAreaOpen; + + return { ...state, toolAreaOpen }; + } + + case 'SET_TOOL_TAB': + { + const { toolTab } = action.payload; + + return { ...state, currentToolTab: toolTab }; + } + + default: + return state; + } +}; + +export default toolarea; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index ff865fa..5d9a240 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -119,6 +119,22 @@ export const resumePeerVideo = (peerName) => }; }; +export const pausePeerScreen = (peerName) => +{ + return { + type : 'PAUSE_PEER_SCREEN', + payload : { peerName } + }; +}; + +export const resumePeerScreen = (peerName) => +{ + return { + type : 'RESUME_PEER_SCREEN', + payload : { peerName } + }; +}; + export const userLogin = () => { return { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 64a1059..61aa34b 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -149,6 +149,24 @@ export default ({ dispatch, getState }) => (next) => break; } + case 'PAUSE_PEER_SCREEN': + { + const { peerName } = action.payload; + + client.pausePeerScreen(peerName); + + break; + } + + case 'RESUME_PEER_SCREEN': + { + const { peerName } = action.payload; + + client.resumePeerScreen(peerName); + + break; + } + case 'RAISE_HAND': { client.sendRaiseHandState(true); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 4fb47fe..6c7270a 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -125,6 +125,14 @@ export const setPeerAudioInProgress = (peerName, flag) => }; }; +export const setPeerScreenInProgress = (peerName, flag) => +{ + return { + type : 'SET_PEER_SCREEN_IN_PROGRESS', + payload : { peerName, flag } + }; +}; + export const setMyRaiseHandState = (flag) => { return { @@ -148,6 +156,21 @@ export const toggleSettings = () => }; }; +export const toggleToolArea = () => +{ + return { + type : 'TOGGLE_TOOL_AREA' + }; +}; + +export const setToolTab = (toolTab) => +{ + return { + type : 'SET_TOOL_TAB', + payload : { toolTab } + }; +}; + export const setMyRaiseHandStateInProgress = (flag) => { return { diff --git a/app/resources/images/icon_tool_area_black.svg b/app/resources/images/icon_tool_area_black.svg new file mode 100644 index 0000000..1acc639 --- /dev/null +++ b/app/resources/images/icon_tool_area_black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/resources/images/icon_tool_area_white.svg b/app/resources/images/icon_tool_area_white.svg new file mode 100644 index 0000000..c860ab2 --- /dev/null +++ b/app/resources/images/icon_tool_area_white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl index 45f5b63..05794b5 100644 --- a/app/stylus/components/Chat.styl +++ b/app/stylus/components/Chat.styl @@ -1,10 +1,10 @@ [data-component='ChatWidget'] { + position: absolute; bottom: 0; display: flex; flex-direction: column; margin: 0 10px 10px 0; max-width: 300px; - position: fixed; right: 0; width: 90vw; z-index: 9999; @@ -27,7 +27,7 @@ &.focus { outline: none; } - + &.on { background-color: rgba(#fff, 0.7); } @@ -44,10 +44,13 @@ box-shadow: 0px 2px 10px 1px #000; } +[data-component='Chat'] { + height: 100%; +} + [data-component='MessageList'] { background-color: rgba(#fff, 0.9); - height: 50vh; - max-height: 350px; + height: 91vmin; overflow-y: scroll; padding-top: 5px; border-radius: 5px 5px 0px 0px; @@ -102,8 +105,8 @@ align-items: center; display: flex; background-color: rgba(#fff, 0.9); - height: 35px; - padding: 5px; + height: 6vmin; + padding: 0.5vmin; border-radius: 0 0 5px 5px; > .new-message { diff --git a/app/stylus/components/Notifications.styl b/app/stylus/components/Notifications.styl index 35d86bf..329fa64 100644 --- a/app/stylus/components/Notifications.styl +++ b/app/stylus/components/Notifications.styl @@ -1,9 +1,9 @@ [data-component='Notifications'] { - position: fixed; + position: absolute; z-index: 9999; pointer-events: none; top: 0; - right: 0; + right: 65px; bottom: 0; padding: 20px; display: flex; diff --git a/app/stylus/components/ParticipantList.styl b/app/stylus/components/ParticipantList.styl new file mode 100644 index 0000000..1f0bd98 --- /dev/null +++ b/app/stylus/components/ParticipantList.styl @@ -0,0 +1,132 @@ +[data-component='ParticipantList'] { + width: 100%; + + > .list { + box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \ + 0 4px 20px 0 rgba(0,0,0,0.19); + + > .list-item { + padding: 0.5vmin; + border-bottom: 1px solid #ddd; + width: 100%; + overflow: hidden; + } + } +} + +[data-component='ListPeer'] { + > .controls { + float: right; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + > .button { + flex: 0 0 auto; + margin: 0.2vmin; + border-radius: 2px; + background-position: center; + background-size: 75%; + background-repeat: no-repeat; + background-color: rgba(#000, 0.5); + cursor: pointer; + transition-property: opacity, background-color; + transition-duration: 0.15s; + + +desktop() { + width: 24px; + height: 24px; + opacity: 0.85; + + &:hover { + opacity: 1; + } + } + + +mobile() { + width: 22px; + height: 22px; + } + + &.unsupported { + pointer-events: none; + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + + &.on { + background-color: rgba(#fff, 0.7); + } + + &.mic { + &.on { + background-image: url('/resources/images/icon_mic_black_on.svg'); + } + + &.off { + background-image: url('/resources/images/icon_remote_mic_white_off.svg'); + background-color: rgba(#d42241, 0.7); + } + + &.unsupported { + background-image: url('/resources/images/icon_mic_white_unsupported.svg'); + } + } + + &.webcam { + &.on { + background-image: url('/resources/images/icon_webcam_black_on.svg'); + } + + &.off { + background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); + background-color: rgba(#d42241, 0.7); + } + + &.unsupported { + background-image: url('/resources/images/icon_webcam_white_unsupported.svg'); + } + } + + &.screen { + &.on { + background-image: url('/resources/images/share-screen-black.svg'); + } + + &.off { + background-image: url('/resources/images/no-share-screen-white.svg'); + background-color: rgba(#d42241, 0.7); + } + + &.unsupported { + background-image: url('/resources/images/no-share-screen-white.svg'); + } + } + } + } + + > .avatar { + padding: 8px 16px; + float: left; + width: auto; + border: none; + display: block; + outline: 0; + border-radius: 50%; + vertical-align: middle; + } + + > .peer-info { + font-size: 1.4vmin; + float: left; + width: auto; + border: none; + display: block; + outline: 0; + padding: 0.6vmin; + } +} diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index ec3e43c..aa62aa0 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -1,268 +1,283 @@ [data-component='Room'] { - position: relative; - height: 100%; - width: 100%; - AppearFadeIn(300ms); - > .state { - position: fixed; - z-index: 100; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - border-radius: 25px; - background-color: rgba(#fff, 0.2); - - +desktop() { - top: 20px; - left: 20px; - width: 124px; - } - - +mobile() { - top: 10px; - left: 10px; - width: 110px; - } - - > .icon { - flex: 0 0 auto; - border-radius: 100%; - - +desktop() { - margin: 5px; - margin-right: 0; - height: 20px; - width: 20px; - } - - +mobile() { - margin: 4px; - margin-right: 0; - height: 16px; - width: 16px; - } - - &.new, &.closed { - background-color: rgba(#aaa, 0.5); - } - - &.connecting { - animation: Room-info-state-connecting .75s infinite linear; - } - - &.connected { - background-color: rgba(#30bd18, 0.75); - - +mobile() { - display: none; - } - } - } - - > .text { - flex: 100 0 auto; - user-select: none; - pointer-events: none; - text-align: center; - text-transform: uppercase; - font-family: 'Roboto'; - font-weight: 400; - color: rgba(#fff, 0.75); - - +desktop() { - font-size: 12px; - } - - +mobile() { - font-size: 10px; - } - - &.connected { - +mobile() { - display: none; - } - } - } - } - - > .room-link-wrapper { - pointer-events: none; + > .room-wrapper { position: absolute; - z-index: 1; top: 0; left: 0; - right: 0; - display: flex; - flex-direction: row; - justify-content: center; + height: 100%; + width: 100%; + transition: width 0.3s; - > .room-link { - width: auto; - background-color: rgba(#fff, 0.75); - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); + > .state { + position: fixed; + z-index: 100; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-radius: 25px; + background-color: rgba(#fff, 0.2); - > a.link { - display: block;; - user-select: none; - pointer-events: auto; - color: #104758; - font-weight: 400; - cursor: pointer; - text-decoration: none; - transition-property: opacity; - transition-duration: 0.25s; - opacity: 0.8; + +desktop() { + top: 20px; + left: 20px; + width: 124px; + } + + +mobile() { + top: 10px; + left: 10px; + width: 110px; + } + + > .icon { + flex: 0 0 auto; + border-radius: 100%; +desktop() { - padding: 10px 20px; - font-size: 16px; + margin: 5px; + margin-right: 0; + height: 20px; + width: 20px; } +mobile() { - padding: 6px 10px; - font-size: 14px; + margin: 4px; + margin-right: 0; + height: 16px; + width: 16px; } - &:hover { - opacity: 1; - text-decoration: underline; + &.new, &.closed { + background-color: rgba(#aaa, 0.5); + } + + &.connecting { + animation: Room-info-state-connecting .75s infinite linear; + } + + &.connected { + background-color: rgba(#30bd18, 0.75); + + +mobile() { + display: none; + } + } + } + + > .text { + flex: 100 0 auto; + user-select: none; + pointer-events: none; + text-align: center; + text-transform: uppercase; + font-family: 'Roboto'; + font-weight: 400; + color: rgba(#fff, 0.75); + + +desktop() { + font-size: 12px; + } + + +mobile() { + font-size: 10px; + } + + &.connected { + +mobile() { + display: none; + } } } } - } - > .me-container { - position: fixed; - z-index: 100; - overflow: hidden; - box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); - transition-property: border-color; - transition-duration: 0.15s; + > .room-link-wrapper { + pointer-events: none; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + display: flex; + flex-direction: row; + justify-content: center; - &.active-speaker { - border-color: #fff; + > .room-link { + width: auto; + background-color: rgba(#fff, 0.75); + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); + + > a.link { + display: block;; + user-select: none; + pointer-events: auto; + color: #104758; + font-weight: 400; + cursor: pointer; + text-decoration: none; + transition-property: opacity; + transition-duration: 0.25s; + opacity: 0.8; + + +desktop() { + padding: 10px 20px; + font-size: 16px; + } + + +mobile() { + padding: 6px 10px; + font-size: 14px; + } + + &:hover { + opacity: 1; + text-decoration: underline; + } + } + } } - +desktop() { - height: 200px; - width: 235px; - bottom: 20px; - left: 20px; - border: 1px solid rgba(#fff, 0.15); - } - - +mobile() { - height: 175px; - width: 150px; - bottom: 10px; - left: 10px; - border: 1px solid rgba(#fff, 0.25); - } - } - - > .sidebar { - position: fixed; - z-index: 101; - top: calc(50% - 60px); - height: 120px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - +desktop() { - left: 20px; - width: 36px; - } - - +mobile() { - left: 10px; - width: 32px; - } - - > .button { - flex: 0 0 auto; - margin: 4px 0; - background-position: center; - background-size: 75%; - background-repeat: no-repeat; - background-color: rgba(#fff, 0.3); - cursor: pointer; - transition-property: opacity, background-color; + > .me-container { + position: fixed; + z-index: 100; + overflow: hidden; + box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); + transition-property: border-color; transition-duration: 0.15s; - border-radius: 100%; + + &.active-speaker { + border-color: #fff; + } +desktop() { - height: 36px; + height: 200px; + width: 235px; + bottom: 20px; + left: 20px; + border: 1px solid rgba(#fff, 0.15); + } + + +mobile() { + height: 175px; + width: 150px; + bottom: 10px; + left: 10px; + border: 1px solid rgba(#fff, 0.25); + } + } + + > .sidebar { + position: fixed; + z-index: 101; + top: calc(50% - 60px); + height: 120px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + +desktop() { + left: 20px; width: 36px; } +mobile() { - height: 32px; + left: 10px; width: 32px; } - &.on { - background-color: rgba(#fff, 0.7); - } + > .button { + flex: 0 0 auto; + margin: 4px 0; + background-position: center; + background-size: 75%; + background-repeat: no-repeat; + background-color: rgba(#fff, 0.3); + cursor: pointer; + transition-property: opacity, background-color; + transition-duration: 0.15s; + border-radius: 100%; - &.disabled { - pointer-events: none; - opacity: 0.5; - } - - &.login { - &.off { - background-image: url('/resources/images/icon_login_white.svg'); + +desktop() { + height: 36px; + width: 36px; } - } - &.settings { - &.off { - background-image: url('/resources/images/icon_settings_white.svg'); + +mobile() { + height: 32px; + width: 32px; } &.on { - background-image: url('/resources/images/icon_settings_black.svg'); - } - } - - &.screen { - &.on { - background-image: url('/resources/images/no-share-screen-black.svg'); + background-color: rgba(#fff, 0.7); } - &.off { - background-image: url('/resources/images/share-screen-white.svg'); + &.disabled { + pointer-events: none; + opacity: 0.5; } - &.unsupported { - background-image: url('/resources/images/no-share-screen-white.svg'); - background-color: rgba(#d42241, 0.7); + &.login { + &.off { + background-image: url('/resources/images/icon_login_white.svg'); + } } - &.need-extension { - background-image: url('/resources/images/share-screen-extension.svg'); - } - } - &.raise-hand { - background-image: url('/resources/images/icon-hand-white.svg'); + &.settings { + &.off { + background-image: url('/resources/images/icon_settings_white.svg'); + } - &.on { - background-image: url('/resources/images/icon-hand-black.svg'); + &.on { + background-image: url('/resources/images/icon_settings_black.svg'); + } } - } - &.leave-meeting { - background-image: url('/resources/images/leave-meeting.svg'); + &.screen { + &.on { + background-image: url('/resources/images/no-share-screen-black.svg'); + } + + &.off { + background-image: url('/resources/images/share-screen-white.svg'); + } + + &.unsupported { + background-image: url('/resources/images/no-share-screen-white.svg'); + background-color: rgba(#d42241, 0.7); + } + + &.need-extension { + background-image: url('/resources/images/share-screen-extension.svg'); + } + } + &.raise-hand { + background-image: url('/resources/images/icon-hand-white.svg'); + + &.on { + background-image: url('/resources/images/icon-hand-black.svg'); + } + } + + &.leave-meeting { + background-image: url('/resources/images/leave-meeting.svg'); + } } } } + + > .toolarea-wrapper { + position: fixed; + top: 0; + right: 0; + width: 20%; + height: 100%; + background-color: #FFF; + transition: width 0.3s; + } } .Dropdown-root { @@ -360,6 +375,60 @@ padding: 8px 10px; } +.react-tabs__tab-list { + border-bottom: 1px solid #aaa; + margin: 0 0 10px; + padding: 0; +} + +.react-tabs__tab { + display: inline-block; + border: 1px solid transparent; + border-bottom: none; + bottom: -1px; + position: relative; + list-style: none; + padding: 6px 12px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.react-tabs__tab--selected { + background: #fff; + border-color: #aaa; + color: black; + border-radius: 5px 5px 0 0; +} + +.react-tabs__tab--disabled { + color: GrayText; + cursor: default; +} + +.react-tabs__tab:focus { + box-shadow: 0 0 5px hsl(208, 99%, 50%); + border-color: hsl(208, 99%, 50%); + outline: none; +} + +.react-tabs__tab:focus:after { + content: ""; + position: absolute; + height: 5px; + left: -4px; + right: -4px; + bottom: -5px; + background: #fff; +} + +.react-tabs__tab-panel { + display: none; +} + +.react-tabs__tab-panel--selected { + display: block; +} + @keyframes Room-info-state-connecting { 50% { background-color: rgba(orange, 0.75); } } diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl index a58d04a..b5f818d 100644 --- a/app/stylus/components/Settings.styl +++ b/app/stylus/components/Settings.styl @@ -1,53 +1,3 @@ [data-component='Settings'] { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 19999; - background-color: rgba(000, 000, 000, 0.5); - AppearFadeIn(500ms); - - > .dialog { - position: absolute; - width: 40vmin; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - background-color: #fff; - border-radius: 4px; - box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); - padding: 1vmin; - - > .header { - > span { - font-size: 2vmin; - font-weight: 400; - } - } - - > .settings { - } - - > .footer { - bottom: 0; - right: 0; - left: 0; - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: flex-end; - - > .button { - flex: 0 0 auto; - margin: 1vmin; - background-color: rgba(#000, 0.8); - color: #fff; - cursor: pointer; - border-radius: 4px; - padding: 0.5vmin; - } - } - } } diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl new file mode 100644 index 0000000..cd177f2 --- /dev/null +++ b/app/stylus/components/ToolArea.styl @@ -0,0 +1,99 @@ +[data-component='ToolAreaButton'] { + position: absolute; + z-index: 101; + top: 20px; + right: 20px; + height: 36px; + width: 36px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .button { + flex: 0 0 auto; + margin: 4px 0; + background-position: center; + background-size: 75%; + background-repeat: no-repeat; + background-color: rgba(#fff, 0.3); + cursor: pointer; + transition-property: opacity, background-color; + transition-duration: 0.15s; + border-radius: 100%; + + +desktop() { + height: 36px; + width: 36px; + } + + +mobile() { + height: 32px; + width: 32px; + } + + &.on { + background-color: rgba(#fff, 0.7); + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + + &.toolarea-button { + background-image: url('/resources/images/icon_tool_area_white.svg'); + + &.on { + background-image: url('/resources/images/icon_tool_area_black.svg'); + } + } + } +} + +[data-component='ToolArea'] { + width: 100%; + height: 100%; + + > .tabs { + display: flex; + flex-wrap: wrap; + height: 100%; + + > label { + order: 1; + display: block; + padding: 1vmin 0 1vmin 0; + cursor: pointer; + background: rgba(#000, 0.3); + font-weight: bold; + transition: background ease 0.2s; + text-align: center; + width: 25%; + font-size: 1.3vmin; + height: 3vmin; + } + + > .tab { + order: 99; + flex-grow: 1; + width: 100%; + height: 100%; + display: none; + padding: 1vmin; + background: #fff; + } + + > input[type="radio"] { + display: none; + } + + > input[type="radio"]:checked + label { + background: #fff; + } + + > input[type="radio"]:checked + label + .tab { + display: block; + } + } +} diff --git a/app/stylus/index.styl b/app/stylus/index.styl index 9f9fec5..4d9561c 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -43,6 +43,8 @@ body { @import './components/Notifications'; @import './components/Chat'; @import './components/Settings'; + @import './components/ToolArea'; + @import './components/ParticipantList'; } // Hack to detect in JS the current media query From c87e006da28ed30d3aa0eefba65cad212a8b00a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 20 Jun 2018 15:04:53 +0200 Subject: [PATCH 32/43] Inital work on screenview. --- app/lib/components/Me.jsx | 25 ++++- app/lib/components/Peer.jsx | 16 ++- app/lib/components/PeerView.jsx | 152 +++++++-------------------- app/lib/components/ScreenView.jsx | 168 ++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 117 deletions(-) create mode 100644 app/lib/components/ScreenView.jsx diff --git a/app/lib/components/Me.jsx b/app/lib/components/Me.jsx index 66226d1..ad3259f 100644 --- a/app/lib/components/Me.jsx +++ b/app/lib/components/Me.jsx @@ -7,6 +7,7 @@ import { getDeviceInfo } from 'mediasoup-client'; import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; import PeerView from './PeerView'; +import ScreenView from './ScreenView'; class Me extends React.Component { @@ -32,6 +33,7 @@ class Me extends React.Component advancedMode, micProducer, webcamProducer, + screenProducer, onChangeDisplayName, onMuteMic, onUnmuteMic, @@ -65,6 +67,12 @@ class Me extends React.Component !webcamProducer.remotelyPaused ); + const screenVisible = ( + Boolean(screenProducer) && + !screenProducer.locallyPaused && + !screenProducer.remotelyPaused + ); + let tip; if (!me.displayNameSet) @@ -115,6 +123,17 @@ class Me extends React.Component onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)} /> + {screenProducer ? + + :null + } + {this._tooltip ? producersArray.find((producer) => producer.source === 'mic'); const webcamProducer = producersArray.find((producer) => producer.source === 'webcam'); + const screenProducer = + producersArray.find((producer) => producer.source === 'screen'); return { connected : state.room.state === 'connected', me : state.me, micProducer : micProducer, - webcamProducer : webcamProducer + webcamProducer : webcamProducer, + screenProducer : screenProducer }; }; diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index b69eb1f..e85e0ba 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -5,6 +5,7 @@ import classnames from 'classnames'; import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; import PeerView from './PeerView'; +import ScreenView from './ScreenView'; const Peer = (props) => { @@ -91,15 +92,22 @@ const Peer = (props) => peer={peer} audioTrack={micConsumer ? micConsumer.track : null} videoTrack={webcamConsumer ? webcamConsumer.track : null} - screenTrack={screenConsumer ? screenConsumer.track : null} videoVisible={videoVisible} videoProfile={videoProfile} - screenVisible={screenVisible} - screenProfile={screenProfile} audioCodec={micConsumer ? micConsumer.codec : null} videoCodec={webcamConsumer ? webcamConsumer.codec : null} - screenCodec={screenConsumer ? screenConsumer.codec : null} /> + + {screenConsumer ? + + :null + }
); }; diff --git a/app/lib/components/PeerView.jsx b/app/lib/components/PeerView.jsx index 8a6ef57..840badd 100644 --- a/app/lib/components/PeerView.jsx +++ b/app/lib/components/PeerView.jsx @@ -14,11 +14,9 @@ export default class PeerView extends React.Component this.state = { - volume : 0, // Integer from 0 to 10., - videoWidth : null, - videoHeight : null, - screenWidth : null, - screenHeight : null + volume : 0, // Integer from 0 to 10., + videoWidth : null, + videoHeight : null }; // Latest received video track. @@ -29,10 +27,6 @@ export default class PeerView extends React.Component // @type {MediaStreamTrack} this._videoTrack = null; - // Latest received screen track. - // @type {MediaStreamTrack} - this._screenTrack = null; - // Hark instance. // @type {Object} this._hark = null; @@ -45,68 +39,41 @@ export default class PeerView extends React.Component { const { isMe, - advancedMode, peer, videoVisible, videoProfile, - screenVisible, - screenProfile, audioCodec, videoCodec, - screenCodec, onChangeDisplayName } = this.props; const { volume, videoWidth, - videoHeight, - screenWidth, - screenHeight + videoHeight } = this.state; return (
- {advancedMode ? -
- {screenVisible ? -
- {audioCodec ? -

{audioCodec}

- :null - } +
+
+ {audioCodec ? +

{audioCodec}

+ :null + } - {screenCodec ? -

{screenCodec} {screenProfile}

- :null - } + {videoCodec ? +

{videoCodec} {videoProfile}

+ :null + } - {(screenVisible && screenWidth !== null) ? -

{screenWidth}x{screenHeight}

- :null - } -
- :
- {audioCodec ? -

{audioCodec}

- :null - } - - {videoCodec ? -

{videoCodec} {videoProfile}

- :null - } - - {(videoVisible && videoWidth !== null) ? -

{videoWidth}x{videoHeight}

- :null - } -
+ {(videoVisible && videoWidth !== null) ? +

{videoWidth}x{videoHeight}

+ :null }
- :null - } +
{isMe ? @@ -130,52 +97,33 @@ export default class PeerView extends React.Component } - {advancedMode ? -
- - - {peer.device.name} {Math.floor(peer.device.version) || null} - -
- :null - } +
+ + + {peer.device.name} {Math.floor(peer.device.version) || null} + +