diff --git a/app/package.json b/app/package.json index ec92c5b..fae4e97 100644 --- a/app/package.json +++ b/app/package.json @@ -27,6 +27,7 @@ "react": "^16.10.2", "react-cookie-consent": "^2.5.0", "react-dom": "^16.10.2", + "react-flip-toolkit": "^7.0.9", "react-intl": "^3.4.0", "react-redux": "^7.1.1", "react-router-dom": "^5.1.2", diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index cf2703d..7909284 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -33,7 +33,7 @@ var config = */ audioOutputSupportedBrowsers : [ - 'chrome', + 'chrome', 'opera' ], // Socket.io request timeout @@ -42,18 +42,36 @@ var config = { tcp : true }, - defaultLayout : 'democratic', // democratic, filmstrip - lastN : 4, - mobileLastN : 1, + defaultAudio : + { + sampleRate : 48000, + channelCount : 1, + volume : 1.0, + autoGainControl : true, + echoCancellation : true, + noiseSuppression : true, + sampleSize : 16 + }, + background : 'images/background.jpg', + defaultLayout : 'democratic', // democratic, filmstrip + // If true, will show media control buttons in separate + // control bar, not in the ME container. + buttonControlBar : false, + // If false, will push videos away to make room for side + // drawer. If true, will overlay side drawer over videos + drawerOverlayed : true, + // Timeout for autohiding topbar and button control bar + hideTimeout : 3000, + lastN : 4, + mobileLastN : 1, // Highest number of speakers user can select - maxLastN : 5, + maxLastN : 5, // If truthy, users can NOT change number of speakers visible - lockLastN : false, - background : 'images/background.jpg', + lockLastN : false, // Add file and uncomment for adding logo to appbar // logo : 'images/logo.svg', - title : 'Multiparty meeting', - theme : + title : 'Multiparty meeting', + theme : { palette : { diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 656f443..3d63d26 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -129,7 +129,8 @@ export default class RoomClient produce, forceTcp, displayName, - muted + muted, + basePath } = {}) { if (!peerId) @@ -152,6 +153,9 @@ export default class RoomClient // Whether we force TCP this._forceTcp = forceTcp; + // URL basepath + this._basePath = basePath; + // Use displayName if (displayName) store.dispatch(settingsActions.setDisplayName(displayName)); @@ -414,6 +418,13 @@ export default class RoomClient break; } + case 'H': // Open help dialog + { + store.dispatch(roomActions.setHelpOpen(true)); + + break; + } + default: { break; @@ -485,9 +496,9 @@ export default class RoomClient window.open(url, 'loginWindow'); } - logout() + logout(roomId = this._roomId) { - window.open('/auth/logout', 'logoutWindow'); + window.open(`/auth/logout?peerId=${this._peerId}&roomId=${roomId}`, 'logoutWindow'); } receiveLoginChildWindow(data) @@ -946,14 +957,10 @@ export default class RoomClient { if (consumer.kind === 'video') { - if (spotlights.indexOf(consumer.appData.peerId) > -1) - { + if (spotlights.includes(consumer.appData.peerId)) await this._resumeConsumer(consumer); - } else - { await this._pauseConsumer(consumer); - } } } } @@ -963,20 +970,65 @@ export default class RoomClient } } - async getAudioTrack() + disconnectLocalHark() { - await navigator.mediaDevices.getUserMedia( - { - audio : true, video : false - }); + logger.debug('disconnectLocalHark() | Stopping harkStream.'); + if (this._harkStream != null) + { + this._harkStream.getAudioTracks()[0].stop(); + this._harkStream = null; + } + + if (this._hark != null) + { + logger.debug('disconnectLocalHark() Stopping hark.'); + this._hark.stop(); + } } - async getVideoTrack() + connectLocalHark(track) { - await navigator.mediaDevices.getUserMedia( + logger.debug('connectLocalHark() | Track:%o', track); + this._harkStream = new MediaStream(); + + this._harkStream.addTrack(track.clone()); + this._harkStream.getAudioTracks()[0].enabled = true; + + if (!this._harkStream.getAudioTracks()[0]) + throw new Error('getMicStream():something went wrong with hark'); + + this._hark = hark(this._harkStream, { play: false }); + + // eslint-disable-next-line no-unused-vars + this._hark.on('volume_change', (dBs, threshold) => + { + // The exact formula to convert from dBs (-100..0) to linear (0..1) is: + // Math.pow(10, dBs / 20) + // However it does not produce a visually useful output, so let exagerate + // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to + // minimize component renderings. + let volume = Math.round(Math.pow(10, dBs / 85) * 10); + + if (volume === 1) + volume = 0; + + volume = Math.round(volume); + + if (this._micProducer && volume !== this._micProducer.volume) { - audio : false, video : true - }); + this._micProducer.volume = volume; + + store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); + } + }); + this._hark.on('speaking', function() + { + store.dispatch(meActions.setIsSpeaking(true)); + }); + this._hark.on('stopped_speaking', function() + { + store.dispatch(meActions.setIsSpeaking(false)); + }); } async changeAudioDevice(deviceId) @@ -987,7 +1039,7 @@ export default class RoomClient meActions.setAudioInProgress(true)); try - { + { const device = this._audioDevices[deviceId]; if (!device) @@ -997,29 +1049,30 @@ export default class RoomClient 'changeAudioDevice() | new selected webcam [device:%o]', device); - if (this._hark != null) - this._hark.stop(); - - if (this._harkStream != null) - { - logger.debug('Stopping hark.'); - this._harkStream.getAudioTracks()[0].stop(); - this._harkStream = null; - } + this.disconnectLocalHark(); if (this._micProducer && this._micProducer.track) this._micProducer.track.stop(); - logger.debug('changeAudioDevice() | calling getUserMedia()'); + logger.debug('changeAudioDevice() | calling getUserMedia() %o', store.getState().settings); const stream = await navigator.mediaDevices.getUserMedia( { audio : { - deviceId : { exact: device.deviceId } + deviceId : { ideal: device.deviceId }, + sampleRate : store.getState().settings.sampleRate, + channelCount : store.getState().settings.channelCount, + volume : store.getState().settings.volume, + autoGainControl : store.getState().settings.autoGainControl, + echoCancellation : store.getState().settings.echoCancellation, + noiseSuppression : store.getState().settings.noiseSuppression, + sampleSize : store.getState().settings.sampleSize } - }); + } + ); + logger.debug('Constraints: %o', stream.getAudioTracks()[0].getConstraints()); const track = stream.getAudioTracks()[0]; if (this._micProducer) @@ -1027,47 +1080,8 @@ export default class RoomClient if (this._micProducer) this._micProducer.volume = 0; + this.connectLocalHark(track); - this._harkStream = new MediaStream(); - - this._harkStream.addTrack(track.clone()); - this._harkStream.getAudioTracks()[0].enabled = true; - - if (!this._harkStream.getAudioTracks()[0]) - throw new Error('changeAudioDevice(): given stream has no audio track'); - - this._hark = hark(this._harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - this._hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exaggerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - volume = Math.round(volume); - - if (this._micProducer && volume !== this._micProducer.volume) - { - this._micProducer.volume = volume; - - store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); - } - }); - this._hark.on('speaking', function() - { - store.dispatch(meActions.setIsSpeaking(true)); - }); - this._hark.on('stopped_speaking', function() - { - store.dispatch(meActions.setIsSpeaking(false)); - }); if (this._micProducer && this._micProducer.id) store.dispatch( producerActions.setProducerTrack(this._micProducer.id, track)); @@ -1388,6 +1402,46 @@ export default class RoomClient peerActions.setPeerKickInProgress(peerId, false)); } + async mutePeer(peerId) + { + logger.debug('mutePeer() [peerId:"%s"]', peerId); + + store.dispatch( + peerActions.setMutePeerInProgress(peerId, true)); + + try + { + await this.sendRequest('moderator:mute', { peerId }); + } + catch (error) + { + logger.error('mutePeer() failed: %o', error); + } + + store.dispatch( + peerActions.setMutePeerInProgress(peerId, false)); + } + + async stopPeerVideo(peerId) + { + logger.debug('stopPeerVideo() [peerId:"%s"]', peerId); + + store.dispatch( + peerActions.setStopPeerVideoInProgress(peerId, true)); + + try + { + await this.sendRequest('moderator:stopVideo', { peerId }); + } + catch (error) + { + logger.error('stopPeerVideo() failed: %o', error); + } + + store.dispatch( + peerActions.setStopPeerVideoInProgress(peerId, false)); + } + async muteAllPeers() { logger.debug('muteAllPeers()'); @@ -1475,9 +1529,7 @@ export default class RoomClient if (consumer.appData.peerId === peerId && consumer.appData.source === type) { if (mute) - { await this._pauseConsumer(consumer); - } else await this._resumeConsumer(consumer); } @@ -1543,6 +1595,26 @@ export default class RoomClient } } + async lowerPeerHand(peerId) + { + logger.debug('lowerPeerHand() [peerId:"%s"]', peerId); + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, true)); + + try + { + await this.sendRequest('moderator:lowerHand', { peerId }); + } + catch (error) + { + logger.error('lowerPeerHand() | [error:"%o"]', error); + } + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, false)); + } + async setRaisedHand(raisedHand) { logger.debug('setRaisedHand: ', raisedHand); @@ -1785,6 +1857,11 @@ export default class RoomClient this._recvTransport = null; } + this._spotlights.clearSpotlights(); + + store.dispatch(peerActions.clearPeers()); + store.dispatch(consumerActions.clearConsumers()); + store.dispatch(roomActions.clearSpotlights()); store.dispatch(roomActions.setRoomState('connecting')); }); @@ -1973,6 +2050,13 @@ export default class RoomClient break; } + case 'overRoomLimit': + { + store.dispatch(roomActions.setOverRoomLimit(true)); + + break; + } + case 'roomReady': { const { turnServers } = notification.data; @@ -2057,15 +2141,21 @@ export default class RoomClient lobbyPeers.forEach((peer) => { store.dispatch( - lobbyPeerActions.addLobbyPeer(peer.peerId)); + lobbyPeerActions.addLobbyPeer(peer.id)); + store.dispatch( lobbyPeerActions.setLobbyPeerDisplayName( peer.displayName, - peer.peerId + peer.id ) ); + store.dispatch( - lobbyPeerActions.setLobbyPeerPicture(peer.picture)); + lobbyPeerActions.setLobbyPeerPicture( + peer.picture, + peer.id + ) + ); }); store.dispatch( @@ -2493,8 +2583,6 @@ export default class RoomClient case 'moderator:mute': { - // const { peerId } = notification.data; - if (this._micProducer && !this._micProducer.paused) { this.muteMic(); @@ -2513,8 +2601,6 @@ export default class RoomClient case 'moderator:stopVideo': { - // const { peerId } = notification.data; - this.disableWebcam(); this.disableScreenSharing(); @@ -2537,6 +2623,13 @@ export default class RoomClient break; } + case 'moderator:lowerHand': + { + this.setRaisedHand(false); + + break; + } + case 'gotRole': { const { peerId, role } = notification.data; @@ -2772,8 +2865,8 @@ export default class RoomClient roles, peers, tracker, - permissionsFromRoles, - userRoles, + roomPermissions, + allowWhenRoleMissing, chatHistory, fileHistory, lastNHistory, @@ -2799,8 +2892,10 @@ export default class RoomClient store.dispatch(meActions.loggedIn(authenticated)); - store.dispatch(roomActions.setUserRoles(userRoles)); - store.dispatch(roomActions.setPermissionsFromRoles(permissionsFromRoles)); + store.dispatch(roomActions.setRoomPermissions(roomPermissions)); + + if (allowWhenRoleMissing) + store.dispatch(roomActions.setAllowWhenRoleMissing(allowWhenRoleMissing)); const myRoles = store.getState().me.roles; @@ -2814,7 +2909,9 @@ export default class RoomClient { text : intl.formatMessage({ id : 'roles.gotRole', - defaultMessage : `You got the role: ${role}` + defaultMessage : 'You got the role: {role}' + }, { + role }) })); } @@ -2856,11 +2953,11 @@ export default class RoomClient (lobbyPeers.length > 0) && lobbyPeers.forEach((peer) => { store.dispatch( - lobbyPeerActions.addLobbyPeer(peer.peerId)); + lobbyPeerActions.addLobbyPeer(peer.id)); store.dispatch( - lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.peerId)); + lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.id)); store.dispatch( - lobbyPeerActions.setLobbyPeerPicture(peer.picture)); + lobbyPeerActions.setLobbyPeerPicture(peer.picture, peer.id)); }); (accessCode != null) && store.dispatch( @@ -3236,11 +3333,20 @@ export default class RoomClient const stream = await navigator.mediaDevices.getUserMedia( { audio : { - deviceId : { ideal: deviceId } + deviceId : { ideal: device.deviceId }, + sampleRate : store.getState().settings.sampleRate, + channelCount : store.getState().settings.channelCount, + volume : store.getState().settings.volume, + autoGainControl : store.getState().settings.autoGainControl, + echoCancellation : store.getState().settings.echoCancellation, + noiseSuppression : store.getState().settings.noiseSuppression, + sampleSize : store.getState().settings.sampleSize } } ); + logger.debug('Constraints: %o', stream.getAudioTracks()[0].getConstraints()); + track = stream.getAudioTracks()[0]; this._micProducer = await this._sendTransport.produce( @@ -3294,51 +3400,8 @@ export default class RoomClient this._micProducer.volume = 0; - if (this._hark != null) - this._hark.stop(); + this.connectLocalHark(track); - if (this._harkStream != null) - this._harkStream.getAudioTracks()[0].stop(); - - this._harkStream = new MediaStream(); - - this._harkStream.addTrack(track.clone()); - - if (!this._harkStream.getAudioTracks()[0]) - throw new Error('enableMic(): given stream has no audio track'); - - this._hark = hark(this._harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - this._hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exaggerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - volume = Math.round(volume); - - if (this._micProducer && volume !== this._micProducer.volume) - { - this._micProducer.volume = volume; - - store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); - } - }); - this._hark.on('speaking', function() - { - store.dispatch(meActions.setIsSpeaking(true)); - }); - this._hark.on('stopped_speaking', function() - { - store.dispatch(meActions.setIsSpeaking(false)); - }); } catch (error) { diff --git a/app/src/ScreenShare.js b/app/src/ScreenShare.js index 180fe2a..2ff1bcc 100644 --- a/app/src/ScreenShare.js +++ b/app/src/ScreenShare.js @@ -225,10 +225,7 @@ export default class ScreenShare return new DisplayMediaScreenShare(); } case 'chrome': - { - return new DisplayMediaScreenShare(); - } - case 'msedge': + case 'edge': { return new DisplayMediaScreenShare(); } diff --git a/app/src/Spotlights.js b/app/src/Spotlights.js index dd119df..c462f09 100644 --- a/app/src/Spotlights.js +++ b/app/src/Spotlights.js @@ -156,6 +156,15 @@ export default class Spotlights extends EventEmitter }); } + clearSpotlights() + { + this._started = false; + + this._peerList = []; + this._selectedSpotlights = []; + this._currentSpotlights = []; + } + _newPeer(id) { logger.debug( diff --git a/app/src/actions/consumerActions.js b/app/src/actions/consumerActions.js index 249d156..659a609 100644 --- a/app/src/actions/consumerActions.js +++ b/app/src/actions/consumerActions.js @@ -10,6 +10,11 @@ export const removeConsumer = (consumerId, peerId) => payload : { consumerId, peerId } }); +export const clearConsumers = () => + ({ + type : 'CLEAR_CONSUMERS' + }); + export const setConsumerPaused = (consumerId, originator) => ({ type : 'SET_CONSUMER_PAUSED', diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index fee30a5..5672b47 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -10,6 +10,11 @@ export const removePeer = (peerId) => payload : { peerId } }); +export const clearPeers = () => + ({ + type : 'CLEAR_PEERS' + }); + export const setPeerDisplayName = (displayName, peerId) => ({ type : 'SET_PEER_DISPLAY_NAME', @@ -40,6 +45,12 @@ export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) => payload : { peerId, raisedHand, raisedHandTimestamp } }); +export const setPeerRaisedHandInProgress = (peerId, flag) => + ({ + type : 'SET_PEER_RAISED_HAND_IN_PROGRESS', + payload : { peerId, flag } + }); + export const setPeerPicture = (peerId, picture) => ({ type : 'SET_PEER_PICTURE', @@ -63,3 +74,15 @@ export const setPeerKickInProgress = (peerId, flag) => type : 'SET_PEER_KICK_IN_PROGRESS', payload : { peerId, flag } }); + +export const setMutePeerInProgress = (peerId, flag) => + ({ + type : 'STOP_PEER_AUDIO_IN_PROGRESS', + payload : { peerId, flag } + }); + +export const setStopPeerVideoInProgress = (peerId, flag) => + ({ + type : 'STOP_PEER_VIDEO_IN_PROGRESS', + payload : { peerId, flag } + }); diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index 30ce37c..5ae45e3 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -40,6 +40,12 @@ export const setSignInRequired = (signInRequired) => payload : { signInRequired } }); +export const setOverRoomLimit = (overRoomLimit) => + ({ + type : 'SET_OVER_ROOM_LIMIT', + payload : { overRoomLimit } + }); + export const setAccessCode = (accessCode) => ({ type : 'SET_ACCESS_CODE', @@ -64,6 +70,18 @@ export const setExtraVideoOpen = (extraVideoOpen) => payload : { extraVideoOpen } }); +export const setHelpOpen = (helpOpen) => + ({ + type : 'SET_HELP_OPEN', + payload : { helpOpen } + }); + +export const setAboutOpen = (aboutOpen) => + ({ + type : 'SET_ABOUT_OPEN', + payload : { aboutOpen } + }); + export const setSettingsTab = (tab) => ({ type : 'SET_SETTINGS_TAB', @@ -112,6 +130,11 @@ export const setSpotlights = (spotlights) => payload : { spotlights } }); +export const clearSpotlights = () => + ({ + type : 'CLEAR_SPOTLIGHTS' + }); + export const toggleJoined = () => ({ type : 'TOGGLE_JOINED' @@ -159,14 +182,14 @@ export const setClearFileSharingInProgress = (flag) => payload : { flag } }); -export const setUserRoles = (userRoles) => +export const setRoomPermissions = (roomPermissions) => ({ - type : 'SET_USER_ROLES', - payload : { userRoles } + type : 'SET_ROOM_PERMISSIONS', + payload : { roomPermissions } }); -export const setPermissionsFromRoles = (permissionsFromRoles) => +export const setAllowWhenRoleMissing = (allowWhenRoleMissing) => ({ - type : 'SET_PERMISSIONS_FROM_ROLES', - payload : { permissionsFromRoles } + type : 'SET_ALLOW_WHEN_ROLE_MISSING', + payload : { allowWhenRoleMissing } }); diff --git a/app/src/actions/settingsActions.js b/app/src/actions/settingsActions.js index 112dd0b..21ff2fd 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -38,6 +38,60 @@ export const togglePermanentTopBar = () => type : 'TOGGLE_PERMANENT_TOPBAR' }); +export const toggleButtonControlBar = () => + ({ + type : 'TOGGLE_BUTTON_CONTROL_BAR' + }); + +export const toggleDrawerOverlayed = () => + ({ + type : 'TOGGLE_DRAWER_OVERLAYED' + }); + +export const toggleShowNotifications = () => + ({ + type : 'TOGGLE_SHOW_NOTIFICATIONS' + }); + +export const setEchoCancellation = (echoCancellation) => + ({ + type : 'SET_ECHO_CANCELLATION', + payload : { echoCancellation } + }); + +export const setAutoGainControl = (autoGainControl) => + ({ + type : 'SET_AUTO_GAIN_CONTROL', + payload : { autoGainControl } + }); + +export const setNoiseSuppression = (noiseSuppression) => + ({ + type : 'SET_NOISE_SUPPRESSION', + payload : { noiseSuppression } + }); + +export const setDefaultAudio = (audio) => + ({ + type : 'SET_DEFAULT_AUDIO', + payload : { audio } + }); + +export const toggleEchoCancellation = () => + ({ + type : 'TOGGLE_ECHO_CANCELLATION' + }); + +export const toggleAutoGainControl = () => + ({ + type : 'TOGGLE_AUTO_GAIN_CONTROL' + }); + +export const toggleNoiseSuppression = () => + ({ + type : 'TOGGLE_NOISE_SUPPRESSION' + }); + export const toggleHiddenControls = () => ({ type : 'TOGGLE_HIDDEN_CONTROLS' diff --git a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js index 8f9843a..050994d 100644 --- a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js +++ b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import IconButton from '@material-ui/core/IconButton'; @@ -61,6 +63,7 @@ const ListLobbyPeer = (props) => peer.promotionInProgress || promotionInProgress } + color='primary' onClick={(e) => { e.stopPropagation(); @@ -84,28 +87,32 @@ ListLobbyPeer.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state, { id }) => +const makeMapStateToProps = (initialState, { id }) => { - return { - peer : state.lobbyPeers[id], - promotionInProgress : state.room.lobbyPeersPromotionInProgress, - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + peer : state.lobbyPeers[id], + promotionInProgress : state.room.lobbyPeersPromotionInProgress, + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && - prev.room.lobbyPeersPromotionInProgress === - next.room.lobbyPeersPromotionInProgress && + prev.room === next.room && + prev.peers === next.peers && // For checking permissions prev.me.roles === next.me.roles && prev.lobbyPeers === next.lobbyPeers ); diff --git a/app/src/components/AccessControl/LockDialog/LockDialog.js b/app/src/components/AccessControl/LockDialog/LockDialog.js index 4d6cd24..c3dbd6a 100644 --- a/app/src/components/AccessControl/LockDialog/LockDialog.js +++ b/app/src/components/AccessControl/LockDialog/LockDialog.js @@ -1,8 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { - lobbyPeersKeySelector + lobbyPeersKeySelector, + makePermissionSelector } from '../../Selectors'; +import { permissions } from '../../../permissions'; import * as appPropTypes from '../../appPropTypes'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; @@ -140,15 +142,20 @@ LockDialog.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - room : state.room, - lobbyPeers : lobbyPeersKeySelector(state), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + room : state.room, + lobbyPeers : lobbyPeersKeySelector(state), + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; const mapDispatchToProps = { @@ -157,7 +164,7 @@ const mapDispatchToProps = { }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { @@ -166,6 +173,7 @@ export default withRoomContext(connect( return ( prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.lobbyPeers === next.lobbyPeers ); } diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 5e8e0dc..8d45473 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; -import { meProducersSelector } from '../Selectors'; +import { + meProducersSelector, + makePermissionSelector +} from '../Selectors'; +import { permissions } from '../../permissions'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; @@ -10,6 +14,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; import VideoView from '../VideoContainers/VideoView'; import Volume from './Volume'; import Fab from '@material-ui/core/Fab'; +import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import MicIcon from '@material-ui/icons/Mic'; import MicOffIcon from '@material-ui/icons/MicOff'; @@ -59,12 +64,47 @@ const styles = (theme) => margin : theme.spacing(1), pointerEvents : 'auto' }, + smallContainer : + { + backgroundColor : 'rgba(255, 255, 255, 0.9)', + margin : '0.5vmin', + padding : '0.5vmin', + boxShadow : '0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)', + pointerEvents : 'auto', + transition : 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + '&:hover' : + { + backgroundColor : 'rgba(213, 213, 213, 1)' + } + }, viewContainer : { position : 'relative', width : '100%', height : '100%' }, + meTag : + { + position : 'absolute', + float : 'left', + top : '50%', + left : '50%', + transform : 'translate(-50%, -50%)', + color : 'rgba(255, 255, 255, 0.5)', + fontSize : '7em', + zIndex : 30, + margin : 0, + opacity : 0, + transition : 'opacity 0.1s ease-in-out', + '&.hover' : + { + opacity : 1 + }, + '&.smallContainer' : + { + fontSize : '3em' + } + }, controls : { position : 'absolute', @@ -86,35 +126,18 @@ const styles = (theme) => '&.hover' : { opacity : 1 - }, - '& p' : - { - position : 'absolute', - float : 'left', - top : '50%', - left : '50%', - transform : 'translate(-50%, -50%)', - color : 'rgba(255, 255, 255, 0.5)', - fontSize : '7em', - margin : 0, - opacity : 0, - transition : 'opacity 0.1s ease-in-out', - '&.hover' : - { - opacity : 1 - } } }, ptt : { position : 'absolute', float : 'left', - top : '10%', + top : '25%', left : '50%', transform : 'translate(-50%, 0%)', color : 'rgba(255, 255, 255, 0.7)', - fontSize : '2vs', - backgroundColor : 'rgba(255, 0, 0, 0.5)', + fontSize : '1.3em', + backgroundColor : 'rgba(255, 0, 0, 0.9)', margin : '4px', padding : theme.spacing(2), zIndex : 31, @@ -145,7 +168,7 @@ const Me = (props) => activeSpeaker, spacing, style, - smallButtons, + smallContainer, advancedMode, micProducer, webcamProducer, @@ -267,6 +290,28 @@ const Me = (props) => 'margin' : spacing }; + let audioScore = null; + + if (micProducer && micProducer.score) + { + audioScore = + micProducer.score.reduce( + (prev, curr) => + (prev.score < curr.score ? prev : curr) + ); + } + + let videoScore = null; + + if (webcamProducer && webcamProducer.score) + { + videoScore = + webcamProducer.score.reduce( + (prev, curr) => + (prev.score < curr.score ? prev : curr) + ); + } + return (
}} style={spacingStyle} > + + { me.browser.platform !== 'mobile' && +
+ +
+ }
-
-
-
setHover(true)} - onMouseOut={() => setHover(false)} - onTouchStart={() => - { - if (touchTimeout) - clearTimeout(touchTimeout); - - setHover(true); - }} - onTouchEnd={() => - { - if (touchTimeout) - clearTimeout(touchTimeout); - - touchTimeout = setTimeout(() => + id='room.me' + defaultMessage='ME' + /> +

+ { !settings.buttonControlBar && +
setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => { - setHover(false); - }, 2000); - }} - > -

- -

+ if (touchTimeout) + clearTimeout(touchTimeout); - - -
- - { - if (micState === 'off') - roomClient.enableMic(); - else if (micState === 'on') - roomClient.muteMic(); - else - roomClient.unmuteMic(); - }} - > - { micState === 'on' ? - - : - - } - -
-
- -
- - { - webcamState === 'on' ? - roomClient.disableWebcam() : - roomClient.enableWebcam(); - }} - > - { webcamState === 'on' ? - - : - - } - -
-
- { me.browser.platform !== 'mobile' && - -
- - { - switch (screenState) + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + > + + + { smallContainer ? +
+ { - case 'on': - { - roomClient.disableScreenSharing(); - break; - } - case 'off': - { - roomClient.enableScreenSharing(); - break; - } - default: - { - break; - } + if (micState === 'off') + roomClient.enableMic(); + else if (micState === 'on') + roomClient.muteMic(); + else + roomClient.unmuteMic(); + }} + > + { micState === 'on' ? + + : + } - }} - > - { (screenState === 'on' || screenState === 'unsupported') && - - } - { screenState === 'off' && - - } - -
+ +
+ : +
+ + { + if (micState === 'off') + roomClient.enableMic(); + else if (micState === 'on') + roomClient.muteMic(); + else + roomClient.unmuteMic(); + }} + > + { micState === 'on' ? + + : + + } + +
+ }
- } -
-
+ + { smallContainer ? +
+ + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + +
+ : +
+ + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + +
+ } +
+ { me.browser.platform !== 'mobile' && + + { smallContainer ? +
+ + { + switch (screenState) + { + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } + } + }} + > + { (screenState === 'on' || screenState === 'unsupported') && + + } + { screenState === 'off' && + + } + + +
+ : +
+ + { + switch (screenState) + { + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } + } + }} + > + { (screenState === 'on' || screenState === 'unsupported') && + + } + { screenState === 'off' && + + } + +
+ } +
+ } + +
+ } videoVisible={videoVisible} audioCodec={micProducer && micProducer.codec} videoCodec={webcamProducer && webcamProducer.codec} + audioScore={audioScore} + videoScore={videoScore} onChangeDisplayName={(displayName) => { roomClient.changeDisplayName(displayName); @@ -502,6 +664,18 @@ const Me = (props) => style={spacingStyle} >
+

+ +

}, 2000); }} > -

