Merge remote-tracking branch 'origin/develop' into feat-audio-settings

auto_join_3.3
Stefan Otto 2020-05-14 04:13:35 +02:00
commit b71731c62e
63 changed files with 3102 additions and 1250 deletions

View File

@ -53,18 +53,26 @@ var config =
voiceActivityMute : false,
sampleSize : 16
},
background : 'images/background.jpg',
defaultLayout : 'democratic', // democratic, filmstrip
lastN : 4,
mobileLastN : 1,
background : 'images/background.jpg',
defaultLayout : 'democratic', // democratic, filmstrip
// If true, will show media control buttons in separate
// control bar, not in the ME container.
buttonControlBar : false,
// If false, will push videos away to make room for side
// drawer. If true, will overlay side drawer over videos
drawerOverlayed : true,
// Timeout for autohiding topbar and button control bar
hideTimeout : 3000,
lastN : 4,
mobileLastN : 1,
// Highest number of speakers user can select
maxLastN : 5,
maxLastN : 5,
// If truthy, users can NOT change number of speakers visible
lockLastN : false,
lockLastN : false,
// Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg',
title : 'Multiparty meeting',
theme :
title : 'Multiparty meeting',
theme :
{
palette :
{

View File

@ -129,7 +129,8 @@ export default class RoomClient
produce,
forceTcp,
displayName,
muted
muted,
basePath
} = {})
{
if (!peerId)
@ -152,6 +153,9 @@ export default class RoomClient
// Whether we force TCP
this._forceTcp = forceTcp;
// URL basepath
this._basePath = basePath;
// Use displayName
if (displayName)
store.dispatch(settingsActions.setDisplayName(displayName));
@ -298,6 +302,16 @@ export default class RoomClient
switch (key)
{
case String.fromCharCode(37):
{
this._spotlights.setPrevAsSelected();
break;
}
case String.fromCharCode(39):
{
this._spotlights.setNextAsSelected();
break;
}
case 'A': // Activate advanced mode
{
store.dispatch(settingsActions.toggleAdvancedMode());
@ -404,6 +418,13 @@ export default class RoomClient
break;
}
case 'H': // Open help dialog
{
store.dispatch(roomActions.setHelpOpen(true));
break;
}
default:
{
break;
@ -936,14 +957,10 @@ export default class RoomClient
{
if (consumer.kind === 'video')
{
if (spotlights.indexOf(consumer.appData.peerId) > -1)
{
if (spotlights.includes(consumer.appData.peerId))
await this._resumeConsumer(consumer);
}
else
{
await this._pauseConsumer(consumer);
}
}
}
}
@ -1385,6 +1402,46 @@ export default class RoomClient
peerActions.setPeerKickInProgress(peerId, false));
}
async mutePeer(peerId)
{
logger.debug('mutePeer() [peerId:"%s"]', peerId);
store.dispatch(
peerActions.setMutePeerInProgress(peerId, true));
try
{
await this.sendRequest('moderator:mute', { peerId });
}
catch (error)
{
logger.error('mutePeer() failed: %o', error);
}
store.dispatch(
peerActions.setMutePeerInProgress(peerId, false));
}
async stopPeerVideo(peerId)
{
logger.debug('stopPeerVideo() [peerId:"%s"]', peerId);
store.dispatch(
peerActions.setStopPeerVideoInProgress(peerId, true));
try
{
await this.sendRequest('moderator:stopVideo', { peerId });
}
catch (error)
{
logger.error('stopPeerVideo() failed: %o', error);
}
store.dispatch(
peerActions.setStopPeerVideoInProgress(peerId, false));
}
async muteAllPeers()
{
logger.debug('muteAllPeers()');
@ -1472,9 +1529,7 @@ export default class RoomClient
if (consumer.appData.peerId === peerId && consumer.appData.source === type)
{
if (mute)
{
await this._pauseConsumer(consumer);
}
else
await this._resumeConsumer(consumer);
}
@ -1802,6 +1857,11 @@ export default class RoomClient
this._recvTransport = null;
}
this._spotlights.clearSpotlights();
store.dispatch(peerActions.clearPeers());
store.dispatch(consumerActions.clearConsumers());
store.dispatch(roomActions.clearSpotlights());
store.dispatch(roomActions.setRoomState('connecting'));
});
@ -2070,15 +2130,21 @@ export default class RoomClient
lobbyPeers.forEach((peer) =>
{
store.dispatch(
lobbyPeerActions.addLobbyPeer(peer.peerId));
lobbyPeerActions.addLobbyPeer(peer.id));
store.dispatch(
lobbyPeerActions.setLobbyPeerDisplayName(
peer.displayName,
peer.peerId
peer.id
)
);
store.dispatch(
lobbyPeerActions.setLobbyPeerPicture(peer.picture));
lobbyPeerActions.setLobbyPeerPicture(
peer.picture,
peer.id
)
);
});
store.dispatch(
@ -2506,8 +2572,6 @@ export default class RoomClient
case 'moderator:mute':
{
// const { peerId } = notification.data;
if (this._micProducer && !this._micProducer.paused)
{
this.muteMic();
@ -2526,8 +2590,6 @@ export default class RoomClient
case 'moderator:stopVideo':
{
// const { peerId } = notification.data;
this.disableWebcam();
this.disableScreenSharing();
@ -2792,8 +2854,8 @@ export default class RoomClient
roles,
peers,
tracker,
permissionsFromRoles,
userRoles,
roomPermissions,
allowWhenRoleMissing,
chatHistory,
fileHistory,
lastNHistory,
@ -2819,8 +2881,10 @@ export default class RoomClient
store.dispatch(meActions.loggedIn(authenticated));
store.dispatch(roomActions.setUserRoles(userRoles));
store.dispatch(roomActions.setPermissionsFromRoles(permissionsFromRoles));
store.dispatch(roomActions.setRoomPermissions(roomPermissions));
if (allowWhenRoleMissing)
store.dispatch(roomActions.setAllowWhenRoleMissing(allowWhenRoleMissing));
const myRoles = store.getState().me.roles;
@ -2878,11 +2942,11 @@ export default class RoomClient
(lobbyPeers.length > 0) && lobbyPeers.forEach((peer) =>
{
store.dispatch(
lobbyPeerActions.addLobbyPeer(peer.peerId));
lobbyPeerActions.addLobbyPeer(peer.id));
store.dispatch(
lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.peerId));
lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.id));
store.dispatch(
lobbyPeerActions.setLobbyPeerPicture(peer.picture));
lobbyPeerActions.setLobbyPeerPicture(peer.picture, peer.id));
});
(accessCode != null) && store.dispatch(

View File

@ -12,6 +12,7 @@ export default class Spotlights extends EventEmitter
this._signalingSocket = signalingSocket;
this._maxSpotlights = maxSpotlights;
this._peerList = [];
this._unmutablePeerList = [];
this._selectedSpotlights = [];
this._currentSpotlights = [];
this._started = false;
@ -45,6 +46,66 @@ export default class Spotlights extends EventEmitter
}
}
setNextAsSelected()
{
let peerId = null;
if (this._selectedSpotlights.length > 0) {
peerId = this._selectedSpotlights[0];
} else if (this._unmutablePeerList.length > 0) {
peerId = this._unmutablePeerList[0];
}
if (peerId != null && this._currentSpotlights.length < this._unmutablePeerList.length) {
let oldIndex = this._unmutablePeerList.indexOf(peerId);
let index = oldIndex;
index++;
do {
if (index >= this._unmutablePeerList.length) {
index = 0;
}
let newSelectedPeer = this._unmutablePeerList[index];
if (!this._currentSpotlights.includes(newSelectedPeer)) {
this.setPeerSpotlight(newSelectedPeer);
break;
}
index++;
if (index === oldIndex) {
break;
}
} while (true);
}
}
setPrevAsSelected()
{
let peerId = null;
if (this._selectedSpotlights.length > 0) {
peerId = this._selectedSpotlights[0];
} else if (this._unmutablePeerList.length > 0) {
peerId = this._unmutablePeerList[0];
}
if (peerId != null && this._currentSpotlights.length < this._unmutablePeerList.length) {
let oldIndex = this._unmutablePeerList.indexOf(peerId);
let index = oldIndex;
index--;
do {
if (index < 0) {
index = this._unmutablePeerList.length - 1;
}
let newSelectedPeer = this._unmutablePeerList[index];
if (!this._currentSpotlights.includes(newSelectedPeer)) {
this.setPeerSpotlight(newSelectedPeer);
break;
}
index--;
if (index === oldIndex) {
break;
}
} while (true);
}
}
setPeerSpotlight(peerId)
{
logger.debug('setPeerSpotlight() [peerId:"%s"]', peerId);
@ -95,6 +156,15 @@ export default class Spotlights extends EventEmitter
});
}
clearSpotlights()
{
this._started = false;
this._peerList = [];
this._selectedSpotlights = [];
this._currentSpotlights = [];
}
_newPeer(id)
{
logger.debug(
@ -105,6 +175,7 @@ export default class Spotlights extends EventEmitter
logger.debug('_handlePeer() | adding peer [peerId: "%s"]', id);
this._peerList.push(id);
this._unmutablePeerList.push(id);
if (this._started)
this._spotlightsUpdated();
@ -117,6 +188,7 @@ export default class Spotlights extends EventEmitter
'room "peerClosed" event [peerId:%o]', id);
this._peerList = this._peerList.filter((peer) => peer !== id);
this._unmutablePeerList = this._unmutablePeerList.filter((peer) => peer !== id);
this._selectedSpotlights = this._selectedSpotlights.filter((peer) => peer !== id);

View File

@ -10,6 +10,11 @@ export const removeConsumer = (consumerId, peerId) =>
payload : { consumerId, peerId }
});
export const clearConsumers = () =>
({
type : 'CLEAR_CONSUMERS'
});
export const setConsumerPaused = (consumerId, originator) =>
({
type : 'SET_CONSUMER_PAUSED',

View File

@ -10,6 +10,11 @@ export const removePeer = (peerId) =>
payload : { peerId }
});
export const clearPeers = () =>
({
type : 'CLEAR_PEERS'
});
export const setPeerDisplayName = (displayName, peerId) =>
({
type : 'SET_PEER_DISPLAY_NAME',
@ -69,3 +74,15 @@ export const setPeerKickInProgress = (peerId, flag) =>
type : 'SET_PEER_KICK_IN_PROGRESS',
payload : { peerId, flag }
});
export const setMutePeerInProgress = (peerId, flag) =>
({
type : 'STOP_PEER_AUDIO_IN_PROGRESS',
payload : { peerId, flag }
});
export const setStopPeerVideoInProgress = (peerId, flag) =>
({
type : 'STOP_PEER_VIDEO_IN_PROGRESS',
payload : { peerId, flag }
});

View File

@ -70,6 +70,18 @@ export const setExtraVideoOpen = (extraVideoOpen) =>
payload : { extraVideoOpen }
});
export const setHelpOpen = (helpOpen) =>
({
type : 'SET_HELP_OPEN',
payload : { helpOpen }
});
export const setAboutOpen = (aboutOpen) =>
({
type : 'SET_ABOUT_OPEN',
payload : { aboutOpen }
});
export const setSettingsTab = (tab) =>
({
type : 'SET_SETTINGS_TAB',
@ -118,6 +130,11 @@ export const setSpotlights = (spotlights) =>
payload : { spotlights }
});
export const clearSpotlights = () =>
({
type : 'CLEAR_SPOTLIGHTS'
});
export const toggleJoined = () =>
({
type : 'TOGGLE_JOINED'
@ -165,14 +182,14 @@ export const setClearFileSharingInProgress = (flag) =>
payload : { flag }
});
export const setUserRoles = (userRoles) =>
export const setRoomPermissions = (roomPermissions) =>
({
type : 'SET_USER_ROLES',
payload : { userRoles }
type : 'SET_ROOM_PERMISSIONS',
payload : { roomPermissions }
});
export const setPermissionsFromRoles = (permissionsFromRoles) =>
export const setAllowWhenRoleMissing = (allowWhenRoleMissing) =>
({
type : 'SET_PERMISSIONS_FROM_ROLES',
payload : { permissionsFromRoles }
type : 'SET_ALLOW_WHEN_ROLE_MISSING',
payload : { allowWhenRoleMissing }
});

View File

