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