- -

- -
- - { - roomClient.disableExtraVideo(producer.id); - }} - > - - -
+ { smallContainer ? +
+ + { + roomClient.disableExtraVideo(producer.id); + }} + > + + + +
+ : +
+ + { + roomClient.disableExtraVideo(producer.id); + }} + > + + +
+ }
@@ -599,40 +788,18 @@ const Me = (props) => style={spacingStyle} >
-
setHover(true)} - onMouseOut={() => setHover(false)} - onTouchStart={() => - { - if (touchTimeout) - clearTimeout(touchTimeout); - - setHover(true); - }} - onTouchEnd={() => - { - - if (touchTimeout) - clearTimeout(touchTimeout); - - touchTimeout = setTimeout(() => - { - setHover(false); - }, 2000); - }} > -

- -

-
+ +

+const makeMapStateToProps = () => { - return { - me : state.me, - ...meProducersSelector(state), - settings : state.settings, - activeSpeaker : state.me.id === state.room.activeSpeakerId, - canShareScreen : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_SCREEN.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN); + + const mapStateToProps = (state) => + { + return { + me : state.me, + ...meProducersSelector(state), + settings : state.settings, + activeSpeaker : state.me.id === state.room.activeSpeakerId, + canShareScreen : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me === next.me && + prev.peers === next.peers && prev.producers === next.producers && - prev.settings === next.settings && - prev.room.activeSpeakerId === next.room.activeSpeakerId + prev.settings === next.settings ); } } diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 3e6e776..9c2a6fa 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -12,6 +12,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; import VideoView from '../VideoContainers/VideoView'; import Tooltip from '@material-ui/core/Tooltip'; import Fab from '@material-ui/core/Fab'; +import IconButton from '@material-ui/core/IconButton'; import VolumeUpIcon from '@material-ui/icons/VolumeUp'; import VolumeOffIcon from '@material-ui/icons/VolumeOff'; import NewWindowIcon from '@material-ui/icons/OpenInNew'; @@ -59,6 +60,19 @@ const styles = (theme) => { margin : theme.spacing(1) }, + smallContainer : + { + backgroundColor : 'rgba(255, 255, 255, 0.9)', + margin : '0.5vmin', + padding : '0.5vmin', + boxShadow : '0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)', + pointerEvents : 'auto', + transition : 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + '&:hover' : + { + backgroundColor : 'rgba(213, 213, 213, 1)' + } + }, viewContainer : { position : 'relative', @@ -130,7 +144,7 @@ const Peer = (props) => toggleConsumerWindow, spacing, style, - smallButtons, + smallContainer, windowConsumer, classes, theme @@ -235,7 +249,30 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
+ { smallContainer ? + + { + micEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); + }} + > + { micEnabled ? + + : + + } + + : className={classes.fab} disabled={!micConsumer} color={micEnabled ? 'default' : 'secondary'} - size={smallButtons ? 'small' : 'large'} + size='large' onClick={() => { micEnabled ? @@ -258,7 +295,7 @@ const Peer = (props) => } -
+ } { browser.platform !== 'mobile' && @@ -269,7 +306,27 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
+ { smallContainer ? + + { + toggleConsumerWindow(webcamConsumer); + }} + > + + + : !videoVisible || (windowConsumer === webcamConsumer.id) } - size={smallButtons ? 'small' : 'large'} + size='large' onClick={() => { toggleConsumerWindow(webcamConsumer); @@ -288,7 +345,7 @@ const Peer = (props) => > -
+ } } @@ -299,7 +356,24 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
+ { smallContainer ? + + { + toggleConsumerFullscreen(webcamConsumer); + }} + > + + + : })} className={classes.fab} disabled={!videoVisible} - size={smallButtons ? 'small' : 'large'} + size='large' onClick={() => { toggleConsumerFullscreen(webcamConsumer); @@ -315,11 +389,12 @@ const Peer = (props) => > -
+ }
})} placement={smallScreen ? 'top' : 'left'} > -
+ { smallContainer ? + + { + toggleConsumerWindow(consumer); + }} + > + + + : !videoVisible || (windowConsumer === consumer.id) } - size={smallButtons ? 'small' : 'large'} + size='large' onClick={() => { toggleConsumerWindow(consumer); @@ -446,7 +541,7 @@ const Peer = (props) => > -
+ } } @@ -457,7 +552,24 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
+ { smallContainer ? + + { + toggleConsumerFullscreen(consumer); + }} + > + + + : })} className={classes.fab} disabled={!videoVisible} - size={smallButtons ? 'small' : 'large'} + size='large' onClick={() => { toggleConsumerFullscreen(consumer); @@ -473,11 +585,12 @@ const Peer = (props) => > -
+ }
})} placement={smallScreen ? 'top' : 'left'} > -
- - { - toggleConsumerWindow(screenConsumer); - }} - > - - -
+ + { + toggleConsumerWindow(screenConsumer); + }} + > + + } @@ -603,26 +714,25 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- - { - toggleConsumerFullscreen(screenConsumer); - }} - > - - -
+ + { + toggleConsumerFullscreen(screenConsumer); + }} + > + +
+ ({ + 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 ( + handleCloseAbout(false)} + classes={{ + paper : classes.dialogPaper + }} + > + + + + + + Contributions to this work were made on behalf of the GÉANT + project, a project that has received funding from the + European Union’s 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.
+
+ 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. +
+ + https://edumeet.org + +
+ + { window.config.logo && Logo } + + +
+ ); +}; + +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))); \ No newline at end of file diff --git a/app/src/components/Controls/ButtonControlBar.js b/app/src/components/Controls/ButtonControlBar.js new file mode 100644 index 0000000..f6e4fd6 --- /dev/null +++ b/app/src/components/Controls/ButtonControlBar.js @@ -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 ( +
+ + + { + micState === 'on' ? + roomClient.muteMic() : + roomClient.unmuteMic(); + }} + > + { micState === 'on' ? + + : + + } + + + + + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + + + + + { + switch (screenState) + { + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } + } + }} + > + { screenState === 'on' || screenState === 'unsupported' ? + + :null + } + { screenState === 'off' ? + + :null + } + + +
+ ); +}; + +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))); \ No newline at end of file diff --git a/app/src/components/Controls/Help.js b/app/src/components/Controls/Help.js new file mode 100644 index 0000000..98415b5 --- /dev/null +++ b/app/src/components/Controls/Help.js @@ -0,0 +1,169 @@ +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' } +]; +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 ( + { handleCloseHelp(false); }} + classes={{ + paper : classes.dialogPaper + }} + > + + + + + + + + + {shortcuts.map((value, index) => + { + return ( +
+ + {value.key} + + +
+ ); + })} + +
+
+ + + +
+ ); +}; + +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))); diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index cbe4bee..bbf408d 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -4,23 +4,28 @@ import PropTypes from 'prop-types'; import { lobbyPeersKeySelector, peersLengthSelector, - raisedHandsSelector + raisedHandsSelector, + makePermissionSelector } from '../Selectors'; +import { permissions } from '../../permissions'; import * as appPropTypes from '../appPropTypes'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import * as roomActions from '../../actions/roomActions'; import * as toolareaActions from '../../actions/toolareaActions'; import { useIntl, FormattedMessage } from 'react-intl'; +import classnames from 'classnames'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import MenuItem from '@material-ui/core/MenuItem'; import Menu from '@material-ui/core/Menu'; +import Popover from '@material-ui/core/Popover'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import Avatar from '@material-ui/core/Avatar'; import Badge from '@material-ui/core/Badge'; +import Paper from '@material-ui/core/Paper'; import ExtensionIcon from '@material-ui/icons/Extension'; import AccountCircle from '@material-ui/icons/AccountCircle'; import FullScreenIcon from '@material-ui/icons/Fullscreen'; @@ -33,9 +38,37 @@ import LockOpenIcon from '@material-ui/icons/LockOpen'; import VideoCallIcon from '@material-ui/icons/VideoCall'; import Button from '@material-ui/core/Button'; import Tooltip from '@material-ui/core/Tooltip'; +import MoreIcon from '@material-ui/icons/MoreVert'; +import HelpIcon from '@material-ui/icons/Help'; +import InfoIcon from '@material-ui/icons/Info'; const styles = (theme) => ({ + persistentDrawerOpen : + { + width : 'calc(100% - 30vw)', + marginLeft : '30vw', + [theme.breakpoints.down('lg')] : + { + width : 'calc(100% - 40vw)', + marginLeft : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : 'calc(100% - 50vw)', + marginLeft : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : 'calc(100% - 70vw)', + marginLeft : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : 'calc(100% - 90vw)', + marginLeft : '90vw' + } + }, menuButton : { margin : 0, @@ -77,9 +110,17 @@ const styles = (theme) => display : 'block' } }, - actionButtons : - { - display : 'flex' + sectionDesktop : { + display : 'none', + [theme.breakpoints.up('md')] : { + display : 'flex' + } + }, + sectionMobile : { + display : 'flex', + [theme.breakpoints.up('md')] : { + display : 'none' + } }, actionButton : { @@ -96,7 +137,7 @@ const styles = (theme) => }, moreAction : { - margin : theme.spacing(0, 0, 0, 1) + margin : theme.spacing(0.5, 0, 0.5, 1.5) } }); @@ -135,16 +176,36 @@ const TopBar = (props) => { const intl = useIntl(); - const [ moreActionsElement, setMoreActionsElement ] = useState(null); + const [ mobileMoreAnchorEl, setMobileMoreAnchorEl ] = useState(null); + const [ anchorEl, setAnchorEl ] = useState(null); + const [ currentMenu, setCurrentMenu ] = useState(null); - const handleMoreActionsOpen = (event) => + const handleExited = () => { - setMoreActionsElement(event.currentTarget); + setCurrentMenu(null); }; - const handleMoreActionsClose = () => + const handleMobileMenuOpen = (event) => { - setMoreActionsElement(null); + setMobileMoreAnchorEl(event.currentTarget); + }; + + const handleMobileMenuClose = () => + { + setMobileMoreAnchorEl(null); + }; + + const handleMenuOpen = (event, menu) => + { + setAnchorEl(event.currentTarget); + setCurrentMenu(menu); + }; + + const handleMenuClose = () => + { + setAnchorEl(null); + + handleMobileMenuClose(); }; const { @@ -153,6 +214,9 @@ const TopBar = (props) => peersLength, lobbyPeers, permanentTopBar, + drawerOverlayed, + toolAreaOpen, + isMobile, myPicture, loggedIn, loginEnabled, @@ -161,6 +225,8 @@ const TopBar = (props) => onFullscreen, setSettingsOpen, setExtraVideoOpen, + setHelpOpen, + setAboutOpen, setLockDialogOpen, toggleToolArea, openUsersTab, @@ -171,7 +237,8 @@ const TopBar = (props) => classes } = props; - const isMoreActionsMenuOpen = Boolean(moreActionsElement); + const isMenuOpen = Boolean(anchorEl); + const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); const lockTooltip = room.locked ? intl.formatMessage({ @@ -210,7 +277,12 @@ const TopBar = (props) => { window.config.title ? window.config.title : 'Multiparty meeting' }
-
- + - - + handleMenuOpen(event, 'moreActions')} + color='inherit' + > + + + { fullscreenEnabled && } -
-
+
+ + + +
+
+ - + { currentMenu === 'moreActions' && + + + { + handleMenuClose(); + setExtraVideoOpen(!room.extraVideoOpen); + }} + > + +

+ +

+
+ + { + handleMenuClose(); + setHelpOpen(!room.helpOpen); + }} + > + +

+ +

+
+ + { + handleMenuClose(); + setAboutOpen(!room.aboutOpen); + }} + > + +

+ +

+
+
+ } + + + { loginEnabled && + + { + handleMenuClose(); + loggedIn ? roomClient.logout() : roomClient.login(); + }} + > + { myPicture ? + + : + + } + { loggedIn ? +

+ +

+ : +

+ +

+ } +
+ } { - handleMoreActionsClose(); - setExtraVideoOpen(!room.extraVideoOpen); + handleMenuClose(); + + if (room.locked) + { + roomClient.unlockRoom(); + } + else + { + roomClient.lockRoom(); + } }} > - + { room.locked ? + + : + + } + { room.locked ? +

+ +

+ : +

+ +

+ } +
+ + { + handleMenuClose(); + setSettingsOpen(!room.settingsOpen); + }} + > +

+

+
+ { lobbyPeers.length > 0 && + + { + handleMenuClose(); + setLockDialogOpen(!room.lockDialogOpen); + }} + > + + + +

+ +

+
+ } + + { + handleMenuClose(); + openUsersTab(); + }} + > + + + +

+ +

+
+ { fullscreenEnabled && + + { + handleMenuClose(); + onFullscreen(); + }} + > + { fullscreen ? + + : + + } +

+ +

+
+ } + handleMenuOpen(event, 'moreActions')} + > + +

+