@ -38,6 +38,16 @@ export const togglePermanentTopBar = () =>
type : 'TOGGLE_PERMANENT_TOPBAR'
});
export const toggleButtonControlBar = () =>
({
type : 'TOGGLE_BUTTON_CONTROL_BAR'
});
export const toggleDrawerOverlayed = () =>
({
type : 'TOGGLE_DRAWER_OVERLAYED'
});
export const toggleShowNotifications = () =>
({
type : 'TOGGLE_SHOW_NOTIFICATIONS'

View File

@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton';
@ -85,28 +87,32 @@ ListLobbyPeer.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state, { id }) =>
const makeMapStateToProps = (initialState, { id }) =>
{
return {
peer : state.lobbyPeers[id],
promotionInProgress : state.room.lobbyPeersPromotionInProgress,
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
{
return {
peer : state.lobbyPeers[id],
promotionInProgress : state.room.lobbyPeersPromotionInProgress,
canPromote : hasPermission(state)
};
};
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room.lobbyPeersPromotionInProgress ===
next.room.lobbyPeersPromotionInProgress &&
prev.room === next.room &&
prev.peers === next.peers && // For checking permissions
prev.me.roles === next.me.roles &&
prev.lobbyPeers === next.lobbyPeers
);

View File

@ -1,8 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import {
lobbyPeersKeySelector
lobbyPeersKeySelector,
makePermissionSelector
} from '../../Selectors';
import { permissions } from '../../../permissions';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
@ -140,15 +142,20 @@ LockDialog.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
const makeMapStateToProps = () =>
{
return {
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state),
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
{
return {
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state),
canPromote : hasPermission(state)
};
};
return mapStateToProps;
};
const mapDispatchToProps = {
@ -157,7 +164,7 @@ const mapDispatchToProps = {
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
mapDispatchToProps,
null,
{
@ -166,6 +173,7 @@ export default withRoomContext(connect(
return (
prev.room === next.room &&
prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers
);
}

View File

@ -1,6 +1,10 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors';
import {
meProducersSelector,
makePermissionSelector
} from '../Selectors';
import { permissions } from '../../permissions';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
@ -79,6 +83,28 @@ const styles = (theme) =>
width : '100%',
height : '100%'
},
meTag :
{
position : 'absolute',
float : 'left',
top : '50%',
left : '50%',
transform : 'translate(-50%, -50%)',
color : 'rgba(255, 255, 255, 0.5)',
fontSize : '7em',
zIndex : 30,
margin : 0,
opacity : 0,
transition : 'opacity 0.1s ease-in-out',
'&.hover' :
{
opacity : 1
},
'&.smallContainer' :
{
fontSize : '3em'
}
},
controls :
{
position : 'absolute',
@ -100,39 +126,18 @@ const styles = (theme) =>
'&.hover' :
{
opacity : 1
},
'& p' :
{
position : 'absolute',
float : 'left',
top : '50%',
left : '50%',
transform : 'translate(-50%, -50%)',
color : 'rgba(255, 255, 255, 0.5)',
fontSize : '7em',
margin : 0,
opacity : 0,
transition : 'opacity 0.1s ease-in-out',
'&.hover' :
{
opacity : 1
},
'&.smallContainer' :
{
fontSize : '3em'
}
}
},
ptt :
{
position : 'absolute',
float : 'left',
top : '10%',
top : '25%',
left : '50%',
transform : 'translate(-50%, 0%)',
color : 'rgba(255, 255, 255, 0.7)',
fontSize : '2vs',
backgroundColor : 'rgba(255, 0, 0, 0.5)',
fontSize : '1.3em',
backgroundColor : 'rgba(255, 0, 0, 0.9)',
margin : '4px',
padding : theme.spacing(2),
zIndex : 31,
@ -285,6 +290,28 @@ const Me = (props) =>
'margin' : spacing
};
let audioScore = null;
if (micProducer && micProducer.score)
{
audioScore =
micProducer.score.reduce(
(prev, curr) =>
(prev.score < curr.score ? prev : curr)
);
}
let videoScore = null;
if (webcamProducer && webcamProducer.score)
{
videoScore =
webcamProducer.score.reduce(
(prev, curr) =>
(prev.score < curr.score ? prev : curr)
);
}
return (
<React.Fragment>
<div
@ -317,263 +344,273 @@ const Me = (props) =>
}}
style={spacingStyle}
>
{ me.browser.platform !== 'mobile' &&
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
}
<div className={classes.viewContainer} style={style}>
{ !smallContainer &&
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
<p className={
classnames(
classes.meTag,
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
}
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p className={
classnames(
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
{ !settings.buttonControlBar &&
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
<React.Fragment>
<Tooltip title={micTip} placement='left'>
<div>
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<React.Fragment>
<Tooltip title={micTip} placement='left'>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.smallContainer}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
}
</div>
</Tooltip>
<Tooltip title={webcamTip} placement='left'>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
}
</div>
</Tooltip>
{ me.browser.platform !== 'mobile' &&
<Tooltip title={screenTip} placement='left'>
<div>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.smallContainer}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color='primary'
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</IconButton>
:
</div>
:
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color={screenState === 'on' ? 'primary' : 'default'}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
}
</div>
</div>
}
</Tooltip>
}
</React.Fragment>
</div>
<Tooltip title={webcamTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</IconButton>
</div>
:
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
</div>
}
</Tooltip>
{ me.browser.platform !== 'mobile' &&
<Tooltip title={screenTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.smallContainer}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color='primary'
size='small'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</IconButton>
</div>
:
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.fab}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color={screenState === 'on' ? 'primary' : 'default'}
size='large'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</Fab>
</div>
}
</Tooltip>
}
</React.Fragment>
</div>
}
<VideoView
isMe
VideoView
advancedMode={advancedMode}
peer={me}
displayName={settings.displayName}
@ -582,6 +619,8 @@ const Me = (props) =>
videoVisible={videoVisible}
audioCodec={micProducer && micProducer.codec}
videoCodec={webcamProducer && webcamProducer.codec}
audioScore={audioScore}
videoScore={videoScore}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
@ -625,6 +664,18 @@ const Me = (props) =>
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<p className={
classnames(
classes.meTag,
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<div
className={classnames(
classes.controls,
@ -651,16 +702,9 @@ const Me = (props) =>
}, 2000);
}}
>
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={webcamTip} placement='left'>
<div>
{ smallContainer ?
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.stopVideo',
@ -678,7 +722,9 @@ const Me = (props) =>
<VideoIcon />
</IconButton>
:
</div>
:
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.stopVideo',
@ -694,8 +740,8 @@ const Me = (props) =>
>
<VideoIcon />
</Fab>
}
</div>
</div>
}
</Tooltip>
</div>
@ -742,40 +788,18 @@ const Me = (props) =>
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
<p className={
classnames(
classes.meTag,
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
</div>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<VideoView
isMe
@ -812,32 +836,37 @@ Me.propTypes =
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
const makeMapStateToProps = () =>
{
return {
me : state.me,
...meProducersSelector(state),
settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_SCREEN.includes(role))
const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN);
const mapStateToProps = (state) =>
{
return {
me : state.me,
...meProducersSelector(state),
settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen : hasPermission(state)
};
};
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room === next.room &&
prev.me === next.me &&
prev.peers === next.peers &&
prev.producers === next.producers &&
prev.settings === next.settings &&
prev.room.activeSpeakerId === next.room.activeSpeakerId
prev.settings === next.settings
);
}
}

View File

@ -249,55 +249,53 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.smallContainer}
disabled={!micConsumer}
color='primary'
size='small'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</Fab>
}
</div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.smallContainer}
disabled={!micConsumer}
color='primary'
size='small'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</Fab>
}
</Tooltip>
{ browser.platform !== 'mobile' &&
@ -308,48 +306,46 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</Fab>
}
</div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</Fab>
}
</Tooltip>
}
@ -360,46 +356,45 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</Fab>
}
</div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</Fab>
}
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
@ -507,48 +502,46 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
}
</div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
}
</Tooltip>
}
@ -559,46 +552,45 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
}
</div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
}
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
@ -694,26 +686,24 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</Tooltip>
}
@ -724,26 +714,25 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
videoContain
consumerSpatialLayers={

View File

@ -0,0 +1,133 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import Link from '@material-ui/core/Link';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
dialogPaper :
{
width : '30vw',
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
logo :
{
marginRight : 'auto'
},
link :
{
display : 'block',
textAlign : 'center'
}
});
const About = ({
aboutOpen,
handleCloseAbout,
classes
}) =>
{
return (
<Dialog
open={aboutOpen}
onClose={() => handleCloseAbout(false)}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.about'
defaultMessage='About'
/>
</DialogTitle>
<DialogContent dividers='true'>
<DialogContentText>
Contributions to this work were made on behalf of the GÉANT
project, a project that has received funding from the
European Unions Horizon 2020 research and innovation
programme under Grant Agreement No. 731122 (GN4-2).
On behalf of GÉANT project, GÉANT Association is the sole
owner of the copyright in all material which was developed
by a member of the GÉANT project.<br />
<br />
GÉANT Vereniging (Association) is registered with the
Chamber of Commerce in Amsterdam with registration number
40535155 and operates in the UK as a branch of GÉANT
Vereniging. Registered office: Hoekenrode 3, 1102BR
Amsterdam, The Netherlands. UK branch address: City House,
126-130 Hills Road, Cambridge CB2 1PQ, UK.
</DialogContentText>
<Link href='https://edumeet.org' target='_blank' rel='noreferrer' color='secondary' variant='h6' className={classes.link}>
https://edumeet.org
</Link>
</DialogContent>
<DialogActions>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Button onClick={() => { handleCloseAbout(false); }} color='primary'>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>
);
};
About.propTypes =
{
roomClient : PropTypes.object.isRequired,
aboutOpen : PropTypes.bool.isRequired,
handleCloseAbout : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
aboutOpen : state.room.aboutOpen
});
const mapDispatchToProps = {
handleCloseAbout : roomActions.setAboutOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.aboutOpen === next.room.aboutOpen
);
}
}
)(withStyles(styles)(About)));

View File

@ -0,0 +1,311 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors';
import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { useIntl } from 'react-intl';
import Fab from '@material-ui/core/Fab';
import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VideoIcon from '@material-ui/icons/Videocam';
import VideoOffIcon from '@material-ui/icons/VideocamOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
const styles = (theme) =>
({
root :
{
position : 'fixed',
display : 'flex',
[theme.breakpoints.up('md')] :
{
top : '50%',
transform : 'translate(0%, -50%)',
flexDirection : 'column',
justifyContent : 'center',
alignItems : 'center',
left : theme.spacing(1)
},
[theme.breakpoints.down('sm')] :
{
flexDirection : 'row',
bottom : theme.spacing(1),
left : '50%',
transform : 'translate(-50%, -0%)'
}
},
fab :
{
margin : theme.spacing(1)
},
show :
{
opacity : 1,
transition : 'opacity .5s'
},
hide :
{
opacity : 0,
transition : 'opacity .5s'
}
});
const ButtonControlBar = (props) =>
{
const intl = useIntl();
const {
roomClient,
toolbarsVisible,
hiddenControls,
me,
micProducer,
webcamProducer,
screenProducer,
classes,
theme
} = props;
let micState;
let micTip;
if (!me.canSendMic)
{
micState = 'unsupported';
micTip = intl.formatMessage({
id : 'device.audioUnsupported',
defaultMessage : 'Audio unsupported'
});
}
else if (!micProducer)
{
micState = 'off';
micTip = intl.formatMessage({
id : 'device.activateAudio',
defaultMessage : 'Activate audio'
});
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
});
}
else
{
micState = 'muted';
micTip = intl.formatMessage({
id : 'device.unMuteAudio',
defaultMessage : 'Unmute audio'
});
}
let webcamState;
let webcamTip;
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = intl.formatMessage({
id : 'device.videoUnsupported',
defaultMessage : 'Video unsupported'
});
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
});
}
else
{
webcamState = 'off';
webcamTip = intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
});
}
let screenState;
let screenTip;
if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = intl.formatMessage({
id : 'device.screenSharingUnsupported',
defaultMessage : 'Screen sharing not supported'
});
}
else if (screenProducer)
{
screenState = 'on';
screenTip = intl.formatMessage({
id : 'device.stopScreenSharing',
defaultMessage : 'Stop screen sharing'
});
}
else
{
screenState = 'off';
screenTip = intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
});
}
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<div
className={
classnames(
classes.root,
hiddenControls ?
(toolbarsVisible ? classes.show : classes.hide) :
classes.show)
}
>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
micState === 'on' ?
roomClient.muteMic() :
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
</Tooltip>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
</Tooltip>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
<Fab
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'primary' : 'default'}
size={smallScreen ? 'large' : 'medium'}
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ screenState === 'on' || screenState === 'unsupported' ?
<ScreenOffIcon/>
:null
}
{ screenState === 'off' ?
<ScreenIcon/>
:null
}
</Fab>
</Tooltip>
</div>
);
};
ButtonControlBar.propTypes =
{
roomClient : PropTypes.any.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
hiddenControls : PropTypes.bool.isRequired,
me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
toolbarsVisible : state.room.toolbarsVisible,
hiddenControls : state.settings.hiddenControls,
...meProducersSelector(state),
me : state.me
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.settings.hiddenControls === next.settings.hiddenControls &&
prev.producers === next.producers &&
prev.me === next.me
);
}
}
)(withStyles(styles, { withTheme: true })(ButtonControlBar)));

View File

@ -0,0 +1,170 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
const shortcuts=[
{ key: 'h', label: 'room.help', defaultMessage: 'Help' },
{ key: 'm', label: 'device.muteAudio', defaultMessage: 'Mute Audio' },
{ key: 'v', label: 'device.stopVideo', defaultMessage: 'Mute Video' },
{ key: '1', label: 'label.democratic', defaultMessage: 'Democratic View' },
{ key: '2', label: 'label.filmstrip', defaultMessage: 'Filmstrip View' },
{ key: 'space', label: 'me.mutedPTT', defaultMessage: 'Push SPACE to talk' },
{ key: 'a', label: 'label.advanced', defaultMessage: 'Show advanced information' },
{ key: String.fromCharCode(8592)+' '+String.fromCharCode(8594), label: 'room.browsePeersSpotlight', defaultMessage: 'Browse participants into Spotlight' }
];
const styles = (theme) =>
({
dialogPaper :
{
width : '30vw',
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
},
display : 'flex',
flexDirection : 'column'
},
paper : {
padding : theme.spacing(1),
textAlign : 'center',
color : theme.palette.text.secondary,
whiteSpace : 'nowrap',
marginRight : theme.spacing(3),
marginBottom : theme.spacing(1),
minWidth : theme.spacing(8)
},
shortcuts : {
display : 'flex',
flexDirection : 'row',
alignItems : 'center'
},
tabsHeader :
{
flexGrow : 1
}
});
const Help = ({
helpOpen,
handleCloseHelp,
classes
}) =>
{
const intl = useIntl();
return (
<Dialog
open={helpOpen}
onClose={() => { handleCloseHelp(false); }}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.help'
defaultMessage='Help'
/>
</DialogTitle>
<Tabs
className={classes.tabsHeader}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
>
<Tab
label={
intl.formatMessage({
id : 'room.shortcutKeys',
defaultMessage : 'Shortcut keys'
})
}
/>
</Tabs>
<DialogContent dividers='true'>
<DialogContentText>
{shortcuts.map((value, index) =>
{
return (
<div key={index} className={classes.shortcuts}>
<Paper className={classes.paper}>
{value.key}
</Paper>
<FormattedMessage
id={value.label}
defaultMessage={value.defaultMessage}
/>
</div>
);
})}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => { handleCloseHelp(false); }} color='primary'>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>
);
};
Help.propTypes =
{
roomClient : PropTypes.object.isRequired,
helpOpen : PropTypes.bool.isRequired,
handleCloseHelp : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
helpOpen : state.room.helpOpen
});
const mapDispatchToProps = {
handleCloseHelp : roomActions.setHelpOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.helpOpen === next.room.helpOpen
);
}
}
)(withStyles(styles)(Help)));

