Merge branch 'develop' into feat-audio-settings

auto_join_3.3
Stefan Otto 2020-05-06 18:29:07 +02:00
commit 59d4bd2ce7
9 changed files with 274 additions and 72 deletions

View File

@ -27,6 +27,7 @@
"react": "^16.10.2", "react": "^16.10.2",
"react-cookie-consent": "^2.5.0", "react-cookie-consent": "^2.5.0",
"react-dom": "^16.10.2", "react-dom": "^16.10.2",
"react-flip-toolkit": "^7.0.9",
"react-intl": "^3.4.0", "react-intl": "^3.4.0",
"react-redux": "^7.1.1", "react-redux": "^7.1.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",

View File

@ -31,8 +31,7 @@ let Spotlights;
let requestTimeout, let requestTimeout,
transportOptions, transportOptions,
lastN, lastN,
mobileLastN, mobileLastN;
defaultResolution;
if (process.env.NODE_ENV !== 'test') if (process.env.NODE_ENV !== 'test')
{ {
@ -40,8 +39,7 @@ if (process.env.NODE_ENV !== 'test')
requestTimeout, requestTimeout,
transportOptions, transportOptions,
lastN, lastN,
mobileLastN, mobileLastN
defaultResolution
} = window.config); } = window.config);
} }
@ -205,9 +203,6 @@ export default class RoomClient
// Our WebTorrent client // Our WebTorrent client
this._webTorrent = null; this._webTorrent = null;
if (defaultResolution)
store.dispatch(settingsActions.setVideoResolution(defaultResolution));
// Max spotlights // Max spotlights
if (device.platform === 'desktop') if (device.platform === 'desktop')
this._maxSpotlights = lastN; this._maxSpotlights = lastN;
@ -1111,6 +1106,37 @@ export default class RoomClient
meActions.setAudioOutputInProgress(false)); meActions.setAudioOutputInProgress(false));
} }
async changeAudioOutputDevice(deviceId)
{
logger.debug('changeAudioOutputDevice() [deviceId: %s]', deviceId);
store.dispatch(
meActions.setAudioOutputInProgress(true));
try
{
const device = this._audioOutputDevices[deviceId];
if (!device)
throw new Error('Selected audio output device no longer avaibale');
logger.debug(
'changeAudioOutputDevice() | new selected [audio output device:%o]',
device);
store.dispatch(settingsActions.setSelectedAudioOutputDevice(deviceId));
await this._updateAudioOutputDevices();
}
catch (error)
{
logger.error('changeAudioOutputDevice() failed: %o', error);
}
store.dispatch(
meActions.setAudioOutputInProgress(false));
}
async changeVideoResolution(resolution) async changeVideoResolution(resolution)
{ {
logger.debug('changeVideoResolution() [resolution: %s]', resolution); logger.debug('changeVideoResolution() [resolution: %s]', resolution);
@ -1538,6 +1564,26 @@ export default class RoomClient
} }
} }
async lowerPeerHand(peerId)
{
logger.debug('lowerPeerHand() [peerId:"%s"]', peerId);
store.dispatch(
peerActions.setPeerRaisedHandInProgress(peerId, true));
try
{
await this.sendRequest('moderator:lowerHand', { peerId });
}
catch (error)
{
logger.error('lowerPeerHand() | [error:"%o"]', error);
}
store.dispatch(
peerActions.setPeerRaisedHandInProgress(peerId, false));
}
async setRaisedHand(raisedHand) async setRaisedHand(raisedHand)
{ {
logger.debug('setRaisedHand: ', raisedHand); logger.debug('setRaisedHand: ', raisedHand);
@ -1780,6 +1826,49 @@ export default class RoomClient
this._recvTransport = null; this._recvTransport = null;
} }
store.dispatch(roomActions.setRoomState('connecting'));
});
store.dispatch(
producerActions.removeProducer(this._screenSharingProducer.id));
this._screenSharingProducer = null;
}
if (this._webcamProducer)
{
this._webcamProducer.close();
store.dispatch(
producerActions.removeProducer(this._webcamProducer.id));
this._webcamProducer = null;
}
if (this._micProducer)
{
this._micProducer.close();
store.dispatch(
producerActions.removeProducer(this._micProducer.id));
this._micProducer = null;
}
if (this._sendTransport)
{
this._sendTransport.close();
this._sendTransport = null;
}
if (this._recvTransport)
{
this._recvTransport.close();
this._recvTransport = null;
}
store.dispatch(roomActions.setRoomState('connecting')); store.dispatch(roomActions.setRoomState('connecting'));
}); });
@ -2539,6 +2628,13 @@ export default class RoomClient
break; break;
} }
case 'moderator:lowerHand':
{
this.setRaisedHand(false);
break;
}
case 'gotRole': case 'gotRole':
{ {
const { peerId, role } = notification.data; const { peerId, role } = notification.data;

View File

@ -40,6 +40,12 @@ export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) =>
payload : { peerId, raisedHand, raisedHandTimestamp } payload : { peerId, raisedHand, raisedHandTimestamp }
}); });
export const setPeerRaisedHandInProgress = (peerId, flag) =>
({
type : 'SET_PEER_RAISED_HAND_IN_PROGRESS',
payload : { peerId, flag }
});
export const setPeerPicture = (peerId, picture) => export const setPeerPicture = (peerId, picture) =>
({ ({
type : 'SET_PEER_PICTURE', type : 'SET_PEER_PICTURE',

View File

@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -23,7 +24,7 @@ const styles = (theme) =>
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem', height : '2rem',
marginTop : theme.spacing(1) marginTop : theme.spacing(0.5)
}, },
peerInfo : peerInfo :
{ {
@ -33,6 +34,10 @@ const styles = (theme) =>
flexGrow : 1, flexGrow : 1,
alignItems : 'center' alignItems : 'center'
}, },
buttons :
{
padding : theme.spacing(1)
},
green : green :
{ {
color : 'rgba(0, 153, 0, 1)' color : 'rgba(0, 153, 0, 1)'
@ -71,7 +76,9 @@ const ListMe = (props) =>
id : 'tooltip.raisedHand', id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand' defaultMessage : 'Raise hand'
})} })}
className={me.raisedHand ? classes.green : null} className={
classnames(me.raisedHand ? classes.green : null, classes.buttons)
}
disabled={me.raisedHandInProgress} disabled={me.raisedHandInProgress}
color='primary' color='primary'
onClick={(e) => onClick={(e) =>

View File

@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { green } from '@material-ui/core/colors';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import VideocamIcon from '@material-ui/icons/Videocam'; import VideocamIcon from '@material-ui/icons/Videocam';
@ -17,6 +18,7 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp'; import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import PanIcon from '@material-ui/icons/PanTool'; import PanIcon from '@material-ui/icons/PanTool';
import RecordVoiceOverIcon from '@material-ui/icons/RecordVoiceOver';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -31,7 +33,7 @@ const styles = (theme) =>
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem', height : '2rem',
marginTop : theme.spacing(1) marginTop : theme.spacing(0.5)
}, },
peerInfo : peerInfo :
{ {
@ -44,11 +46,16 @@ const styles = (theme) =>
indicators : indicators :
{ {
display : 'flex', display : 'flex',
padding : theme.spacing(1.5) padding : theme.spacing(1)
},
buttons :
{
padding : theme.spacing(1)
}, },
green : green :
{ {
color : 'rgba(0, 153, 0, 1)' color : 'rgba(0, 153, 0, 1)',
marginLeft : theme.spacing(2)
} }
}); });
@ -59,6 +66,7 @@ const ListPeer = (props) =>
const { const {
roomClient, roomClient,
isModerator, isModerator,
spotlight,
peer, peer,
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
@ -94,11 +102,30 @@ const ListPeer = (props) =>
<div className={classes.peerInfo}> <div className={classes.peerInfo}>
{peer.displayName} {peer.displayName}
</div> </div>
<div className={classes.indicators}>
{ peer.raisedHand && { peer.raisedHand &&
<PanIcon className={classes.green} /> <IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled={!isModerator || peer.raisedHandInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.lowerPeerHand(peer.id);
}}
>
<PanIcon />
</IconButton>
}
{ spotlight &&
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled
>
<RecordVoiceOverIcon />
</IconButton>
} }
</div>
{ screenConsumer && { screenConsumer &&
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
@ -114,6 +141,7 @@ const ListPeer = (props) =>
})} })}
color={screenVisible ? 'primary' : 'secondary'} color={screenVisible ? 'primary' : 'secondary'}
disabled={peer.peerScreenInProgress} disabled={peer.peerScreenInProgress}
className={classes.buttons}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -145,6 +173,7 @@ const ListPeer = (props) =>
})} })}
color={webcamEnabled ? 'primary' : 'secondary'} color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress} disabled={peer.peerVideoInProgress}
className={classes.buttons}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -175,6 +204,7 @@ const ListPeer = (props) =>
})} })}
color={micEnabled ? 'primary' : 'secondary'} color={micEnabled ? 'primary' : 'secondary'}
disabled={peer.peerAudioInProgress} disabled={peer.peerAudioInProgress}
className={classes.buttons}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -205,6 +235,7 @@ const ListPeer = (props) =>
defaultMessage : 'Kick out participant' defaultMessage : 'Kick out participant'
})} })}
disabled={peer.peerKickInProgress} disabled={peer.peerKickInProgress}
className={classes.buttons}
color='secondary' color='secondary'
onClick={(e) => onClick={(e) =>
{ {
@ -227,6 +258,7 @@ ListPeer.propTypes =
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool, isModerator : PropTypes.bool,
spotlight : PropTypes.bool,
peer : appPropTypes.Peer.isRequired, peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
passivePeersSelector, participantListSelector
spotlightSortedPeersSelector
} from '../../Selectors'; } from '../../Selectors';
import classNames from 'classnames'; import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Flipper, Flipped } from 'react-flip-toolkit';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer'; import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
@ -76,9 +76,9 @@ class ParticipantList extends React.PureComponent
roomClient, roomClient,
advancedMode, advancedMode,
isModerator, isModerator,
passivePeers, participants,
spotlights,
selectedPeerId, selectedPeerId,
spotlightPeers,
classes classes
} = this.props; } = this.props;
@ -107,50 +107,42 @@ class ParticipantList extends React.PureComponent
<ul className={classes.list}> <ul className={classes.list}>
<li className={classes.listheader}> <li className={classes.listheader}>
<FormattedMessage <FormattedMessage
id='room.spotlights' id='label.participants'
defaultMessage='Participants in Spotlight' defaultMessage='Participants'
/> />
</li> </li>
{ spotlightPeers.map((peer) => ( <Flipper
flipKey={participants}
>
{ participants.map((peer) => (
<Flipped key={peer.id} flipId={peer.id}>
<li <li
key={peer.id} key={peer.id}
className={classNames(classes.listItem, { className={classnames(classes.listItem, {
selected : peer.id === selectedPeerId selected : peer.id === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peer.id)} onClick={() => roomClient.setSelectedPeer(peer.id)}
> >
{ spotlights.includes(peer.id) ?
<ListPeer <ListPeer
id={peer.id} id={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
isModerator={isModerator} isModerator={isModerator}
spotlight
> >
<Volume small id={peer.id} /> <Volume small id={peer.id} />
</ListPeer> </ListPeer>
</li> :
))}
</ul>
<ul className={classes.list}>
<li className={classes.listheader}>
<FormattedMessage
id='room.passive'
defaultMessage='Passive Participants'
/>
</li>
{ passivePeers.map((peer) => (
<li
key={peer.id}
className={classNames(classes.listItem, {
selected : peer.id === selectedPeerId
})}
onClick={() => roomClient.setSelectedPeer(peer.id)}
>
<ListPeer <ListPeer
id={peer.id} id={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
isModerator={isModerator} isModerator={isModerator}
/> />
}
</li> </li>
</Flipped>
))} ))}
</Flipper>
</ul> </ul>
</div> </div>
); );
@ -162,9 +154,9 @@ ParticipantList.propTypes =
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool, isModerator : PropTypes.bool,
passivePeers : PropTypes.array, participants : PropTypes.array,
spotlights : PropTypes.array,
selectedPeerId : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
@ -174,9 +166,9 @@ const mapStateToProps = (state) =>
isModerator : isModerator :
state.me.roles.some((role) => state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)),
passivePeers : passivePeersSelector(state), participants : participantListSelector(state),
selectedPeerId : state.room.selectedPeerId, spotlights : state.room.spotlights,
spotlightPeers : spotlightSortedPeersSelector(state) selectedPeerId : state.room.selectedPeerId
}; };
}; };