@@ -444,9 +769,12 @@ TopBar.propTypes = { roomClient : PropTypes.object.isRequired, room : appPropTypes.Room.isRequired, + isMobile : PropTypes.bool.isRequired, peersLength : PropTypes.number, lobbyPeers : PropTypes.array, - permanentTopBar : PropTypes.bool, + permanentTopBar : PropTypes.bool.isRequired, + drawerOverlayed : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, myPicture : PropTypes.string, loggedIn : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired, @@ -456,6 +784,8 @@ TopBar.propTypes = setToolbarsVisible : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired, setExtraVideoOpen : PropTypes.func.isRequired, + setHelpOpen : PropTypes.func.isRequired, + setAboutOpen : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, openUsersTab : PropTypes.func.isRequired, @@ -467,27 +797,38 @@ TopBar.propTypes = theme : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - room : state.room, - peersLength : peersLengthSelector(state), - lobbyPeers : lobbyPeersKeySelector(state), - permanentTopBar : state.settings.permanentTopBar, - loggedIn : state.me.loggedIn, - loginEnabled : state.me.loginEnabled, - myPicture : state.me.picture, - unread : state.toolarea.unreadMessages + - state.toolarea.unreadFiles + raisedHandsSelector(state), - canProduceExtraVideo : - state.me.roles.some((role) => - state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)), - canLock : - state.me.roles.some((role) => - state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasExtraVideoPermission = + makePermissionSelector(permissions.EXTRA_VIDEO); + + const hasLockPermission = + makePermissionSelector(permissions.CHANGE_ROOM_LOCK); + + const hasPromotionPermission = + makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + ({ + room : state.room, + isMobile : state.me.browser.platform === 'mobile', + peersLength : peersLengthSelector(state), + lobbyPeers : lobbyPeersKeySelector(state), + permanentTopBar : state.settings.permanentTopBar, + drawerOverlayed : state.settings.drawerOverlayed, + toolAreaOpen : state.toolarea.toolAreaOpen, + loggedIn : state.me.loggedIn, + loginEnabled : state.me.loginEnabled, + myPicture : state.me.picture, + unread : state.toolarea.unreadMessages + + state.toolarea.unreadFiles + raisedHandsSelector(state), + canProduceExtraVideo : hasExtraVideoPermission(state), + canLock : hasLockPermission(state), + canPromote : hasPromotionPermission(state) + }); + + return mapStateToProps; +}; const mapDispatchToProps = (dispatch) => ({ @@ -503,6 +844,14 @@ const mapDispatchToProps = (dispatch) => { dispatch(roomActions.setExtraVideoOpen(extraVideoOpen)); }, + setHelpOpen : (helpOpen) => + { + dispatch(roomActions.setHelpOpen(helpOpen)); + }, + setAboutOpen : (aboutOpen) => + { + dispatch(roomActions.setAboutOpen(aboutOpen)); + }, setLockDialogOpen : (lockDialogOpen) => { dispatch(roomActions.setLockDialogOpen(lockDialogOpen)); @@ -519,7 +868,7 @@ const mapDispatchToProps = (dispatch) => }); export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { @@ -530,12 +879,15 @@ export default withRoomContext(connect( prev.peers === next.peers && prev.lobbyPeers === next.lobbyPeers && prev.settings.permanentTopBar === next.settings.permanentTopBar && + prev.settings.drawerOverlayed === next.settings.drawerOverlayed && prev.me.loggedIn === next.me.loggedIn && + prev.me.browser === next.me.browser && prev.me.loginEnabled === next.me.loginEnabled && prev.me.picture === next.me.picture && prev.me.roles === next.me.roles && prev.toolarea.unreadMessages === next.toolarea.unreadMessages && - prev.toolarea.unreadFiles === next.toolarea.unreadFiles + prev.toolarea.unreadFiles === next.toolarea.unreadFiles && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } } diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index a8493db..0ad8ef8 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -83,6 +83,10 @@ const styles = (theme) => green : { color : 'rgba(0, 153, 0, 1)' + }, + red : + { + color : 'rgba(153, 0, 0, 1)' } }); @@ -220,7 +224,7 @@ const JoinDialog = ({ myPicture={myPicture} onLogin={() => { - loggedIn ? roomClient.logout() : roomClient.login(roomId); + loggedIn ? roomClient.logout(roomId) : roomClient.login(roomId); }} loggedIn={loggedIn} > @@ -281,6 +285,16 @@ const JoinDialog = ({ }} fullWidth /> + {!room.inLobby && room.overRoomLimit && + + + + } @@ -419,6 +433,7 @@ export default withRoomContext(connect( return ( prev.room.inLobby === next.room.inLobby && prev.room.signInRequired === next.room.signInRequired && + prev.room.overRoomLimit === next.room.overRoomLimit && prev.settings.displayName === next.settings.displayName && prev.me.displayNameInProgress === next.me.displayNameInProgress && prev.me.loginEnabled === next.me.loginEnabled && diff --git a/app/src/components/MeetingDrawer/Chat/ChatInput.js b/app/src/components/MeetingDrawer/Chat/ChatInput.js index 480bb26..bb07f98 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatInput.js +++ b/app/src/components/MeetingDrawer/Chat/ChatInput.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; import IconButton from '@material-ui/core/IconButton'; @@ -119,26 +121,32 @@ ChatInput.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - displayName : state.settings.displayName, - picture : state.me.picture, - canChat : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SEND_CHAT.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.SEND_CHAT); + + const mapStateToProps = (state) => + ({ + displayName : state.settings.displayName, + picture : state.me.picture, + canChat : hasPermission(state) + }); + + return mapStateToProps; +}; export default withRoomContext( connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.settings.displayName === next.settings.displayName && prev.me.picture === next.me.picture ); diff --git a/app/src/components/MeetingDrawer/Chat/ChatModerator.js b/app/src/components/MeetingDrawer/Chat/ChatModerator.js index a35675b..d7b1cad 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatModerator.js +++ b/app/src/components/MeetingDrawer/Chat/ChatModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ ChatModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isChatModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT); + + const mapStateToProps = (state) => + ({ + isChatModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js index 6278e46..8af1d8b 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js @@ -4,6 +4,8 @@ import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import FileList from './FileList'; import FileSharingModerator from './FileSharingModerator'; import Paper from '@material-ui/core/Paper'; @@ -25,6 +27,10 @@ const styles = (theme) => button : { margin : theme.spacing(1) + }, + shareButtonsWrapper : + { + display : 'flex' } }); @@ -42,6 +48,7 @@ const FileSharing = (props) => const { canShareFiles, + browser, canShare, classes } = props; @@ -57,29 +64,61 @@ const FileSharing = (props) => defaultMessage : 'File sharing not supported' }); + const buttonGalleryDescription = canShareFiles ? + intl.formatMessage({ + id : 'label.shareGalleryFile', + defaultMessage : 'Share image' + }) + : + intl.formatMessage({ + id : 'label.fileSharingUnsupported', + defaultMessage : 'File sharing not supported' + }); + return ( - (e.target.value = null)} - id='share-files-button' - /> - - +
+ (e.target.value = null)} + id='share-files-button' + /> + + + { + (browser.platform === 'mobile') && canShareFiles && canShare && + } +
); @@ -87,34 +126,43 @@ const FileSharing = (props) => FileSharing.propTypes = { roomClient : PropTypes.any.isRequired, + browser : PropTypes.object.isRequired, canShareFiles : PropTypes.bool.isRequired, tabOpen : PropTypes.bool.isRequired, canShare : PropTypes.bool.isRequired, classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - canShareFiles : state.me.canShareFiles, - tabOpen : state.toolarea.currentToolTab === 'files', - canShare : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_FILE.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_FILE); + + const mapStateToProps = (state) => + { + return { + canShareFiles : state.me.canShareFiles, + browser : state.me.browser, + tabOpen : state.toolarea.currentToolTab === 'files', + canShare : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && + prev.me.browser === next.me.browser && prev.me.roles === next.me.roles && prev.me.canShareFiles === next.me.canShareFiles && + prev.peers === next.peers && prev.toolarea.currentToolTab === next.toolarea.currentToolTab ); } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js index e38e54c..05f35e8 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ FileSharingModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isFileSharingModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_FILES.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_FILES); + + const mapStateToProps = (state) => + ({ + isFileSharingModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index 762af00..33873d2 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -2,10 +2,12 @@ import React from 'react'; import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; import * as appPropTypes from '../../appPropTypes'; import { useIntl } from 'react-intl'; import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; import PanIcon from '@material-ui/icons/PanTool'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; @@ -22,7 +24,7 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', - marginTop : theme.spacing(1) + marginTop : theme.spacing(0.5) }, peerInfo : { @@ -32,6 +34,10 @@ const styles = (theme) => flexGrow : 1, alignItems : 'center' }, + buttons : + { + padding : theme.spacing(1) + }, green : { color : 'rgba(0, 153, 0, 1)' @@ -58,22 +64,33 @@ const ListMe = (props) =>
{settings.displayName}
- - { - e.stopPropagation(); - - roomClient.setRaisedHand(!me.raisedHand); - }} + placement='bottom' > - - + + { + e.stopPropagation(); + + roomClient.setRaisedHand(!me.raisedHand); + }} + > + + +
); }; diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index d4b1409..0ca5d89 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -6,9 +6,13 @@ import PropTypes from 'prop-types'; import * as appPropTypes from '../../appPropTypes'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { green } from '@material-ui/core/colors'; import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; import VideocamIcon from '@material-ui/icons/Videocam'; import VideocamOffIcon from '@material-ui/icons/VideocamOff'; +import MicIcon from '@material-ui/icons/Mic'; +import MicOffIcon from '@material-ui/icons/MicOff'; import VolumeUpIcon from '@material-ui/icons/VolumeUp'; import VolumeOffIcon from '@material-ui/icons/VolumeOff'; import ScreenIcon from '@material-ui/icons/ScreenShare'; @@ -16,6 +20,7 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ExitIcon from '@material-ui/icons/ExitToApp'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import PanIcon from '@material-ui/icons/PanTool'; +import RecordVoiceOverIcon from '@material-ui/icons/RecordVoiceOver'; const styles = (theme) => ({ @@ -30,7 +35,7 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', - marginTop : theme.spacing(1) + marginTop : theme.spacing(0.5) }, peerInfo : { @@ -43,11 +48,16 @@ const styles = (theme) => indicators : { display : 'flex', - padding : theme.spacing(1.5) + padding : theme.spacing(1) + }, + buttons : + { + padding : theme.spacing(1) }, green : { - color : 'rgba(0, 153, 0, 1)' + color : 'rgba(0, 153, 0, 1)', + marginLeft : theme.spacing(2) } }); @@ -58,6 +68,7 @@ const ListPeer = (props) => const { roomClient, isModerator, + spotlight, peer, micConsumer, webcamConsumer, @@ -93,95 +104,207 @@ const ListPeer = (props) =>
{peer.displayName}
-
- { peer.raisedHand && - - } -
- { screenConsumer && + { peer.raisedHand && + { + e.stopPropagation(); + + roomClient.lowerPeerHand(peer.id); + }} + > + + + } + { spotlight && + + + + } + { screenConsumer && spotlight && + - { - e.stopPropagation(); - - screenVisible ? - roomClient.modifyPeerConsumer(peer.id, 'screen', true) : - roomClient.modifyPeerConsumer(peer.id, 'screen', false); - }} + placement='bottom' > - { screenVisible ? - - : - - } - - } - - { - e.stopPropagation(); + + { + e.stopPropagation(); - webcamEnabled ? - roomClient.modifyPeerConsumer(peer.id, 'webcam', true) : - roomClient.modifyPeerConsumer(peer.id, 'webcam', false); - }} - > - { webcamEnabled ? - - : - - } - - + { screenVisible ? + + : + + } + + + } + { spotlight && + + + { + e.stopPropagation(); + + webcamEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'webcam', true) : + roomClient.modifyPeerConsumer(peer.id, 'webcam', false); + }} + > + { webcamEnabled ? + + : + + } + + + } + - { - e.stopPropagation(); - - micEnabled ? - roomClient.modifyPeerConsumer(peer.id, 'mic', true) : - roomClient.modifyPeerConsumer(peer.id, 'mic', false); - }} + placement='bottom' > - { micEnabled ? - - : - - } - - { isModerator && { e.stopPropagation(); - roomClient.kickPeer(peer.id); + micEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); }} > - + { micEnabled ? + + : + + } + + { isModerator && + + + { + e.stopPropagation(); + + roomClient.kickPeer(peer.id); + }} + > + + + + } + { isModerator && micConsumer && + + + { + e.stopPropagation(); + + roomClient.mutePeer(peer.id); + }} + > + { !micConsumer.remotelyPaused ? + + : + + } + + + } + { isModerator && webcamConsumer && + + + { + e.stopPropagation(); + + roomClient.stopPeerVideo(peer.id); + }} + > + { !webcamConsumer.remotelyPaused ? + + : + + } + + } {children}
@@ -193,6 +316,7 @@ ListPeer.propTypes = roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, isModerator : PropTypes.bool, + spotlight : PropTypes.bool, peer : appPropTypes.Peer.isRequired, micConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer, diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index af35dbd..a416a64 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -1,13 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; import { - passivePeersSelector, - spotlightSortedPeersSelector + participantListSelector, + makePermissionSelector } from '../../Selectors'; -import classNames from 'classnames'; +import { permissions } from '../../../permissions'; +import classnames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import PropTypes from 'prop-types'; +import { Flipper, Flipped } from 'react-flip-toolkit'; import { FormattedMessage } from 'react-intl'; import ListPeer from './ListPeer'; import ListMe from './ListMe'; @@ -76,9 +78,9 @@ class ParticipantList extends React.PureComponent roomClient, advancedMode, isModerator, - passivePeers, + participants, + spotlights, selectedPeerId, - spotlightPeers, classes } = this.props; @@ -107,50 +109,42 @@ class ParticipantList extends React.PureComponent
  • - { spotlightPeers.map((peer) => ( -
  • roomClient.setSelectedPeer(peer.id)} - > - - - -
  • - ))} -