View File

@ -4,14 +4,17 @@ import PropTypes from 'prop-types';
import {
lobbyPeersKeySelector,
peersLengthSelector,
raisedHandsSelector
raisedHandsSelector,
makePermissionSelector
} from '../Selectors';
import { permissions } from '../../permissions';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
@ -36,9 +39,36 @@ import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
import MoreIcon from '@material-ui/icons/MoreVert';
import HelpIcon from '@material-ui/icons/Help';
import InfoIcon from '@material-ui/icons/Info';
const styles = (theme) =>
({
persistentDrawerOpen :
{
width : 'calc(100% - 30vw)',
marginLeft : '30vw',
[theme.breakpoints.down('lg')] :
{
width : 'calc(100% - 40vw)',
marginLeft : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : 'calc(100% - 50vw)',
marginLeft : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : 'calc(100% - 70vw)',
marginLeft : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : 'calc(100% - 90vw)',
marginLeft : '90vw'
}
},
menuButton :
{
margin : 0,
@ -184,6 +214,9 @@ const TopBar = (props) =>
peersLength,
lobbyPeers,
permanentTopBar,
drawerOverlayed,
toolAreaOpen,
isMobile,
myPicture,
loggedIn,
loginEnabled,
@ -192,6 +225,8 @@ const TopBar = (props) =>
onFullscreen,
setSettingsOpen,
setExtraVideoOpen,
setHelpOpen,
setAboutOpen,
setLockDialogOpen,
toggleToolArea,
openUsersTab,
@ -242,7 +277,12 @@ const TopBar = (props) =>
<React.Fragment>
<AppBar
position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
className={classnames(
room.toolbarsVisible || permanentTopBar ?
classes.show : classes.hide,
!(isMobile || drawerOverlayed) && toolAreaOpen ?
classes.persistentDrawerOpen : null
)}
>
<Toolbar>
<PulsingBadge
@ -272,18 +312,25 @@ const TopBar = (props) =>
</Typography>
<div className={classes.grow} />
<div className={classes.sectionDesktop}>
<IconButton
aria-owns={
isMenuOpen &&
currentMenu === 'moreActions' ?
'material-appbar' : undefined
}
aria-haspopup='true'
onClick={(event) => handleMenuOpen(event, 'moreActions')}
color='inherit'
<Tooltip
title={intl.formatMessage({
id : 'label.moreActions',
defaultMessage : 'More actions'
})}
>
<ExtensionIcon />
</IconButton>
<IconButton
aria-owns={
isMenuOpen &&
currentMenu === 'moreActions' ?
'material-appbar' : undefined
}
aria-haspopup='true'
onClick={(event) => handleMenuOpen(event, 'moreActions')}
color='inherit'
>
<ExtensionIcon />
</IconButton>
</Tooltip>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
@ -483,6 +530,46 @@ const TopBar = (props) =>
/>
</p>
</MenuItem>
<MenuItem
onClick={() =>
{
handleMenuClose();
setHelpOpen(!room.helpOpen);
}}
>
<HelpIcon
aria-label={intl.formatMessage({
id : 'room.help',
defaultMessage : 'Help'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='room.help'
defaultMessage='Help'
/>
</p>
</MenuItem>
<MenuItem
onClick={() =>
{
handleMenuClose();
setAboutOpen(!room.aboutOpen);
}}
>
<InfoIcon
aria-label={intl.formatMessage({
id : 'room.about',
defaultMessage : 'About'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='room.about'
defaultMessage='About'
/>
</p>
</MenuItem>
</Paper>
}
</Popover>
@ -682,9 +769,12 @@ TopBar.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
isMobile : PropTypes.bool.isRequired,
peersLength : PropTypes.number,
lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool,
permanentTopBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
@ -694,6 +784,8 @@ TopBar.propTypes =
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired,
setHelpOpen : PropTypes.func.isRequired,
setAboutOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
@ -705,27 +797,38 @@ TopBar.propTypes =
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
room : state.room,
peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)),
canLock :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)),
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
});
const makeMapStateToProps = () =>
{
const hasExtraVideoPermission =
makePermissionSelector(permissions.EXTRA_VIDEO);
const hasLockPermission =
makePermissionSelector(permissions.CHANGE_ROOM_LOCK);
const hasPromotionPermission =
makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
({
room : state.room,
isMobile : state.me.browser.platform === 'mobile',
peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar,
drawerOverlayed : state.settings.drawerOverlayed,
toolAreaOpen : state.toolarea.toolAreaOpen,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo : hasExtraVideoPermission(state),
canLock : hasLockPermission(state),
canPromote : hasPromotionPermission(state)
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) =>
({
@ -741,6 +844,14 @@ const mapDispatchToProps = (dispatch) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
},
setHelpOpen : (helpOpen) =>
{
dispatch(roomActions.setHelpOpen(helpOpen));
},
setAboutOpen : (aboutOpen) =>
{
dispatch(roomActions.setAboutOpen(aboutOpen));
},
setLockDialogOpen : (lockDialogOpen) =>
{
dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
@ -757,7 +868,7 @@ const mapDispatchToProps = (dispatch) =>
});
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
mapDispatchToProps,
null,
{
@ -768,12 +879,15 @@ export default withRoomContext(connect(
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.drawerOverlayed === next.settings.drawerOverlayed &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.browser === next.me.browser &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture &&
prev.me.roles === next.me.roles &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles
prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}
}

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton';
@ -119,26 +121,32 @@ ChatInput.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
displayName : state.settings.displayName,
picture : state.me.picture,
canChat :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SEND_CHAT.includes(role))
});
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SEND_CHAT);
const mapStateToProps = (state) =>
({
displayName : state.settings.displayName,
picture : state.me.picture,
canChat : hasPermission(state)
});
return mapStateToProps;
};
export default withRoomContext(
connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room === next.room &&
prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.settings.displayName === next.settings.displayName &&
prev.me.picture === next.me.picture
);

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
@ -76,16 +78,21 @@ ChatModerator.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isChatModerator :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)),
room : state.room
});
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT);
const mapStateToProps = (state) =>
({
isChatModerator : hasPermission(state),
room : state.room
});
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{
return (
prev.room === next.room &&
prev.me === next.me
prev.me === next.me &&
prev.peers === next.peers
);
}
}

View File

@ -4,6 +4,8 @@ import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import FileList from './FileList';
import FileSharingModerator from './FileSharingModerator';
import Paper from '@material-ui/core/Paper';
@ -131,30 +133,36 @@ FileSharing.propTypes = {
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
const makeMapStateToProps = () =>
{
return {
canShareFiles : state.me.canShareFiles,
browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files',
canShare :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_FILE.includes(role))
const hasPermission = makePermissionSelector(permissions.SHARE_FILE);
const mapStateToProps = (state) =>
{
return {
canShareFiles : state.me.canShareFiles,
browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files',
canShare : hasPermission(state)
};
};
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room === next.room &&
prev.me.browser === next.me.browser &&
prev.me.roles === next.me.roles &&
prev.me.canShareFiles === next.me.canShareFiles &&
prev.peers === next.peers &&
prev.toolarea.currentToolTab === next.toolarea.currentToolTab
);
}

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
@ -76,16 +78,21 @@ FileSharingModerator.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isFileSharingModerator :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_FILES.includes(role)),
room : state.room
});
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_FILES);
const mapStateToProps = (state) =>
({
isFileSharingModerator : hasPermission(state),
room : state.room
});
return mapStateToProps;
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{
return (
prev.room === next.room &&
prev.me === next.me
prev.me === next.me &&
prev.peers === next.peers
);
}
}

View File

@ -11,6 +11,8 @@ import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import VideocamIcon from '@material-ui/icons/Videocam';
import VideocamOffIcon from '@material-ui/icons/VideocamOff';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
@ -126,7 +128,7 @@ const ListPeer = (props) =>
<RecordVoiceOverIcon />
</IconButton>
}
{ screenConsumer &&
{ screenConsumer && spotlight &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteScreenSharing',
@ -159,37 +161,39 @@ const ListPeer = (props) =>
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
{ spotlight &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
className={classes.buttons}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
placement='bottom'
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
className={classes.buttons}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipant',
@ -248,6 +252,60 @@ const ListPeer = (props) =>
</IconButton>
</Tooltip>
}
{ isModerator && micConsumer &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantAudioModerator',
defaultMessage : 'Mute participant audio globally'
})}
placement='bottom'
>
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled={!isModerator || peer.stopPeerAudioInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.mutePeer(peer.id);
}}
>
{ !micConsumer.remotelyPaused ?
<MicIcon />
:
<MicOffIcon />
}
</IconButton>
</Tooltip>
}
{ isModerator && webcamConsumer &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideoModerator',
defaultMessage : 'Mute participant video globally'
})}
placement='bottom'
>
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled={!isModerator || peer.stopPeerVideoInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.stopPeerVideo(peer.id);
}}
>
{ !webcamConsumer.remotelyPaused ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
}
{children}
</div>
);

View File

@ -1,8 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import {
participantListSelector
participantListSelector,
makePermissionSelector
} from '../../Selectors';
import { permissions } from '../../../permissions';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
@ -160,31 +162,34 @@ ParticipantList.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
const makeMapStateToProps = () =>
{
return {
isModerator :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)),
participants : participantListSelector(state),
spotlights : state.room.spotlights,
selectedPeerId : state.room.selectedPeerId
const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM);
const mapStateToProps = (state) =>
{
return {
isModerator : hasPermission(state),
participants : participantListSelector(state),
spotlights : state.room.spotlights,
selectedPeerId : state.room.selectedPeerId
};
};
return mapStateToProps;
};
const ParticipantListContainer = withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room === next.room &&
prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerId === next.room.selectedPeerId
prev.peers === next.peers
);
}
}

View File

@ -11,10 +11,9 @@ import Peer from '../Containers/Peer';
import Me from '../Containers/Me';
const RATIO = 1.334;
const PADDING_V = 50;
const PADDING_H = 0;
const PADDING = 60;
const styles = () =>
const styles = (theme) =>
({
root :
{
@ -23,6 +22,7 @@ const styles = () =>
display : 'flex',
flexDirection : 'row',
flexWrap : 'wrap',
overflow : 'hidden',
justifyContent : 'center',
alignItems : 'center',
alignContent : 'center'
@ -36,6 +36,14 @@ const styles = () =>
{
paddingTop : 60,
transition : 'padding .5s'
},
buttonControlBar :
{
paddingLeft : 60,
[theme.breakpoints.down('sm')] :
{
paddingLeft : 0
}
}
});
@ -66,9 +74,11 @@ class Democratic extends React.PureComponent
return;
}
const width = this.peersRef.current.clientWidth - PADDING_H;
const height = this.peersRef.current.clientHeight -
(this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H);
const width =
this.peersRef.current.clientWidth - (this.props.buttonControlBar ? PADDING : 0);
const height =
this.peersRef.current.clientHeight -
(this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING : 0);
let x, y, space;
@ -130,6 +140,7 @@ class Democratic extends React.PureComponent
spotlightsPeers,
toolbarsVisible,
permanentTopBar,
buttonControlBar,
classes
} = this.props;
@ -144,7 +155,8 @@ class Democratic extends React.PureComponent
className={classnames(
classes.root,
toolbarsVisible || permanentTopBar ?
classes.showingToolBar : classes.hiddenToolBar
classes.showingToolBar : classes.hiddenToolBar,
buttonControlBar ? classes.buttonControlBar : null
)}
ref={this.peersRef}
>
@ -172,21 +184,25 @@ class Democratic extends React.PureComponent
Democratic.propTypes =
{
advancedMode : PropTypes.bool,
boxes : PropTypes.number,
spotlightsPeers : PropTypes.array.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired
advancedMode : PropTypes.bool,
boxes : PropTypes.number,
spotlightsPeers : PropTypes.array.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool.isRequired,
buttonControlBar : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
boxes : videoBoxesSelector(state),
spotlightsPeers : spotlightPeersSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
permanentTopBar : state.settings.permanentTopBar
boxes : videoBoxesSelector(state),
spotlightsPeers : spotlightPeersSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
permanentTopBar : state.settings.permanentTopBar,
buttonControlBar : state.settings.buttonControlBar,
toolAreaOpen : state.toolarea.toolAreaOpen
};
};
@ -203,8 +219,10 @@ export default connect(
prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.settings.permanentTopBar === next.settings.permanentTopBar
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.buttonControlBar === next.settings.buttonControlBar &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}
}
)(withStyles(styles)(Democratic));
)(withStyles(styles, { withTheme: true })(Democratic));

View File

