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, voiceActivityMute : false,
sampleSize : 16 sampleSize : 16
}, },
background : 'images/background.jpg', background : 'images/background.jpg',
defaultLayout : 'democratic', // democratic, filmstrip defaultLayout : 'democratic', // democratic, filmstrip
lastN : 4, // If true, will show media control buttons in separate
mobileLastN : 1, // 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 // Highest number of speakers user can select
maxLastN : 5, maxLastN : 5,
// If truthy, users can NOT change number of speakers visible // If truthy, users can NOT change number of speakers visible
lockLastN : false, lockLastN : false,
// Add file and uncomment for adding logo to appbar // Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg', // logo : 'images/logo.svg',
title : 'Multiparty meeting', title : 'Multiparty meeting',
theme : theme :
{ {
palette : palette :
{ {

View File

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

View File

@ -12,6 +12,7 @@ export default class Spotlights extends EventEmitter
this._signalingSocket = signalingSocket; this._signalingSocket = signalingSocket;
this._maxSpotlights = maxSpotlights; this._maxSpotlights = maxSpotlights;
this._peerList = []; this._peerList = [];
this._unmutablePeerList = [];
this._selectedSpotlights = []; this._selectedSpotlights = [];
this._currentSpotlights = []; this._currentSpotlights = [];
this._started = false; 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) setPeerSpotlight(peerId)
{ {
logger.debug('setPeerSpotlight() [peerId:"%s"]', 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) _newPeer(id)
{ {
logger.debug( logger.debug(
@ -105,6 +175,7 @@ export default class Spotlights extends EventEmitter
logger.debug('_handlePeer() | adding peer [peerId: "%s"]', id); logger.debug('_handlePeer() | adding peer [peerId: "%s"]', id);
this._peerList.push(id); this._peerList.push(id);
this._unmutablePeerList.push(id);
if (this._started) if (this._started)
this._spotlightsUpdated(); this._spotlightsUpdated();
@ -117,6 +188,7 @@ export default class Spotlights extends EventEmitter
'room "peerClosed" event [peerId:%o]', id); 'room "peerClosed" event [peerId:%o]', id);
this._peerList = this._peerList.filter((peer) => peer !== 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); this._selectedSpotlights = this._selectedSpotlights.filter((peer) => peer !== id);

View File

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

View File

@ -10,6 +10,11 @@ export const removePeer = (peerId) =>
payload : { peerId } payload : { peerId }
}); });
export const clearPeers = () =>
({
type : 'CLEAR_PEERS'
});
export const setPeerDisplayName = (displayName, peerId) => export const setPeerDisplayName = (displayName, peerId) =>
({ ({
type : 'SET_PEER_DISPLAY_NAME', type : 'SET_PEER_DISPLAY_NAME',
@ -69,3 +74,15 @@ export const setPeerKickInProgress = (peerId, flag) =>
type : 'SET_PEER_KICK_IN_PROGRESS', type : 'SET_PEER_KICK_IN_PROGRESS',
payload : { peerId, flag } 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 } 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) => export const setSettingsTab = (tab) =>
({ ({
type : 'SET_SETTINGS_TAB', type : 'SET_SETTINGS_TAB',
@ -118,6 +130,11 @@ export const setSpotlights = (spotlights) =>
payload : { spotlights } payload : { spotlights }
}); });
export const clearSpotlights = () =>
({
type : 'CLEAR_SPOTLIGHTS'
});
export const toggleJoined = () => export const toggleJoined = () =>
({ ({
type : 'TOGGLE_JOINED' type : 'TOGGLE_JOINED'
@ -165,14 +182,14 @@ export const setClearFileSharingInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setUserRoles = (userRoles) => export const setRoomPermissions = (roomPermissions) =>
({ ({
type : 'SET_USER_ROLES', type : 'SET_ROOM_PERMISSIONS',
payload : { userRoles } payload : { roomPermissions }
}); });
export const setPermissionsFromRoles = (permissionsFromRoles) => export const setAllowWhenRoleMissing = (allowWhenRoleMissing) =>
({ ({
type : 'SET_PERMISSIONS_FROM_ROLES', type : 'SET_ALLOW_WHEN_ROLE_MISSING',
payload : { permissionsFromRoles } payload : { allowWhenRoleMissing }
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -249,55 +249,53 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
{ smallContainer ? <IconButton
<IconButton aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'device.muteAudio',
id : 'device.muteAudio', defaultMessage : 'Mute audio'
defaultMessage : 'Mute audio' })}
})} className={classes.smallContainer}
className={classes.smallContainer} disabled={!micConsumer}
disabled={!micConsumer} color='primary'
color='primary' size='small'
size='small' onClick={() =>
onClick={() => {
{ micEnabled ?
micEnabled ? roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', false);
roomClient.modifyPeerConsumer(peer.id, 'mic', false); }}
}} >
> { micEnabled ?
{ micEnabled ? <VolumeUpIcon />
<VolumeUpIcon /> :
: <VolumeOffIcon />
<VolumeOffIcon /> }
} </IconButton>
</IconButton> :
: <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'device.muteAudio',
id : 'device.muteAudio', defaultMessage : 'Mute audio'
defaultMessage : 'Mute audio' })}
})} className={classes.fab}
className={classes.fab} disabled={!micConsumer}
disabled={!micConsumer} color={micEnabled ? 'default' : 'secondary'}
color={micEnabled ? 'default' : 'secondary'} size='large'
size='large' onClick={() =>
onClick={() => {
{ micEnabled ?
micEnabled ? roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', false);
roomClient.modifyPeerConsumer(peer.id, 'mic', false); }}
}} >
> { micEnabled ?
{ micEnabled ? <VolumeUpIcon />
<VolumeUpIcon /> :
: <VolumeOffIcon />
<VolumeOffIcon /> }
} </Fab>
</Fab> }
}
</div>
</Tooltip> </Tooltip>
{ browser.platform !== 'mobile' && { browser.platform !== 'mobile' &&
@ -308,48 +306,46 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
{ smallContainer ? <IconButton
<IconButton aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.newWindow',
id : 'label.newWindow', defaultMessage : 'New window'
defaultMessage : 'New window' })}
})} className={classes.smallContainer}
className={classes.smallContainer} disabled={
disabled={ !videoVisible ||
!videoVisible || (windowConsumer === webcamConsumer.id)
(windowConsumer === webcamConsumer.id) }
} size='small'
size='small' color='primary'
color='primary' onClick={() =>
onClick={() => {
{ toggleConsumerWindow(webcamConsumer);
toggleConsumerWindow(webcamConsumer); }}
}} >
> <NewWindowIcon />
<NewWindowIcon /> </IconButton>
</IconButton> :
: <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.newWindow',
id : 'label.newWindow', defaultMessage : 'New window'
defaultMessage : 'New window' })}
})} className={classes.fab}
className={classes.fab} disabled={
disabled={ !videoVisible ||
!videoVisible || (windowConsumer === webcamConsumer.id)
(windowConsumer === webcamConsumer.id) }
} size='large'
size='large' onClick={() =>
onClick={() => {
{ toggleConsumerWindow(webcamConsumer);
toggleConsumerWindow(webcamConsumer); }}
}} >
> <NewWindowIcon />
<NewWindowIcon /> </Fab>
</Fab> }
}
</div>
</Tooltip> </Tooltip>
} }
@ -360,46 +356,45 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
{ smallContainer ? <IconButton
<IconButton aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.fullscreen',
id : 'label.fullscreen', defaultMessage : 'Fullscreen'
defaultMessage : 'Fullscreen' })}
})} className={classes.smallContainer}
className={classes.smallContainer} disabled={!videoVisible}
disabled={!videoVisible} size='small'
size='small' color='primary'
color='primary' onClick={() =>
onClick={() => {
{ toggleConsumerFullscreen(webcamConsumer);
toggleConsumerFullscreen(webcamConsumer); }}
}} >
> <FullScreenIcon />
<FullScreenIcon /> </IconButton>
</IconButton> :
: <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.fullscreen',
id : 'label.fullscreen', defaultMessage : 'Fullscreen'
defaultMessage : 'Fullscreen' })}
})} className={classes.fab}
className={classes.fab} disabled={!videoVisible}
disabled={!videoVisible} size='large'
size='large' onClick={() =>
onClick={() => {
{ toggleConsumerFullscreen(webcamConsumer);
toggleConsumerFullscreen(webcamConsumer); }}
}} >
> <FullScreenIcon />
<FullScreenIcon /> </Fab>
</Fab> }
}
</div>
</Tooltip> </Tooltip>
</div> </div>
<VideoView <VideoView
showQuality
advancedMode={advancedMode} advancedMode={advancedMode}
peer={peer} peer={peer}
displayName={peer.displayName} displayName={peer.displayName}
@ -507,48 +502,46 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
{ smallContainer ? <IconButton
<IconButton aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.newWindow',
id : 'label.newWindow', defaultMessage : 'New window'
defaultMessage : 'New window' })}
})} className={classes.smallContainer}
className={classes.smallContainer} disabled={
disabled={ !videoVisible ||
!videoVisible || (windowConsumer === consumer.id)
(windowConsumer === consumer.id) }
} size='small'
size='small' color='primary'
color='primary' onClick={() =>
onClick={() => {
{ toggleConsumerWindow(consumer);
toggleConsumerWindow(consumer); }}
}} >
> <NewWindowIcon />
<NewWindowIcon /> </IconButton>
</IconButton> :
: <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.newWindow',
id : 'label.newWindow', defaultMessage : 'New window'
defaultMessage : 'New window' })}
})} className={classes.fab}
className={classes.fab} disabled={
disabled={ !videoVisible ||
!videoVisible || (windowConsumer === consumer.id)
(windowConsumer === consumer.id) }
} size='large'
size='large' onClick={() =>
onClick={() => {
{ toggleConsumerWindow(consumer);
toggleConsumerWindow(consumer); }}
}} >
> <NewWindowIcon />
<NewWindowIcon /> </Fab>
</Fab> }
}
</div>
</Tooltip> </Tooltip>
} }
@ -559,46 +552,45 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
{ smallContainer ? <IconButton
<IconButton aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.fullscreen',
id : 'label.fullscreen', defaultMessage : 'Fullscreen'
defaultMessage : 'Fullscreen' })}
})} className={classes.smallContainer}
className={classes.smallContainer} disabled={!videoVisible}
disabled={!videoVisible} size='small'
size='small' color='primary'
color='primary' onClick={() =>
onClick={() => {
{ toggleConsumerFullscreen(consumer);
toggleConsumerFullscreen(consumer); }}
}} >
> <FullScreenIcon />
<FullScreenIcon /> </IconButton>
</IconButton> :
: <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.fullscreen',
id : 'label.fullscreen', defaultMessage : 'Fullscreen'
defaultMessage : 'Fullscreen' })}
})} className={classes.fab}
className={classes.fab} disabled={!videoVisible}
disabled={!videoVisible} size='large'
size='large' onClick={() =>
onClick={() => {
{ toggleConsumerFullscreen(consumer);
toggleConsumerFullscreen(consumer); }}
}} >
> <FullScreenIcon />
<FullScreenIcon /> </Fab>
</Fab> }
}
</div>
</Tooltip> </Tooltip>
</div> </div>
<VideoView <VideoView
showQuality
advancedMode={advancedMode} advancedMode={advancedMode}
peer={peer} peer={peer}
displayName={peer.displayName} displayName={peer.displayName}
@ -694,26 +686,24 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.newWindow',
id : 'label.newWindow', defaultMessage : 'New window'
defaultMessage : 'New window' })}
})} className={classes.fab}
className={classes.fab} disabled={
disabled={ !screenVisible ||
!screenVisible || (windowConsumer === screenConsumer.id)
(windowConsumer === screenConsumer.id) }
} size={smallContainer ? 'small' : 'large'}
size={smallContainer ? 'small' : 'large'} onClick={() =>
onClick={() => {
{ toggleConsumerWindow(screenConsumer);
toggleConsumerWindow(screenConsumer); }}
}} >
> <NewWindowIcon />
<NewWindowIcon /> </Fab>
</Fab>
</div>
</Tooltip> </Tooltip>
} }
@ -724,26 +714,25 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> <Fab
<Fab aria-label={intl.formatMessage({
aria-label={intl.formatMessage({ id : 'label.fullscreen',
id : 'label.fullscreen', defaultMessage : 'Fullscreen'
defaultMessage : 'Fullscreen' })}
})} className={classes.fab}
className={classes.fab} disabled={!screenVisible}
disabled={!screenVisible} size={smallContainer ? 'small' : 'large'}
size={smallContainer ? 'small' : 'large'} onClick={() =>
onClick={() => {
{ toggleConsumerFullscreen(screenConsumer);
toggleConsumerFullscreen(screenConsumer); }}
}} >
> <FullScreenIcon />
<FullScreenIcon /> </Fab>
</Fab>
</div>
</Tooltip> </Tooltip>
</div> </div>
<VideoView <VideoView
showQuality
advancedMode={advancedMode} advancedMode={advancedMode}
videoContain videoContain
consumerSpatialLayers={ 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 { import {
lobbyPeersKeySelector, lobbyPeersKeySelector,
peersLengthSelector, peersLengthSelector,
raisedHandsSelector raisedHandsSelector,
makePermissionSelector
} from '../Selectors'; } from '../Selectors';
import { permissions } from '../../permissions';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions'; import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions'; import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem'; 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 Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import MoreIcon from '@material-ui/icons/MoreVert'; import MoreIcon from '@material-ui/icons/MoreVert';
import HelpIcon from '@material-ui/icons/Help';
import InfoIcon from '@material-ui/icons/Info';
const styles = (theme) => 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 : menuButton :
{ {
margin : 0, margin : 0,
@ -184,6 +214,9 @@ const TopBar = (props) =>
peersLength, peersLength,
lobbyPeers, lobbyPeers,
permanentTopBar, permanentTopBar,
drawerOverlayed,
toolAreaOpen,
isMobile,
myPicture, myPicture,
loggedIn, loggedIn,
loginEnabled, loginEnabled,
@ -192,6 +225,8 @@ const TopBar = (props) =>
onFullscreen, onFullscreen,
setSettingsOpen, setSettingsOpen,
setExtraVideoOpen, setExtraVideoOpen,
setHelpOpen,
setAboutOpen,
setLockDialogOpen, setLockDialogOpen,
toggleToolArea, toggleToolArea,
openUsersTab, openUsersTab,
@ -242,7 +277,12 @@ const TopBar = (props) =>
<React.Fragment> <React.Fragment>
<AppBar <AppBar
position='fixed' 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> <Toolbar>
<PulsingBadge <PulsingBadge
@ -272,18 +312,25 @@ const TopBar = (props) =>
</Typography> </Typography>
<div className={classes.grow} /> <div className={classes.grow} />
<div className={classes.sectionDesktop}> <div className={classes.sectionDesktop}>
<IconButton <Tooltip
aria-owns={ title={intl.formatMessage({
isMenuOpen && id : 'label.moreActions',
currentMenu === 'moreActions' ? defaultMessage : 'More actions'
'material-appbar' : undefined })}
}
aria-haspopup='true'
onClick={(event) => handleMenuOpen(event, 'moreActions')}
color='inherit'
> >
<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 && { fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}> <Tooltip title={fullscreenTooltip}>
<IconButton <IconButton
@ -483,6 +530,46 @@ const TopBar = (props) =>
/> />
</p> </p>
</MenuItem> </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> </Paper>
} }
</Popover> </Popover>
@ -682,9 +769,12 @@ TopBar.propTypes =
{ {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
isMobile : PropTypes.bool.isRequired,
peersLength : PropTypes.number, peersLength : PropTypes.number,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool, permanentTopBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
myPicture : PropTypes.string, myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired, loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired,
@ -694,6 +784,8 @@ TopBar.propTypes =
setToolbarsVisible : PropTypes.func.isRequired, setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired, setExtraVideoOpen : PropTypes.func.isRequired,
setHelpOpen : PropTypes.func.isRequired,
setAboutOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired, openUsersTab : PropTypes.func.isRequired,
@ -705,27 +797,38 @@ TopBar.propTypes =
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const makeMapStateToProps = () =>
({ {
room : state.room, const hasExtraVideoPermission =
peersLength : peersLengthSelector(state), makePermissionSelector(permissions.EXTRA_VIDEO);
lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar, const hasLockPermission =
loggedIn : state.me.loggedIn, makePermissionSelector(permissions.CHANGE_ROOM_LOCK);
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture, const hasPromotionPermission =
unread : state.toolarea.unreadMessages + makePermissionSelector(permissions.PROMOTE_PEER);
state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo : const mapStateToProps = (state) =>
state.me.roles.some((role) => ({
state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)), room : state.room,
canLock : isMobile : state.me.browser.platform === 'mobile',
state.me.roles.some((role) => peersLength : peersLengthSelector(state),
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)), lobbyPeers : lobbyPeersKeySelector(state),
canPromote : permanentTopBar : state.settings.permanentTopBar,
state.me.roles.some((role) => drawerOverlayed : state.settings.drawerOverlayed,
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) 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) => const mapDispatchToProps = (dispatch) =>
({ ({
@ -741,6 +844,14 @@ const mapDispatchToProps = (dispatch) =>
{ {
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen)); dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
}, },
setHelpOpen : (helpOpen) =>
{
dispatch(roomActions.setHelpOpen(helpOpen));
},
setAboutOpen : (aboutOpen) =>
{
dispatch(roomActions.setAboutOpen(aboutOpen));
},
setLockDialogOpen : (lockDialogOpen) => setLockDialogOpen : (lockDialogOpen) =>
{ {
dispatch(roomActions.setLockDialogOpen(lockDialogOpen)); dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
@ -757,7 +868,7 @@ const mapDispatchToProps = (dispatch) =>
}); });
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
{ {
@ -768,12 +879,15 @@ export default withRoomContext(connect(
prev.peers === next.peers && prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers && prev.lobbyPeers === next.lobbyPeers &&
prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.drawerOverlayed === next.settings.drawerOverlayed &&
prev.me.loggedIn === next.me.loggedIn && prev.me.loggedIn === next.me.loggedIn &&
prev.me.browser === next.me.browser &&
prev.me.loginEnabled === next.me.loginEnabled && prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture && prev.me.picture === next.me.picture &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages && 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 { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase'; import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
@ -119,26 +121,32 @@ ChatInput.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const makeMapStateToProps = () =>
({ {
displayName : state.settings.displayName, const hasPermission = makePermissionSelector(permissions.SEND_CHAT);
picture : state.me.picture,
canChat : const mapStateToProps = (state) =>
state.me.roles.some((role) => ({
state.room.permissionsFromRoles.SEND_CHAT.includes(role)) displayName : state.settings.displayName,
}); picture : state.me.picture,
canChat : hasPermission(state)
});
return mapStateToProps;
};
export default withRoomContext( export default withRoomContext(
connect( connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.settings.displayName === next.settings.displayName && prev.settings.displayName === next.settings.displayName &&
prev.me.picture === next.me.picture prev.me.picture === next.me.picture
); );

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
@ -76,16 +78,21 @@ ChatModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const makeMapStateToProps = () =>
({ {
isChatModerator : const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT);
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)), const mapStateToProps = (state) =>
room : state.room ({
}); isChatModerator : hasPermission(state),
room : state.room
});
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && 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 { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import FileList from './FileList'; import FileList from './FileList';
import FileSharingModerator from './FileSharingModerator'; import FileSharingModerator from './FileSharingModerator';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
@ -131,30 +133,36 @@ FileSharing.propTypes = {
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const makeMapStateToProps = () =>
{ {
return { const hasPermission = makePermissionSelector(permissions.SHARE_FILE);
canShareFiles : state.me.canShareFiles,
browser : state.me.browser, const mapStateToProps = (state) =>
tabOpen : state.toolarea.currentToolTab === 'files', {
canShare : return {
state.me.roles.some((role) => canShareFiles : state.me.canShareFiles,
state.room.permissionsFromRoles.SHARE_FILE.includes(role)) browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files',
canShare : hasPermission(state)
};
}; };
return mapStateToProps;
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.browser === next.me.browser && prev.me.browser === next.me.browser &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.me.canShareFiles === next.me.canShareFiles && prev.me.canShareFiles === next.me.canShareFiles &&
prev.peers === next.peers &&
prev.toolarea.currentToolTab === next.toolarea.currentToolTab prev.toolarea.currentToolTab === next.toolarea.currentToolTab
); );
} }

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
@ -76,16 +78,21 @@ FileSharingModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const makeMapStateToProps = () =>
({ {
isFileSharingModerator : const hasPermission = makePermissionSelector(permissions.MODERATE_FILES);
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_FILES.includes(role)), const mapStateToProps = (state) =>
room : state.room ({
}); isFileSharingModerator : hasPermission(state),
room : state.room
});
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && 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 Tooltip from '@material-ui/core/Tooltip';
import VideocamIcon from '@material-ui/icons/Videocam'; import VideocamIcon from '@material-ui/icons/Videocam';
import VideocamOffIcon from '@material-ui/icons/VideocamOff'; 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 VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff'; import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import ScreenIcon from '@material-ui/icons/ScreenShare'; import ScreenIcon from '@material-ui/icons/ScreenShare';
@ -126,7 +128,7 @@ const ListPeer = (props) =>
<RecordVoiceOverIcon /> <RecordVoiceOverIcon />
</IconButton> </IconButton>
} }
{ screenConsumer && { screenConsumer && spotlight &&
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
id : 'tooltip.muteScreenSharing', id : 'tooltip.muteScreenSharing',
@ -159,37 +161,39 @@ const ListPeer = (props) =>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} }
<Tooltip { spotlight &&
title={intl.formatMessage({ <Tooltip
id : 'tooltip.muteParticipantVideo', title={intl.formatMessage({
defaultMessage : 'Mute participant video'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo', id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video' defaultMessage : 'Mute participant video'
})} })}
color={webcamEnabled ? 'primary' : 'secondary'} placement='bottom'
disabled={peer.peerVideoInProgress}
className={classes.buttons}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
> >
{ webcamEnabled ? <IconButton
<VideocamIcon /> aria-label={intl.formatMessage({
: id : 'tooltip.muteParticipantVideo',
<VideocamOffIcon /> defaultMessage : 'Mute participant video'
} })}
</IconButton> color={webcamEnabled ? 'primary' : 'secondary'}
</Tooltip> 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 <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
id : 'tooltip.muteParticipant', id : 'tooltip.muteParticipant',
@ -248,6 +252,60 @@ const ListPeer = (props) =>
</IconButton> </IconButton>
</Tooltip> </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} {children}
</div> </div>
); );

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import { createSelector } from 'reselect'; 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 producersSelect = (state) => state.producers;
const consumersSelect = (state) => state.consumers; const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights; 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 = ({ const AppearenceSettings = ({
isMobile,
room, room,
settings, settings,
onTogglePermanentTopBar, onTogglePermanentTopBar,
onToggleHiddenControls, onToggleHiddenControls,
onToggleButtonControlBar,
onToggleShowNotifications, onToggleShowNotifications,
onToggleDrawerOverlayed,
handleChangeMode, handleChangeMode,
classes classes
}) => }) =>
@ -102,6 +105,24 @@ const AppearenceSettings = ({
defaultMessage : 'Hidden media controls' 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 <FormControlLabel
className={classes.setting} className={classes.setting}
control={<Checkbox checked={settings.showNotifications} onChange={onToggleShowNotifications} value='showNotifications' />} control={<Checkbox checked={settings.showNotifications} onChange={onToggleShowNotifications} value='showNotifications' />}
@ -116,17 +137,21 @@ const AppearenceSettings = ({
AppearenceSettings.propTypes = AppearenceSettings.propTypes =
{ {
isMobile : PropTypes.bool.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired, settings : PropTypes.object.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired, onTogglePermanentTopBar : PropTypes.func.isRequired,
onToggleHiddenControls : PropTypes.func.isRequired, onToggleHiddenControls : PropTypes.func.isRequired,
onToggleButtonControlBar : PropTypes.func.isRequired,
onToggleShowNotifications : PropTypes.func.isRequired, onToggleShowNotifications : PropTypes.func.isRequired,
onToggleDrawerOverlayed : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired, handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
isMobile : state.me.browser.platform === 'mobile',
room : state.room, room : state.room,
settings : state.settings settings : state.settings
}); });
@ -135,6 +160,8 @@ const mapDispatchToProps = {
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
onToggleHiddenControls : settingsActions.toggleHiddenControls, onToggleHiddenControls : settingsActions.toggleHiddenControls,
onToggleShowNotifications : settingsActions.toggleShowNotifications, onToggleShowNotifications : settingsActions.toggleShowNotifications,
onToggleButtonControlBar : settingsActions.toggleButtonControlBar,
onToggleDrawerOverlayed : settingsActions.toggleDrawerOverlayed,
handleChangeMode : roomActions.setDisplayMode handleChangeMode : roomActions.setDisplayMode
}; };
@ -146,6 +173,7 @@ export default connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.me.browser === next.me.browser &&
prev.room === next.room && prev.room === next.room &&
prev.settings === next.settings prev.settings === next.settings
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,17 @@ const producers = (state = initialState, action) =>
return { ...state, [producerId]: newProducer }; 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: default:
return state; return state;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -51,20 +51,24 @@
"room.videoPaused": "Video gestoppt", "room.videoPaused": "Video gestoppt",
"room.muteAll": "Alle stummschalten", "room.muteAll": "Alle stummschalten",
"room.stopAllVideo": "Alle Videos stoppen", "room.stopAllVideo": "Alle Videos stoppen",
"room.closeMeeting": "Meeting schließen", "room.closeMeeting": "Meeting beenden",
"room.clearChat": null, "room.clearChat": "Liste löschen",
"room.clearFileSharing": null, "room.clearFileSharing": "Liste löschen",
"room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung", "room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung",
"room.moderatoractions": null, "room.moderatoractions": "Moderator Aktionen",
"room.raisedHand": null, "room.raisedHand": "{displayName} hebt die Hand",
"room.loweredHand": null, "room.loweredHand": "{displayName} senkt die Hand",
"room.extraVideo": null, "room.extraVideo": "Video hinzufügen",
"room.overRoomLimit": null, "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.gotRole": "Rolle erhalten: {role}",
"roles.lostRole": null, "roles.lostRole": "Rolle entzogen: {role}",
"tooltip.login": "Anmelden", "tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden", "tooltip.logout": "Abmelden",
@ -76,11 +80,14 @@
"tooltip.lobby": "Warteraum", "tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen", "tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer", "tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": "Teilnehmer rauswerfen", "tooltip.kickParticipant": "Rauswerfen",
"tooltip.muteParticipant": null, "tooltip.muteParticipant": "Stummschalten",
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": "Video stoppen",
"tooltip.raisedHand": null, "tooltip.raisedHand": "Hand heben",
"tooltip.muteScreenSharing": null, "tooltip.muteScreenSharing": "Stoppe Bildschirmfreigabe",
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Name des Raums", "label.roomName": "Name des Raums",
"label.chooseRoomButton": "Weiter", "label.chooseRoomButton": "Weiter",
@ -94,23 +101,23 @@
"label.filesharing": "Dateien", "label.filesharing": "Dateien",
"label.participants": "Teilnehmer", "label.participants": "Teilnehmer",
"label.shareFile": "Datei hochladen", "label.shareFile": "Datei hochladen",
"label.shareGalleryFile": null, "label.shareGalleryFile": "Bild teilen",
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt", "label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt", "label.unknown": "Unbekannt",
"label.democratic": "Demokratisch", "label.democratic": "Demokratisch",
"label.filmstrip": "Filmstreifen", "label.filmstrip": "Filmstreifen",
"label.low": "Niedrig", "label.low": "Niedrig",
"label.medium": "Medium", "label.medium": "Mittel",
"label.high": "Hoch (HD)", "label.high": "Hoch (HD)",
"label.veryHigh": "Sehr hoch (FHD)", "label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Schließen", "label.close": "Schließen",
"label.media": null, "label.media": "Audio / Video",
"label.appearence": null, "label.appearance": "Ansicht",
"label.advanced": null, "label.advanced": "Erweitert",
"label.addVideo": null, "label.addVideo": "Video hinzufügen",
"label.promoteAllPeers": null, "label.promoteAllPeers": "Alle Teilnehmer reinlassen",
"label.moreActions": null, "label.moreActions": "Weitere Aktionen",
"settings.settings": "Einstellungen", "settings.settings": "Einstellungen",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -128,9 +135,14 @@
"settings.advancedMode": "Erweiterter Modus", "settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste", "settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos", "settings.lastn": "Anzahl der sichtbaren Videos",
"settings.hiddenControls": null, "settings.hiddenControls": "Medienwerkzeugleiste automatisch ausblenden",
"settings.notificationSounds": null, "settings.notificationSounds": "Audiosignal bei Benachrichtigungen",
"settings.showNotifications": null, "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.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei",
@ -172,8 +184,8 @@
"devices.cameraDisconnected": "Kamera getrennt", "devices.cameraDisconnected": "Kamera getrennt",
"devices.cameraError": "Fehler mit deiner Kamera", "devices.cameraError": "Fehler mit deiner Kamera",
"moderator.clearChat": null, "moderator.clearChat": "Moderator hat Chat gelöscht",
"moderator.clearFiles": null, "moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht",
"moderator.muteAudio": null, "moderator.muteAudio": "Moderator hat dich stummgeschaltet",
"moderator.muteVideo": null "moderator.muteVideo": "Moderator hat dein Video gestoppt"
} }

View File

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

View File

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

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} put their hand down", "room.loweredHand": "{displayName} put their hand down",
"room.extraVideo": "Extra video", "room.extraVideo": "Extra video",
"room.overRoomLimit": "The room is full, retry after some time.", "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", "me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": "Mute participant video", "tooltip.muteParticipantVideo": "Mute participant video",
"tooltip.raisedHand": "Raise hand", "tooltip.raisedHand": "Raise hand",
"tooltip.muteScreenSharing": "Mute participant share", "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.roomName": "Room name",
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": "Continue",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Close", "label.close": "Close",
"label.media": "Media", "label.media": "Media",
"label.appearence": "Appearence", "label.appearance": "Appearence",
"label.advanced": "Advanced", "label.advanced": "Advanced",
"label.addVideo": "Add video", "label.addVideo": "Add video",
"label.promoteAllPeers": "Promote all", "label.promoteAllPeers": "Promote all",
@ -131,6 +138,11 @@
"settings.hiddenControls": "Hidden media controls", "settings.hiddenControls": "Hidden media controls",
"settings.notificationSounds": "Notification sounds", "settings.notificationSounds": "Notification sounds",
"settings.showNotifications": "Show notifications", "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.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file", "filesharing.startingFileShare": "Attempting to share file",

View File

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

View File

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

View File

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

View File

@ -60,6 +60,10 @@
"room.loweredHand": "{displayName} leeresztette a kezét", "room.loweredHand": "{displayName} leeresztette a kezét",
"room.extraVideo": "Kiegészítő videó", "room.extraVideo": "Kiegészítő videó",
"room.overRoomLimit": "A konferenciaszoba betelt..", "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", "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.muteParticipantVideo": "Résztvevő videóstreamének némítása",
"tooltip.raisedHand": "Jelentkezés", "tooltip.raisedHand": "Jelentkezés",
"tooltip.muteScreenSharing": "Képernyőmegosztás szüneteltetése", "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.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább", "label.chooseRoomButton": "Tovább",
@ -106,7 +113,7 @@
"label.ultra": "Ultra magas (UHD)", "label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár", "label.close": "Bezár",
"label.media": "Média", "label.media": "Média",
"label.appearence": "Megjelenés", "label.appearance": "Megjelenés",
"label.advanced": "Részletek", "label.advanced": "Részletek",
"label.addVideo": "Videó hozzáadása", "label.addVideo": "Videó hozzáadása",
"label.promoteAllPeers": "Mindenkit beengedek", "label.promoteAllPeers": "Mindenkit beengedek",
@ -130,7 +137,12 @@
"settings.lastn": "A látható videók száma", "settings.lastn": "A látható videók száma",
"settings.hiddenControls": "Média Gombok automatikus elrejtése", "settings.hiddenControls": "Média Gombok automatikus elrejtése",
"settings.notificationSounds": "Értesítések hangjelzéssel", "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.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása", "filesharing.startingFileShare": "Fájl megosztása",

View File

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

View File

@ -59,6 +59,10 @@
"room.raisedHand": "{displayName} pacēla roku", "room.raisedHand": "{displayName} pacēla roku",
"room.loweredHand": "{displayName} nolaida roku", "room.loweredHand": "{displayName} nolaida roku",
"room.extraVideo": "Papildus video", "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", "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.muteParticipantVideo": "Atslēgt dalībnieka video",
"tooltip.raisedHand": "Pacelt roku", "tooltip.raisedHand": "Pacelt roku",
"tooltip.muteScreenSharing": null, "tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Sapulces telpas nosaukums (ID)", "label.roomName": "Sapulces telpas nosaukums (ID)",
"label.chooseRoomButton": "Turpināt", "label.chooseRoomButton": "Turpināt",
@ -104,7 +111,7 @@
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Aizvērt", "label.close": "Aizvērt",
"label.media": "Mediji", "label.media": "Mediji",
"label.appearence": "Izskats", "label.appearance": "Izskats",
"label.advanced": "Advancēts", "label.advanced": "Advancēts",
"label.addVideo": "Pievienot video", "label.addVideo": "Pievienot video",
"label.moreActions": null, "label.moreActions": null,
@ -125,6 +132,11 @@
"settings.hiddenControls": "Slēpto mediju vadība", "settings.hiddenControls": "Slēpto mediju vadība",
"settings.notificationSounds": "Paziņojumu skaņas", "settings.notificationSounds": "Paziņojumu skaņas",
"settings.showNotifications": null, "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.saveFileError": "Nav iespējams saglabāt failu",
"filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu",

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,10 @@
"room.loweredHand": null, "room.loweredHand": null,
"room.extraVideo": null, "room.extraVideo": null,
"room.overRoomLimit": null, "room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null, "me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null, "tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null, "tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Oda adı", "label.roomName": "Oda adı",
"label.chooseRoomButton": "Devam", "label.chooseRoomButton": "Devam",
@ -106,7 +113,7 @@
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Kapat", "label.close": "Kapat",
"label.media": null, "label.media": null,
"label.appearence": null, "label.appearance": null,
"label.advanced": null, "label.advanced": null,
"label.addVideo": null, "label.addVideo": null,
"label.promoteAllPeers": null, "label.promoteAllPeers": null,
@ -128,6 +135,11 @@
"settings.hiddenControls": null, "settings.hiddenControls": null,
"settings.notificationSounds": null, "settings.notificationSounds": null,
"settings.showNotifications": null, "settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.saveFileError": "Dosya kaydedilemiyor",
"filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", "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.loweredHand": null,
"room.extraVideo": null, "room.extraVideo": null,
"room.overRoomLimit": null, "room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"room.browsePeersSpotlight": null,
"me.mutedPTT": null, "me.mutedPTT": null,
@ -81,6 +85,9 @@
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null, "tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null, "tooltip.muteScreenSharing": null,
"tooltip.muteParticipantAudioModerator": null,
"tooltip.muteParticipantVideoModerator": null,
"tooltip.muteScreenSharingModerator": null,
"label.roomName": "Назва кімнати", "label.roomName": "Назва кімнати",
"label.chooseRoomButton": "Продовжити", "label.chooseRoomButton": "Продовжити",
@ -106,7 +113,7 @@
"label.ultra": "Ультра (UHD)", "label.ultra": "Ультра (UHD)",
"label.close": "Закрити", "label.close": "Закрити",
"label.media": null, "label.media": null,
"label.appearence": null, "label.appearance": null,
"label.advanced": null, "label.advanced": null,
"label.addVideo": null, "label.addVideo": null,
"label.promoteAllPeers": null, "label.promoteAllPeers": null,
@ -131,6 +138,11 @@
"settings.hiddenControls": null, "settings.hiddenControls": null,
"settings.notificationSounds": null, "settings.notificationSounds": null,
"settings.showNotifications": null, "settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.saveFileError": "Неможливо зберегти файл",
"filesharing.startingFileShare": "Спроба поділитися файлом", "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 os = require('os');
const userRoles = require('../userRoles'); 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 AwaitQueue = require('awaitqueue');
// const axios = require('axios'); // const axios = require('axios');
@ -216,44 +234,50 @@ module.exports =
accessFromRoles : { accessFromRoles : {
// The role(s) will gain access to the room // The role(s) will gain access to the room
// even if it is locked (!) // 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 // The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your // going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could // server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping // add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ] [BYPASS_LOBBY] : [ userRoles.NORMAL ]
}, },
permissionsFromRoles : { permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room // 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 // 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 // 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 // The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ], [MODERATE_CHAT] : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen // 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 // 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 // The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ], [SHARE_FILE] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files // 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) // 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 // When truthy, the room will be open to all users when as long as there
// are allready users in the room // are allready users in the room
activateOnHostJoin : true, activateOnHostJoin : true,
// When set, maxUsersPerRoom defines how many users can join // When set, maxUsersPerRoom defines how many users can join
// a single room. If not set, there is no limit. // a single room. If not set, there is no limit.
// maxUsersPerRoom : 20, // maxUsersPerRoom : 20,
// Room size before spreading to new router // Room size before spreading to new router
routerScaleSize : 40, routerScaleSize : 40,
// Mediasoup settings // Mediasoup settings
mediasoup : mediasoup :
{ {
numWorkers : Object.keys(os.cpus()).length, numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings. // mediasoup Worker settings.

View File

@ -46,7 +46,7 @@ class Lobby extends EventEmitter
return Object.values(this._peers).map((peer) => return Object.values(this._peers).map((peer) =>
({ ({
peerId : peer.id, id : peer.id,
displayName : peer.displayName, displayName : peer.displayName,
picture : peer.picture picture : peer.picture
})); }));
@ -154,8 +154,6 @@ class Lobby extends EventEmitter
this.emit('lobbyEmpty'); this.emit('lobbyEmpty');
}; };
this._notification(peer.socket, 'enteredLobby');
this._peers[peer.id] = peer; this._peers[peer.id] = peer;
peer.on('gotRole', peer.gotRoleHandler); peer.on('gotRole', peer.gotRoleHandler);
@ -165,6 +163,8 @@ class Lobby extends EventEmitter
peer.socket.on('request', peer.socketRequestHandler); peer.socket.on('request', peer.socketRequestHandler);
peer.on('close', peer.closeHandler); peer.on('close', peer.closeHandler);
this._notification(peer.socket, 'enteredLobby');
} }
async _handleSocketRequest(peer, request, cb) async _handleSocketRequest(peer, request, cb)
@ -189,7 +189,8 @@ class Lobby extends EventEmitter
cb(); cb();
break; break;
} }
case 'changePicture': case 'changePicture':
{ {
const { picture } = request.data; 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 // Iterate and close all mediasoup Transport associated to this Peer, so all
// its Producers and Consumers will also be closed. // its Producers and Consumers will also be closed.
this.transports.forEach((transport) => for (const transport of this.transports.values())
{ {
transport.close(); transport.close();
}); }
if (this.socket) if (this.socket)
this.socket.disconnect(true); this.socket.disconnect(true);

View File

@ -1,36 +1,57 @@
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const AwaitQueue = require('awaitqueue');
const axios = require('axios'); const axios = require('axios');
const Logger = require('./Logger'); const Logger = require('./Logger');
const Lobby = require('./Lobby'); const Lobby = require('./Lobby');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const userRoles = require('../userRoles'); 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 config = require('../config/config');
const logger = new Logger('Room'); const logger = new Logger('Room');
// In case they are not configured properly // In case they are not configured properly
const accessFromRoles = const roomAccess =
{ {
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
BYPASS_LOBBY : [ userRoles.NORMAL ], [BYPASS_LOBBY] : [ userRoles.NORMAL ],
...config.accessFromRoles ...config.accessFromRoles
}; };
const permissionsFromRoles = const roomPermissions =
{ {
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ],
PROMOTE_PEER : [ userRoles.NORMAL ], [PROMOTE_PEER] : [ userRoles.NORMAL ],
SEND_CHAT : [ userRoles.NORMAL ], [SEND_CHAT] : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ], [MODERATE_CHAT] : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ], [SHARE_SCREEN] : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ], [EXTRA_VIDEO] : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ], [SHARE_FILE] : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ], [MODERATE_FILES] : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ], [MODERATE_ROOM] : [ userRoles.MODERATOR ],
...config.permissionsFromRoles ...config.permissionsFromRoles
}; };
const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || [];
const ROUTER_SCALE_SIZE = config.routerScaleSize || 40; const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
class Room extends EventEmitter class Room extends EventEmitter
@ -97,6 +118,9 @@ class Room extends EventEmitter
// Closed flag. // Closed flag.
this._closed = false; this._closed = false;
// Joining queue
this._queue = new AwaitQueue();
// Locked flag. // Locked flag.
this._locked = false; this._locked = false;
@ -148,6 +172,10 @@ class Room extends EventEmitter
this._closed = true; this._closed = true;
this._queue.close();
this._queue = null;
if (this._selfDestructTimeout) if (this._selfDestructTimeout)
clearTimeout(this._selfDestructTimeout); clearTimeout(this._selfDestructTimeout);
@ -221,9 +249,8 @@ class Room extends EventEmitter
// Returning user // Returning user
if (returning) if (returning)
this._peerJoining(peer, true); this._peerJoining(peer, true);
else if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) else if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
this._peerJoining(peer); this._peerJoining(peer);
else if ( else if (
'maxUsersPerRoom' in config && 'maxUsersPerRoom' in config &&
@ -239,7 +266,7 @@ class Room extends EventEmitter
else else
{ {
// Has a role that is allowed to bypass lobby // 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._peerJoining(peer) :
this._handleGuest(peer); this._handleGuest(peer);
} }
@ -271,11 +298,7 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer); this._peerJoining(promotedPeer);
for ( for (const peer of this._getAllowedPeers(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
} }
@ -283,9 +306,8 @@ class Room extends EventEmitter
this._lobby.on('peerRolesChanged', (peer) => this._lobby.on('peerRolesChanged', (peer) =>
{ {
if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -294,7 +316,7 @@ class Room extends EventEmitter
if ( // Has a role that is allowed to bypass lobby if ( // Has a role that is allowed to bypass lobby
!this._locked && !this._locked &&
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) this._hasAccess(peer, BYPASS_LOBBY)
) )
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -307,11 +329,7 @@ class Room extends EventEmitter
{ {
const { id, displayName } = changedPeer; const { id, displayName } = changedPeer;
for ( for (const peer of this._getAllowedPeers(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
} }
@ -321,11 +339,7 @@ class Room extends EventEmitter
{ {
const { id, picture } = changedPeer; const { id, picture } = changedPeer;
for ( for (const peer of this._getAllowedPeers(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
} }
@ -337,11 +351,7 @@ class Room extends EventEmitter
const { id } = closedPeer; const { id } = closedPeer;
for ( for (const peer of this._getAllowedPeers(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
} }
@ -401,7 +411,7 @@ class Room extends EventEmitter
); );
} }
async dump() dump()
{ {
return { return {
roomId : this._roomId, roomId : this._roomId,
@ -447,118 +457,91 @@ class Room extends EventEmitter
{ {
this._lobby.parkPeer(parkPeer); this._lobby.parkPeer(parkPeer);
for ( for (const peer of this._getAllowedPeers(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
} }
} }
async _peerJoining(peer, returning = false) _peerJoining(peer, returning = false)
{ {
peer.socket.join(this._roomId); this._queue.push(async () =>
// 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._notification(peer.socket, 'roomBack'); peer.socket.join(this._roomId);
}
else
{
const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
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; // Assign routerId
peer.routerId = await this._getRouterId();
if ('turnAPIURI' in config)
this._handlePeer(peer);
if (returning)
{ {
try this._notification(peer.socket, 'roomBack');
{
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) 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) _handlePeer(peer)
{ {
logger.debug('_handlePeer() [peer:"%s"]', peer.id); 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', () => peer.on('close', () =>
{ {
if (this._closed) this._handlePeerClose(peer);
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();
}); });
peer.on('displayNameChanged', ({ oldDisplayName }) => peer.on('displayNameChanged', ({ oldDisplayName }) =>
@ -602,7 +585,7 @@ class Room extends EventEmitter
// Got permission to promote peers, notify peer of // Got permission to promote peers, notify peer of
// peers in lobby // peers in lobby
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole)) if (roomPermissions.PROMOTE_PEER.includes(newRole))
{ {
const lobbyPeers = this._lobby.peerList(); const lobbyPeers = this._lobby.peerList();
@ -624,6 +607,69 @@ class Room extends EventEmitter
role : oldRole role : oldRole
}, true, true); }, 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) async _handleSocketRequest(peer, request, cb)
@ -660,22 +706,15 @@ class Room extends EventEmitter
// Tell the new Peer about already joined Peers. // Tell the new Peer about already joined Peers.
// And also create Consumers for existing Producers. // And also create Consumers for existing Producers.
const joinedPeers = const joinedPeers = this._getJoinedPeers(peer);
[
...this._getJoinedPeers()
];
const peerInfos = joinedPeers const peerInfos = joinedPeers
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo)); .map((joinedPeer) => (joinedPeer.peerInfo));
let lobbyPeers = []; let lobbyPeers = [];
if ( // Allowed to promote peers, notify about lobbypeers // Allowed to promote peers, notify about lobbypeers
peer.roles.some((role) => if (this._hasPermission(peer, PROMOTE_PEER))
permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
lobbyPeers = this._lobby.peerList(); lobbyPeers = this._lobby.peerList();
cb(null, { cb(null, {
@ -683,8 +722,9 @@ class Room extends EventEmitter
peers : peerInfos, peers : peerInfos,
tracker : config.fileTracker, tracker : config.fileTracker,
authenticated : peer.authenticated, authenticated : peer.authenticated,
permissionsFromRoles : permissionsFromRoles, roomPermissions : roomPermissions,
userRoles : userRoles, userRoles : userRoles,
allowWhenRoleMissing : roomAllowWhenRoleMissing,
chatHistory : this._chatHistory, chatHistory : this._chatHistory,
fileHistory : this._fileHistory, fileHistory : this._fileHistory,
lastNHistory : this._lastN, lastNHistory : this._lastN,
@ -711,7 +751,7 @@ class Room extends EventEmitter
} }
// Notify the new Peer to all other Peers. // 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( this._notification(
otherPeer.socket, otherPeer.socket,
@ -821,15 +861,13 @@ class Room extends EventEmitter
if ( if (
appData.source === 'screen' && appData.source === 'screen' &&
!peer.roles.some( !this._hasPermission(peer, SHARE_SCREEN)
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
if ( if (
appData.source === 'extravideo' && appData.source === 'extravideo' &&
!peer.roles.some( !this._hasPermission(peer, EXTRA_VIDEO)
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
@ -882,7 +920,7 @@ class Room extends EventEmitter
cb(null, { id: producer.id }); cb(null, { id: producer.id });
// Optimization: Create a server-side Consumer for each Peer. // 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( this._createConsumer(
{ {
@ -1144,9 +1182,7 @@ class Room extends EventEmitter
case 'chatMessage': case 'chatMessage':
{ {
if ( if (!this._hasPermission(peer, SEND_CHAT))
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { chatMessage } = request.data; const { chatMessage } = request.data;
@ -1167,11 +1203,7 @@ class Room extends EventEmitter
case 'moderator:clearChat': case 'moderator:clearChat':
{ {
if ( if (!this._hasPermission(peer, MODERATE_CHAT))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._chatHistory = []; this._chatHistory = [];
@ -1187,11 +1219,7 @@ class Room extends EventEmitter
case 'lockRoom': case 'lockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = true; this._locked = true;
@ -1209,11 +1237,7 @@ class Room extends EventEmitter
case 'unlockRoom': case 'unlockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = false; this._locked = false;
@ -1271,11 +1295,7 @@ class Room extends EventEmitter
case 'promotePeer': case 'promotePeer':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1290,11 +1310,7 @@ class Room extends EventEmitter
case 'promoteAllPeers': case 'promoteAllPeers':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._lobby.promoteAllPeers(); this._lobby.promoteAllPeers();
@ -1307,11 +1323,7 @@ class Room extends EventEmitter
case 'sendFile': case 'sendFile':
{ {
if ( if (!this._hasPermission(peer, SHARE_FILE))
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { magnetUri } = request.data; const { magnetUri } = request.data;
@ -1332,11 +1344,7 @@ class Room extends EventEmitter
case 'moderator:clearFileSharing': case 'moderator:clearFileSharing':
{ {
if ( if (!this._hasPermission(peer, MODERATE_FILES))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_FILES.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._fileHistory = []; this._fileHistory = [];
@ -1369,13 +1377,28 @@ class Room extends EventEmitter
break; 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': case 'moderator:muteAll':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1386,13 +1409,28 @@ class Room extends EventEmitter
break; 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': case 'moderator:stopAllVideo':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1405,11 +1443,7 @@ class Room extends EventEmitter
case 'moderator:closeMeeting': case 'moderator:closeMeeting':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._notification(peer.socket, 'moderator:kick', null, true); this._notification(peer.socket, 'moderator:kick', null, true);
@ -1424,11 +1458,7 @@ class Room extends EventEmitter
case 'moderator:kickPeer': case 'moderator:kickPeer':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1449,11 +1479,7 @@ class Room extends EventEmitter
case 'moderator:lowerHand': case 'moderator:lowerHand':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; 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. * Helper to get the list of joined peers.
*/ */
_getJoinedPeers({ excludePeer = undefined } = {}) _getJoinedPeers(excludePeer = undefined)
{ {
return Object.values(this._peers) return Object.values(this._peers)
.filter((peer) => peer.joined && peer !== excludePeer); .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) return Object.values(this._peers)
.filter( .filter(
@ -1648,7 +1712,7 @@ class Room extends EventEmitter
peer.joined === joined && peer.joined === joined &&
peer !== excludePeer && peer !== excludePeer &&
peer.roles.some( peer.roles.some(
(role) => permission.includes(role) (role) => roomPermissions[permission].includes(role)
) )
); );
} }

View File

@ -7,7 +7,7 @@
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js", "start": "node server.js",
"connect": "node connect.js", "connect": "node connect.js",
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/" "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() async function run()
{ {
// Open the interactive server. try
await interactiveServer(rooms, peers);
// start Prometheus exporter
if (config.prometheus)
{ {
await promExporter(rooms, peers, config.prometheus); // Open the interactive server.
} await interactiveServer(rooms, peers);
if (typeof(config.auth) === 'undefined') // start Prometheus exporter
{ if (config.prometheus)
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())
{ {
room.logStatus(); await promExporter(rooms, peers, config.prometheus);
} }
}, 120000);
// check for deserted rooms if (typeof(config.auth) === 'undefined')
setInterval(() =>
{
for (const room of rooms.values())
{ {
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() function statusLog()
@ -379,38 +368,45 @@ async function setupAuth()
app.get( app.get(
'/auth/callback', '/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }), passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
async (req, res) => async (req, res, next) =>
{ {
const state = JSON.parse(base64.decode(req.query.state)); try
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({ const state = JSON.parse(base64.decode(req.query.state));
peer,
roomId, const { peerId, roomId } = state;
userinfo : req.user._userinfo
}); 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); logger.error('room creation or room joining failed [error:"%o"]', error);
socket.disconnect(true); if (socket)
socket.disconnect(true);
return; return;
}); });