View File

@ -17,6 +17,11 @@ const peersValueSelector = createSelector(
(peers) => Object.values(peers) (peers) => Object.values(peers)
); );
export const peersValueSelector = createSelector(
peersSelector,
(peers) => Object.values(peers)
);
export const lobbyPeersKeySelector = createSelector( export const lobbyPeersKeySelector = createSelector(
lobbyPeersSelector, lobbyPeersSelector,
(lobbyPeers) => Object.keys(lobbyPeers) (lobbyPeers) => Object.keys(lobbyPeers)
@ -113,10 +118,33 @@ export const spotlightPeersSelector = createSelector(
export const spotlightSortedPeersSelector = createSelector( export const spotlightSortedPeersSelector = createSelector(
spotlightsSelector, spotlightsSelector,
peersValueSelector, peersValueSelector,
(spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id)) (spotlights, peers) =>
peers.filter((peer) => spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
); );
const raisedHandSortedPeers = createSelector(
peersValueSelector,
(peers) => peers.filter((peer) => peer.raisedHand)
.sort((a, b) => a.raisedHandTimestamp - b.raisedHandTimestamp)
);
const peersSortedSelector = createSelector(
spotlightsSelector,
peersValueSelector,
(spotlights, peers) =>
peers.filter((peer) => !spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
export const participantListSelector = createSelector(
raisedHandSortedPeers,
spotlightSortedPeersSelector,
peersSortedSelector,
(raisedHands, spotlights, peers) =>
[ ...raisedHands, ...spotlights, ...peers ]
);
export const peersLengthSelector = createSelector( export const peersLengthSelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.values(peers).length (peers) => Object.values(peers).length

View File

@ -27,6 +27,12 @@ const peer = (state = {}, action) =>
raisedHandTimestamp : action.payload.raisedHandTimestamp raisedHandTimestamp : action.payload.raisedHandTimestamp
}; };
case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
return {
...state,
raisedHandInProgress : action.payload.flag
};
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
{ {
const consumers = [ ...state.consumers, action.payload.consumer.id ]; const consumers = [ ...state.consumers, action.payload.consumer.id ];
@ -91,6 +97,7 @@ const peers = (state = {}, action) =>
case 'SET_PEER_AUDIO_IN_PROGRESS': case 'SET_PEER_AUDIO_IN_PROGRESS':
case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS':
case 'SET_PEER_RAISED_HAND': case 'SET_PEER_RAISED_HAND':
case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
case 'SET_PEER_PICTURE': case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE': case 'ADD_PEER_ROLE':

View File

@ -117,6 +117,8 @@ class Room extends EventEmitter
this._peers = {}; this._peers = {};
this._selfDestructTimeout = null;
// Array of mediasoup Router instances. // Array of mediasoup Router instances.
this._mediasoupRouters = mediasoupRouters; this._mediasoupRouters = mediasoupRouters;
@ -146,6 +148,11 @@ class Room extends EventEmitter
this._closed = true; this._closed = true;
if (this._selfDestructTimeout)
clearTimeout(this._selfDestructTimeout);
this._selfDestructTimeout = null;
this._chatHistory = null; this._chatHistory = null;
this._fileHistory = null; this._fileHistory = null;
@ -411,7 +418,10 @@ class Room extends EventEmitter
{ {
logger.debug('selfDestructCountdown() started'); logger.debug('selfDestructCountdown() started');
setTimeout(() => if (this._selfDestructTimeout)
clearTimeout(this._selfDestructTimeout);
this._selfDestructTimeout = setTimeout(() =>
{ {
if (this._closed) if (this._closed)
return; return;
@ -1430,6 +1440,29 @@ class Room extends EventEmitter
break; break;
} }
case 'moderator:lowerHand':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized');
const { peerId } = request.data;
const lowerPeer = this._peers[peerId];
if (!lowerPeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(lowerPeer.socket, 'moderator:lowerHand');
cb();
break;
}
default: default:
{ {
logger.error('unknown request.method "%s"', request.method); logger.error('unknown request.method "%s"', request.method);