@ -25,6 +25,7 @@ const styles = () =>
height : '100%',
width : '100%',
display : 'grid',
overflow : 'hidden',
gridTemplateColumns : '1fr',
gridTemplateRows : '1fr 0.25fr'
},
@ -334,6 +335,7 @@ Filmstrip.propTypes = {
spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number,
toolbarsVisible : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired
};
@ -349,6 +351,7 @@ const mapStateToProps = (state) =>
spotlights : state.room.spotlights,
boxes : videoBoxesSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
toolAreaOpen : state.toolarea.toolAreaOpen,
permanentTopBar : state.settings.permanentTopBar
};
};
@ -364,6 +367,7 @@ export default withRoomContext(connect(
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.selectedPeerId === next.room.selectedPeerId &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.peers === next.peers &&
prev.consumers === next.consumers &&

View File

@ -12,6 +12,7 @@ import { FormattedMessage } from 'react-intl';
import CookieConsent from 'react-cookie-consent';
import CssBaseline from '@material-ui/core/CssBaseline';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
import Drawer from '@material-ui/core/Drawer';
import Hidden from '@material-ui/core/Hidden';
import Notifications from './Notifications/Notifications';
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
@ -25,8 +26,11 @@ import Settings from './Settings/Settings';
import TopBar from './Controls/TopBar';
import WakeLock from 'react-wakelock-react16';
import ExtraVideo from './Controls/ExtraVideo';
import ButtonControlBar from './Controls/ButtonControlBar';
import Help from './Controls/Help';
import About from './Controls/About';
const TIMEOUT = 5 * 1000;
const TIMEOUT = window.config.hideTimeout || 5000;
const styles = (theme) =>
({
@ -42,6 +46,27 @@ const styles = (theme) =>
backgroundSize : 'cover',
backgroundRepeat : 'no-repeat'
},
drawer :
{
width : '30vw',
flexShrink : 0,
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
drawerPaper :
{
width : '30vw',
@ -143,6 +168,8 @@ class Room extends React.PureComponent
browser,
advancedMode,
showNotifications,
buttonControlBar,
drawerOverlayed,
toolAreaOpen,
toggleToolArea,
classes,
@ -155,6 +182,8 @@ class Room extends React.PureComponent
democratic : Democratic
}[room.mode];
const container = window !== undefined ? window.document.body : undefined;
return (
<div className={classes.root}>
{ !isElectron() &&
@ -191,22 +220,44 @@ class Room extends React.PureComponent
onFullscreen={this.handleToggleFullscreen}
/>
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
onOpen={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
>
<MeetingDrawer closeDrawer={toggleToolArea} />
</SwipeableDrawer>
</Hidden>
</nav>
{ (browser.platform === 'mobile' || drawerOverlayed) ?
<nav>
<Hidden implementation='css'>
<SwipeableDrawer
container={container}
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
onOpen={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
ModalProps={{
keepMounted : true // Better open performance on mobile.
}}
>
<MeetingDrawer closeDrawer={toggleToolArea} />
</SwipeableDrawer>
</Hidden>
</nav>
:
<nav className={toolAreaOpen ? classes.drawer : null}>
<Hidden implementation='css'>
<Drawer
variant='persistent'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={toolAreaOpen}
onClose={() => toggleToolArea()}
classes={{
paper : classes.drawerPaper
}}
>
<MeetingDrawer closeDrawer={toggleToolArea} />
</Drawer>
</Hidden>
</nav>
}
{ browser.platform === 'mobile' && browser.os !== 'ios' &&
<WakeLock />
@ -214,6 +265,10 @@ class Room extends React.PureComponent
<View advancedMode={advancedMode} />
{ buttonControlBar &&
<ButtonControlBar />
}
{ room.lockDialogOpen &&
<LockDialog />
}
@ -225,6 +280,13 @@ class Room extends React.PureComponent
{ room.extraVideoOpen &&
<ExtraVideo />
}
{ room.helpOpen &&
<Help />
}
{ room.aboutOpen &&
<About />
}
</div>
);
}
@ -236,6 +298,8 @@ Room.propTypes =
browser : PropTypes.object.isRequired,
advancedMode : PropTypes.bool.isRequired,
showNotifications : PropTypes.bool.isRequired,
buttonControlBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
@ -249,6 +313,8 @@ const mapStateToProps = (state) =>
browser : state.me.browser,
advancedMode : state.settings.advancedMode,
showNotifications : state.settings.showNotifications,
buttonControlBar : state.settings.buttonControlBar,
drawerOverlayed : state.settings.drawerOverlayed,
toolAreaOpen : state.toolarea.toolAreaOpen
});
@ -276,6 +342,8 @@ export default connect(
prev.me.browser === next.me.browser &&
prev.settings.advancedMode === next.settings.advancedMode &&
prev.settings.showNotifications === next.settings.showNotifications &&
prev.settings.buttonControlBar === next.settings.buttonControlBar &&
prev.settings.drawerOverlayed === next.settings.drawerOverlayed &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}

View File

@ -1,5 +1,8 @@
import { createSelector } from 'reselect';
const meRolesSelect = (state) => state.me.roles;
const roomPermissionsSelect = (state) => state.room.roomPermissions;
const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing;
const producersSelect = (state) => state.producers;
const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights;
@ -217,3 +220,53 @@ export const makePeerConsumerSelector = () =>
}
);
};
// Very important that the Components that use this
// selector need to check at least these state changes:
//
// areStatesEqual : (next, prev) =>
// {
// return (
// prev.room.roomPermissions === next.room.roomPermissions &&
// prev.room.allowWhenRoleMissing === next.room.allowWhenRoleMissing &&
// prev.peers === next.peers &&
// prev.me.roles === next.me.roles
// );
// }
export const makePermissionSelector = (permission) =>
{
return createSelector(
meRolesSelect,
roomPermissionsSelect,
roomAllowWhenRoleMissing,
peersValueSelector,
(roles, roomPermissions, allowWhenRoleMissing, peers) =>
{
if (!roomPermissions)
return false;
const permitted = roles.some((role) =>
roomPermissions[permission].includes(role)
);
if (permitted)
return true;
if (!allowWhenRoleMissing)
return false;
// Allow if config is set, and no one is present
if (allowWhenRoleMissing.includes(permission) &&
peers.filter(
(peer) =>
peer.roles.some(
(role) => roomPermissions[permission].includes(role)
)
).length === 0
)
return true;
return false;
}
);
};

View File

@ -26,11 +26,14 @@ const styles = (theme) =>
});
const AppearenceSettings = ({
isMobile,
room,
settings,
onTogglePermanentTopBar,
onToggleHiddenControls,
onToggleButtonControlBar,
onToggleShowNotifications,
onToggleDrawerOverlayed,
handleChangeMode,
classes
}) =>
@ -102,6 +105,24 @@ const AppearenceSettings = ({
defaultMessage : 'Hidden media controls'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.buttonControlBar} onChange={onToggleButtonControlBar} value='buttonControlBar' />}
label={intl.formatMessage({
id : 'settings.buttonControlBar',
defaultMessage : 'Separate media controls'
})}
/>
{ !isMobile &&
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.drawerOverlayed} onChange={onToggleDrawerOverlayed} value='drawerOverlayed' />}
label={intl.formatMessage({
id : 'settings.drawerOverlayed',
defaultMessage : 'Side drawer over content'
})}
/>
}
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.showNotifications} onChange={onToggleShowNotifications} value='showNotifications' />}
@ -116,17 +137,21 @@ const AppearenceSettings = ({
AppearenceSettings.propTypes =
{
isMobile : PropTypes.bool.isRequired,
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
onToggleHiddenControls : PropTypes.func.isRequired,
onToggleButtonControlBar : PropTypes.func.isRequired,
onToggleShowNotifications : PropTypes.func.isRequired,
onToggleDrawerOverlayed : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isMobile : state.me.browser.platform === 'mobile',
room : state.room,
settings : state.settings
});
@ -135,6 +160,8 @@ const mapDispatchToProps = {
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
onToggleHiddenControls : settingsActions.toggleHiddenControls,
onToggleShowNotifications : settingsActions.toggleShowNotifications,
onToggleButtonControlBar : settingsActions.toggleButtonControlBar,
onToggleDrawerOverlayed : settingsActions.toggleDrawerOverlayed,
handleChangeMode : roomActions.setDisplayMode
};
@ -146,6 +173,7 @@ export default connect(
areStatesEqual : (next, prev) =>
{
return (
prev.me.browser === next.me.browser &&
prev.room === next.room &&
prev.settings === next.settings
);

View File

@ -298,7 +298,7 @@ const MediaSettings = ({
/>}
label={intl.formatMessage({
id : 'settings.echoCancellation',
defaultMessage : 'Echo Cancellation'
defaultMessage : 'Echo cancellation'
})}
/>
<FormControlLabel
@ -313,7 +313,7 @@ const MediaSettings = ({
/>}
label={intl.formatMessage({
id : 'settings.autoGainControl',
defaultMessage : 'Auto Gain Control'
defaultMessage : 'Auto gain control'
})}
/>
<FormControlLabel
@ -328,7 +328,7 @@ const MediaSettings = ({
/>}
label={intl.formatMessage({
id : 'settings.noiseSuppression',
defaultMessage : 'Noise Suppression'
defaultMessage : 'Noise suppression'
})}
/>
<FormControlLabel

View File

@ -95,7 +95,7 @@ const Settings = ({
/>
<Tab
label={intl.formatMessage({
id : 'label.appearence',
id : 'label.appearance',
defaultMessage : 'Appearence'
})}
/>

View File

@ -152,6 +152,7 @@ class VideoView extends React.PureComponent
{
const {
isMe,
showQuality,
isScreen,
displayName,
showPeerInfo,
@ -177,58 +178,63 @@ class VideoView extends React.PureComponent
videoHeight
} = this.state;
let quality = <SignalCellularOffIcon style={{ color: red[500] }}/>;
let quality = null;
if (videoScore || audioScore)
if (showQuality)
{
const score = videoScore ? videoScore : audioScore;
quality = <SignalCellularOffIcon style={{ color: red[500] }}/>;
switch (score.producerScore)
if (videoScore || audioScore)
{
case 0:
case 1:
const score = videoScore ? videoScore : audioScore;
switch (isMe ? score.score : score.producerScore)
{
quality = <SignalCellular0BarIcon style={{ color: red[500] }}/>;
break;
}
case 2:
case 3:
{
quality = <SignalCellular1BarIcon style={{ color: red[500] }}/>;
break;
}
case 4:
case 5:
case 6:
{
quality = <SignalCellular2BarIcon style={{ color: orange[500] }}/>;
break;
}
case 7:
case 8:
case 9:
{
quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>;
break;
}
case 10:
{
quality = null;
break;
}
default:
{
break;
case 0:
case 1:
{
quality = <SignalCellular0BarIcon style={{ color: red[500] }}/>;
break;
}
case 2:
case 3:
{
quality = <SignalCellular1BarIcon style={{ color: red[500] }}/>;
break;
}
case 4:
case 5:
case 6:
{
quality = <SignalCellular2BarIcon style={{ color: orange[500] }}/>;
break;
}
case 7:
case 8:
case 9:
{
quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>;
break;
}
case 10:
{
quality = null;
break;
}
default:
{
break;
}
}
}
}
@ -258,7 +264,7 @@ class VideoView extends React.PureComponent
<p>{videoWidth}x{videoHeight}</p>
}
</div>
{ !isMe &&
{ showQuality &&
<div className={classnames(classes.box, 'right')}>
{
quality
@ -438,6 +444,7 @@ class VideoView extends React.PureComponent
VideoView.propTypes =
{
isMe : PropTypes.bool,
showQuality : PropTypes.bool,
isScreen : PropTypes.bool,
displayName : PropTypes.string,
showPeerInfo : PropTypes.bool,

View File

@ -31,7 +31,8 @@ import messagesFrench from './translations/fr';
import messagesGreek from './translations/el';
import messagesRomanian from './translations/ro';
import messagesPortuguese from './translations/pt';
import messagesChinese from './translations/cn';
import messagesChineseSimplified from './translations/cn';
import messagesChineseTraditional from './translations/tw';
import messagesSpanish from './translations/es';
import messagesCroatian from './translations/hr';
import messagesCzech from './translations/cs';
@ -58,7 +59,8 @@ const messages =
'el' : messagesGreek,
'ro' : messagesRomanian,
'pt' : messagesPortuguese,
'zh' : messagesChinese,
'zh-hans' : messagesChineseSimplified,
'zh-hant' : messagesChineseTraditional,
'es' : messagesSpanish,
'hr' : messagesCroatian,
'cs' : messagesCzech,
@ -68,7 +70,15 @@ const messages =
'lv' : messagesLatvian
};
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
let browserLanguage = (navigator.language || navigator.browserLanguage).toLowerCase()
let locale = browserLanguage.split(/[-_]/)[0]; // language without region code
if (locale === 'zh')
{
if (browserLanguage === 'zh-cn')
locale = 'zh-hans'
else
locale = 'zh-hant'
}
const intl = createIntl({
locale,
@ -115,6 +125,13 @@ function run()
const forceTcp = parameters.get('forceTcp') === 'true';
const displayName = parameters.get('displayName');
const muted = parameters.get('muted') === 'true';
const { pathname } = window.location;
let basePath = pathname.substring(0, pathname.lastIndexOf('/'));
if (!basePath)
basePath = '/';
// Get current device.
const device = deviceInfo();
@ -134,7 +151,8 @@ function run()
produce,
forceTcp,
displayName,
muted
muted,
basePath
});
global.CLIENT = roomClient;
@ -146,7 +164,7 @@ function run()
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<Router>
<Router basename={basePath}>
<Suspense fallback={<LoadingView />}>
<React.Fragment>
<Route exact path='/' component={ChooseRoom} />

View File

@ -0,0 +1,20 @@
export const permissions = {
// The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK',
// The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : 'PROMOTE_PEER',
// The role(s) have permission to send chat messages
SEND_CHAT : 'SEND_CHAT',
// The role(s) have permission to moderate chat
MODERATE_CHAT : 'MODERATE_CHAT',
// The role(s) have permission to share screen
SHARE_SCREEN : 'SHARE_SCREEN',
// The role(s) have permission to produce extra video
EXTRA_VIDEO : 'EXTRA_VIDEO',
// The role(s) have permission to share files
SHARE_FILE : 'SHARE_FILE',
// The role(s) have permission to moderate files
MODERATE_FILES : 'MODERATE_FILES',
// The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : 'MODERATE_ROOM'
};

View File

@ -110,6 +110,11 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer };
}
case 'CLEAR_CONSUMERS':
{
return initialState;
}
default:
return state;
}

View File

@ -1,4 +1,6 @@
const peer = (state = {}, action) =>
const initialState = {};
const peer = (state = initialState, action) =>
{
switch (action.type)
{
@ -68,12 +70,24 @@ const peer = (state = {}, action) =>
return { ...state, roles };
}
case 'STOP_PEER_AUDIO_IN_PROGRESS':
return {
...state,
stopPeerAudioInProgress : action.payload.flag
};
case 'STOP_PEER_VIDEO_IN_PROGRESS':
return {
...state,
stopPeerVideoInProgress : action.payload.flag
};
default:
return state;
}
};
const peers = (state = {}, action) =>
const peers = (state = initialState, action) =>
{
switch (action.type)
{
@ -102,6 +116,8 @@ const peers = (state = {}, action) =>
case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE':
case 'REMOVE_PEER_ROLE':
case 'STOP_PEER_AUDIO_IN_PROGRESS':
case 'STOP_PEER_VIDEO_IN_PROGRESS':
{
const oldPeer = state[action.payload.peerId];
@ -125,6 +141,11 @@ const peers = (state = {}, action) =>
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
}
case 'CLEAR_PEERS':
{
return initialState;
}
default:
return state;
}

View File

@ -60,6 +60,17 @@ const producers = (state = initialState, action) =>
return { ...state, [producerId]: newProducer };
}
case 'SET_PRODUCER_SCORE':
{
const { producerId, score } = action.payload;
const producer = state[producerId];
const newProducer = { ...producer, score };
return { ...state, [producerId]: newProducer };
}
default:
return state;
}

View File

@ -22,6 +22,8 @@ const initialState =
spotlights : [],
settingsOpen : false,
extraVideoOpen : false,
helpOpen : false,
aboutOpen : false,
currentSettingsTab : 'media', // media, appearence, advanced
lockDialogOpen : false,
joined : false,
@ -31,18 +33,8 @@ const initialState =
closeMeetingInProgress : false,
clearChatInProgress : false,
clearFileSharingInProgress : false,
userRoles : { NORMAL: 'normal' }, // Default role
permissionsFromRoles : {
CHANGE_ROOM_LOCK : [],
PROMOTE_PEER : [],
SEND_CHAT : [],
MODERATE_CHAT : [],
SHARE_SCREEN : [],
EXTRA_VIDEO : [],
SHARE_FILE : [],
MODERATE_FILES : [],
MODERATE_ROOM : []
}
roomPermissions : null,
allowWhenRoleMissing : null
};
const room = (state = initialState, action) =>
@ -130,6 +122,20 @@ const room = (state = initialState, action) =>
return { ...state, extraVideoOpen };
}
case 'SET_HELP_OPEN':
{
const { helpOpen } = action.payload;
return { ...state, helpOpen };
}
case 'SET_ABOUT_OPEN':
{
const { aboutOpen } = action.payload;
return { ...state, aboutOpen };
}
case 'SET_SETTINGS_TAB':
{
const { tab } = action.payload;
@ -206,6 +212,11 @@ const room = (state = initialState, action) =>
return { ...state, spotlights };
}
case 'CLEAR_SPOTLIGHTS':
{
return { ...state, spotlights: [] };
}
case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS':
return { ...state, lobbyPeersPromotionInProgress: action.payload.flag };
@ -224,18 +235,18 @@ const room = (state = initialState, action) =>
case 'CLEAR_FILE_SHARING_IN_PROGRESS':
return { ...state, clearFileSharingInProgress: action.payload.flag };
case 'SET_USER_ROLES':
case 'SET_ROOM_PERMISSIONS':
{
const { userRoles } = action.payload;
const { roomPermissions } = action.payload;
return { ...state, userRoles };
return { ...state, roomPermissions };
}
case 'SET_PERMISSIONS_FROM_ROLES':
case 'SET_ALLOW_WHEN_ROLE_MISSING':
{
const { permissionsFromRoles } = action.payload;
const { allowWhenRoleMissing } = action.payload;
return { ...state, permissionsFromRoles };
return { ...state, allowWhenRoleMissing };
}
default:

View File

@ -20,6 +20,8 @@ const initialState =
hiddenControls : false,
showNotifications : true,
notificationSounds : true,
buttonControlBar : window.config.buttonControlBar || false,
drawerOverlayed : window.config.drawerOverlayed || true,
...window.config.defaultAudio
};
@ -161,6 +163,20 @@ const settings = (state = initialState, action) =>
return { ...state, permanentTopBar };
}
case 'TOGGLE_BUTTON_CONTROL_BAR':
{
const buttonControlBar = !state.buttonControlBar;
return { ...state, buttonControlBar };
}
case 'TOGGLE_DRAWER_OVERLAYED':
{
const drawerOverlayed = !state.drawerOverlayed;
return { ...state, drawerOverlayed };
}
case 'TOGGLE_HIDDEN_CONTROLS':
{
const hiddenControls = !state.hiddenControls;

View File

@ -60,7 +60,11 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
"roles.gotRole": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "房间名称",
"label.chooseRoomButton": "继续",
@ -106,7 +113,7 @@
"label.ultra": "超高 (UHD)",
"label.close": "关闭",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "无法保存文件",
"filesharing.startingFileShare": "正在尝试共享文件",

View File

@ -59,6 +59,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -80,6 +84,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Jméno místnosti",
"label.chooseRoomButton": "Pokračovat",
@ -105,7 +112,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Zavřít",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -130,6 +137,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Není možné uložit soubor",
"filesharing.startingFileShare": "Pokouším se sdílet soubor",

View File

@ -51,20 +51,24 @@
"room.videoPaused": "Video gestoppt",
"room.muteAll": "Alle stummschalten",
"room.stopAllVideo": "Alle Videos stoppen",
"room.closeMeeting": "Meeting schließen",
"room.clearChat": null,
"room.clearFileSharing": null,
"room.closeMeeting": "Meeting beenden",
"room.clearChat": "Liste löschen",
"room.clearFileSharing": "Liste löschen",
"room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung",
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.moderatoractions": "Moderator Aktionen",
"room.raisedHand": "{displayName} hebt die Hand",
"room.loweredHand": "{displayName} senkt die Hand",
"room.extraVideo": "Video hinzufügen",
"room.overRoomLimit": "Der Raum ist voll, probiere es später nochmal",
"room.help": "Hilfe",
"room.about": "Über",
"room.shortcutKeys": "Tastaturkürzel",
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
"me.mutedPTT": "Du bist stummgeschaltet. Halte die SPACE-Taste um zu sprechen",
"roles.gotRole": null,
"roles.lostRole": null,
"roles.gotRole": "Rolle erhalten: {role}",
"roles.lostRole": "Rolle entzogen: {role}",
"tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden",
@ -76,11 +80,14 @@
"tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": "Teilnehmer rauswerfen",
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.kickParticipant": "Rauswerfen",
"tooltip.muteParticipant": "Stummschalten",
"tooltip.muteParticipantVideo": "Video stoppen",
"tooltip.raisedHand": "Hand heben",
"tooltip.muteScreenSharing": "Stoppe Bildschirmfreigabe",
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Name des Raums",
"label.chooseRoomButton": "Weiter",
@ -94,23 +101,23 @@
"label.filesharing": "Dateien",
"label.participants": "Teilnehmer",
"label.shareFile": "Datei hochladen",
"label.shareGalleryFile": null,
"label.shareGalleryFile": "Bild teilen",
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt",
"label.democratic": "Demokratisch",
"label.filmstrip": "Filmstreifen",
"label.low": "Niedrig",
"label.medium": "Medium",
"label.medium": "Mittel",
"label.high": "Hoch (HD)",
"label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Schließen",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"label.media": "Audio / Video",
"label.appearance": "Ansicht",
"label.advanced": "Erweitert",
"label.addVideo": "Video hinzufügen",
"label.promoteAllPeers": "Alle Teilnehmer reinlassen",
"label.moreActions": "Weitere Aktionen",
"settings.settings": "Einstellungen",
"settings.camera": "Kamera",
@ -128,9 +135,14 @@
"settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.hiddenControls": "Medienwerkzeugleiste automatisch ausblenden",
"settings.notificationSounds": "Audiosignal bei Benachrichtigungen",
"settings.showNotifications": "Zeige Benachrichtigungen",
"settings.buttonControlBar": "Separate seitliche Medienwerkzeugleiste",
"settings.echoCancellation": "Echounterdrückung",
"settings.autoGainControl": "Automatische Pegelregelung (Audioeingang)",
"settings.noiseSuppression": "Rauschunterdrückung",
"settings.drawerOverlayed": "Seitenpanel verdeckt Hauptinhalt",
"filesharing.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei",
@ -172,8 +184,8 @@
"devices.cameraDisconnected": "Kamera getrennt",
"devices.cameraError": "Fehler mit deiner Kamera",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "Moderator hat Chat gelöscht",
"moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht",
"moderator.muteAudio": "Moderator hat dich stummgeschaltet",
"moderator.muteVideo": "Moderator hat dein Video gestoppt"
}

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Luk",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen",

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Κλείσιμο",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} put their hand down",
"room.extraVideo": "Extra video",
"room.overRoomLimit": "The room is full, retry after some time.",
"room.help": "Help",
"room.about": "About",
"room.shortcutKeys": "Shortcut Keys",
"room.browsePeersSpotlight": null,
"me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": "Mute participant video",
"tooltip.raisedHand": "Raise hand",
"tooltip.muteScreenSharing": "Mute participant share",
"tooltip.muteParticipantAudioModerator": "Mute participant audio globally",
"tooltip.muteParticipantVideoModerator": "Mute participant video globally",
"tooltip.muteScreenSharingModerator": "Mute participant screen share globally",
"label.roomName": "Room name",
"label.chooseRoomButton": "Continue",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Close",
"label.media": "Media",
"label.appearence": "Appearence",
"label.appearance": "Appearence",
"label.advanced": "Advanced",
"label.addVideo": "Add video",
"label.promoteAllPeers": "Promote all",
@ -131,6 +138,11 @@
"settings.hiddenControls": "Hidden media controls",
"settings.notificationSounds": "Notification sounds",
"settings.showNotifications": "Show notifications",
"settings.buttonControlBar": "Separate media controls",
"settings.echoCancellation": "Echo cancellation",
"settings.autoGainControl": "Auto gain control",
"settings.noiseSuppression": "Noise suppression",
"settings.drawerOverlayed": "Side drawer over content",
"filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file",

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Cerrar",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero",

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer",
@ -106,7 +113,7 @@
"label.ultra": "Ultra Haute Définition",
"label.close": "Fermer",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier",

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} je spustio ruku",
"room.extraVideo": "Dodatni video",
"room.overRoomLimit": "Soba je popunjena, pokušajte ponovno kasnije.",
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": "Ne primaj video sudionika",
"tooltip.raisedHand": "Podigni ruku",
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi",
@ -106,7 +113,7 @@
"label.ultra": "Ultra visoka (UHD)",
"label.close": "Zatvori",
"label.media": "Medij",
"label.appearence": "Prikaz",
"label.appearance": "Prikaz",
"label.advanced": "Napredno",
"label.addVideo": "Dodaj video",
"label.promoteAllPeers": "Promoviraj sve",
@ -131,6 +138,11 @@
"settings.hiddenControls": "Skrivene kontrole medija",
"settings.notificationSounds": "Zvuk obavijesti",
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Nije moguće spremiti datoteku",
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} leeresztette a kezét",
"room.extraVideo": "Kiegészítő videó",
"room.overRoomLimit": "A konferenciaszoba betelt..",
"room.help": "Segítség",
"room.about": "Névjegy",
"room.shortcutKeys": "Billentyűparancsok",
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Némítva vagy, ha beszélnél nyomd le a szóköz billentyűt",
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": "Résztvevő videóstreamének némítása",
"tooltip.raisedHand": "Jelentkezés",
"tooltip.muteScreenSharing": "Képernyőmegosztás szüneteltetése",
"tooltip.muteParticipantAudioModerator": "Résztvevő hangjának általános némítása",
"tooltip.muteParticipantVideoModerator": "Résztvevő videójának általános némítása",
"tooltip.muteScreenSharingModerator": "Résztvevő képernyőmegosztásának általános némítása",
"label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább",
@ -106,7 +113,7 @@
"label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár",
"label.media": "Média",
"label.appearence": "Megjelenés",
"label.appearance": "Megjelenés",
"label.advanced": "Részletek",
"label.addVideo": "Videó hozzáadása",
"label.promoteAllPeers": "Mindenkit beengedek",
@ -130,7 +137,12 @@
"settings.lastn": "A látható videók száma",
"settings.hiddenControls": "Média Gombok automatikus elrejtése",
"settings.notificationSounds": "Értesítések hangjelzéssel",
"settings.showNotifications": null,
"settings.showNotifications": "Értesítések megjelenítése",
"settings.buttonControlBar": "Médiavezérlő gombok leválasztása",
"settings.echoCancellation": "Visszhangelnyomás",
"settings.autoGainControl": "Automatikus hangerő",
"settings.noiseSuppression": "Zajelnyomás",
"settings.drawerOverlayed": "Oldalsáv a tartalom felett",
"filesharing.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása",