-
    -
  • - -
  • - { passivePeers.map((peer) => ( -
  • roomClient.setSelectedPeer(peer.id)} - > - -
  • - ))} + + { participants.map((peer) => ( + +
  • roomClient.setSelectedPeer(peer.id)} + > + { spotlights.includes(peer.id) ? + + + + : + + } +
  • +
    + ))} +
); @@ -162,37 +156,40 @@ ParticipantList.propTypes = roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, isModerator : PropTypes.bool, - passivePeers : PropTypes.array, + participants : PropTypes.array, + spotlights : PropTypes.array, selectedPeerId : PropTypes.string, - spotlightPeers : PropTypes.array, classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - isModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), - passivePeers : passivePeersSelector(state), - selectedPeerId : state.room.selectedPeerId, - spotlightPeers : spotlightSortedPeersSelector(state) + const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM); + + const mapStateToProps = (state) => + { + return { + isModerator : hasPermission(state), + participants : participantListSelector(state), + spotlights : state.room.spotlights, + selectedPeerId : state.room.selectedPeerId + }; }; + + return mapStateToProps; }; const ParticipantListContainer = withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && - prev.peers === next.peers && - prev.room.spotlights === next.room.spotlights && - prev.room.selectedPeerId === next.room.selectedPeerId + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index e2a703e..e2e774c 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -11,10 +11,9 @@ import Peer from '../Containers/Peer'; import Me from '../Containers/Me'; const RATIO = 1.334; -const PADDING_V = 50; -const PADDING_H = 0; +const PADDING = 60; -const styles = () => +const styles = (theme) => ({ root : { @@ -23,6 +22,7 @@ const styles = () => display : 'flex', flexDirection : 'row', flexWrap : 'wrap', + overflow : 'hidden', justifyContent : 'center', alignItems : 'center', alignContent : 'center' @@ -36,6 +36,14 @@ const styles = () => { paddingTop : 60, transition : 'padding .5s' + }, + buttonControlBar : + { + paddingLeft : 60, + [theme.breakpoints.down('sm')] : + { + paddingLeft : 0 + } } }); @@ -66,9 +74,11 @@ class Democratic extends React.PureComponent return; } - const width = this.peersRef.current.clientWidth - PADDING_H; - const height = this.peersRef.current.clientHeight - - (this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H); + const width = + this.peersRef.current.clientWidth - (this.props.buttonControlBar ? PADDING : 0); + const height = + this.peersRef.current.clientHeight - + (this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING : 0); let x, y, space; @@ -130,6 +140,7 @@ class Democratic extends React.PureComponent spotlightsPeers, toolbarsVisible, permanentTopBar, + buttonControlBar, classes } = this.props; @@ -144,7 +155,8 @@ class Democratic extends React.PureComponent className={classnames( classes.root, toolbarsVisible || permanentTopBar ? - classes.showingToolBar : classes.hiddenToolBar + classes.showingToolBar : classes.hiddenToolBar, + buttonControlBar ? classes.buttonControlBar : null )} ref={this.peersRef} > @@ -172,21 +184,25 @@ class Democratic extends React.PureComponent Democratic.propTypes = { - advancedMode : PropTypes.bool, - boxes : PropTypes.number, - spotlightsPeers : PropTypes.array.isRequired, - toolbarsVisible : PropTypes.bool.isRequired, - permanentTopBar : PropTypes.bool, - classes : PropTypes.object.isRequired + advancedMode : PropTypes.bool, + boxes : PropTypes.number, + spotlightsPeers : PropTypes.array.isRequired, + toolbarsVisible : PropTypes.bool.isRequired, + permanentTopBar : PropTypes.bool.isRequired, + buttonControlBar : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => { return { - boxes : videoBoxesSelector(state), - spotlightsPeers : spotlightPeersSelector(state), - toolbarsVisible : state.room.toolbarsVisible, - permanentTopBar : state.settings.permanentTopBar + boxes : videoBoxesSelector(state), + spotlightsPeers : spotlightPeersSelector(state), + toolbarsVisible : state.room.toolbarsVisible, + permanentTopBar : state.settings.permanentTopBar, + buttonControlBar : state.settings.buttonControlBar, + toolAreaOpen : state.toolarea.toolAreaOpen }; }; @@ -203,8 +219,10 @@ export default connect( prev.consumers === next.consumers && prev.room.spotlights === next.room.spotlights && prev.room.toolbarsVisible === next.room.toolbarsVisible && - prev.settings.permanentTopBar === next.settings.permanentTopBar + prev.settings.permanentTopBar === next.settings.permanentTopBar && + prev.settings.buttonControlBar === next.settings.buttonControlBar && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } } -)(withStyles(styles)(Democratic)); +)(withStyles(styles, { withTheme: true })(Democratic)); diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index f78e5b5..120e985 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -25,6 +25,7 @@ const styles = () => height : '100%', width : '100%', display : 'grid', + overflow : 'hidden', gridTemplateColumns : '1fr', gridTemplateRows : '1fr 0.25fr' }, @@ -49,7 +50,7 @@ const styles = () => }, '&.active' : { - opacity : '0.6' + borderColor : 'var(--selected-peer-border-color)' } }, hiddenToolBar : @@ -123,6 +124,9 @@ class Filmstrip extends React.PureComponent const root = this.rootContainer.current; + if (!root) + return; + const availableWidth = root.clientWidth; // Grid is: // 4/5 speaker @@ -279,7 +283,7 @@ class Filmstrip extends React.PureComponent
@@ -302,7 +306,7 @@ class Filmstrip extends React.PureComponent advancedMode={advancedMode} id={peerId} style={peerStyle} - smallButtons + smallContainer /> @@ -331,6 +335,7 @@ Filmstrip.propTypes = { spotlights : PropTypes.array.isRequired, boxes : PropTypes.number, toolbarsVisible : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, permanentTopBar : PropTypes.bool, classes : PropTypes.object.isRequired }; @@ -346,6 +351,7 @@ const mapStateToProps = (state) => spotlights : state.room.spotlights, boxes : videoBoxesSelector(state), toolbarsVisible : state.room.toolbarsVisible, + toolAreaOpen : state.toolarea.toolAreaOpen, permanentTopBar : state.settings.permanentTopBar }; }; @@ -361,6 +367,7 @@ export default withRoomContext(connect( prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.selectedPeerId === next.room.selectedPeerId && prev.room.toolbarsVisible === next.room.toolbarsVisible && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.peers === next.peers && prev.consumers === next.consumers && diff --git a/app/src/components/Room.js b/app/src/components/Room.js index cdda66f..e862be6 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -12,6 +12,7 @@ import { FormattedMessage } from 'react-intl'; import CookieConsent from 'react-cookie-consent'; import CssBaseline from '@material-ui/core/CssBaseline'; import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; +import Drawer from '@material-ui/core/Drawer'; import Hidden from '@material-ui/core/Hidden'; import Notifications from './Notifications/Notifications'; import MeetingDrawer from './MeetingDrawer/MeetingDrawer'; @@ -25,8 +26,11 @@ import Settings from './Settings/Settings'; import TopBar from './Controls/TopBar'; import WakeLock from 'react-wakelock-react16'; import ExtraVideo from './Controls/ExtraVideo'; +import ButtonControlBar from './Controls/ButtonControlBar'; +import Help from './Controls/Help'; +import About from './Controls/About'; -const TIMEOUT = 5 * 1000; +const TIMEOUT = window.config.hideTimeout || 5000; const styles = (theme) => ({ @@ -42,6 +46,27 @@ const styles = (theme) => backgroundSize : 'cover', backgroundRepeat : 'no-repeat' }, + drawer : + { + width : '30vw', + flexShrink : 0, + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + }, drawerPaper : { width : '30vw', @@ -142,6 +167,9 @@ class Room extends React.PureComponent room, browser, advancedMode, + showNotifications, + buttonControlBar, + drawerOverlayed, toolAreaOpen, toggleToolArea, classes, @@ -154,6 +182,8 @@ class Room extends React.PureComponent democratic : Democratic }[room.mode]; + const container = window !== undefined ? window.document.body : undefined; + return (
{ !isElectron() && @@ -178,7 +208,9 @@ class Room extends React.PureComponent - + { showNotifications && + + } @@ -188,22 +220,44 @@ class Room extends React.PureComponent onFullscreen={this.handleToggleFullscreen} /> - + { (browser.platform === 'mobile' || drawerOverlayed) ? + + : + + } { browser.platform === 'mobile' && browser.os !== 'ios' && @@ -211,6 +265,10 @@ class Room extends React.PureComponent + { buttonControlBar && + + } + { room.lockDialogOpen && } @@ -222,6 +280,13 @@ class Room extends React.PureComponent { room.extraVideoOpen && } + { room.helpOpen && + + } + { room.aboutOpen && + + } +
); } @@ -232,6 +297,9 @@ Room.propTypes = room : appPropTypes.Room.isRequired, browser : PropTypes.object.isRequired, advancedMode : PropTypes.bool.isRequired, + showNotifications : PropTypes.bool.isRequired, + buttonControlBar : PropTypes.bool.isRequired, + drawerOverlayed : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired, setToolbarsVisible : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, @@ -241,10 +309,13 @@ Room.propTypes = const mapStateToProps = (state) => ({ - room : state.room, - browser : state.me.browser, - advancedMode : state.settings.advancedMode, - toolAreaOpen : state.toolarea.toolAreaOpen + room : state.room, + browser : state.me.browser, + advancedMode : state.settings.advancedMode, + showNotifications : state.settings.showNotifications, + buttonControlBar : state.settings.buttonControlBar, + drawerOverlayed : state.settings.drawerOverlayed, + toolAreaOpen : state.toolarea.toolAreaOpen }); const mapDispatchToProps = (dispatch) => @@ -270,6 +341,9 @@ export default connect( prev.room === next.room && prev.me.browser === next.me.browser && prev.settings.advancedMode === next.settings.advancedMode && + prev.settings.showNotifications === next.settings.showNotifications && + prev.settings.buttonControlBar === next.settings.buttonControlBar && + prev.settings.drawerOverlayed === next.settings.drawerOverlayed && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index fd22aff..8ac1176 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -1,5 +1,8 @@ import { createSelector } from 'reselect'; +const meRolesSelect = (state) => state.me.roles; +const roomPermissionsSelect = (state) => state.room.roomPermissions; +const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing; const producersSelect = (state) => state.producers; const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; @@ -12,7 +15,8 @@ const peersKeySelector = createSelector( peersSelector, (peers) => Object.keys(peers) ); -const peersValueSelector = createSelector( + +export const peersValueSelector = createSelector( peersSelector, (peers) => Object.values(peers) ); @@ -113,8 +117,31 @@ export const spotlightPeersSelector = createSelector( export const spotlightSortedPeersSelector = createSelector( spotlightsSelector, peersValueSelector, - (spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id)) - .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) + (spotlights, peers) => + peers.filter((peer) => spotlights.includes(peer.id) && !peer.raisedHand) + .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) +); + +const raisedHandSortedPeers = createSelector( + peersValueSelector, + (peers) => peers.filter((peer) => peer.raisedHand) + .sort((a, b) => a.raisedHandTimestamp - b.raisedHandTimestamp) +); + +const peersSortedSelector = createSelector( + spotlightsSelector, + peersValueSelector, + (spotlights, peers) => + peers.filter((peer) => !spotlights.includes(peer.id) && !peer.raisedHand) + .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) +); + +export const participantListSelector = createSelector( + raisedHandSortedPeers, + spotlightSortedPeersSelector, + peersSortedSelector, + (raisedHands, spotlights, peers) => + [ ...raisedHands, ...spotlights, ...peers ] ); export const peersLengthSelector = createSelector( @@ -193,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; + } + ); +}; \ No newline at end of file diff --git a/app/src/components/Settings/AppearenceSettings.js b/app/src/components/Settings/AppearenceSettings.js index 705b2f6..46cc898 100644 --- a/app/src/components/Settings/AppearenceSettings.js +++ b/app/src/components/Settings/AppearenceSettings.js @@ -26,10 +26,14 @@ const styles = (theme) => }); const AppearenceSettings = ({ + isMobile, room, settings, onTogglePermanentTopBar, onToggleHiddenControls, + onToggleButtonControlBar, + onToggleShowNotifications, + onToggleDrawerOverlayed, handleChangeMode, classes }) => @@ -101,30 +105,64 @@ const AppearenceSettings = ({ defaultMessage : 'Hidden media controls' })} /> + } + label={intl.formatMessage({ + id : 'settings.buttonControlBar', + defaultMessage : 'Separate media controls' + })} + /> + { !isMobile && + } + label={intl.formatMessage({ + id : 'settings.drawerOverlayed', + defaultMessage : 'Side drawer over content' + })} + /> + } + } + label={intl.formatMessage({ + id : 'settings.showNotifications', + defaultMessage : 'Show notifications' + })} + />
); }; AppearenceSettings.propTypes = { - room : appPropTypes.Room.isRequired, - settings : PropTypes.object.isRequired, - onTogglePermanentTopBar : PropTypes.func.isRequired, - onToggleHiddenControls : PropTypes.func.isRequired, - handleChangeMode : PropTypes.func.isRequired, - classes : PropTypes.object.isRequired + isMobile : PropTypes.bool.isRequired, + room : appPropTypes.Room.isRequired, + settings : PropTypes.object.isRequired, + onTogglePermanentTopBar : PropTypes.func.isRequired, + onToggleHiddenControls : PropTypes.func.isRequired, + onToggleButtonControlBar : PropTypes.func.isRequired, + onToggleShowNotifications : PropTypes.func.isRequired, + onToggleDrawerOverlayed : PropTypes.func.isRequired, + handleChangeMode : PropTypes.func.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => ({ + isMobile : state.me.browser.platform === 'mobile', room : state.room, settings : state.settings }); const mapDispatchToProps = { - onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, - onToggleHiddenControls : settingsActions.toggleHiddenControls, - handleChangeMode : roomActions.setDisplayMode + onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, + onToggleHiddenControls : settingsActions.toggleHiddenControls, + onToggleShowNotifications : settingsActions.toggleShowNotifications, + onToggleButtonControlBar : settingsActions.toggleButtonControlBar, + onToggleDrawerOverlayed : settingsActions.toggleDrawerOverlayed, + handleChangeMode : roomActions.setDisplayMode }; export default connect( @@ -135,6 +173,7 @@ export default connect( areStatesEqual : (next, prev) => { return ( + prev.me.browser === next.me.browser && prev.room === next.room && prev.settings === next.settings ); diff --git a/app/src/components/Settings/MediaSettings.js b/app/src/components/Settings/MediaSettings.js index fa9728b..28a69a2 100644 --- a/app/src/components/Settings/MediaSettings.js +++ b/app/src/components/Settings/MediaSettings.js @@ -3,12 +3,15 @@ import { connect } from 'react-redux'; import * as appPropTypes from '../appPropTypes'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../RoomContext'; +import * as settingsActions from '../../actions/settingsActions'; import PropTypes from 'prop-types'; import { useIntl, FormattedMessage } from 'react-intl'; import MenuItem from '@material-ui/core/MenuItem'; import FormHelperText from '@material-ui/core/FormHelperText'; import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; import Select from '@material-ui/core/Select'; +import Checkbox from '@material-ui/core/Checkbox'; const styles = (theme) => ({ @@ -23,6 +26,9 @@ const styles = (theme) => }); const MediaSettings = ({ + setEchoCancellation, + setAutoGainControl, + setNoiseSuppression, roomClient, me, settings, @@ -247,6 +253,51 @@ const MediaSettings = ({ /> + + { + setEchoCancellation(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.echoCancellation', + defaultMessage : 'Echo cancellation' + })} + /> + + { + setAutoGainControl(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.autoGainControl', + defaultMessage : 'Auto gain control' + })} + /> + + { + setNoiseSuppression(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.noiseSuppression', + defaultMessage : 'Noise suppression' + })} + /> ); @@ -254,10 +305,13 @@ const MediaSettings = ({ MediaSettings.propTypes = { - roomClient : PropTypes.any.isRequired, - me : appPropTypes.Me.isRequired, - settings : PropTypes.object.isRequired, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + setEchoCancellation : PropTypes.func.isRequired, + setAutoGainControl : PropTypes.func.isRequired, + setNoiseSuppression : PropTypes.func.isRequired, + me : appPropTypes.Me.isRequired, + settings : PropTypes.object.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => @@ -268,9 +322,15 @@ const mapStateToProps = (state) => }; }; +const mapDispatchToProps = { + setEchoCancellation : settingsActions.setEchoCancellation, + setAutoGainControl : settingsActions.toggleAutoGainControl, + setNoiseSuppression : settingsActions.toggleNoiseSuppression +}; + export default withRoomContext(connect( mapStateToProps, - null, + mapDispatchToProps, null, { areStatesEqual : (next, prev) => diff --git a/app/src/components/Settings/Settings.js b/app/src/components/Settings/Settings.js index 6633829..cbfb8b1 100644 --- a/app/src/components/Settings/Settings.js +++ b/app/src/components/Settings/Settings.js @@ -95,7 +95,7 @@ const Settings = ({ /> diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index fd84bae..0b01a22 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -4,13 +4,12 @@ import classnames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import EditableInput from '../Controls/EditableInput'; import Logger from '../../Logger'; -import { green, yellow, orange, red } from '@material-ui/core/colors'; +import { yellow, orange, red } from '@material-ui/core/colors'; import SignalCellularOffIcon from '@material-ui/icons/SignalCellularOff'; import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar'; import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar'; import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar'; import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar'; -import SignalCellularAltIcon from '@material-ui/icons/SignalCellularAlt'; const logger = new Logger('VideoView'); @@ -153,6 +152,7 @@ class VideoView extends React.PureComponent { const { isMe, + showQuality, isScreen, displayName, showPeerInfo, @@ -162,8 +162,6 @@ class VideoView extends React.PureComponent videoMultiLayer, audioScore, videoScore, - // consumerSpatialLayers, - // consumerTemporalLayers, consumerCurrentSpatialLayer, consumerCurrentTemporalLayer, consumerPreferredSpatialLayer, @@ -180,58 +178,63 @@ class VideoView extends React.PureComponent videoHeight } = this.state; - let quality = ; + let quality = null; - if (videoScore || audioScore) + if (showQuality) { - const score = videoScore ? videoScore : audioScore; + quality = ; - switch (score.producerScore) + if (videoScore || audioScore) { - case 0: - case 1: + const score = videoScore ? videoScore : audioScore; + + switch (isMe ? score.score : score.producerScore) { - quality = ; - - break; - } - - case 2: - case 3: - { - quality = ; - - break; - } - - case 4: - case 5: - case 6: - { - quality = ; - - break; - } - - case 7: - case 8: - { - quality = ; - - break; - } - - case 9: - case 10: - { - quality = ; - - break; - } - - default: - { - break; + case 0: + case 1: + { + quality = ; + + break; + } + + case 2: + case 3: + { + quality = ; + + break; + } + + case 4: + case 5: + case 6: + { + quality = ; + + break; + } + + case 7: + case 8: + case 9: + { + quality = ; + + break; + } + + case 10: + { + quality = null; + + break; + } + + default: + { + break; + } } } } @@ -261,7 +264,7 @@ class VideoView extends React.PureComponent

{videoWidth}x{videoHeight}

} - { !isMe && + { showQuality &&
{ quality @@ -441,6 +444,7 @@ class VideoView extends React.PureComponent VideoView.propTypes = { isMe : PropTypes.bool, + showQuality : PropTypes.bool, isScreen : PropTypes.bool, displayName : PropTypes.string, showPeerInfo : PropTypes.bool, diff --git a/app/src/index.js b/app/src/index.js index 90bfa4d..e89d06d 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -38,6 +38,7 @@ import messagesCzech from './translations/cs'; import messagesItalian from './translations/it'; import messagesUkrainian from './translations/uk'; import messagesTurkish from './translations/tr'; +import messagesLatvian from './translations/lv'; import './index.css'; @@ -63,7 +64,8 @@ const messages = 'cs' : messagesCzech, 'it' : messagesItalian, 'uk' : messagesUkrainian, - 'tr' : messagesTurkish + 'tr' : messagesTurkish, + 'lv' : messagesLatvian }; const locale = navigator.language.split(/[-_]/)[0]; // language without region code @@ -113,6 +115,13 @@ function run() const forceTcp = parameters.get('forceTcp') === 'true'; const displayName = parameters.get('displayName'); const muted = parameters.get('muted') === 'true'; + + const { pathname } = window.location; + + let basePath = pathname.substring(0, pathname.lastIndexOf('/')); + + if (!basePath) + basePath = '/'; // Get current device. const device = deviceInfo(); @@ -132,7 +141,8 @@ function run() produce, forceTcp, displayName, - muted + muted, + basePath }); global.CLIENT = roomClient; @@ -144,7 +154,7 @@ function run() } persistor={persistor}> - + }> diff --git a/app/src/permissions.js b/app/src/permissions.js new file mode 100644 index 0000000..864bdbd --- /dev/null +++ b/app/src/permissions.js @@ -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' +}; \ No newline at end of file diff --git a/app/src/reducers/consumers.js b/app/src/reducers/consumers.js index 68a4a4a..6be31ae 100644 --- a/app/src/reducers/consumers.js +++ b/app/src/reducers/consumers.js @@ -110,6 +110,11 @@ const consumers = (state = initialState, action) => return { ...state, [consumerId]: newConsumer }; } + case 'CLEAR_CONSUMERS': + { + return initialState; + } + default: return state; } diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index 4c8bee1..32d6fef 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -1,4 +1,6 @@ -const peer = (state = {}, action) => +const initialState = {}; + +const peer = (state = initialState, action) => { switch (action.type) { @@ -26,6 +28,12 @@ const peer = (state = {}, action) => raisedHand : action.payload.raisedHand, raisedHandTimestamp : action.payload.raisedHandTimestamp }; + + case 'SET_PEER_RAISED_HAND_IN_PROGRESS': + return { + ...state, + raisedHandInProgress : action.payload.flag + }; case 'ADD_CONSUMER': { @@ -62,12 +70,24 @@ const peer = (state = {}, action) => return { ...state, roles }; } + case 'STOP_PEER_AUDIO_IN_PROGRESS': + return { + ...state, + stopPeerAudioInProgress : action.payload.flag + }; + + case 'STOP_PEER_VIDEO_IN_PROGRESS': + return { + ...state, + stopPeerVideoInProgress : action.payload.flag + }; + default: return state; } }; -const peers = (state = {}, action) => +const peers = (state = initialState, action) => { switch (action.type) { @@ -91,10 +111,13 @@ const peers = (state = {}, action) => case 'SET_PEER_AUDIO_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_RAISED_HAND': + case 'SET_PEER_RAISED_HAND_IN_PROGRESS': case 'SET_PEER_PICTURE': case 'ADD_CONSUMER': case 'ADD_PEER_ROLE': case 'REMOVE_PEER_ROLE': + case 'STOP_PEER_AUDIO_IN_PROGRESS': + case 'STOP_PEER_VIDEO_IN_PROGRESS': { const oldPeer = state[action.payload.peerId]; @@ -118,6 +141,11 @@ const peers = (state = {}, action) => return { ...state, [oldPeer.id]: peer(oldPeer, action) }; } + case 'CLEAR_PEERS': + { + return initialState; + } + default: return state; } diff --git a/app/src/reducers/producers.js b/app/src/reducers/producers.js index 64aee90..a27c06e 100644 --- a/app/src/reducers/producers.js +++ b/app/src/reducers/producers.js @@ -60,6 +60,17 @@ const producers = (state = initialState, action) => return { ...state, [producerId]: newProducer }; } + case 'SET_PRODUCER_SCORE': + { + const { producerId, score } = action.payload; + + const producer = state[producerId]; + + const newProducer = { ...producer, score }; + + return { ...state, [producerId]: newProducer }; + } + default: return state; } diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index f4bc6ab..d784219 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -6,6 +6,7 @@ const initialState = locked : false, inLobby : false, signInRequired : false, + overRoomLimit : false, // access code to the room if locked and joinByAccessCode == true accessCode : '', // if true: accessCode is a possibility to open the room @@ -21,6 +22,8 @@ const initialState = spotlights : [], settingsOpen : false, extraVideoOpen : false, + helpOpen : false, + aboutOpen : false, currentSettingsTab : 'media', // media, appearence, advanced lockDialogOpen : false, joined : false, @@ -30,18 +33,8 @@ const initialState = closeMeetingInProgress : false, clearChatInProgress : false, clearFileSharingInProgress : false, - userRoles : { NORMAL: 'normal' }, // Default role - permissionsFromRoles : { - CHANGE_ROOM_LOCK : [], - PROMOTE_PEER : [], - SEND_CHAT : [], - MODERATE_CHAT : [], - SHARE_SCREEN : [], - EXTRA_VIDEO : [], - SHARE_FILE : [], - MODERATE_FILES : [], - MODERATE_ROOM : [] - } + roomPermissions : null, + allowWhenRoleMissing : null }; const room = (state = initialState, action) => @@ -88,7 +81,12 @@ const room = (state = initialState, action) => return { ...state, signInRequired }; } + case 'SET_OVER_ROOM_LIMIT': + { + const { overRoomLimit } = action.payload; + return { ...state, overRoomLimit }; + } case 'SET_ACCESS_CODE': { const { accessCode } = action.payload; @@ -124,6 +122,20 @@ const room = (state = initialState, action) => return { ...state, extraVideoOpen }; } + case 'SET_HELP_OPEN': + { + const { helpOpen } = action.payload; + + return { ...state, helpOpen }; + } + + case 'SET_ABOUT_OPEN': + { + const { aboutOpen } = action.payload; + + return { ...state, aboutOpen }; + } + case 'SET_SETTINGS_TAB': { const { tab } = action.payload; @@ -200,6 +212,11 @@ const room = (state = initialState, action) => return { ...state, spotlights }; } + case 'CLEAR_SPOTLIGHTS': + { + return { ...state, spotlights: [] }; + } + case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS': return { ...state, lobbyPeersPromotionInProgress: action.payload.flag }; @@ -218,18 +235,18 @@ const room = (state = initialState, action) => case 'CLEAR_FILE_SHARING_IN_PROGRESS': return { ...state, clearFileSharingInProgress: action.payload.flag }; - case 'SET_USER_ROLES': + case 'SET_ROOM_PERMISSIONS': { - const { userRoles } = action.payload; + const { roomPermissions } = action.payload; - return { ...state, userRoles }; + return { ...state, roomPermissions }; } - case 'SET_PERMISSIONS_FROM_ROLES': + case 'SET_ALLOW_WHEN_ROLE_MISSING': { - const { permissionsFromRoles } = action.payload; + const { allowWhenRoleMissing } = action.payload; - return { ...state, permissionsFromRoles }; + return { ...state, allowWhenRoleMissing }; } default: diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index 549219c..1c375bf 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -4,12 +4,23 @@ const initialState = selectedWebcam : null, selectedAudioDevice : null, advancedMode : false, + sampleRate : 48000, + channelCount : 1, + volume : 1.0, + autoGainControl : true, + echoCancellation : true, + noiseSuppression : true, + sampleSize : 16, // low, medium, high, veryhigh, ultra resolution : window.config.defaultResolution || 'medium', lastN : 4, permanentTopBar : true, hiddenControls : false, - notificationSounds : true + showNotifications : true, + notificationSounds : true, + buttonControlBar : window.config.buttonControlBar || false, + drawerOverlayed : window.config.drawerOverlayed || true, + ...window.config.defaultAudio }; const settings = (state = initialState, action) => @@ -45,6 +56,83 @@ const settings = (state = initialState, action) => return { ...state, advancedMode }; } + case 'SET_SAMPLE_RATE': + { + const { sampleRate } = action.payload; + + return { ...state, sampleRate }; + } + + case 'SET_CHANNEL_COUNT': + { + const { channelCount } = action.payload; + + return { ...state, channelCount }; + } + + case 'SET_VOLUME': + { + const { volume } = action.payload; + + return { ...state, volume }; + } + + case 'SET_AUTO_GAIN_CONTROL': + { + const { autoGainControl } = action.payload; + + return { ...state, autoGainControl }; + } + + case 'SET_ECHO_CANCELLATION': + { + const { echoCancellation } = action.payload; + + return { ...state, echoCancellation }; + } + + case 'SET_NOISE_SUPPRESSION': + { + const { noiseSuppression } = action.payload; + + return { ...state, noiseSuppression }; + } + + case 'SET_DEFAULT_AUDIO': + { + const { audio } = action.payload; + + return { ...state, audio }; + } + + case 'TOGGLE_AUTO_GAIN_CONTROL': + { + const autoGainControl = !state.autoGainControl; + + return { ...state, autoGainControl }; + } + + case 'TOGGLE_ECHO_CANCELLATION': + { + const echoCancellation = !state.echoCancellation; + + return { ...state, echoCancellation }; + } + + case 'TOGGLE_NOISE_SUPPRESSION': + { + const noiseSuppression = !state.noiseSuppression; + + return { ...state, noiseSuppression }; + } + + case 'SET_SAMPLE_SIZE': + { + const { sampleSize } = action.payload; + + return { ...state, sampleSize }; + } + case 'SET_LAST_N': { const { lastN } = action.payload; @@ -59,6 +147,20 @@ const settings = (state = initialState, action) => return { ...state, permanentTopBar }; } + case 'TOGGLE_BUTTON_CONTROL_BAR': + { + const buttonControlBar = !state.buttonControlBar; + + return { ...state, buttonControlBar }; + } + + case 'TOGGLE_DRAWER_OVERLAYED': + { + const drawerOverlayed = !state.drawerOverlayed; + + return { ...state, drawerOverlayed }; + } + case 'TOGGLE_HIDDEN_CONTROLS': { const hiddenControls = !state.hiddenControls; @@ -73,6 +175,13 @@ const settings = (state = initialState, action) => return { ...state, notificationSounds }; } + case 'TOGGLE_SHOW_NOTIFICATIONS': + { + const showNotifications = !state.showNotifications; + + return { ...state, showNotifications }; + } + case 'SET_VIDEO_RESOLUTION': { const { resolution } = action.payload; diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index e82e9d5..0ae3fe0 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -59,7 +59,11 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, - + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, + "me.mutedPTT": null, "roles.gotRole": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "房间名称", "label.chooseRoomButton": "继续", @@ -92,6 +100,7 @@ "label.filesharing": "文件共享", "label.participants": "参与者", "label.shareFile": "共享文件", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "不支持文件共享", "label.unknown": "未知", "label.democratic": "民主视图", @@ -103,10 +112,11 @@ "label.ultra": "超高 (UHD)", "label.close": "关闭", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "设置", "settings.camera": "视频设备", @@ -126,6 +136,12 @@ "settings.lastn": "可见视频数量", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "无法保存文件", "filesharing.startingFileShare": "正在尝试共享文件", diff --git a/app/src/translations/cs.json b/app/src/translations/cs.json index 1a2b3c5..8edfc1c 100644 --- a/app/src/translations/cs.json +++ b/app/src/translations/cs.json @@ -58,6 +58,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -78,6 +82,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Jméno místnosti", "label.chooseRoomButton": "Pokračovat", @@ -91,6 +99,7 @@ "label.filesharing": "Sdílení souborů", "label.participants": "Účastníci", "label.shareFile": "Sdílet soubor", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Sdílení souborů není podporováno", "label.unknown": "Neznámý", "label.democratic": "Rozvržení: Demokratické", @@ -102,10 +111,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Zavřít", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Nastavení", "settings.camera": "Kamera", @@ -125,6 +135,12 @@ "settings.lastn": null, "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Není možné uložit soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index a6f6ac2..0a2368a 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -51,19 +51,23 @@ "room.videoPaused": "Video gestoppt", "room.muteAll": "Alle stummschalten", "room.stopAllVideo": "Alle Videos stoppen", - "room.closeMeeting": "Meeting schließen", - "room.clearChat": null, - "room.clearFileSharing": null, + "room.closeMeeting": "Meeting beenden", + "room.clearChat": "Liste löschen", + "room.clearFileSharing": "Liste löschen", "room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung", - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, + "room.moderatoractions": "Moderator Aktionen", + "room.raisedHand": "{displayName} hebt die Hand", + "room.loweredHand": "{displayName} senkt die Hand", + "room.extraVideo": "Video hinzufügen", + "room.overRoomLimit": "Der Raum ist voll, probiere es später nochmal", + "room.help": "Hilfe", + "room.about": "Über", + "room.shortcutKeys": "Tastaturkürzel", - "me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen", + "me.mutedPTT": "Du bist stummgeschaltet. Halte die SPACE-Taste um zu sprechen", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "Rolle erhalten: {role}", + "roles.lostRole": "Rolle entzogen: {role}", "tooltip.login": "Anmelden", "tooltip.logout": "Abmelden", @@ -75,10 +79,14 @@ "tooltip.lobby": "Warteraum", "tooltip.settings": "Einstellungen", "tooltip.participants": "Teilnehmer", - "tooltip.kickParticipant": "Teilnehmer rauswerfen", - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, + "tooltip.kickParticipant": "Rauswerfen", + "tooltip.muteParticipant": "Stummschalten", + "tooltip.muteParticipantVideo": "Video stoppen", + "tooltip.raisedHand": "Hand heben", + "tooltip.muteScreenSharing": "Stoppe Bildschirmfreigabe", + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Name des Raums", "label.chooseRoomButton": "Weiter", @@ -92,21 +100,23 @@ "label.filesharing": "Dateien", "label.participants": "Teilnehmer", "label.shareFile": "Datei hochladen", + "label.shareGalleryFile": "Bild teilen", "label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt", "label.unknown": "Unbekannt", "label.democratic": "Demokratisch", "label.filmstrip": "Filmstreifen", "label.low": "Niedrig", - "label.medium": "Medium", + "label.medium": "Mittel", "label.high": "Hoch (HD)", "label.veryHigh": "Sehr hoch (FHD)", "label.ultra": "Ultra (UHD)", "label.close": "Schließen", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, + "label.media": "Audio / Video", + "label.appearance": "Ansicht", + "label.advanced": "Erweitert", + "label.addVideo": "Video hinzufügen", + "label.promoteAllPeers": "Alle Teilnehmer reinlassen", + "label.moreActions": "Weitere Aktionen", "settings.settings": "Einstellungen", "settings.camera": "Kamera", @@ -124,8 +134,14 @@ "settings.advancedMode": "Erweiterter Modus", "settings.permanentTopBar": "Permanente obere Leiste", "settings.lastn": "Anzahl der sichtbaren Videos", - "settings.hiddenControls": null, - "settings.notificationSounds": null, + "settings.hiddenControls": "Medienwerkzeugleiste automatisch ausblenden", + "settings.notificationSounds": "Audiosignal bei Benachrichtigungen", + "settings.showNotifications": "Zeige Benachrichtigungen", + "settings.buttonControlBar": "Separate seitliche Medienwerkzeugleiste", + "settings.echoCancellation": "Echounterdrückung", + "settings.autoGainControl": "Automatische Pegelregelung (Audioeingang)", + "settings.noiseSuppression": "Rauschunterdrückung", + "settings.drawerOverlayed": "Seitenpanel verdeckt Hauptinhalt", "filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei", @@ -167,8 +183,8 @@ "devices.cameraDisconnected": "Kamera getrennt", "devices.cameraError": "Fehler mit deiner Kamera", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "Moderator hat Chat gelöscht", + "moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht", + "moderator.muteAudio": "Moderator hat dich stummgeschaltet", + "moderator.muteVideo": "Moderator hat dein Video gestoppt" } diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index 4027363..fb9e6fc 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Værelsesnavn", "label.chooseRoomButton": "Fortsæt", @@ -92,6 +100,7 @@ "label.filesharing": "Fildeling", "label.participants": "Deltagere", "label.shareFile": "Del fil", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Fildeling er ikke understøttet", "label.unknown": "Ukendt", "label.democracy": "Galleri visning", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Luk", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Indstillinger", "settings.camera": "Kamera", @@ -126,6 +136,12 @@ "settings.lastn": "Antal synlige videoer", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.startingFileShare": "Forsøger at dele filen", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index 5f93f42..2d9cb94 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Όνομα δωματίου", "label.chooseRoomButton": "Συνέχεια", @@ -92,6 +100,7 @@ "label.filesharing": "Διαμοιρασμοός αρχείου", "label.participants": "Συμμετέχοντες", "label.shareFile": "Διαμοιραστείτε ένα αρχείο", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται", "label.unknown": "Άγνωστο", "label.democratic": null, @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Κλείσιμο", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Ρυθμίσεις", "settings.camera": "Κάμερα", @@ -126,6 +136,12 @@ "settings.lastn": "Αριθμός ορατών βίντεο", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 8fe4fa1..5014089 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -59,6 +59,10 @@ "room.raisedHand": "{displayName} raised their hand", "room.loweredHand": "{displayName} put their hand down", "room.extraVideo": "Extra video", + "room.overRoomLimit": "The room is full, retry after some time.", + "room.help": "Help", + "room.about": "About", + "room.shortcutKeys": "Shortcut Keys", "me.mutedPTT": "You are muted, hold down SPACE-BAR to talk", @@ -79,6 +83,10 @@ "tooltip.muteParticipant": "Mute participant", "tooltip.muteParticipantVideo": "Mute participant video", "tooltip.raisedHand": "Raise hand", + "tooltip.muteScreenSharing": "Mute participant share", + "tooltip.muteParticipantAudioModerator": "Mute participant audio globally", + "tooltip.muteParticipantVideoModerator": "Mute participant video globally", + "tooltip.muteScreenSharingModerator": "Mute participant screen share globally", "label.roomName": "Room name", "label.chooseRoomButton": "Continue", @@ -92,6 +100,7 @@ "label.filesharing": "File sharing", "label.participants": "Participants", "label.shareFile": "Share file", + "label.shareGalleryFile": "Share image", "label.fileSharingUnsupported": "File sharing not supported", "label.unknown": "Unknown", "label.democratic": "Democratic view", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Close", "label.media": "Media", - "label.appearence": "Appearence", + "label.appearance": "Appearence", "label.advanced": "Advanced", "label.addVideo": "Add video", "label.promoteAllPeers": "Promote all", + "label.moreActions": "More actions", "settings.settings": "Settings", "settings.camera": "Camera", @@ -126,6 +136,12 @@ "settings.lastn": "Number of visible videos", "settings.hiddenControls": "Hidden media controls", "settings.notificationSounds": "Notification sounds", + "settings.showNotifications": "Show notifications", + "settings.buttonControlBar": "Separate media controls", + "settings.echoCancellation": "Echo cancellation", + "settings.autoGainControl": "Auto gain control", + "settings.noiseSuppression": "Noise suppression", + "settings.drawerOverlayed": "Side drawer over content", "filesharing.saveFileError": "Unable to save file", "filesharing.startingFileShare": "Attempting to share file", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index d73506c..629bb2a 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Nombre de la sala", "label.chooseRoomButton": "Continuar", @@ -92,6 +100,7 @@ "label.filesharing": "Compartir ficheros", "label.participants": "Participantes", "label.shareFile": "Compartir fichero", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Compartir ficheros no está disponible", "label.unknown": "Desconocido", "label.democratic": "Vista democrática", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Cerrar", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Ajustes", "settings.camera": "Cámara", @@ -126,6 +136,12 @@ "settings.lastn": "Cantidad de videos visibles", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero", diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index f884c36..9776ac0 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Nom de la salle", "label.chooseRoomButton": "Continuer", @@ -92,6 +100,7 @@ "label.filesharing": "Partage de fichier", "label.participants": "Participants", "label.shareFile": "Partager un fichier", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Partage de fichier non supporté", "label.unknown": "Inconnu", "label.democratic": "Vue démocratique", @@ -103,10 +112,11 @@ "label.ultra": "Ultra Haute Définition", "label.close": "Fermer", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Paramètres", "settings.camera": "Caméra", @@ -126,6 +136,12 @@ "settings.lastn": "Nombre de vidéos visibles", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.startingFileShare": "Début du transfert de fichier", diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index eb9d08e..4788483 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -52,18 +52,22 @@ "room.muteAll": "Utišaj sve", "room.stopAllVideo": "Ugasi sve kamere", "room.closeMeeting": "Završi sastanak", - "room.clearChat": null, - "room.clearFileSharing": null, + "room.clearChat": "Izbriši razgovor", + "room.clearFileSharing": "Izbriši dijeljene datoteke", "room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora", - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, + "room.moderatoractions": "Akcije moderatora", + "room.raisedHand": "{displayName} je podigao ruku", + "room.loweredHand": "{displayName} je spustio ruku", + "room.extraVideo": "Dodatni video", + "room.overRoomLimit": "Soba je popunjena, pokušajte ponovno kasnije.", + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "Dodijeljena vam je uloga: {role}", + "roles.lostRole": "Uloga: {role} je povučena", "tooltip.login": "Prijava", "tooltip.logout": "Odjava", @@ -74,11 +78,15 @@ "tooltip.leaveFullscreen": "Izađi iz punog ekrana", "tooltip.lobby": "Prikaži predvorje", "tooltip.settings": "Prikaži postavke", - "tooltip.participants": "Pokažite sudionike", + "tooltip.participants": "Prikaži sudionike", "tooltip.kickParticipant": "Izbaci sudionika", - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, + "tooltip.muteParticipant": "Utišaj sudionika", + "tooltip.muteParticipantVideo": "Ne primaj video sudionika", + "tooltip.raisedHand": "Podigni ruku", + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Naziv sobe", "label.chooseRoomButton": "Nastavi", @@ -92,6 +100,7 @@ "label.filesharing": "Dijeljenje datoteka", "label.participants": "Sudionici", "label.shareFile": "Dijeli datoteku", + "label.shareGalleryFile": "Dijeli sliku", "label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano", "label.unknown": "Nepoznato", "label.democratic":"Demokratski prikaz", @@ -102,11 +111,12 @@ "label.veryHigh": "Vrlo visoka (FHD)", "label.ultra": "Ultra visoka (UHD)", "label.close": "Zatvori", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, + "label.media": "Medij", + "label.appearance": "Prikaz", + "label.advanced": "Napredno", + "label.addVideo": "Dodaj video", + "label.promoteAllPeers": "Promoviraj sve", + "label.moreActions": null, "settings.settings": "Postavke", "settings.camera": "Kamera", @@ -116,16 +126,22 @@ "settings.selectAudio": "Odaberi uređaj za zvuk", "settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk", "settings.audioOutput": "Uređaj za izlaz zvuka", - "settings.selectAudioOutput": "Odaberite audio izlazni uređaj", - "settings.cantSelectAudioOutput": "Nije moguće odabrati audio izlazni uređaj", + "settings.selectAudioOutput": "Odaberite izlazni uređaj za zvuk", + "settings.cantSelectAudioOutput": "Nije moguće odabrati izlazni uređaj za zvuk", "settings.resolution": "Odaberi video rezoluciju", "settings.layout": "Način prikaza", "settings.selectRoomLayout": "Odaberi način prikaza", "settings.advancedMode": "Napredne mogućnosti", "settings.permanentTopBar": "Stalna gornja šipka", "settings.lastn": "Broj vidljivih videozapisa", - "settings.hiddenControls": null, - "settings.notificationSounds": null, + "settings.hiddenControls": "Skrivene kontrole medija", + "settings.notificationSounds": "Zvuk obavijesti", + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", @@ -167,8 +183,8 @@ "devices.cameraDisconnected": "Kamera odspojena", "devices.cameraError": "Greška prilikom pristupa kameri", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "Moderator je izbrisao razgovor", + "moderator.clearFiles": "Moderator je izbrisao datoteke", + "moderator.muteAudio": "Moderator je utišao tvoj zvuk", + "moderator.muteVideo": "Moderator je zaustavio tvoj video" } diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index f25b06a..dee4034 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -1,24 +1,24 @@ { "socket.disconnected": "A kapcsolat lebomlott", "socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás", - "socket.reconnected": "Sikeres újarkapcsolódás", + "socket.reconnected": "Sikeres újrakapcsolódás", "socket.requestError": "Sikertelen szerver lekérés", - "room.chooseRoom": null, + "room.chooseRoom": "Válaszd ki a konferenciaszobát", "room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ", - "room.consentUnderstand": "I understand", - "room.joined": "Csatlakozátál a konferenciához", + "room.consentUnderstand": "Megértettem", + "room.joined": "Csatlakoztál a konferenciához", "room.cantJoin": "Sikertelen csatlakozás a konferenciához", "room.youLocked": "A konferenciába való belépés letiltva", - "room.cantLock": "Sikertelen a konferenciaba való belépés letiltása", + "room.cantLock": "Sikertelen a konferenciába való belépés letiltása", "room.youUnLocked": "A konferenciába való belépés engedélyezve", "room.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése", "room.locked": "A konferenciába való belépés letiltva", "room.unlocked": "A konferenciába való belépés engedélyezve", "room.newLobbyPeer": "Új részvevő lépett be a konferencia előszobájába", "room.lobbyPeerLeft": "A konferencia előszobájából a részvevő távozott", - "room.lobbyPeerChangedDisplayName": "Az előszobai résztvevő meváltoztatta a nevét: {displayName}", - "room.lobbyPeerChangedPicture": "Az előszobai résztvevő meváltoztatta a képét", + "room.lobbyPeerChangedDisplayName": "Az előszobai résztvevő megváltoztatta a nevét: {displayName}", + "room.lobbyPeerChangedPicture": "Az előszobai résztvevő megváltoztatta a képét", "room.setAccessCode": "A konferencia hozzáférési kódja megváltozott", "room.accessCodeOn": "A konferencia hozzáférési kódja aktiválva", "room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva", @@ -39,8 +39,8 @@ "room.audioOnly": "csak Hang", "room.audioVideo": "Hang és Videó", "room.youAreReady": "Ok, kész vagy", - "room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferecnia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.", - "room.locketWait": "A konferencia szobába a a belépés tilos - Várj amíg valaki be nem enged ...", + "room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferencia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.", + "room.locketWait": "Az automatikus belépés tiltva van - Várj amíg valaki beenged ...", "room.lobbyAdministration": "Előszoba adminisztráció", "room.peersInLobby": "Résztvevők az előszobában", "room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában", @@ -49,25 +49,29 @@ "room.spotlights": "Látható résztvevők", "room.passive": "Passzív résztvevők", "room.videoPaused": "Ez a videóstream szünetel", - "room.muteAll": null, - "room.stopAllVideo": null, - "room.closeMeeting": null, - "room.clearChat": null, - "room.clearFileSharing": null, - "room.speechUnsupported": null, - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, + "room.muteAll": "Mindenki némítása", + "room.stopAllVideo": "Mindenki video némítása", + "room.closeMeeting": "Konferencia lebontása", + "room.clearChat": "Chat történelem kiürítése", + "room.clearFileSharing": "File megosztás kiürítése", + "room.speechUnsupported": "A böngésződ nem támogatja a hangfelismerést", + "room.moderatoractions": "Moderátor funkciók", + "room.raisedHand": "{displayName} jelentkezik", + "room.loweredHand": "{displayName} leeresztette a kezét", + "room.extraVideo": "Kiegészítő videó", + "room.overRoomLimit": "A konferenciaszoba betelt..", + "room.help": "Segítség", + "room.about": "Névjegy", + "room.shortcutKeys": "Billentyűparancsok", - "me.mutedPTT": null, + "me.mutedPTT": "Némítva vagy, ha beszélnél nyomd le a szóköz billentyűt", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "{role} szerepet kaptál", + "roles.lostRole": "Elvesztetted a {role} szerepet", "tooltip.login": "Belépés", "tooltip.logout": "Kilépés", - "tooltip.admitFromLobby": "Beenegdem az előszobából", + "tooltip.admitFromLobby": "Beengedem az előszobából", "tooltip.lockRoom": "A konferenciába való belépés letiltása", "tooltip.unLockRoom": "konferenciába való belépés engedélyezése", "tooltip.enterFullscreen": "Teljes képernyős mód", @@ -75,10 +79,14 @@ "tooltip.lobby": "Az előszobában várakozók listája", "tooltip.settings": "Beállítások", "tooltip.participants": "Résztvevők", - "tooltip.kickParticipant": null, - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, + "tooltip.kickParticipant": "Résztvevő kirúgása", + "tooltip.muteParticipant": "Résztvevő némítása", + "tooltip.muteParticipantVideo": "Résztvevő videóstreamének némítása", + "tooltip.raisedHand": "Jelentkezés", + "tooltip.muteScreenSharing": "Képernyőmegosztás szüneteltetése", + "tooltip.muteParticipantAudioModerator": "Résztvevő hangjának általános némítása", + "tooltip.muteParticipantVideoModerator": "Résztvevő videójának általános némítása", + "tooltip.muteScreenSharingModerator": "Résztvevő képernyőmegosztásának általános némítása", "label.roomName": "Konferencia", "label.chooseRoomButton": "Tovább", @@ -92,6 +100,7 @@ "label.filesharing": "Fájl megosztás", "label.participants": "Résztvevők", "label.shareFile": "Fájl megosztása", + "label.shareGalleryFile": "Fájl megosztás galériából", "label.fileSharingUnsupported": "Fájl megosztás nem támogatott", "label.unknown": "Ismeretlen", "label.democratic": "Egyforma képméretű képkiosztás", @@ -102,11 +111,12 @@ "label.veryHigh": "Nagyon magas (FHD)", "label.ultra": "Ultra magas (UHD)", "label.close": "Bezár", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, + "label.media": "Média", + "label.appearance": "Megjelenés", + "label.advanced": "Részletek", + "label.addVideo": "Videó hozzáadása", + "label.promoteAllPeers": "Mindenkit beengedek", + "label.moreActions": "További műveletek", "settings.settings": "Beállítások", "settings.camera": "Kamera", @@ -124,13 +134,19 @@ "settings.advancedMode": "Részletes információk", "settings.permanentTopBar": "Állandó felső sáv", "settings.lastn": "A látható videók száma", - "settings.hiddenControls": null, - "settings.notificationSounds": null, + "settings.hiddenControls": "Média Gombok automatikus elrejtése", + "settings.notificationSounds": "Értesítések hangjelzéssel", + "settings.showNotifications": "Értesítések megjelenítése", + "settings.buttonControlBar": "Médiavezérlő gombok leválasztása", + "settings.echoCancellation": "Visszhangelnyomás", + "settings.autoGainControl": "Automatikus hangerő", + "settings.noiseSuppression": "Zajelnyomás", + "settings.drawerOverlayed": "Oldalsáv a tartalom felett", "filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.startingFileShare": "Fájl megosztása", "filesharing.successfulFileShare": "A fájl sikeresen megosztva", - "filesharing.unableToShare": "Sikereteln fájl megosztás", + "filesharing.unableToShare": "Sikertelen fájl megosztás", "filesharing.error": "Hiba a fájlmegosztás során", "filesharing.finished": "A fájl letöltés befejeződött", "filesharing.save": "Mentés", @@ -140,7 +156,7 @@ "devices.devicesChanged": "Az eszközei megváltoztak, konfiguráld őket be a beállítások menüben", - "device.audioUnsupported": "A hnag nem támogatott", + "device.audioUnsupported": "A hang nem támogatott", "device.activateAudio": "Hang aktiválása", "device.muteAudio": "Hang némítása", "device.unMuteAudio": "Hang némítás kikapcsolása", @@ -151,9 +167,9 @@ "device.screenSharingUnsupported": "A képernyő megosztás nem támogatott", "device.startScreenSharing": "Képernyőmegosztás indítása", - "device.stopScreenSharing": "Képernyőmegosztás leáłłítása", + "device.stopScreenSharing": "Képernyőmegosztás leállítása", - "devices.microphoneDisconnected": "Microphone kapcsolat bontva", + "devices.microphoneDisconnected": "Mikrofon kapcsolat bontva", "devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben", "devices.microphoneMute": "A mikrofon némítva lett", "devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva", @@ -167,8 +183,8 @@ "devices.cameraDisconnected": "A kamera kapcsolata lebomlott", "devices.cameraError": "Hiba történt a kamera elérése során", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "A moderátor kiürítette a chat történelmet", + "moderator.clearFiles": "A moderátor kiürítette a file megosztás történelmet", + "moderator.muteAudio": "A moderátor elnémította a hangod", + "moderator.muteVideo": "A moderátor elnémította a videód" } diff --git a/app/src/translations/it.json b/app/src/translations/it.json index d027e7a..7a8bf62 100644 --- a/app/src/translations/it.json +++ b/app/src/translations/it.json @@ -49,25 +49,29 @@ "room.spotlights": "Partecipanti in Evidenza", "room.passive": "Participanti Passivi", "room.videoPaused": "Il video è in pausa", - "room.muteAll": null, - "room.stopAllVideo": null, - "room.closeMeeting": null, - "room.clearChat": null, - "room.clearFileSharing": null, - "room.speechUnsupported": null, - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, - - "me.mutedPTT": null, + "room.muteAll": "Muta tutti", + "room.stopAllVideo": "Ferma tutti i video", + "room.closeMeeting": "Termina meeting", + "room.clearChat": "Pulisci chat", + "room.clearFileSharing": "Pulisci file sharing", + "room.speechUnsupported": "Il tuo browser non supporta il riconoscimento vocale", + "room.moderatoractions": "Azioni moderatore", + "room.raisedHand": "{displayName} ha alzato la mano", + "room.loweredHand": "{displayName} ha abbassato la mano", + "room.extraVideo": "Video extra", + "room.overRoomLimit": "La stanza è piena, riprova più tardi.", + "room.help": "Aiuto", + "room.about": "Informazioni su", + "room.shortcutKeys": "Scorciatoie da tastiera", - "roles.gotRole": null, - "roles.lostRole": null, + "me.mutedPTT": "Sei mutato, tieni premuto SPAZIO per parlare", + + "roles.gotRole": "Hai ottenuto il ruolo: {role}", + "roles.lostRole": "Hai perso il ruolo: {role}", "tooltip.login": "Log in", "tooltip.logout": "Log out", - "tooltip.admitFromLobby": "Ammetti dalla lobby", + "tooltip.admitFromLobby": "Accetta partecipante dalla lobby", "tooltip.lockRoom": "Blocca stanza", "tooltip.unLockRoom": "Sblocca stanza", "tooltip.enterFullscreen": "Modalità schermo intero", @@ -75,9 +79,14 @@ "tooltip.lobby": "Mostra lobby", "tooltip.settings": "Mostra impostazioni", "tooltip.participants": "Mostra partecipanti", - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, + "tooltip.kickParticipant": "Espelli partecipante", + "tooltip.muteParticipant": "Muta partecipante", + "tooltip.muteParticipantVideo": "Ferma video partecipante", + "tooltip.raisedHand": "Mano alzata", + "tooltip.muteScreenSharing": "Ferma condivisione schermo partecipante", + "tooltip.muteParticipantAudioModerator": "Sospendi audio globale", + "tooltip.muteParticipantVideoModerator": "Sospendi video globale", + "tooltip.muteScreenSharingModerator": "Sospendi condivisione schermo globale", "label.roomName": "Nome della stanza", "label.chooseRoomButton": "Continua", @@ -91,6 +100,7 @@ "label.filesharing": "Condivisione file", "label.participants": "Partecipanti", "label.shareFile": "Condividi file", + "label.shareGalleryFile": "Condividi immagine", "label.fileSharingUnsupported": "Condivisione file non supportata", "label.unknown": "Sconosciuto", "label.democratic": "Vista Democratica", @@ -101,11 +111,12 @@ "label.veryHigh": "Molto alta (FHD)", "label.ultra": "Ultra (UHD)", "label.close": "Chiudi", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, + "label.media": "Media", + "label.appearance": "Aspetto", + "label.advanced": "Avanzate", + "label.addVideo": "Aggiungi video", + "label.promoteAllPeers": "Promuovi tutti", + "label.moreActions": "Altre azioni", "settings.settings": "Impostazioni", "settings.camera": "Videocamera", @@ -123,8 +134,14 @@ "settings.advancedMode": "Modalità avanzata", "settings.permanentTopBar": "Barra superiore permanente", "settings.lastn": "Numero di video visibili", - "settings.hiddenControls": null, - "settings.notificationSounds": null, + "settings.hiddenControls": "Controlli media nascosti", + "settings.notificationSounds": "Suoni di notifica", + "settings.showNotifications": "Mostra notifiche", + "settings.buttonControlBar": "Controlli media separati", + "settings.echoCancellation": "Cancellazione echo", + "settings.autoGainControl": "Controllo guadagno automatico", + "settings.noiseSuppression": "Riduzione del rumore", + "settings.drawerOverlayed": "Barra laterale sovrapposta", "filesharing.saveFileError": "Impossibile salvare file", "filesharing.startingFileShare": "Tentativo di condivisione file", @@ -140,7 +157,7 @@ "devices.devicesChanged": "Il tuo dispositivo è cambiato, configura i dispositivi nel menù di impostazioni", "device.audioUnsupported": "Dispositivo audio non supportato", - "device.activateAudio": "Attiva audio", + "device.activateAudio": "Attiva audio", "device.muteAudio": "Silenzia audio", "device.unMuteAudio": "Riattiva audio", @@ -166,8 +183,8 @@ "devices.cameraDisconnected": "Videocamera scollegata", "devices.cameraError": "Errore con l'accesso alla videocamera", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null -} + "moderator.clearChat": "Il moderatore ha pulito la chat", + "moderator.clearFiles": "Il moderatore ha pulito i file", + "moderator.muteAudio": "Il moderatore ha mutato il tuo audio", + "moderator.muteVideo": "Il moderatore ha fermato il tuo video" +} \ No newline at end of file diff --git a/app/src/translations/lv.json b/app/src/translations/lv.json new file mode 100644 index 0000000..27adce1 --- /dev/null +++ b/app/src/translations/lv.json @@ -0,0 +1,184 @@ +{ + "socket.disconnected": "Esat bezsaistē", + "socket.reconnecting": "Esat bezsaistē, tiek mēģināts pievienoties", + "socket.reconnected": "Esat atkārtoti pievienojies", + "socket.requestError": "Kļūme servera pieprasījumā", + + "room.chooseRoom": "Ievadiet sapulces telpas nosaukumu (ID), kurai vēlaties pievienoties", + "room.cookieConsent": "Lai uzlabotu lietotāja pieredzi, šī vietne izmanto sīkfailus", + "room.consentUnderstand": "Es saprotu un piekrītu", + "room.joined": "Jūs esiet pievienojies sapulces telpai", + "room.cantJoin": "Nav iespējams pievienoties sapulces telpai", + "room.youLocked": "Jūs aizslēdzāt sapulces telpu", + "room.cantLock": "Nav iespējams aizslēgt sapulces telpu", + "room.youUnLocked": "Jūs atslēdzāt sapulces telpu", + "room.cantUnLock": "Nav iespējams atslēgt sapulces telpu", + "room.locked": "Sapulces telpa tagad ir AIZSLĒGTA", + "room.unlocked": "Sapulces telpa tagad ir ATSLĒGTA", + "room.newLobbyPeer": "Jauns dalībnieks ienācis uzgaidāmajā telpā", + "room.lobbyPeerLeft": "Dalībnieks uzgaidāmo telpu pameta", + "room.lobbyPeerChangedDisplayName": "Dalībnieks uzgaidāmajā telpā nomainīja vārdu uz {displayName}", + "room.lobbyPeerChangedPicture": "Dalībnieks uzgaidāmajā telpā nomainīja pašattēlu", + "room.setAccessCode": "Pieejas kods sapulces telpai aktualizēts", + "room.accessCodeOn": "Pieejas kods sapulces telpai tagad ir aktivēts", + "room.accessCodeOff": "Pieejas kods sapulces telpai tagad ir deaktivēts (atslēgts)", + "room.peerChangedDisplayName": "{oldDisplayName} pārsaucās par {displayName}", + "room.newPeer": "{displayName} pievienojās sapulces telpai", + "room.newFile": "Pieejams jauns fails", + "room.toggleAdvancedMode": "Pārslēgt uz advancēto režīmu", + "room.setDemocraticView": "Nomainīts izkārtojums uz demokrātisko skatu", + "room.setFilmStripView": "Nomainīts izkārtojums uz diapozitīvu (filmstrip) skatu", + "room.loggedIn": "Jūs esat ierakstījies (sistēmā)", + "room.loggedOut": "Jūs esat izrakstījies (no sistēmas)", + "room.changedDisplayName": "Jūsu vārds mainīts uz {displayName}", + "room.changeDisplayNameError": "Gadījās ķibele ar Jūsu vārda nomaiņu", + "room.chatError": "Nav iespējams nosūtīt tērziņa ziņu", + "room.aboutToJoin": "Jūs grasāties pievienoties sapulcei", + "room.roomId": "Sapulces telpas nosaukums (ID): {roomName}", + "room.setYourName": "Norādiet savu dalības vārdu un izvēlieties kā vēlaties pievienoties sapulcei:", + "room.audioOnly": "Vienīgi audio", + "room.audioVideo": "Audio & video", + "room.youAreReady": "Ok, Jūs esiet gatavi!", + "room.emptyRequireLogin": "Sapulces telpa ir tukša! Jūs varat Ierakstīties sistēmā, lai uzsāktu vadīt sapulci vai pagaidīt kamēr pievienojas sapulces rīkotājs/vadītājs", + "room.locketWait": "Sapulce telpa ir slēgta. Jūs atrodaties tās uzgaidāmajā telpā. Uzkavējieties, kamēr kāds Jūs sapulcē ielaiž ...", + "room.lobbyAdministration": "Uzgaidāmās telpas administrēšana", + "room.peersInLobby": "Dalībnieki uzgaidāmajā telpā", + "room.lobbyEmpty": "Pašreiz uzgaidāmajā telpā neviena nav", + "room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}", + "room.me": "Es", + "room.spotlights": "Aktīvie (referējošie) dalībnieki", + "room.passive": "Pasīvie dalībnieki", + "room.videoPaused": "Šis video ir pauzēts", + "room.muteAll": "Noklusināt visus dalībnieku mikrofonus", + "room.stopAllVideo": "Izslēgt visu dalībnieku kameras", + "room.closeMeeting": "Beigt sapulci", + "room.clearChat": "Nodzēst visus tērziņus", + "room.clearFileSharing": "Notīrīt visus kopīgotos failus", + "room.speechUnsupported": "Jūsu pārlūks neatbalsta balss atpazīšanu", + "room.moderatoractions": "Moderatora rīcība", + "room.raisedHand": "{displayName} pacēla roku", + "room.loweredHand": "{displayName} nolaida roku", + "room.extraVideo": "Papildus video", + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, + + "me.mutedPTT": "Jūs esat noklusināts. Turiet taustiņu SPACE-BAR, lai runātu", + + "roles.gotRole": "Jūs ieguvāt lomu: {role}", + "roles.lostRole": "Jūs zaudējāt lomu: {role}", + + "tooltip.login": "Ierakstīties", + "tooltip.logout": "Izrakstīties", + "tooltip.admitFromLobby": "Ielaist no uzgaidāmās telpas", + "tooltip.lockRoom": "Aizslēgt sapulces telpu", + "tooltip.unLockRoom": "Atlēgt sapulces telpu", + "tooltip.enterFullscreen": "Aktivēt pilnekrāna režīmu", + "tooltip.leaveFullscreen": "Pamest pilnekrānu", + "tooltip.lobby": "Parādīt uzgaidāmo telpu", + "tooltip.settings": "Parādīt iestatījumus", + "tooltip.participants": "Parādīt dalībniekus", + "tooltip.kickParticipant": "Izvadīt (izspert) dalībnieku", + "tooltip.muteParticipant": "Noklusināt dalībnieku", + "tooltip.muteParticipantVideo": "Atslēgt dalībnieka video", + "tooltip.raisedHand": "Pacelt roku", + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, + + "label.roomName": "Sapulces telpas nosaukums (ID)", + "label.chooseRoomButton": "Turpināt", + "label.yourName": "Jūu vārds", + "label.newWindow": "Jauns logs", + "label.fullscreen": "Pilnekrāns", + "label.openDrawer": "Atvērt atvilkni", + "label.leave": "Pamest", + "label.chatInput": "Rakstiet tērziņa ziņu...", + "label.chat": "Tērzētava", + "label.filesharing": "Failu koplietošana", + "label.participants": "Dalībnieki", + "label.shareFile": "Koplietot failu", + "label.fileSharingUnsupported": "Failu koplietošana netiek atbalstīta", + "label.unknown": "Nezināms", + "label.democratic": "Demokrātisks skats", + "label.filmstrip": "Diapozitīvu (filmstrip) skats", + "label.low": "Zema", + "label.medium": "Vidēja", + "label.high": "Augsta (HD)", + "label.veryHigh": "Ļoti augsta (FHD)", + "label.ultra": "Ultra (UHD)", + "label.close": "Aizvērt", + "label.media": "Mediji", + "label.appearance": "Izskats", + "label.advanced": "Advancēts", + "label.addVideo": "Pievienot video", + "label.moreActions": null, + + "settings.settings": "Iestatījumi", + "settings.camera": "Kamera", + "settings.selectCamera": "Izvēlieties kameru (video ierīci)", + "settings.cantSelectCamera": "Nav iespējams lietot šo kameru (video ierīci)", + "settings.audio": "Skaņas ierīce", + "settings.selectAudio": "Izvēlieties skaņas ierīci", + "settings.cantSelectAudio": "Nav iespējams lietot šo skaņas (audio) ierīci", + "settings.resolution": "Iestatiet jūsu video izšķirtspēju", + "settings.layout": "Sapulces telpas izkārtojums", + "settings.selectRoomLayout": "Iestatiet sapulces telpas izkārtojumu", + "settings.advancedMode": "Advancētais režīms", + "settings.permanentTopBar": "Pastāvīga augšējā (ekrānaugšas) josla", + "settings.lastn": "Jums redzamo video/kameru skaits", + "settings.hiddenControls": "Slēpto mediju vadība", + "settings.notificationSounds": "Paziņojumu skaņas", + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, + + "filesharing.saveFileError": "Nav iespējams saglabāt failu", + "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", + "filesharing.successfulFileShare": "Fails sekmīgi kopīgots", + "filesharing.unableToShare": "Nav iespējams kopīgot failu", + "filesharing.error": "Atgadījās faila kopīgošanas kļūme", + "filesharing.finished": "Fails ir lejupielādēts", + "filesharing.save": "Saglabāt", + "filesharing.sharedFile": "{displayName} kopīgoja failu", + "filesharing.download": "Lejuplādēt", + "filesharing.missingSeeds": "Ja šis process aizņem ilgu laiku, iespējams nav neviena, kas sēklo (seed) šo torentu. Mēģiniet palūgt kādu atkārtoti augšuplādēt Jūsu gribēto failu.", + + "devices.devicesChanged": "Jūsu ierīces pamainījās. Iestatījumu izvēlnē (dialogā) iestatiet jaunās ierīces.", + + "device.audioUnsupported": "Skaņa (audio) netiek atbalstīta", + "device.activateAudio": "Iespējot/aktivēt mikrofonu (izejošo skaņu)", + "device.muteAudio": "Atslēgt/noklusināt mikrofonu (izejošo skaņu) ", + "device.unMuteAudio": "Ieslēgt mikrofonu (izejošo skaņu)", + + "device.videoUnsupported": "Kamera (izejošais video) netiek atbalstīta", + "device.startVideo": "Ieslēgt kameru (izejošo video)", + "device.stopVideo": "Izslēgt kameru (izejošo video)", + + "device.screenSharingUnsupported": "Ekrāna kopīgošana netiek atbalstīta", + "device.startScreenSharing": "Sākt ekrāna kopīgošanu", + "device.stopScreenSharing": "Beigt ekrāna kopīgošanu", + + "devices.microphoneDisconnected": "Mikrofons atvienots", + "devices.microphoneError": "Atgadījās kļūme, piekļūstot jūsu mikrofonam", + "devices.microPhoneMute": "Mikrofons izslēgts/noklusināts", + "devices.micophoneUnMute": "Mikrofons ieslēgts", + "devices.microphoneEnable": "Mikrofons iespējots", + "devices.microphoneMuteError": "Nav iespējams izslēgt Jūsu mikrofonu", + "devices.microphoneUnMuteError": "Nav iespējams ieslēgt Jūsu mikrofonu", + + "devices.screenSharingDisconnected" : "Ekrāna kopīgošana nenotiek (atvienota)", + "devices.screenSharingError": "Atgadījās kļūme, piekļūstot Jūsu ekrānam", + + "devices.cameraDisconnected": "Kamera atvienota", + "devices.cameraError": "Atgadījās kļūme, piekļūstot Jūsu kamerai", + + "moderator.clearChat": "Moderators nodzēsa tērziņus", + "moderator.clearFiles": "Moderators notīrīja failus", + "moderator.muteAudio": "Moderators noklusināja jūsu mikrofonu", + "moderator.muteVideo": "Moderators atslēdza jūsu kameru" +} diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index e803bda..172fcf9 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -59,6 +59,10 @@ "room.raisedHand": "{displayName} rakk opp hånden", "room.loweredHand": "{displayName} tok ned hånden", "room.extraVideo": "Ekstra video", + "room.overRoomLimit": "Rommet er fullt, prøv igjen om litt.", + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke", @@ -79,6 +83,10 @@ "tooltip.muteParticipant": "Demp deltaker", "tooltip.muteParticipantVideo": "Demp deltakervideo", "tooltip.raisedHand": "Rekk opp hånden", + "tooltip.muteScreenSharing": "Demp deltaker skjermdeling", + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Møtenavn", "label.chooseRoomButton": "Fortsett", @@ -92,6 +100,7 @@ "label.filesharing": "Fildeling", "label.participants": "Deltakere", "label.shareFile": "Del fil", + "label.shareGalleryFile": "Del bilde", "label.fileSharingUnsupported": "Fildeling ikke støttet", "label.unknown": "Ukjent", "label.democratic": "Demokratisk", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Lukk", "label.media": "Media", - "label.appearence": "Utseende", + "label.appearance": "Utseende", "label.advanced": "Avansert", "label.addVideo": "Legg til video", "label.promoteAllPeers": "Slipp inn alle", + "label.moreActions": "Flere handlinger", "settings.settings": "Innstillinger", "settings.camera": "Kamera", @@ -126,6 +136,12 @@ "settings.lastn": "Antall videoer synlig", "settings.hiddenControls": "Skjul media knapper", "settings.notificationSounds": "Varslingslyder", + "settings.showNotifications": "Vis varslinger", + "settings.buttonControlBar": "Separate media knapper", + "settings.echoCancellation": "Echokansellering", + "settings.autoGainControl": "Auto gain kontroll", + "settings.noiseSuppression": "Støy reduksjon", + "settings.drawerOverlayed": "Sidemeny over innhold", "filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.startingFileShare": "Starter fildeling", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 399f788..dd12971 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -49,21 +49,25 @@ "room.spotlights": "Aktywni uczestnicy", "room.passive": "Pasywni uczestnicy", "room.videoPaused": "To wideo jest wstrzymane.", - "room.muteAll": null, - "room.stopAllVideo": null, - "room.closeMeeting": null, - "room.clearChat": null, - "room.clearFileSharing": null, - "room.speechUnsupported": null, - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, + "room.muteAll": "Wycisz wszystkich", + "room.stopAllVideo": "Zatrzymaj wszystkie Video", + "room.closeMeeting": "Zamknij spotkanie", + "room.clearChat": "Wyczyść Chat", + "room.clearFileSharing": "Wyczyść pliki", + "room.speechUnsupported": "Twoja przeglądarka nie rozpoznaje mowy", + "room.moderatoractions": "Akcje moderatora", + "room.raisedHand": "{displayName} podniósł rękę", + "room.loweredHand": "{displayName} opuścił rękę", + "room.extraVideo": "Dodatkowe Video", + "room.overRoomLimit": "Pokój jest pełny, spróbuj za jakiś czas.", + "room.help": "Pomoc", + "room.about": "O pogramie", + "room.shortcutKeys": "Skróty klawiaturowe", - "me.mutedPTT": null, + "me.mutedPTT": "Masz wyciszony mikrofon, przytrzymaj spację aby mówić", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "Masz rolę {role}", + "roles.lostRole": "Nie masz już roli {role}", "tooltip.login": "Zaloguj", "tooltip.logout": "Wyloguj", @@ -75,10 +79,14 @@ "tooltip.lobby": "Pokaż poczekalnię", "tooltip.settings": "Pokaż ustawienia", "tooltip.participants": "Pokaż uczestników", - "tooltip.kickParticipant": null, - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, + "tooltip.kickParticipant": "Wyrzuć użytkownika", + "tooltip.muteParticipant": "Wycisz użytkownika", + "tooltip.muteParticipantVideo": "Wyłącz wideo użytkownika", + "tooltip.raisedHand": "Podnieś rękę", + "tooltip.muteScreenSharing": "Anuluj udostępniania pulpitu przez użytkownika", + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Nazwa konferencji", "label.chooseRoomButton": "Kontynuuj", @@ -92,6 +100,7 @@ "label.filesharing": "Udostępnianie plików", "label.participants": "Uczestnicy", "label.shareFile": "Udostępnij plik", + "label.shareGalleryFile": "Udostępnij obraz", "label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane", "label.unknown": "Nieznane", "label.democratic": "Układ demokratyczny", @@ -102,11 +111,12 @@ "label.veryHigh": "Bardzo wysoka (FHD)", "label.ultra": "Ultra (UHD)", "label.close": "Zamknij", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, + "label.media": "Media", + "label.appearance": "Wygląd", + "label.advanced": "Zaawansowane", + "label.addVideo": "Dodaj wideo", + "label.promoteAllPeers": "Wpuść wszystkich", + "label.moreActions": "Więcej akcji", "settings.settings": "Ustawienia", "settings.camera": "Kamera", @@ -124,8 +134,14 @@ "settings.advancedMode": "Tryb zaawansowany", "settings.permanentTopBar": "Stały górny pasek", "settings.lastn": "Liczba widocznych uczestników (zdalnych)", - "settings.hiddenControls": null, - "settings.notificationSounds": null, + "settings.hiddenControls": "Ukryte kontrolki mediów", + "settings.notificationSounds": "Powiadomienia dźwiękiem", + "settings.showNotifications": "Pokaż powiadomienia", + "settings.buttonControlBar": "Rozdziel kontrolki mediów", + "settings.echoCancellation": "Usuwanie echa", + "settings.autoGainControl": "Auto korekta wzmocnienia", + "settings.noiseSuppression": "Wyciszenie szumów", + "settings.drawerOverlayed": "Szuflada nad zawartością", "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", @@ -167,8 +183,8 @@ "devices.cameraDisconnected": "Kamera odłączona", "devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "Moderator wyczyścił chat", + "moderator.clearFiles": "Moderator wyczyścił pliki", + "moderator.muteAudio": "Moderator wyciszył audio", + "moderator.muteVideo": "Moderator wyciszył twoje video" } \ No newline at end of file diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index d66d8da..13fd0f6 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Nome da sala", "label.chooseRoomButton": "Continuar", @@ -92,6 +100,7 @@ "label.filesharing": "Partilha de ficheiro", "label.participants": "Participantes", "label.shareFile": "Partilhar ficheiro", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Partilha de ficheiro não disponível", "label.unknown": "Desconhecido", "label.democratic": "Vista democrática", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Fechar", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Definições", "settings.camera": "Camera", @@ -126,6 +136,12 @@ "settings.lastn": "Número de vídeos visíveis", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index b37b36a..8e05b0b 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Numele camerei", "label.chooseRoomButton": "Continuare", @@ -92,6 +100,7 @@ "label.filesharing": "Partajarea fișierelor", "label.participants": "Participanți", "label.shareFile": "Partajează fișierul", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată", "label.unknown": "Necunoscut", "label.democratic": "Distribuție egală a dimensiunii imaginii", @@ -103,10 +112,11 @@ "label.ultra": "Rezoluție ultra înaltă (UHD)", "label.close": "Închide", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Setări", "settings.camera": "Cameră video", @@ -126,6 +136,12 @@ "settings.lastn": "Numărul de videoclipuri vizibile", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.startingFileShare": "Partajarea fișierului", diff --git a/app/src/translations/tr.json b/app/src/translations/tr.json index d3d20e3..dc05c2f 100644 --- a/app/src/translations/tr.json +++ b/app/src/translations/tr.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Oda adı", "label.chooseRoomButton": "Devam", @@ -92,6 +100,7 @@ "label.filesharing": "Dosya paylaşım", "label.participants": "Katılımcı", "label.shareFile": "Dosya paylaş", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Dosya paylaşımı desteklenmiyor", "label.unknown": "Bilinmeyen", "label.democratic": "Demokratik görünüm", @@ -103,10 +112,11 @@ "label.ultra": "Ultra (UHD)", "label.close": "Kapat", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Ayarlar", "settings.camera": "Kamera", @@ -123,6 +133,12 @@ "settings.lastn": "İzlenebilir video sayısı", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json index dcb3c7d..b06aef4 100644 --- a/app/src/translations/uk.json +++ b/app/src/translations/uk.json @@ -59,6 +59,10 @@ "room.raisedHand": null, "room.loweredHand": null, "room.extraVideo": null, + "room.overRoomLimit": null, + "room.help": null, + "room.about": null, + "room.shortcutKeys": null, "me.mutedPTT": null, @@ -79,6 +83,10 @@ "tooltip.muteParticipant": null, "tooltip.muteParticipantVideo": null, "tooltip.raisedHand": null, + "tooltip.muteScreenSharing": null, + "tooltip.muteParticipantAudioModerator": null, + "tooltip.muteParticipantVideoModerator": null, + "tooltip.muteScreenSharingModerator": null, "label.roomName": "Назва кімнати", "label.chooseRoomButton": "Продовжити", @@ -92,6 +100,7 @@ "label.filesharing": "Обмін файлами", "label.participants": "Учасники", "label.shareFile": "Надіслати файл", + "label.shareGalleryFile": null, "label.fileSharingUnsupported": "Обмін файлами не підтримується", "label.unknown": "Невідомо", "label.democrat": "Демократичний вигляд", @@ -103,10 +112,11 @@ "label.ultra": "Ультра (UHD)", "label.close": "Закрити", "label.media": null, - "label.appearence": null, + "label.appearance": null, "label.advanced": null, "label.addVideo": null, "label.promoteAllPeers": null, + "label.moreActions": null, "settings.settings": "Налаштування", "settings.camera": "Камера", @@ -126,6 +136,12 @@ "settings.lastn": "Кількість видимих ​​відео", "settings.hiddenControls": null, "settings.notificationSounds": null, + "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.startingFileShare": "Спроба поділитися файлом", diff --git a/server/access.js b/server/access.js new file mode 100644 index 0000000..479e9e2 --- /dev/null +++ b/server/access.js @@ -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' +}; \ No newline at end of file diff --git a/server/config/config.example.js b/server/config/config.example.js index 6ff279a..e08f910 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,5 +1,23 @@ const os = require('os'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = require('../permissions'); + // const AwaitQueue = require('awaitqueue'); // const axios = require('axios'); @@ -96,8 +114,8 @@ module.exports = this._queue = new AwaitQueue(); } - // rooms: number of rooms - // peers: number of peers + // rooms: rooms object + // peers: peers object // eslint-disable-next-line no-unused-vars async log({ rooms, peers }) { @@ -106,9 +124,9 @@ module.exports = // Do your logging in here, use queue to keep correct order // eslint-disable-next-line no-console - console.log('Number of rooms: ', rooms); + console.log('Number of rooms: ', rooms.size); // eslint-disable-next-line no-console - console.log('Number of peers: ', peers); + console.log('Number of peers: ', peers.size); }) .catch((error) => { @@ -216,39 +234,50 @@ module.exports = accessFromRoles : { // The role(s) will gain access to the room // even if it is locked (!) - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], // The role(s) will gain access to the room without // going into the lobby. If you want to restrict access to your // server to only directly allow authenticated users, you could // add the userRoles.AUTHENTICATED to the user in the userMapping // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - BYPASS_LOBBY : [ userRoles.NORMAL ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] }, permissionsFromRoles : { // The role(s) have permission to lock/unlock a room - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], // The role(s) have permission to promote a peer from the lobby - PROMOTE_PEER : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], // The role(s) have permission to send chat messages - SEND_CHAT : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], // The role(s) have permission to moderate chat - MODERATE_CHAT : [ userRoles.MODERATOR ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], // The role(s) have permission to share screen - SHARE_SCREEN : [ userRoles.NORMAL ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], // The role(s) have permission to produce extra video - EXTRA_VIDEO : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], // The role(s) have permission to share files - SHARE_FILE : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], // The role(s) have permission to moderate files - MODERATE_FILES : [ userRoles.MODERATOR ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], // The role(s) have permission to moderate room (e.g. kick user) - MODERATE_ROOM : [ userRoles.MODERATOR ] + [MODERATE_ROOM] : [ userRoles.MODERATOR ] }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], // When truthy, the room will be open to all users when as long as there // are allready users in the room - activateOnHostJoin : true, + activateOnHostJoin : true, + // When set, maxUsersPerRoom defines how many users can join + // a single room. If not set, there is no limit. + // maxUsersPerRoom : 20, + // Room size before spreading to new router + routerScaleSize : 40, // Mediasoup settings - mediasoup : + mediasoup : { numWorkers : Object.keys(os.cpus()).length, // mediasoup Worker settings. diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js index 0bcfb66..45cdc06 100644 --- a/server/lib/Lobby.js +++ b/server/lib/Lobby.js @@ -46,7 +46,7 @@ class Lobby extends EventEmitter return Object.values(this._peers).map((peer) => ({ - peerId : peer.id, + id : peer.id, displayName : peer.displayName, picture : peer.picture })); @@ -154,8 +154,6 @@ class Lobby extends EventEmitter this.emit('lobbyEmpty'); }; - this._notification(peer.socket, 'enteredLobby'); - this._peers[peer.id] = peer; peer.on('gotRole', peer.gotRoleHandler); @@ -165,6 +163,8 @@ class Lobby extends EventEmitter peer.socket.on('request', peer.socketRequestHandler); peer.on('close', peer.closeHandler); + + this._notification(peer.socket, 'enteredLobby'); } async _handleSocketRequest(peer, request, cb) @@ -189,7 +189,8 @@ class Lobby extends EventEmitter cb(); break; - } + } + case 'changePicture': { const { picture } = request.data; diff --git a/server/lib/Peer.js b/server/lib/Peer.js index a345a16..8e11ca2 100644 --- a/server/lib/Peer.js +++ b/server/lib/Peer.js @@ -39,6 +39,8 @@ class Peer extends EventEmitter this._email = null; + this._routerId = null; + this._rtpCapabilities = null; this._raisedHand = false; @@ -62,10 +64,10 @@ class Peer extends EventEmitter // Iterate and close all mediasoup Transport associated to this Peer, so all // its Producers and Consumers will also be closed. - this.transports.forEach((transport) => + for (const transport of this.transports.values()) { transport.close(); - }); + } if (this.socket) this.socket.disconnect(true); @@ -238,6 +240,16 @@ class Peer extends EventEmitter this._email = email; } + get routerId() + { + return this._routerId; + } + + set routerId(routerId) + { + this._routerId = routerId; + } + get rtpCapabilities() { return this._rtpCapabilities; diff --git a/server/lib/Room.js b/server/lib/Room.js index 9200be9..74accc4 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1,36 +1,59 @@ const EventEmitter = require('events').EventEmitter; +const AwaitQueue = require('awaitqueue'); const axios = require('axios'); const Logger = require('./Logger'); const Lobby = require('./Lobby'); const { v4: uuidv4 } = require('uuid'); const jwt = require('jsonwebtoken'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const permissions = require('../permissions'), { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = permissions; + const config = require('../config/config'); const logger = new Logger('Room'); // In case they are not configured properly -const accessFromRoles = +const roomAccess = { - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], - BYPASS_LOBBY : [ userRoles.NORMAL ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + [BYPASS_LOBBY] : [ userRoles.NORMAL ], ...config.accessFromRoles }; -const permissionsFromRoles = +const roomPermissions = { - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], - PROMOTE_PEER : [ userRoles.NORMAL ], - SEND_CHAT : [ userRoles.NORMAL ], - MODERATE_CHAT : [ userRoles.MODERATOR ], - SHARE_SCREEN : [ userRoles.NORMAL ], - EXTRA_VIDEO : [ userRoles.NORMAL ], - SHARE_FILE : [ userRoles.NORMAL ], - MODERATE_FILES : [ userRoles.MODERATOR ], - MODERATE_ROOM : [ userRoles.MODERATOR ], + [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], + [MODERATE_ROOM] : [ userRoles.MODERATOR ], ...config.permissionsFromRoles }; +const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || []; + +const ROUTER_SCALE_SIZE = config.routerScaleSize || 40; + class Room extends EventEmitter { /** @@ -38,32 +61,49 @@ class Room extends EventEmitter * * @async * - * @param {mediasoup.Worker} mediasoupWorker - The mediasoup Worker in which a new + * @param {mediasoup.Worker} mediasoupWorkers - The mediasoup Worker in which a new * mediasoup Router must be created. * @param {String} roomId - Id of the Room instance. */ - static async create({ mediasoupWorker, roomId }) + static async create({ mediasoupWorkers, roomId }) { logger.info('create() [roomId:"%s"]', roomId); + // Shuffle workers to get random cores + let shuffledWorkers = mediasoupWorkers.sort(() => Math.random() - 0.5); + // Router media codecs. const mediaCodecs = config.mediasoup.router.mediaCodecs; - // Create a mediasoup Router. - const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs }); + const mediasoupRouters = new Map(); - // Create a mediasoup AudioLevelObserver. - const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver( + let firstRouter = null; + + for (const worker of shuffledWorkers) + { + const router = await worker.createRouter({ mediaCodecs }); + + if (!firstRouter) + firstRouter = router; + + mediasoupRouters.set(router.id, router); + } + + // Create a mediasoup AudioLevelObserver on first router + const audioLevelObserver = await firstRouter.createAudioLevelObserver( { maxEntries : 1, threshold : -80, interval : 800 }); - return new Room({ roomId, mediasoupRouter, audioLevelObserver }); + firstRouter = null; + shuffledWorkers = null; + + return new Room({ roomId, mediasoupRouters, audioLevelObserver }); } - constructor({ roomId, mediasoupRouter, audioLevelObserver }) + constructor({ roomId, mediasoupRouters, audioLevelObserver }) { logger.info('constructor() [roomId:"%s"]', roomId); @@ -78,6 +118,9 @@ class Room extends EventEmitter // Closed flag. this._closed = false; + // Joining queue + this._queue = new AwaitQueue(); + // Locked flag. this._locked = false; @@ -98,8 +141,15 @@ class Room extends EventEmitter this._peers = {}; - // mediasoup Router instance. - this._mediasoupRouter = mediasoupRouter; + this._selfDestructTimeout = null; + + // Array of mediasoup Router instances. + this._mediasoupRouters = mediasoupRouters; + + // The router we are currently putting peers in + this._routerIterator = this._mediasoupRouters.values(); + + this._currentRouter = this._routerIterator.next().value; // mediasoup AudioLevelObserver. this._audioLevelObserver = audioLevelObserver; @@ -122,8 +172,23 @@ class Room extends EventEmitter this._closed = true; + this._queue.close(); + + this._queue = null; + + if (this._selfDestructTimeout) + clearTimeout(this._selfDestructTimeout); + + this._selfDestructTimeout = null; + + this._chatHistory = null; + + this._fileHistory = null; + this._lobby.close(); + this._lobby = null; + // Close the peers. for (const peer in this._peers) { @@ -133,8 +198,19 @@ class Room extends EventEmitter this._peers = null; - // Close the mediasoup Router. - this._mediasoupRouter.close(); + // Close the mediasoup Routers. + for (const router of this._mediasoupRouters.values()) + { + router.close(); + } + + this._routerIterator = null; + + this._currentRouter = null; + + this._mediasoupRouters.clear(); + + this._audioLevelObserver = null; // Emit 'close' event. this.emit('close'); @@ -173,21 +249,34 @@ class Room extends EventEmitter // Returning user if (returning) this._peerJoining(peer, true); - else if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + else if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) this._peerJoining(peer); + else if ( + 'maxUsersPerRoom' in config && + ( + Object.keys(this._peers).length + + this._lobby.peerList().length + ) >= config.maxUsersPerRoom) + { + this._handleOverRoomLimit(peer); + } else if (this._locked) this._parkPeer(peer); else { // Has a role that is allowed to bypass lobby - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ? + this._hasAccess(peer, BYPASS_LOBBY) ? this._peerJoining(peer) : this._handleGuest(peer); } } + _handleOverRoomLimit(peer) + { + this._notification(peer.socket, 'overRoomLimit'); + } + _handleGuest(peer) { if (config.activateOnHostJoin && !this.checkEmpty()) @@ -209,11 +298,7 @@ class Room extends EventEmitter this._peerJoining(promotedPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); } @@ -221,9 +306,8 @@ class Room extends EventEmitter this._lobby.on('peerRolesChanged', (peer) => { - if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) { this._lobby.promotePeer(peer.id); @@ -232,7 +316,7 @@ class Room extends EventEmitter if ( // Has a role that is allowed to bypass lobby !this._locked && - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) + this._hasAccess(peer, BYPASS_LOBBY) ) { this._lobby.promotePeer(peer.id); @@ -245,11 +329,7 @@ class Room extends EventEmitter { const { id, displayName } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); } @@ -259,11 +339,7 @@ class Room extends EventEmitter { const { id, picture } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); } @@ -275,11 +351,7 @@ class Room extends EventEmitter const { id } = closedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); } @@ -339,7 +411,7 @@ class Room extends EventEmitter ); } - async dump() + dump() { return { roomId : this._roomId, @@ -356,7 +428,10 @@ class Room extends EventEmitter { logger.debug('selfDestructCountdown() started'); - setTimeout(() => + if (this._selfDestructTimeout) + clearTimeout(this._selfDestructTimeout); + + this._selfDestructTimeout = setTimeout(() => { if (this._closed) return; @@ -382,115 +457,91 @@ class Room extends EventEmitter { this._lobby.parkPeer(parkPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); } } - async _peerJoining(peer, returning = false) + _peerJoining(peer, returning = false) { - peer.socket.join(this._roomId); - - // If we don't have this peer, add to end - !this._lastN.includes(peer.id) && this._lastN.push(peer.id); - - this._peers[peer.id] = peer; - - this._handlePeer(peer); - - if (returning) + this._queue.push(async () => { - this._notification(peer.socket, 'roomBack'); - } - else - { - const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true }); + peer.socket.join(this._roomId); - peer.socket.handshake.session.token = token; + // If we don't have this peer, add to end + !this._lastN.includes(peer.id) && this._lastN.push(peer.id); - peer.socket.handshake.session.save(); + this._peers[peer.id] = peer; - let turnServers; - - if ('turnAPIURI' in config) + // Assign routerId + peer.routerId = await this._getRouterId(); + + this._handlePeer(peer); + + if (returning) { - try - { - const { data } = await axios.get( - config.turnAPIURI, - { - params : { - ...config.turnAPIparams, - 'api_key' : config.turnAPIKey, - 'ip' : peer.socket.request.connection.remoteAddress - } - }); - - turnServers = [ { - urls : data.uris, - username : data.username, - credential : data.password - } ]; - } - catch (error) - { - if ('backupTurnServers' in config) - turnServers = config.backupTurnServers; - - logger.error('_peerJoining() | error on REST turn [error:"%o"]', error); - } + this._notification(peer.socket, 'roomBack'); } - else if ('backupTurnServers' in config) + else { - turnServers = config.backupTurnServers; + const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true }); + + peer.socket.handshake.session.token = token; + + peer.socket.handshake.session.save(); + + let turnServers; + + if ('turnAPIURI' in config) + { + try + { + const { data } = await axios.get( + config.turnAPIURI, + { + params : { + ...config.turnAPIparams, + 'api_key' : config.turnAPIKey, + 'ip' : peer.socket.request.connection.remoteAddress + } + }); + + turnServers = [ { + urls : data.uris, + username : data.username, + credential : data.password + } ]; + } + catch (error) + { + if ('backupTurnServers' in config) + turnServers = config.backupTurnServers; + + logger.error('_peerJoining() | error on REST turn [error:"%o"]', error); + } + } + else if ('backupTurnServers' in config) + { + turnServers = config.backupTurnServers; + } + + this._notification(peer.socket, 'roomReady', { turnServers }); } - - this._notification(peer.socket, 'roomReady', { turnServers }); - } + }) + .catch((error) => + { + logger.error('_peerJoining() [error:"%o"]', error); + }); } _handlePeer(peer) { logger.debug('_handlePeer() [peer:"%s"]', peer.id); - peer.socket.on('request', (request, cb) => - { - logger.debug( - 'Peer "request" event [method:"%s", peerId:"%s"]', - request.method, peer.id); - - this._handleSocketRequest(peer, request, cb) - .catch((error) => - { - logger.error('"request" failed [error:"%o"]', error); - - cb(error); - }); - }); - peer.on('close', () => { - if (this._closed) - return; - - // If the Peer was joined, notify all Peers. - if (peer.joined) - this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); - - // Remove from lastN - this._lastN = this._lastN.filter((id) => id !== peer.id); - - delete this._peers[peer.id]; - - // If this is the last Peer in the room and - // lobby is empty, close the room after a while. - if (this.checkEmpty() && this._lobby.checkEmpty()) - this.selfDestructCountdown(); + this._handlePeerClose(peer); }); peer.on('displayNameChanged', ({ oldDisplayName }) => @@ -534,7 +585,7 @@ class Room extends EventEmitter // Got permission to promote peers, notify peer of // peers in lobby - if (permissionsFromRoles.PROMOTE_PEER.includes(newRole)) + if (roomPermissions.PROMOTE_PEER.includes(newRole)) { const lobbyPeers = this._lobby.peerList(); @@ -556,15 +607,81 @@ class Room extends EventEmitter role : oldRole }, true, true); }); + + peer.socket.on('request', (request, cb) => + { + logger.debug( + 'Peer "request" event [method:"%s", peerId:"%s"]', + request.method, peer.id); + + this._handleSocketRequest(peer, request, cb) + .catch((error) => + { + logger.error('"request" failed [error:"%o"]', error); + + cb(error); + }); + }); + + // Peer left before we were done joining + if (peer.closed) + this._handlePeerClose(peer); + } + + _handlePeerClose(peer) + { + logger.debug('_handlePeerClose() [peer:"%s"]', peer.id); + + if (this._closed) + return; + + // If the Peer was joined, notify all Peers. + if (peer.joined) + this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); + + // Remove from lastN + this._lastN = this._lastN.filter((id) => id !== peer.id); + + // Need this to know if this peer was the last with PROMOTE_PEER + const hasPromotePeer = peer.roles.some((role) => + roomPermissions[PROMOTE_PEER].includes(role) + ); + + delete this._peers[peer.id]; + + // No peers left with PROMOTE_PEER, might need to give + // lobbyPeers to peers that are left. + if ( + hasPromotePeer && + !this._lobby.checkEmpty() && + roomAllowWhenRoleMissing.includes(PROMOTE_PEER) && + this._getPeersWithPermission(PROMOTE_PEER).length === 0 + ) + { + const lobbyPeers = this._lobby.peerList(); + + for (const allowedPeer of this._getAllowedPeers(PROMOTE_PEER)) + { + this._notification(allowedPeer.socket, 'parkedPeers', { lobbyPeers }); + } + } + + // If this is the last Peer in the room and + // lobby is empty, close the room after a while. + if (this.checkEmpty() && this._lobby.checkEmpty()) + this.selfDestructCountdown(); } async _handleSocketRequest(peer, request, cb) { + const router = + this._mediasoupRouters.get(peer.routerId); + switch (request.method) { case 'getRouterRtpCapabilities': { - cb(null, this._mediasoupRouter.rtpCapabilities); + cb(null, router.rtpCapabilities); break; } @@ -589,24 +706,25 @@ class Room extends EventEmitter // Tell the new Peer about already joined Peers. // And also create Consumers for existing Producers. - const joinedPeers = - [ - ...this._getJoinedPeers() - ]; + const joinedPeers = this._getJoinedPeers(peer); const peerInfos = joinedPeers - .filter((joinedPeer) => joinedPeer.id !== peer.id) .map((joinedPeer) => (joinedPeer.peerInfo)); - const lobbyPeers = this._lobby.peerList(); + let lobbyPeers = []; + + // Allowed to promote peers, notify about lobbypeers + if (this._hasPermission(peer, PROMOTE_PEER)) + lobbyPeers = this._lobby.peerList(); cb(null, { roles : peer.roles, peers : peerInfos, tracker : config.fileTracker, authenticated : peer.authenticated, - permissionsFromRoles : permissionsFromRoles, + roomPermissions : roomPermissions, userRoles : userRoles, + allowWhenRoleMissing : roomAllowWhenRoleMissing, chatHistory : this._chatHistory, fileHistory : this._fileHistory, lastNHistory : this._lastN, @@ -633,7 +751,7 @@ class Room extends EventEmitter } // Notify the new Peer to all other Peers. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._notification( otherPeer.socket, @@ -673,7 +791,7 @@ class Room extends EventEmitter webRtcTransportOptions.enableTcp = true; } - const transport = await this._mediasoupRouter.createWebRtcTransport( + const transport = await router.createWebRtcTransport( webRtcTransportOptions ); @@ -743,15 +861,13 @@ class Room extends EventEmitter if ( appData.source === 'screen' && - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_SCREEN.includes(role)) + !this._hasPermission(peer, SHARE_SCREEN) ) throw new Error('peer not authorized'); if ( appData.source === 'extravideo' && - !peer.roles.some( - (role) => permissionsFromRoles.EXTRA_VIDEO.includes(role)) + !this._hasPermission(peer, EXTRA_VIDEO) ) throw new Error('peer not authorized'); @@ -772,6 +888,19 @@ class Room extends EventEmitter const producer = await transport.produce({ kind, rtpParameters, appData }); + const pipeRouters = this._getRoutersToPipeTo(peer.routerId); + + for (const [ routerId, destinationRouter ] of this._mediasoupRouters) + { + if (pipeRouters.includes(routerId)) + { + await router.pipeToRouter({ + producerId : producer.id, + router : destinationRouter + }); + } + } + // Store the Producer into the Peer data Object. peer.addProducer(producer.id, producer); @@ -791,7 +920,7 @@ class Room extends EventEmitter cb(null, { id: producer.id }); // Optimization: Create a server-side Consumer for each Peer. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._createConsumer( { @@ -1053,9 +1182,7 @@ class Room extends EventEmitter case 'chatMessage': { - if ( - !peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role)) - ) + if (!this._hasPermission(peer, SEND_CHAT)) throw new Error('peer not authorized'); const { chatMessage } = request.data; @@ -1076,11 +1203,7 @@ class Room extends EventEmitter case 'moderator:clearChat': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_CHAT.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_CHAT)) throw new Error('peer not authorized'); this._chatHistory = []; @@ -1096,11 +1219,7 @@ class Room extends EventEmitter case 'lockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = true; @@ -1118,11 +1237,7 @@ class Room extends EventEmitter case 'unlockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = false; @@ -1180,11 +1295,7 @@ class Room extends EventEmitter case 'promotePeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1199,11 +1310,7 @@ class Room extends EventEmitter case 'promoteAllPeers': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); this._lobby.promoteAllPeers(); @@ -1216,11 +1323,7 @@ class Room extends EventEmitter case 'sendFile': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_FILE.includes(role) - ) - ) + if (!this._hasPermission(peer, SHARE_FILE)) throw new Error('peer not authorized'); const { magnetUri } = request.data; @@ -1241,11 +1344,7 @@ class Room extends EventEmitter case 'moderator:clearFileSharing': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_FILES.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_FILES)) throw new Error('peer not authorized'); this._fileHistory = []; @@ -1278,13 +1377,28 @@ class Room extends EventEmitter break; } + case 'moderator:mute': + { + if (!this._hasPermission(peer, MODERATE_ROOM)) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const mutePeer = this._peers[peerId]; + + if (!mutePeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(mutePeer.socket, 'moderator:mute'); + + cb(); + + break; + } + case 'moderator:muteAll': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1295,13 +1409,28 @@ class Room extends EventEmitter break; } + case 'moderator:stopVideo': + { + if (!this._hasPermission(peer, MODERATE_ROOM)) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const stopVideoPeer = this._peers[peerId]; + + if (!stopVideoPeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(stopVideoPeer.socket, 'moderator:stopVideo'); + + cb(); + + break; + } + case 'moderator:stopAllVideo': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1314,11 +1443,7 @@ class Room extends EventEmitter case 'moderator:closeMeeting': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); this._notification(peer.socket, 'moderator:kick', null, true); @@ -1333,11 +1458,7 @@ class Room extends EventEmitter case 'moderator:kickPeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1356,6 +1477,25 @@ class Room extends EventEmitter break; } + case 'moderator:lowerHand': + { + if (!this._hasPermission(peer, MODERATE_ROOM)) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const lowerPeer = this._peers[peerId]; + + if (!lowerPeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(lowerPeer.socket, 'moderator:lowerHand'); + + cb(); + + break; + } + default: { logger.error('unknown request.method "%s"', request.method); @@ -1379,6 +1519,8 @@ class Room extends EventEmitter producer.id ); + const router = this._mediasoupRouters.get(producerPeer.routerId); + // Optimization: // - Create the server-side Consumer. If video, do it paused. // - Tell its Peer about it and wait for its response. @@ -1389,7 +1531,7 @@ class Room extends EventEmitter // NOTE: Don't create the Consumer if the remote Peer cannot consume it. if ( !consumerPeer.rtpCapabilities || - !this._mediasoupRouter.canConsume( + !router.canConsume( { producerId : producer.id, rtpCapabilities : consumerPeer.rtpCapabilities @@ -1515,16 +1657,54 @@ class Room extends EventEmitter } } + _hasPermission(peer, permission) + { + const hasPermission = peer.roles.some((role) => + roomPermissions[permission].includes(role) + ); + + if (hasPermission) + return true; + + // Allow if config is set, and no one is present + if ( + roomAllowWhenRoleMissing.includes(permission) && + this._getPeersWithPermission(permission).length === 0 + ) + return true; + + return false; + } + + _hasAccess(peer, access) + { + return peer.roles.some((role) => roomAccess[access].includes(role)); + } + /** * Helper to get the list of joined peers. */ - _getJoinedPeers({ excludePeer = undefined } = {}) + _getJoinedPeers(excludePeer = undefined) { return Object.values(this._peers) .filter((peer) => peer.joined && peer !== excludePeer); } - _getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true }) + _getAllowedPeers(permission = null, excludePeer = undefined, joined = true) + { + const peers = this._getPeersWithPermission(permission, excludePeer, joined); + + if (peers.length > 0) + return peers; + + // Allow if config is set, and no one is present + if (roomAllowWhenRoleMissing.includes(permission)) + return Object.values(this._peers); + + return peers; + } + + _getPeersWithPermission(permission = null, excludePeer = undefined, joined = true) { return Object.values(this._peers) .filter( @@ -1532,7 +1712,7 @@ class Room extends EventEmitter peer.joined === joined && peer !== excludePeer && peer.roles.some( - (role) => permission.includes(role) + (role) => roomPermissions[permission].includes(role) ) ); } @@ -1601,6 +1781,84 @@ class Room extends EventEmitter socket.emit('notification', { method, data }); } } + + async _pipeProducersToNewRouter() + { + const peersToPipe = + Object.values(this._peers) + .filter((peer) => peer.routerId !== this._currentRouter.id); + + for (const peer of peersToPipe) + { + const srcRouter = this._mediasoupRouters.get(peer.routerId); + + for (const producerId of peer.producers.keys()) + { + await srcRouter.pipeToRouter({ + producerId, + router : this._currentRouter + }); + } + } + } + + async _getRouterId() + { + if (this._currentRouter) + { + const routerLoad = + Object.values(this._peers) + .filter((peer) => peer.routerId === this._currentRouter.id).length; + + if (routerLoad >= ROUTER_SCALE_SIZE) + { + this._currentRouter = this._routerIterator.next().value; + + if (this._currentRouter) + { + await this._pipeProducersToNewRouter(); + + return this._currentRouter.id; + } + } + else + { + return this._currentRouter.id; + } + } + + return this._getLeastLoadedRouter(); + } + + // Returns an array of router ids we need to pipe to + _getRoutersToPipeTo(originRouterId) + { + return Object.values(this._peers) + .map((peer) => peer.routerId) + .filter((routerId, index, self) => + routerId !== originRouterId && self.indexOf(routerId) === index + ); + } + + _getLeastLoadedRouter() + { + let load = Infinity; + let id; + + for (const routerId of this._mediasoupRouters.keys()) + { + const routerLoad = + Object.values(this._peers).filter((peer) => peer.routerId === routerId).length; + + if (routerLoad < load) + { + id = routerId; + load = routerLoad; + } + } + + return id; + } } module.exports = Room; diff --git a/server/package.json b/server/package.json index ef96f36..d407d03 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "license": "MIT", "main": "lib/index.js", "scripts": { - "start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js", + "start": "node server.js", "connect": "node connect.js", "lint": "eslint -c .eslintrc.json --ext .js *.js lib/" }, diff --git a/server/permissions.js b/server/permissions.js new file mode 100644 index 0000000..dd3bdbb --- /dev/null +++ b/server/permissions.js @@ -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' +}; \ No newline at end of file diff --git a/server/server.js b/server/server.js index dbf9a8e..27dc12c 100755 --- a/server/server.js +++ b/server/server.js @@ -35,6 +35,7 @@ const RedisStore = require('connect-redis')(expressSession); const sharedSession = require('express-socket.io-session'); const interactiveServer = require('./lib/interactiveServer'); const promExporter = require('./lib/promExporter'); +const { v4: uuidv4 } = require('uuid'); /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); @@ -55,10 +56,6 @@ if ('StatusLogger' in config) // @type {Array} const mediasoupWorkers = []; -// Index of next mediasoup Worker to use. -// @type {Number} -let nextMediasoupWorkerIdx = 0; - // Map of Room instances indexed by roomId. const rooms = new Map(); @@ -130,50 +127,58 @@ let oidcStrategy; async function run() { - // Open the interactive server. - await interactiveServer(rooms, peers); - - // start Prometheus exporter - if (config.prometheus) + try { - await promExporter(rooms, peers, config.prometheus); - } + // Open the interactive server. + await interactiveServer(rooms, peers); - if (typeof(config.auth) === 'undefined') - { - logger.warn('Auth is not configured properly!'); - } - else - { - await setupAuth(); - } - - // Run a mediasoup Worker. - await runMediasoupWorkers(); - - // Run HTTPS server. - await runHttpsServer(); - - // Run WebSocketServer. - await runWebSocketServer(); - - // Log rooms status every 30 seconds. - setInterval(() => - { - for (const room of rooms.values()) + // start Prometheus exporter + if (config.prometheus) { - room.logStatus(); + await promExporter(rooms, peers, config.prometheus); } - }, 120000); - // check for deserted rooms - setInterval(() => - { - for (const room of rooms.values()) + if (typeof(config.auth) === 'undefined') { - room.checkEmpty(); + logger.warn('Auth is not configured properly!'); } - }, 10000); + else + { + await setupAuth(); + } + + // Run a mediasoup Worker. + await runMediasoupWorkers(); + + // Run HTTPS server. + await runHttpsServer(); + + // Run WebSocketServer. + await runWebSocketServer(); + + const errorHandler = (err, req, res, next) => + { + const trackingId = uuidv4(); + + res.status(500).send( + `

Internal Server Error

+

If you report this error, please also report this + tracking ID which makes it possible to locate your session + in the logs which are available to the system administrator: + ${trackingId}

` + ); + 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() @@ -181,8 +186,8 @@ function statusLog() if (statusLogger) { statusLogger.log({ - rooms : rooms.size, - peers : peers.size + rooms : rooms, + peers : peers }); } } @@ -363,38 +368,45 @@ async function setupAuth() app.get( '/auth/callback', passport.authenticate('oidc', { failureRedirect: '/auth/login' }), - async (req, res) => + async (req, res, next) => { - const state = JSON.parse(base64.decode(req.query.state)); - - const { peerId, roomId } = state; - - req.session.peerId = peerId; - req.session.roomId = roomId; - - let peer = peers.get(peerId); - - if (!peer) // User has no socket session yet, make temporary - peer = new Peer({ id: peerId, roomId }); - - if (peer.roomId !== roomId) // The peer is mischievous - throw new Error('peer authenticated with wrong room'); - - if (typeof config.userMapping === 'function') + try { - await config.userMapping({ - peer, - roomId, - userinfo : req.user._userinfo - }); + const state = JSON.parse(base64.decode(req.query.state)); + + const { peerId, roomId } = state; + + req.session.peerId = peerId; + req.session.roomId = roomId; + + let peer = peers.get(peerId); + + if (!peer) // User has no socket session yet, make temporary + peer = new Peer({ id: peerId, roomId }); + + if (peer.roomId !== roomId) // The peer is mischievous + throw new Error('peer authenticated with wrong room'); + + if (typeof config.userMapping === 'function') + { + await config.userMapping({ + peer, + roomId, + userinfo : req.user._userinfo + }); + } + + peer.authenticated = true; + + res.send(loginHelper({ + displayName : peer.displayName, + picture : peer.picture + })); + } + catch (error) + { + return next(error); } - - peer.authenticated = true; - - res.send(loginHelper({ - displayName : peer.displayName, - picture : peer.picture - })); } ); } @@ -581,7 +593,8 @@ async function runWebSocketServer() { logger.error('room creation or room joining failed [error:"%o"]', error); - socket.disconnect(true); + if (socket) + socket.disconnect(true); return; }); @@ -619,19 +632,6 @@ async function runMediasoupWorkers() } } -/** - * Get next mediasoup Worker. - */ -function getMediasoupWorker() -{ - const worker = mediasoupWorkers[nextMediasoupWorkerIdx]; - - if (++nextMediasoupWorkerIdx === mediasoupWorkers.length) - nextMediasoupWorkerIdx = 0; - - return worker; -} - /** * Get a Room instance (or create one if it does not exist). */ @@ -644,9 +644,9 @@ async function getOrCreateRoom({ roomId }) { logger.info('creating a new Room [roomId:"%s"]', roomId); - const mediasoupWorker = getMediasoupWorker(); + // const mediasoupWorker = getMediasoupWorker(); - room = await Room.create({ mediasoupWorker, roomId }); + room = await Room.create({ mediasoupWorkers, roomId }); rooms.set(roomId, room);