View File

@ -59,8 +59,12 @@
"room.raisedHand": "{displayName} ha alzato la mano",
"room.loweredHand": "{displayName} ha abbassato la mano",
"room.extraVideo": "Video extra",
"room.overRoomLimit": null,
"room.overRoomLimit": "La stanza è piena, riprova più tardi.",
"room.help": "Aiuto",
"room.about": "Informazioni su",
"room.shortcutKeys": "Scorciatoie da tastiera",
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Sei mutato, tieni premuto SPAZIO per parlare",
"roles.gotRole": "Hai ottenuto il ruolo: {role}",
@ -68,7 +72,7 @@
"tooltip.login": "Log in",
"tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Ammetti dalla lobby",
"tooltip.admitFromLobby": "Accetta partecipante dalla lobby",
"tooltip.lockRoom": "Blocca stanza",
"tooltip.unLockRoom": "Sblocca stanza",
"tooltip.enterFullscreen": "Modalità schermo intero",
@ -76,10 +80,14 @@
"tooltip.lobby": "Mostra lobby",
"tooltip.settings": "Mostra impostazioni",
"tooltip.participants": "Mostra partecipanti",
"tooltip.kickParticipant": "Espelli partecipante",
"tooltip.muteParticipant": "Muta partecipante",
"tooltip.muteParticipantVideo": "Ferma video partecipante",
"tooltip.raisedHand": "Mano alzata",
"tooltip.muteScreenSharing": null,
"tooltip.muteScreenSharing": "Ferma condivisione schermo partecipante",
"tooltip.muteParticipantAudioModerator": "Sospendi audio globale",
"tooltip.muteParticipantVideoModerator": "Sospendi video globale",
"tooltip.muteScreenSharingModerator": "Sospendi condivisione schermo globale",
"label.roomName": "Nome della stanza",
"label.chooseRoomButton": "Continua",
@ -93,7 +101,7 @@
"label.filesharing": "Condivisione file",
"label.participants": "Partecipanti",
"label.shareFile": "Condividi file",
"label.shareGalleryFile": null,
"label.shareGalleryFile": "Condividi immagine",
"label.fileSharingUnsupported": "Condivisione file non supportata",
"label.unknown": "Sconosciuto",
"label.democratic": "Vista Democratica",
@ -105,11 +113,11 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Chiudi",
"label.media": "Media",
"label.appearence": "Aspetto",
"label.appearance": "Aspetto",
"label.advanced": "Avanzate",
"label.addVideo": "Aggiungi video",
"label.promoteAllPeers": "Promuovi tutti",
"label.moreActions": null,
"label.moreActions": "Altre azioni",
"settings.settings": "Impostazioni",
"settings.camera": "Videocamera",
@ -129,7 +137,12 @@
"settings.lastn": "Numero di video visibili",
"settings.hiddenControls": "Controlli media nascosti",
"settings.notificationSounds": "Suoni di notifica",
"settings.showNotifications": null,
"settings.showNotifications": "Mostra notifiche",
"settings.buttonControlBar": "Controlli media separati",
"settings.echoCancellation": "Cancellazione echo",
"settings.autoGainControl": "Controllo guadagno automatico",
"settings.noiseSuppression": "Riduzione del rumore",
"settings.drawerOverlayed": "Barra laterale sovrapposta",
"filesharing.saveFileError": "Impossibile salvare file",
"filesharing.startingFileShare": "Tentativo di condivisione file",
@ -145,7 +158,7 @@
"devices.devicesChanged": "Il tuo dispositivo è cambiato, configura i dispositivi nel menù di impostazioni",
"device.audioUnsupported": "Dispositivo audio non supportato",
"device.activateAudio": "Attiva audio",
"device.activateAudio": "Attiva audio",
"device.muteAudio": "Silenzia audio",
"device.unMuteAudio": "Riattiva audio",
@ -175,4 +188,4 @@
"moderator.clearFiles": "Il moderatore ha pulito i file",
"moderator.muteAudio": "Il moderatore ha mutato il tuo audio",
"moderator.muteVideo": "Il moderatore ha fermato il tuo video"
}
}

View File

@ -59,6 +59,10 @@
"room.raisedHand": "{displayName} pacēla roku",
"room.loweredHand": "{displayName} nolaida roku",
"room.extraVideo": "Papildus video",
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Jūs esat noklusināts. Turiet taustiņu SPACE-BAR, lai runātu",
@ -80,6 +84,9 @@
"tooltip.muteParticipantVideo": "Atslēgt dalībnieka video",
"tooltip.raisedHand": "Pacelt roku",
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Sapulces telpas nosaukums (ID)",
"label.chooseRoomButton": "Turpināt",
@ -104,7 +111,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Aizvērt",
"label.media": "Mediji",
"label.appearence": "Izskats",
"label.appearance": "Izskats",
"label.advanced": "Advancēts",
"label.addVideo": "Pievienot video",
"label.moreActions": null,
@ -125,6 +132,11 @@
"settings.hiddenControls": "Slēpto mediju vadība",
"settings.notificationSounds": "Paziņojumu skaņas",
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Nav iespējams saglabāt failu",
"filesharing.startingFileShare": "Tiek mēģināts kopīgot failu",

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} tok ned hånden",
"room.extraVideo": "Ekstra video",
"room.overRoomLimit": "Rommet er fullt, prøv igjen om litt.",
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke",
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": "Demp deltakervideo",
"tooltip.raisedHand": "Rekk opp hånden",
"tooltip.muteScreenSharing": "Demp deltaker skjermdeling",
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Lukk",
"label.media": "Media",
"label.appearence": "Utseende",
"label.appearance": "Utseende",
"label.advanced": "Avansert",
"label.addVideo": "Legg til video",
"label.promoteAllPeers": "Slipp inn alle",
@ -131,6 +138,11 @@
"settings.hiddenControls": "Skjul media knapper",
"settings.notificationSounds": "Varslingslyder",
"settings.showNotifications": "Vis varslinger",
"settings.buttonControlBar": "Separate media knapper",
"settings.echoCancellation": "Echokansellering",
"settings.autoGainControl": "Auto gain kontroll",
"settings.noiseSuppression": "Støy reduksjon",
"settings.drawerOverlayed": "Sidemeny over innhold",
"filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling",

View File

@ -49,22 +49,26 @@
"room.spotlights": "Aktywni uczestnicy",
"room.passive": "Pasywni uczestnicy",
"room.videoPaused": "To wideo jest wstrzymane.",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.muteAll": "Wycisz wszystkich",
"room.stopAllVideo": "Zatrzymaj wszystkie Video",
"room.closeMeeting": "Zamknij spotkanie",
"room.clearChat": "Wyczyść Chat",
"room.clearFileSharing": "Wyczyść pliki",
"room.speechUnsupported": "Twoja przeglądarka nie rozpoznaje mowy",
"room.moderatoractions": "Akcje moderatora",
"room.raisedHand": "{displayName} podniósł rękę",
"room.loweredHand": "{displayName} opuścił rękę",
"room.extraVideo": "Dodatkowe Video",
"room.overRoomLimit": "Pokój jest pełny, spróbuj za jakiś czas.",
"room.help": "Pomoc",
"room.about": "O pogramie",
"room.shortcutKeys": "Skróty klawiaturowe",
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
"me.mutedPTT": "Masz wyciszony mikrofon, przytrzymaj spację aby mówić",
"roles.gotRole": null,
"roles.lostRole": null,
"roles.gotRole": "Masz rolę {role}",
"roles.lostRole": "Nie masz już roli {role}",
"tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj",
@ -76,11 +80,14 @@
"tooltip.lobby": "Pokaż poczekalnię",
"tooltip.settings": "Pokaż ustawienia",
"tooltip.participants": "Pokaż uczestników",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.kickParticipant": "Wyrzuć użytkownika",
"tooltip.muteParticipant": "Wycisz użytkownika",
"tooltip.muteParticipantVideo": "Wyłącz wideo użytkownika",
"tooltip.raisedHand": "Podnieś rękę",
"tooltip.muteScreenSharing": "Anuluj udostępniania pulpitu przez użytkownika",
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj",
@ -94,7 +101,7 @@
"label.filesharing": "Udostępnianie plików",
"label.participants": "Uczestnicy",
"label.shareFile": "Udostępnij plik",
"label.shareGalleryFile": null,
"label.shareGalleryFile": "Udostępnij obraz",
"label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane",
"label.unknown": "Nieznane",
"label.democratic": "Układ demokratyczny",
@ -105,12 +112,12 @@
"label.veryHigh": "Bardzo wysoka (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Zamknij",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"label.media": "Media",
"label.appearance": "Wygląd",
"label.advanced": "Zaawansowane",
"label.addVideo": "Dodaj wideo",
"label.promoteAllPeers": "Wpuść wszystkich",
"label.moreActions": "Więcej akcji",
"settings.settings": "Ustawienia",
"settings.camera": "Kamera",
@ -128,9 +135,14 @@
"settings.advancedMode": "Tryb zaawansowany",
"settings.permanentTopBar": "Stały górny pasek",
"settings.lastn": "Liczba widocznych uczestników (zdalnych)",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.hiddenControls": "Ukryte kontrolki mediów",
"settings.notificationSounds": "Powiadomienia dźwiękiem",
"settings.showNotifications": "Pokaż powiadomienia",
"settings.buttonControlBar": "Rozdziel kontrolki mediów",
"settings.echoCancellation": "Usuwanie echa",
"settings.autoGainControl": "Auto korekta wzmocnienia",
"settings.noiseSuppression": "Wyciszenie szumów",
"settings.drawerOverlayed": "Szuflada nad zawartością",
"filesharing.saveFileError": "Nie można zapisać pliku",
"filesharing.startingFileShare": "Próba udostępnienia pliku",
@ -172,8 +184,8 @@
"devices.cameraDisconnected": "Kamera odłączona",
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "Moderator wyczyścił chat",
"moderator.clearFiles": "Moderator wyczyścił pliki",
"moderator.muteAudio": "Moderator wyciszył audio",
"moderator.muteVideo": "Moderator wyciszył twoje video"
}

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Fechar",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro",

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare",
@ -106,7 +113,7 @@
"label.ultra": "Rezoluție ultra înaltă (UHD)",
"label.close": "Închide",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului",

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Oda adı",
"label.chooseRoomButton": "Devam",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)",
"label.close": "Kapat",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -128,6 +135,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Dosya kaydedilemiyor",
"filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor",

View File

@ -0,0 +1,190 @@
{
"socket.disconnected": "您已斷開連接",
"socket.reconnecting": "嘗試重新連接",
"socket.reconnected": "您已重新連接",
"socket.requestError": "服務器請求錯誤",
"room.chooseRoom": "選擇您要加入的房間的名稱",
"room.cookieConsent": "這個網站使用Cookies來提升您的使用者體驗",
"room.consentUnderstand": "了解",
"room.joined": "您已加入房間",
"room.cantJoin": "無法加入房間",
"room.youLocked": "您已鎖定房間",
"room.cantLock": "無法鎖定房間",
"room.youUnLocked": "您解鎖了房間",
"room.cantUnLock": "無法解鎖房間",
"room.locked": "房間已鎖定",
"room.unlocked": "房間現已解鎖",
"room.newLobbyPeer": "新參與者進入大廳",
"room.lobbyPeerLeft": "參與者離開大廳",
"room.lobbyPeerChangedDisplayName": "大廳的參與者將名稱變更為 {displayName}",
"room.lobbyPeerChangedPicture": "大廳的參與者變更了圖片",
"room.setAccessCode": "設置房間的進入密碼",
"room.accessCodeOn": "房間的進入密碼現已啟用",
"room.accessCodeOff": "房間的進入密碼已停用",
"room.peerChangedDisplayName": "{oldDisplayName} 已變更名稱為 {displayName}",
"room.newPeer": "{displayName} 加入了會議室",
"room.newFile": "有新文件",
"room.toggleAdvancedMode": "切換進階模式",
"room.setDemocraticView": "已更改為使用者佈局",
"room.setFilmStripView": "已更改為投影片佈局",
"room.loggedIn": "您已登入",
"room.loggedOut": "您已登出",
"room.changedDisplayName": "您的顯示名稱已變更為 {displayName}",
"room.changeDisplayNameError": "更改顯示名稱時發生錯誤",
"room.chatError": "無法發送聊天消息",
"room.aboutToJoin": "您即將參加會議",
"room.roomId": "房間ID: {roomName}",
"room.setYourName": "設置您的顯示名稱,並選擇您想加入的方式:",
"room.audioOnly": "僅通話",
"room.audioVideo": "通話和視訊",
"room.youAreReady": "準備完畢!",
"room.emptyRequireLogin": "房間是空的! 您可以登錄以開始會議或等待主持人加入",
"room.locketWait": "房間已鎖定! 請等待其他人允許您進入...",
"room.lobbyAdministration": "大廳管理",
"room.peersInLobby": "大廳的參與者",
"room.lobbyEmpty": "大廳目前沒有人",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "我",
"room.spotlights": "Spotlight中的參與者",
"room.passive": "被動參與者",
"room.videoPaused": "視訊已關閉",
"room.muteAll": "全部靜音",
"room.stopAllVideo": "關閉全部視訊",
"room.closeMeeting": "關閉會議",
"room.clearChat": "清除聊天",
"room.clearFileSharing": "清除檔案",
"room.speechUnsupported": "您的瀏覽器不支援語音辨識",
"room.moderatoractions": "管理員動作",
"room.raisedHand": "{displayName} 舉手了",
"room.loweredHand": "{displayName} 放下了他的手",
"room.extraVideo": "其他視訊",
"room.overRoomLimit": "房間已滿,請稍後重試",
"room.help": "幫助",
"room.about": "關於",
"room.shortcutKeys": "鍵盤快速鍵",
"me.mutedPTT": "您已靜音,請按下 空白鍵 來說話",
"roles.gotRole": "您已取得身份: {role}",
"roles.lostRole": "您的 {role} 身份已被撤銷",
"tooltip.login": "登入",
"tooltip.logout": "登出",
"tooltip.admitFromLobby": "從大廳允許",
"tooltip.lockRoom": "鎖定房間",
"tooltip.unLockRoom": "解鎖房間",
"tooltip.enterFullscreen": "進入全螢幕",
"tooltip.leaveFullscreen": "退出全螢幕",
"tooltip.lobby": "顯示大廳",
"tooltip.settings": "顯示設置",
"tooltip.participants": "顯示參加者",
"tooltip.kickParticipant": "踢出",
"tooltip.muteParticipant": "靜音",
"tooltip.muteParticipantVideo": "隱藏視訊",
"tooltip.raisedHand": "舉手",
"tooltip.muteScreenSharing": "隱藏螢幕分享",
"tooltip.muteParticipantAudioModerator": "關閉聲音",
"tooltip.muteParticipantVideoModerator": "關閉視訊",
"tooltip.muteScreenSharingModerator": "關閉螢幕分享",
"label.roomName": "房間名稱",
"label.chooseRoomButton": "繼續",
"label.yourName": "您的名字",
"label.newWindow": "新視窗",
"label.fullscreen": "全螢幕",
"label.openDrawer": "打開側邊欄",
"label.leave": "離開",
"label.chatInput": "輸入聊天訊息",
"label.chat": "聊天",
"label.filesharing": "文件分享",
"label.participants": "參與者",
"label.shareFile": "分享文件",
"label.shareGalleryFile": "分享圖片",
"label.fileSharingUnsupported": "不支援文件分享",
"label.unknown": "未知",
"label.democratic": "使用者佈局",
"label.filmstrip": "投影片佈局",
"label.low": "低",
"label.medium": "中",
"label.high": "高 (HD)",
"label.veryHigh": "非常高 (FHD)",
"label.ultra": "超高 (UHD)",
"label.close": "關閉",
"label.media": "媒體",
"label.appearance": "外觀",
"label.advanced": "進階",
"label.addVideo": "新增視訊",
"label.promoteAllPeers": "提升全部",
"label.moreActions": "更多",
"settings.settings": "設置",
"settings.camera": "視訊來源",
"settings.selectCamera": "選擇視訊來源",
"settings.cantSelectCamera": "無法選擇此視訊來源",
"settings.audio": "音訊來源",
"settings.selectAudio": "選擇音訊來源",
"settings.cantSelectAudio": "無法選擇音訊來源",
"settings.audioOutput": "音訊輸出",
"settings.selectAudioOutput": "選擇音訊輸出設備",
"settings.cantSelectAudioOutput": "無法選擇音訊輸出設備",
"settings.resolution": "選擇視訊解析度",
"settings.layout": "房間佈局",
"settings.selectRoomLayout": "選擇房間佈局",
"settings.advancedMode": "進階模式",
"settings.permanentTopBar": "固定頂端列",
"settings.lastn": "視訊數量上限",
"settings.hiddenControls": "隱藏控制按鈕",
"settings.notificationSounds": "通知音效",
"settings.showNotifications": "顯示通知",
"settings.buttonControlBar": "獨立控制按鈕",
"settings.echoCancellation": "回音消除",
"settings.autoGainControl": "自動增益控制",
"settings.noiseSuppression": "噪音消除",
"settings.drawerOverlayed": "側邊欄覆蓋畫面",
"filesharing.saveFileError": "無法保存文件",
"filesharing.startingFileShare": "開始分享文件",
"filesharing.successfulFileShare": "文件已成功分享",
"filesharing.unableToShare": "無法分享文件",
"filesharing.error": "文件分享發生錯誤",
"filesharing.finished": "文件分享成功",
"filesharing.save": "保存文件",
"filesharing.sharedFile": "{displayName} 分享了一個文件",
"filesharing.download": "下載文件",
"filesharing.missingSeeds": "如果過了很久還是無法下載,則可能沒有人播種了。請讓上傳者重新上傳您想要的文件。",
"devices.devicesChanged": "您的設備已更改,請在設置中設定您的設備",
"device.audioUnsupported": "不支援您的音訊格式",
"device.activateAudio": "開啟音訊",
"device.muteAudio": "靜音",
"device.unMuteAudio": "取消靜音",
"device.videoUnsupported": "不支援您的視訊格式",
"device.startVideo": "開啟視訊",
"device.stopVideo": "關閉視訊",
"device.screenSharingUnsupported": "不支援您的螢幕分享格式",
"device.startScreenSharing": "開始螢幕分享",
"device.stopScreenSharing": "停止螢幕分享",
"devices.microphoneDisconnected": "麥克風已斷開",
"devices.microphoneError": "麥克風發生錯誤",
"devices.microphoneMute": "麥克風靜音",
"devices.microphoneUnMute": "取消麥克風靜音",
"devices.microphoneEnable": "麥克風已啟用",
"devices.microphoneMuteError": "無法使麥克風靜音",
"devices.microphoneUnMuteError": "無法取消麥克風靜音",
"devices.screenSharingDisconnected" : "螢幕分享已斷開",
"devices.screenSharingError": "螢幕分享時發生錯誤",
"devices.cameraDisconnected": "相機已斷開連接",
"devices.cameraError": "存取相機時發生錯誤",
"moderator.clearChat": "管理員清除了聊天",
"moderator.clearFiles": "管理員清除了所有檔案",
"moderator.muteAudio": "您已被管理員靜音",
"moderator.muteVideo": "您的視訊已被管理員關閉"
}

View File

@ -60,6 +60,10 @@
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Назва кімнати",
"label.chooseRoomButton": "Продовжити",
@ -106,7 +113,7 @@
"label.ultra": "Ультра (UHD)",
"label.close": "Закрити",
"label.media": null,
"label.appearence": null,
"label.appearance": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Неможливо зберегти файл",
"filesharing.startingFileShare": "Спроба поділитися файлом",

11
server/access.js 100644
View File

@ -0,0 +1,11 @@
module.exports = {
// The role(s) will gain access to the room
// even if it is locked (!)
BYPASS_ROOM_LOCK : 'BYPASS_ROOM_LOCK',
// The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : 'BYPASS_LOBBY'
};

View File

@ -1,5 +1,23 @@
const os = require('os');
const userRoles = require('../userRoles');
const {
BYPASS_ROOM_LOCK,
BYPASS_LOBBY
} = require('../access');
const {
CHANGE_ROOM_LOCK,
PROMOTE_PEER,
SEND_CHAT,
MODERATE_CHAT,
SHARE_SCREEN,
EXTRA_VIDEO,
SHARE_FILE,
MODERATE_FILES,
MODERATE_ROOM
} = require('../permissions');
// const AwaitQueue = require('awaitqueue');
// const axios = require('axios');
@ -216,44 +234,50 @@ module.exports =
accessFromRoles : {
// The role(s) will gain access to the room
// even if it is locked (!)
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
[BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
// The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ]
[BYPASS_LOBBY] : [ userRoles.NORMAL ]
},
permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
[CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ],
// The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : [ userRoles.NORMAL ],
[PROMOTE_PEER] : [ userRoles.NORMAL ],
// The role(s) have permission to send chat messages
SEND_CHAT : [ userRoles.NORMAL ],
[SEND_CHAT] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ],
[MODERATE_CHAT] : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ],
[SHARE_SCREEN] : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
EXTRA_VIDEO : [ userRoles.NORMAL ],
[EXTRA_VIDEO] : [ userRoles.NORMAL ],
// The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ],
[SHARE_FILE] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files
MODERATE_FILES : [ userRoles.MODERATOR ],
[MODERATE_FILES] : [ userRoles.MODERATOR ],
// The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : [ userRoles.MODERATOR ]
[MODERATE_ROOM] : [ userRoles.MODERATOR ]
},
// Array of permissions. If no peer with the permission in question
// is in the room, all peers are permitted to do the action. The peers
// that are allowed because of this rule will not be able to do this
// action as soon as a peer with the permission joins. In this example
// everyone will be able to lock/unlock room until a MODERATOR joins.
allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ],
// When truthy, the room will be open to all users when as long as there
// are allready users in the room
activateOnHostJoin : true,
activateOnHostJoin : true,
// When set, maxUsersPerRoom defines how many users can join
// a single room. If not set, there is no limit.
// maxUsersPerRoom : 20,
// Room size before spreading to new router
routerScaleSize : 40,
routerScaleSize : 40,
// Mediasoup settings
mediasoup :
mediasoup :
{
numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings.

View File

@ -46,7 +46,7 @@ class Lobby extends EventEmitter
return Object.values(this._peers).map((peer) =>
({
peerId : peer.id,
id : peer.id,
displayName : peer.displayName,
picture : peer.picture
}));
@ -154,8 +154,6 @@ class Lobby extends EventEmitter
this.emit('lobbyEmpty');
};
this._notification(peer.socket, 'enteredLobby');
this._peers[peer.id] = peer;
peer.on('gotRole', peer.gotRoleHandler);
@ -165,6 +163,8 @@ class Lobby extends EventEmitter
peer.socket.on('request', peer.socketRequestHandler);
peer.on('close', peer.closeHandler);
this._notification(peer.socket, 'enteredLobby');
}
async _handleSocketRequest(peer, request, cb)
@ -189,7 +189,8 @@ class Lobby extends EventEmitter
cb();
break;
}
}
case 'changePicture':
{
const { picture } = request.data;

View File

@ -64,10 +64,10 @@ class Peer extends EventEmitter
// Iterate and close all mediasoup Transport associated to this Peer, so all
// its Producers and Consumers will also be closed.
this.transports.forEach((transport) =>
for (const transport of this.transports.values())
{
transport.close();
});
}
if (this.socket)
this.socket.disconnect(true);

View File

@ -1,36 +1,57 @@
const EventEmitter = require('events').EventEmitter;
const AwaitQueue = require('awaitqueue');
const axios = require('axios');
const Logger = require('./Logger');
const Lobby = require('./Lobby');
const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken');
const userRoles = require('../userRoles');
const {
BYPASS_ROOM_LOCK,
BYPASS_LOBBY
} = require('../access');
const permissions = require('../permissions'), {
CHANGE_ROOM_LOCK,
PROMOTE_PEER,
SEND_CHAT,
MODERATE_CHAT,
SHARE_SCREEN,
EXTRA_VIDEO,
SHARE_FILE,
MODERATE_FILES,
MODERATE_ROOM
} = permissions;
const config = require('../config/config');
const logger = new Logger('Room');
// In case they are not configured properly
const accessFromRoles =
const roomAccess =
{
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
BYPASS_LOBBY : [ userRoles.NORMAL ],
[BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
[BYPASS_LOBBY] : [ userRoles.NORMAL ],
...config.accessFromRoles
};
const permissionsFromRoles =
const roomPermissions =
{
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
PROMOTE_PEER : [ userRoles.NORMAL ],
SEND_CHAT : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ],
[CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ],
[PROMOTE_PEER] : [ userRoles.NORMAL ],
[SEND_CHAT] : [ userRoles.NORMAL ],
[MODERATE_CHAT] : [ userRoles.MODERATOR ],
[SHARE_SCREEN] : [ userRoles.NORMAL ],
[EXTRA_VIDEO] : [ userRoles.NORMAL ],
[SHARE_FILE] : [ userRoles.NORMAL ],
[MODERATE_FILES] : [ userRoles.MODERATOR ],
[MODERATE_ROOM] : [ userRoles.MODERATOR ],
...config.permissionsFromRoles
};
const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || [];
const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
class Room extends EventEmitter
@ -97,6 +118,9 @@ class Room extends EventEmitter
// Closed flag.
this._closed = false;
// Joining queue
this._queue = new AwaitQueue();
// Locked flag.
this._locked = false;
@ -148,6 +172,10 @@ class Room extends EventEmitter
this._closed = true;
this._queue.close();
this._queue = null;
if (this._selfDestructTimeout)
clearTimeout(this._selfDestructTimeout);
@ -221,9 +249,8 @@ class Room extends EventEmitter
// Returning user
if (returning)
this._peerJoining(peer, true);
else if ( // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
)
// Has a role that is allowed to bypass room lock
else if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
this._peerJoining(peer);
else if (
'maxUsersPerRoom' in config &&
@ -239,7 +266,7 @@ class Room extends EventEmitter
else
{
// Has a role that is allowed to bypass lobby
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ?
this._hasAccess(peer, BYPASS_LOBBY) ?
this._peerJoining(peer) :
this._handleGuest(peer);
}
@ -271,11 +298,7 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer);
for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
}
@ -283,9 +306,8 @@ class Room extends EventEmitter
this._lobby.on('peerRolesChanged', (peer) =>
{
if ( // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
)
// Has a role that is allowed to bypass room lock
if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
{
this._lobby.promotePeer(peer.id);
@ -294,7 +316,7 @@ class Room extends EventEmitter
if ( // Has a role that is allowed to bypass lobby
!this._locked &&
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role))
this._hasAccess(peer, BYPASS_LOBBY)
)
{
this._lobby.promotePeer(peer.id);
@ -307,11 +329,7 @@ class Room extends EventEmitter
{
const { id, displayName } = changedPeer;
for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
}
@ -321,11 +339,7 @@ class Room extends EventEmitter
{
const { id, picture } = changedPeer;
for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
}
@ -337,11 +351,7 @@ class Room extends EventEmitter
const { id } = closedPeer;
for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
}
@ -401,7 +411,7 @@ class Room extends EventEmitter
);
}
async dump()
dump()
{
return {
roomId : this._roomId,
@ -447,118 +457,91 @@ class Room extends EventEmitter
{
this._lobby.parkPeer(parkPeer);
for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
}
}
async _peerJoining(peer, returning = false)
_peerJoining(peer, returning = false)
{
peer.socket.join(this._roomId);
// If we don't have this peer, add to end
!this._lastN.includes(peer.id) && this._lastN.push(peer.id);
this._peers[peer.id] = peer;
// Assign routerId
peer.routerId = await this._getRouterId();
this._handlePeer(peer);
if (returning)
this._queue.push(async () =>
{
this._notification(peer.socket, 'roomBack');
}
else
{
const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
peer.socket.join(this._roomId);
peer.socket.handshake.session.token = token;
// If we don't have this peer, add to end
!this._lastN.includes(peer.id) && this._lastN.push(peer.id);
peer.socket.handshake.session.save();
this._peers[peer.id] = peer;
let turnServers;
if ('turnAPIURI' in config)
// Assign routerId
peer.routerId = await this._getRouterId();
this._handlePeer(peer);
if (returning)
{
try
{
const { data } = await axios.get(
config.turnAPIURI,
{
params : {
...config.turnAPIparams,
'api_key' : config.turnAPIKey,
'ip' : peer.socket.request.connection.remoteAddress
}
});
turnServers = [ {
urls : data.uris,
username : data.username,
credential : data.password
} ];
}
catch (error)
{
if ('backupTurnServers' in config)
turnServers = config.backupTurnServers;
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
}
this._notification(peer.socket, 'roomBack');
}
else if ('backupTurnServers' in config)
else
{
turnServers = config.backupTurnServers;
const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
peer.socket.handshake.session.token = token;
peer.socket.handshake.session.save();
let turnServers;
if ('turnAPIURI' in config)
{
try
{
const { data } = await axios.get(
config.turnAPIURI,
{
params : {
...config.turnAPIparams,
'api_key' : config.turnAPIKey,
'ip' : peer.socket.request.connection.remoteAddress
}
});
turnServers = [ {
urls : data.uris,
username : data.username,
credential : data.password
} ];
}
catch (error)
{
if ('backupTurnServers' in config)
turnServers = config.backupTurnServers;
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
}
}
else if ('backupTurnServers' in config)
{
turnServers = config.backupTurnServers;
}
this._notification(peer.socket, 'roomReady', { turnServers });
}
this._notification(peer.socket, 'roomReady', { turnServers });
}
})
.catch((error) =>
{
logger.error('_peerJoining() [error:"%o"]', error);
});
}
_handlePeer(peer)
{
logger.debug('_handlePeer() [peer:"%s"]', peer.id);
peer.socket.on('request', (request, cb) =>
{
logger.debug(
'Peer "request" event [method:"%s", peerId:"%s"]',
request.method, peer.id);
this._handleSocketRequest(peer, request, cb)
.catch((error) =>
{
logger.error('"request" failed [error:"%o"]', error);
cb(error);
});
});
peer.on('close', () =>
{
if (this._closed)
return;
// If the Peer was joined, notify all Peers.
if (peer.joined)
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
// Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
delete this._peers[peer.id];
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty())
this.selfDestructCountdown();
this._handlePeerClose(peer);
});
peer.on('displayNameChanged', ({ oldDisplayName }) =>
@ -602,7 +585,7 @@ class Room extends EventEmitter
// Got permission to promote peers, notify peer of
// peers in lobby
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole))
if (roomPermissions.PROMOTE_PEER.includes(newRole))
{
const lobbyPeers = this._lobby.peerList();
@ -624,6 +607,69 @@ class Room extends EventEmitter
role : oldRole
}, true, true);
});
peer.socket.on('request', (request, cb) =>
{
logger.debug(
'Peer "request" event [method:"%s", peerId:"%s"]',
request.method, peer.id);
this._handleSocketRequest(peer, request, cb)
.catch((error) =>
{
logger.error('"request" failed [error:"%o"]', error);
cb(error);
});
});
// Peer left before we were done joining
if (peer.closed)
this._handlePeerClose(peer);
}
_handlePeerClose(peer)
{
logger.debug('_handlePeerClose() [peer:"%s"]', peer.id);
if (this._closed)
return;
// If the Peer was joined, notify all Peers.
if (peer.joined)
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
// Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
// Need this to know if this peer was the last with PROMOTE_PEER
const hasPromotePeer = peer.roles.some((role) =>
roomPermissions[PROMOTE_PEER].includes(role)
);
delete this._peers[peer.id];
// No peers left with PROMOTE_PEER, might need to give
// lobbyPeers to peers that are left.
if (
hasPromotePeer &&
!this._lobby.checkEmpty() &&
roomAllowWhenRoleMissing.includes(PROMOTE_PEER) &&
this._getPeersWithPermission(PROMOTE_PEER).length === 0
)
{
const lobbyPeers = this._lobby.peerList();
for (const allowedPeer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(allowedPeer.socket, 'parkedPeers', { lobbyPeers });
}
}
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty())
this.selfDestructCountdown();
}
async _handleSocketRequest(peer, request, cb)
@ -660,22 +706,15 @@ class Room extends EventEmitter
// Tell the new Peer about already joined Peers.
// And also create Consumers for existing Producers.
const joinedPeers =
[
...this._getJoinedPeers()
];
const joinedPeers = this._getJoinedPeers(peer);
const peerInfos = joinedPeers
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo));
let lobbyPeers = [];
if ( // Allowed to promote peers, notify about lobbypeers
peer.roles.some((role) =>
permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
// Allowed to promote peers, notify about lobbypeers
if (this._hasPermission(peer, PROMOTE_PEER))
lobbyPeers = this._lobby.peerList();
cb(null, {
@ -683,8 +722,9 @@ class Room extends EventEmitter
peers : peerInfos,
tracker : config.fileTracker,
authenticated : peer.authenticated,
permissionsFromRoles : permissionsFromRoles,
roomPermissions : roomPermissions,
userRoles : userRoles,
allowWhenRoleMissing : roomAllowWhenRoleMissing,
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
@ -711,7 +751,7 @@ class Room extends EventEmitter
}
// Notify the new Peer to all other Peers.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
for (const otherPeer of this._getJoinedPeers(peer))
{
this._notification(
otherPeer.socket,
@ -821,15 +861,13 @@ class Room extends EventEmitter
if (
appData.source === 'screen' &&
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
!this._hasPermission(peer, SHARE_SCREEN)
)
throw new Error('peer not authorized');
if (
appData.source === 'extravideo' &&
!peer.roles.some(
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
!this._hasPermission(peer, EXTRA_VIDEO)
)
throw new Error('peer not authorized');
@ -882,7 +920,7 @@ class Room extends EventEmitter
cb(null, { id: producer.id });
// Optimization: Create a server-side Consumer for each Peer.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
for (const otherPeer of this._getJoinedPeers(peer))
{
this._createConsumer(
{
@ -1144,9 +1182,7 @@ class Room extends EventEmitter
case 'chatMessage':
{
if (
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
)
if (!this._hasPermission(peer, SEND_CHAT))
throw new Error('peer not authorized');
const { chatMessage } = request.data;
@ -1167,11 +1203,7 @@ class Room extends EventEmitter
case 'moderator:clearChat':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_CHAT))
throw new Error('peer not authorized');
this._chatHistory = [];
@ -1187,11 +1219,7 @@ class Room extends EventEmitter
case 'lockRoom':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
throw new Error('peer not authorized');
this._locked = true;
@ -1209,11 +1237,7 @@ class Room extends EventEmitter
case 'unlockRoom':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
throw new Error('peer not authorized');
this._locked = false;
@ -1271,11 +1295,7 @@ class Room extends EventEmitter
case 'promotePeer':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
if (!this._hasPermission(peer, PROMOTE_PEER))
throw new Error('peer not authorized');
const { peerId } = request.data;
@ -1290,11 +1310,7 @@ class Room extends EventEmitter
case 'promoteAllPeers':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
if (!this._hasPermission(peer, PROMOTE_PEER))
throw new Error('peer not authorized');
this._lobby.promoteAllPeers();
@ -1307,11 +1323,7 @@ class Room extends EventEmitter
case 'sendFile':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
)
)
if (!this._hasPermission(peer, SHARE_FILE))
throw new Error('peer not authorized');
const { magnetUri } = request.data;
@ -1332,11 +1344,7 @@ class Room extends EventEmitter
case 'moderator:clearFileSharing':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_FILES.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_FILES))
throw new Error('peer not authorized');
this._fileHistory = [];
@ -1369,13 +1377,28 @@ class Room extends EventEmitter
break;
}
case 'moderator:mute':
{
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
const mutePeer = this._peers[peerId];
if (!mutePeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(mutePeer.socket, 'moderator:mute');
cb();
break;
}
case 'moderator:muteAll':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
// Spread to others
@ -1386,13 +1409,28 @@ class Room extends EventEmitter
break;
}
case 'moderator:stopVideo':
{
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
const stopVideoPeer = this._peers[peerId];
if (!stopVideoPeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(stopVideoPeer.socket, 'moderator:stopVideo');
cb();
break;
}
case 'moderator:stopAllVideo':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
// Spread to others
@ -1405,11 +1443,7 @@ class Room extends EventEmitter
case 'moderator:closeMeeting':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
this._notification(peer.socket, 'moderator:kick', null, true);
@ -1424,11 +1458,7 @@ class Room extends EventEmitter
case 'moderator:kickPeer':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
@ -1449,11 +1479,7 @@ class Room extends EventEmitter
case 'moderator:lowerHand':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
@ -1631,16 +1657,54 @@ class Room extends EventEmitter
}
}
_hasPermission(peer, permission)
{
const hasPermission = peer.roles.some((role) =>
roomPermissions[permission].includes(role)
);
if (hasPermission)
return true;
// Allow if config is set, and no one is present
if (
roomAllowWhenRoleMissing.includes(permission) &&
this._getPeersWithPermission(permission).length === 0
)
return true;
return false;
}
_hasAccess(peer, access)
{
return peer.roles.some((role) => roomAccess[access].includes(role));
}
/**
* Helper to get the list of joined peers.
*/
_getJoinedPeers({ excludePeer = undefined } = {})
_getJoinedPeers(excludePeer = undefined)
{
return Object.values(this._peers)
.filter((peer) => peer.joined && peer !== excludePeer);
}
_getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true })
_getAllowedPeers(permission = null, excludePeer = undefined, joined = true)
{
const peers = this._getPeersWithPermission(permission, excludePeer, joined);
if (peers.length > 0)
return peers;
// Allow if config is set, and no one is present
if (roomAllowWhenRoleMissing.includes(permission))
return Object.values(this._peers);
return peers;
}
_getPeersWithPermission(permission = null, excludePeer = undefined, joined = true)
{
return Object.values(this._peers)
.filter(
@ -1648,7 +1712,7 @@ class Room extends EventEmitter
peer.joined === joined &&
peer !== excludePeer &&
peer.roles.some(
(role) => permission.includes(role)
(role) => roomPermissions[permission].includes(role)
)
);
}

View File

@ -7,7 +7,7 @@
"license": "MIT",
"main": "lib/index.js",
"scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js",
"start": "node server.js",
"connect": "node connect.js",
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
},

View File

@ -0,0 +1,20 @@
module.exports = {
// The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK',
// The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : 'PROMOTE_PEER',
// The role(s) have permission to send chat messages
SEND_CHAT : 'SEND_CHAT',
// The role(s) have permission to moderate chat
MODERATE_CHAT : 'MODERATE_CHAT',
// The role(s) have permission to share screen
SHARE_SCREEN : 'SHARE_SCREEN',
// The role(s) have permission to produce extra video
EXTRA_VIDEO : 'EXTRA_VIDEO',
// The role(s) have permission to share files
SHARE_FILE : 'SHARE_FILE',
// The role(s) have permission to moderate files
MODERATE_FILES : 'MODERATE_FILES',
// The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : 'MODERATE_ROOM'
};

View File

@ -127,69 +127,58 @@ let oidcStrategy;
async function run()
{
// Open the interactive server.
await interactiveServer(rooms, peers);
// start Prometheus exporter
if (config.prometheus)
try
{
await promExporter(rooms, peers, config.prometheus);
}
// Open the interactive server.
await interactiveServer(rooms, peers);
if (typeof(config.auth) === 'undefined')
{
logger.warn('Auth is not configured properly!');
}
else
{
await setupAuth();
}
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
// eslint-disable-next-line no-unused-vars
function errorHandler(err, req, res, next)
{
const trackingId = uuidv4();
res.status(500).send(
`<h1>Internal Server Error</h1>
<p>If you report this error, please also report this
<i>tracking ID</i> which makes it possible to locate your session
in the logs which are available to the system administrator:
<b>${trackingId}</b></p>`
);
logger.error(
'Express error handler dump with tracking ID: %s, error dump: %o',
trackingId, err);
}
app.use(errorHandler);
// Log rooms status every 30 seconds.
setInterval(() =>
{
for (const room of rooms.values())
// start Prometheus exporter
if (config.prometheus)
{
room.logStatus();
await promExporter(rooms, peers, config.prometheus);
}
}, 120000);
// check for deserted rooms
setInterval(() =>
{
for (const room of rooms.values())
if (typeof(config.auth) === 'undefined')
{
room.checkEmpty();
logger.warn('Auth is not configured properly!');
}
}, 10000);
else
{
await setupAuth();
}
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
const errorHandler = (err, req, res, next) =>
{
const trackingId = uuidv4();
res.status(500).send(
`<h1>Internal Server Error</h1>
<p>If you report this error, please also report this
<i>tracking ID</i> which makes it possible to locate your session
in the logs which are available to the system administrator:
<b>${trackingId}</b></p>`
);
logger.error(
'Express error handler dump with tracking ID: %s, error dump: %o',
trackingId, err);
};
// eslint-disable-next-line no-unused-vars
app.use(errorHandler);
}
catch (error)
{
logger.error('run() [error:"%o"]', error);
}
}
function statusLog()
@ -379,38 +368,45 @@ async function setupAuth()
app.get(
'/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
async (req, res) =>
async (req, res, next) =>
{
const state = JSON.parse(base64.decode(req.query.state));
const { peerId, roomId } = state;
req.session.peerId = peerId;
req.session.roomId = roomId;
let peer = peers.get(peerId);
if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId });
if (peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (typeof config.userMapping === 'function')
try
{
await config.userMapping({
peer,
roomId,
userinfo : req.user._userinfo
});
const state = JSON.parse(base64.decode(req.query.state));
const { peerId, roomId } = state;
req.session.peerId = peerId;
req.session.roomId = roomId;
let peer = peers.get(peerId);
if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId });
if (peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (typeof config.userMapping === 'function')
{
await config.userMapping({
peer,
roomId,
userinfo : req.user._userinfo
});
}
peer.authenticated = true;
res.send(loginHelper({
displayName : peer.displayName,
picture : peer.picture
}));
}
catch (error)
{
return next(error);
}
peer.authenticated = true;
res.send(loginHelper({
displayName : peer.displayName,
picture : peer.picture
}));
}
);
}
@ -597,7 +593,8 @@ async function runWebSocketServer()
{
logger.error('room creation or room joining failed [error:"%o"]', error);
socket.disconnect(true);
if (socket)
socket.disconnect(true);
return;
});