Merge branch 'develop' into feat-network-indicator

auto_join_3.3
Roman Drozd 2020-05-12 21:04:29 +02:00
commit 7b8ea2c756
92 changed files with 7320 additions and 2182 deletions

1
CONTRIBUTING.md 100644
View File

@ -0,0 +1 @@
Source code contributions should pass static code analysis as performed by `npm run lint` in `server` and `app` respectively.

View File

@ -62,14 +62,6 @@ OR
## Configure multiparty-meeting servers ## Configure multiparty-meeting servers
### App config
mm/configs/app/config.js
``` js
multipartyServer : 'meet.example.com',
```
### Server config ### Server config
mm/configs/server/config.js mm/configs/server/config.js

21
LICENSE.md 100644
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 GÉANT Association
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -134,7 +134,7 @@ This started as a fork of the [work](https://github.com/versatica/mediasoup-demo
## License ## License
MIT MIT License (see `LICENSE.md`)
Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project. Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project.

View File

@ -1,6 +1,6 @@
{ {
"name": "multiparty-meeting", "name": "multiparty-meeting",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting service", "description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
@ -27,6 +27,7 @@
"react": "^16.10.2", "react": "^16.10.2",
"react-cookie-consent": "^2.5.0", "react-cookie-consent": "^2.5.0",
"react-dom": "^16.10.2", "react-dom": "^16.10.2",
"react-flip-toolkit": "^7.0.9",
"react-intl": "^3.4.0", "react-intl": "^3.4.0",
"react-redux": "^7.1.1", "react-redux": "^7.1.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
@ -60,6 +61,7 @@
], ],
"devDependencies": { "devDependencies": {
"electron": "^7.1.1", "electron": "^7.1.1",
"eslint-plugin-react": "^7.19.0",
"foreman": "^3.0.1", "foreman": "^3.0.1",
"redux-mock-store": "^1.5.3" "redux-mock-store": "^1.5.3"
} }

View File

@ -25,6 +25,7 @@ var config =
{ scaleResolutionDownBy: 2 }, { scaleResolutionDownBy: 2 },
{ scaleResolutionDownBy: 1 } { scaleResolutionDownBy: 1 }
], ],
/** /**
* White listing browsers that support audio output device selection. * White listing browsers that support audio output device selection.
* It is not yet fully implemented in Firefox. * It is not yet fully implemented in Firefox.
@ -41,9 +42,32 @@ var config =
{ {
tcp : true tcp : true
}, },
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, lastN : 4,
mobileLastN : 1, mobileLastN : 1,
background : 'images/background.jpg', // Highest number of speakers user can select
maxLastN : 5,
// If truthy, users can NOT change number of speakers visible
lockLastN : false,
// Add file and uncomment for adding logo to appbar // Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg', // logo : 'images/logo.svg',
title : 'Multiparty meeting', title : 'Multiparty meeting',

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pleaceholder for Privacy Statetment/Policy, AUP</title>
</head>
<body>
<h1>Privacy Statement</h1>
<h1>Privacy Policy</h1>
<h1>Acceptable use policy (AUP)</h1>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -225,10 +225,7 @@ export default class ScreenShare
return new DisplayMediaScreenShare(); return new DisplayMediaScreenShare();
} }
case 'chrome': case 'chrome':
{ case 'edge':
return new DisplayMediaScreenShare();
}
case 'msedge':
{ {
return new DisplayMediaScreenShare(); return new DisplayMediaScreenShare();
} }

View File

@ -95,6 +95,15 @@ export default class Spotlights extends EventEmitter
}); });
} }
clearSpotlights()
{
this._started = false;
this._peerList = [];
this._selectedSpotlights = [];
this._currentSpotlights = [];
}
_newPeer(id) _newPeer(id)
{ {
logger.debug( logger.debug(

View File

@ -42,8 +42,8 @@ beforeEach(() =>
loggedIn : false, loggedIn : false,
loginEnabled : true, loginEnabled : true,
picture : null, picture : null,
raiseHand : false, raisedHand : false,
raiseHandInProgress : false, raisedHandInProgress : false,
screenShareInProgress : false, screenShareInProgress : false,
webcamDevices : null, webcamDevices : null,
webcamInProgress : false webcamInProgress : false

View File

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

View File

@ -63,9 +63,9 @@ export const setWebcamDevices = (devices) =>
payload : { devices } payload : { devices }
}); });
export const setMyRaiseHandState = (flag) => export const setRaisedHand = (flag) =>
({ ({
type : 'SET_MY_RAISE_HAND_STATE', type : 'SET_RAISED_HAND',
payload : { flag } payload : { flag }
}); });
@ -93,9 +93,9 @@ export const setScreenShareInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setMyRaiseHandStateInProgress = (flag) => export const setRaisedHandInProgress = (flag) =>
({ ({
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS', type : 'SET_RAISED_HAND_IN_PROGRESS',
payload : { flag } payload : { flag }
}); });

View File

@ -10,6 +10,11 @@ export const removePeer = (peerId) =>
payload : { peerId } payload : { peerId }
}); });
export const clearPeers = () =>
({
type : 'CLEAR_PEERS'
});
export const setPeerDisplayName = (displayName, peerId) => export const setPeerDisplayName = (displayName, peerId) =>
({ ({
type : 'SET_PEER_DISPLAY_NAME', type : 'SET_PEER_DISPLAY_NAME',
@ -34,10 +39,16 @@ export const setPeerScreenInProgress = (peerId, flag) =>
payload : { peerId, flag } payload : { peerId, flag }
}); });
export const setPeerRaiseHandState = (peerId, raiseHandState) => export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) =>
({ ({
type : 'SET_PEER_RAISE_HAND_STATE', type : 'SET_PEER_RAISED_HAND',
payload : { peerId, raiseHandState } payload : { peerId, raisedHand, raisedHandTimestamp }
});
export const setPeerRaisedHandInProgress = (peerId, flag) =>
({
type : 'SET_PEER_RAISED_HAND_IN_PROGRESS',
payload : { peerId, flag }
}); });
export const setPeerPicture = (peerId, picture) => export const setPeerPicture = (peerId, picture) =>
@ -63,3 +74,15 @@ export const setPeerKickInProgress = (peerId, flag) =>
type : 'SET_PEER_KICK_IN_PROGRESS', type : 'SET_PEER_KICK_IN_PROGRESS',
payload : { peerId, flag } payload : { peerId, flag }
}); });
export const setMutePeerInProgress = (peerId, flag) =>
({
type : 'STOP_PEER_AUDIO_IN_PROGRESS',
payload : { peerId, flag }
});
export const setStopPeerVideoInProgress = (peerId, flag) =>
({
type : 'STOP_PEER_VIDEO_IN_PROGRESS',
payload : { peerId, flag }
});

View File

@ -40,6 +40,12 @@ export const setSignInRequired = (signInRequired) =>
payload : { signInRequired } payload : { signInRequired }
}); });
export const setOverRoomLimit = (overRoomLimit) =>
({
type : 'SET_OVER_ROOM_LIMIT',
payload : { overRoomLimit }
});
export const setAccessCode = (accessCode) => export const setAccessCode = (accessCode) =>
({ ({
type : 'SET_ACCESS_CODE', type : 'SET_ACCESS_CODE',
@ -52,13 +58,37 @@ export const setJoinByAccessCode = (joinByAccessCode) =>
payload : { joinByAccessCode } payload : { joinByAccessCode }
}); });
export const setSettingsOpen = ({ settingsOpen }) => export const setSettingsOpen = (settingsOpen) =>
({ ({
type : 'SET_SETTINGS_OPEN', type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen } payload : { settingsOpen }
}); });
export const setLockDialogOpen = ({ lockDialogOpen }) => export const setExtraVideoOpen = (extraVideoOpen) =>
({
type : 'SET_EXTRA_VIDEO_OPEN',
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',
payload : { tab }
});
export const setLockDialogOpen = (lockDialogOpen) =>
({ ({
type : 'SET_LOCK_DIALOG_OPEN', type : 'SET_LOCK_DIALOG_OPEN',
payload : { lockDialogOpen } payload : { lockDialogOpen }
@ -100,6 +130,11 @@ export const setSpotlights = (spotlights) =>
payload : { spotlights } payload : { spotlights }
}); });
export const clearSpotlights = () =>
({
type : 'CLEAR_SPOTLIGHTS'
});
export const toggleJoined = () => export const toggleJoined = () =>
({ ({
type : 'TOGGLE_JOINED' type : 'TOGGLE_JOINED'
@ -111,6 +146,12 @@ export const toggleConsumerFullscreen = (consumerId) =>
payload : { consumerId } payload : { consumerId }
}); });
export const setLobbyPeersPromotionInProgress = (flag) =>
({
type : 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS',
payload : { flag }
});
export const setMuteAllInProgress = (flag) => export const setMuteAllInProgress = (flag) =>
({ ({
type : 'MUTE_ALL_IN_PROGRESS', type : 'MUTE_ALL_IN_PROGRESS',
@ -141,14 +182,14 @@ export const setClearFileSharingInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setUserRoles = (userRoles) => export const setRoomPermissions = (roomPermissions) =>
({ ({
type : 'SET_USER_ROLES', type : 'SET_ROOM_PERMISSIONS',
payload : { userRoles } payload : { roomPermissions }
}); });
export const setPermissionsFromRoles = (permissionsFromRoles) => export const setAllowWhenRoleMissing = (allowWhenRoleMissing) =>
({ ({
type : 'SET_PERMISSIONS_FROM_ROLES', type : 'SET_ALLOW_WHEN_ROLE_MISSING',
payload : { permissionsFromRoles } payload : { allowWhenRoleMissing }
}); });

View File

@ -38,6 +38,70 @@ export const togglePermanentTopBar = () =>
type : 'TOGGLE_PERMANENT_TOPBAR' type : 'TOGGLE_PERMANENT_TOPBAR'
}); });
export const toggleButtonControlBar = () =>
({
type : 'TOGGLE_BUTTON_CONTROL_BAR'
});
export const toggleDrawerOverlayed = () =>
({
type : 'TOGGLE_DRAWER_OVERLAYED'
});
export const toggleShowNotifications = () =>
({
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'
});
export const toggleNotificationSounds = () =>
({
type : 'TOGGLE_NOTIFICATION_SOUNDS'
});
export const setLastN = (lastN) => export const setLastN = (lastN) =>
({ ({
type : 'SET_LAST_N', type : 'SET_LAST_N',

View File

@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
@ -27,6 +29,7 @@ const ListLobbyPeer = (props) =>
const { const {
roomClient, roomClient,
peer, peer,
promotionInProgress,
canPromote, canPromote,
classes classes
} = props; } = props;
@ -55,7 +58,12 @@ const ListLobbyPeer = (props) =>
})} })}
> >
<IconButton <IconButton
disabled={!canPromote || peer.promotionInProgress} disabled={
!canPromote ||
peer.promotionInProgress ||
promotionInProgress
}
color='primary'
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -74,29 +82,37 @@ ListLobbyPeer.propTypes =
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peer : PropTypes.object.isRequired, peer : PropTypes.object.isRequired,
promotionInProgress : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired, canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state, { id }) => const makeMapStateToProps = (initialState, { id }) =>
{
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
{ {
return { return {
peer : state.lobbyPeers[id], peer : state.lobbyPeers[id],
canPromote : promotionInProgress : state.room.lobbyPeersPromotionInProgress,
state.me.roles.some((role) => canPromote : hasPermission(state)
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.peers === next.peers && // For checking permissions
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
lobbyPeersKeySelector lobbyPeersKeySelector,
makePermissionSelector
} from '../../Selectors'; } from '../../Selectors';
import { permissions } from '../../../permissions';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
@ -15,14 +17,6 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
// import FormLabel from '@material-ui/core/FormLabel';
// import FormControl from '@material-ui/core/FormControl';
// import FormGroup from '@material-ui/core/FormGroup';
// import FormControlLabel from '@material-ui/core/FormControlLabel';
// import Checkbox from '@material-ui/core/Checkbox';
// import InputLabel from '@material-ui/core/InputLabel';
// import OutlinedInput from '@material-ui/core/OutlinedInput';
// import Switch from '@material-ui/core/Switch';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader'; import ListSubheader from '@material-ui/core/ListSubheader';
import ListLobbyPeer from './ListLobbyPeer'; import ListLobbyPeer from './ListLobbyPeer';
@ -59,11 +53,11 @@ const styles = (theme) =>
}); });
const LockDialog = ({ const LockDialog = ({
// roomClient, roomClient,
room, room,
handleCloseLockDialog, handleCloseLockDialog,
// handleAccessCode,
lobbyPeers, lobbyPeers,
canPromote,
classes classes
}) => }) =>
{ {
@ -71,7 +65,7 @@ const LockDialog = ({
<Dialog <Dialog
className={classes.root} className={classes.root}
open={room.lockDialogOpen} open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })} onClose={() => handleCloseLockDialog(false)}
classes={{ classes={{
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
@ -82,54 +76,6 @@ const LockDialog = ({
defaultMessage='Lobby administration' defaultMessage='Lobby administration'
/> />
</DialogTitle> </DialogTitle>
{/*
<FormControl component='fieldset' className={classes.formControl}>
<FormLabel component='legend'>Room lock</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Switch checked={room.locked} onChange={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
/>}
label='Lock'
/>
TODO: access code
<FormControlLabel disabled={ room.locked ? false : true }
control={
<Checkbox checked={room.joinByAccessCode}
onChange={
(event) => roomClient.setJoinByAccessCode(event.target.checked)
}
/>}
label='Join by Access code'
/>
<InputLabel htmlFor='access-code-input' />
<OutlinedInput
disabled={ room.locked ? false : true }
id='acces-code-input'
label='Access code'
labelWidth={0}
variant='outlined'
value={room.accessCode}
onChange={(event) => handleAccessCode(event.target.value)}
>
</OutlinedInput>
<Button onClick={() => roomClient.setAccessCode(room.accessCode)} color='primary'>
save
</Button>
</FormGroup>
</FormControl>
*/}
{ lobbyPeers.length > 0 ? { lobbyPeers.length > 0 ?
<List <List
dense dense
@ -160,7 +106,21 @@ const LockDialog = ({
</DialogContent> </DialogContent>
} }
<DialogActions> <DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'> <Button
disabled={
lobbyPeers.length === 0 ||
!canPromote ||
room.lobbyPeersPromotionInProgress
}
onClick={() => roomClient.promoteAllLobbyPeers()}
color='primary'
>
<FormattedMessage
id='label.promoteAllPeers'
defaultMessage='Promote all'
/>
</Button>
<Button onClick={() => handleCloseLockDialog(false)} color='primary'>
<FormattedMessage <FormattedMessage
id='label.close' id='label.close'
defaultMessage='Close' defaultMessage='Close'
@ -173,41 +133,47 @@ const LockDialog = ({
LockDialog.propTypes = LockDialog.propTypes =
{ {
// roomClient : PropTypes.any.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
handleCloseLockDialog : PropTypes.func.isRequired, handleCloseLockDialog : PropTypes.func.isRequired,
handleAccessCode : PropTypes.func.isRequired, handleAccessCode : PropTypes.func.isRequired,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
canPromote : PropTypes.bool,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
room : state.room, room : state.room,
lobbyPeers : lobbyPeersKeySelector(state) lobbyPeers : lobbyPeersKeySelector(state),
canPromote : hasPermission(state)
}; };
}; };
return mapStateToProps;
};
const mapDispatchToProps = { const mapDispatchToProps = {
handleCloseLockDialog : roomActions.setLockDialogOpen, handleCloseLockDialog : roomActions.setLockDialogOpen,
handleAccessCode : roomActions.setAccessCode handleAccessCode : roomActions.setAccessCode
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.locked === next.room.locked && prev.room === next.room &&
prev.room.joinByAccessCode === next.room.joinByAccessCode && prev.me.roles === next.me.roles &&
prev.room.accessCode === next.room.accessCode && prev.peers === next.peers &&
prev.room.code === next.room.code &&
prev.room.lockDialogOpen === next.room.lockDialogOpen &&
prev.room.codeHidden === next.room.codeHidden &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );
} }

View File

@ -86,7 +86,7 @@ const DialogTitle = withStyles(styles)((props) =>
return ( return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}> <MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography> <Typography variant='h5'>{children}</Typography>
</MuiDialogTitle> </MuiDialogTitle>
); );
@ -125,7 +125,7 @@ const ChooseRoom = ({
}} }}
> >
<DialogTitle> <DialogTitle>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config.title ? window.config.title : 'Multiparty meeting' }
<hr /> <hr />
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>

View File

@ -1,6 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors'; import {
meProducersSelector,
makePermissionSelector
} from '../Selectors';
import { permissions } from '../../permissions';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -10,6 +14,7 @@ import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume'; import Volume from './Volume';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic'; import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff'; import MicOffIcon from '@material-ui/icons/MicOff';
@ -59,12 +64,47 @@ const styles = (theme) =>
margin : theme.spacing(1), margin : theme.spacing(1),
pointerEvents : 'auto' 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 : viewContainer :
{ {
position : 'relative', position : 'relative',
width : '100%', width : '100%',
height : '100%' height : '100%'
}, },
meTag :
{
position : 'absolute',
float : 'left',
top : '50%',
left : '50%',
transform : 'translate(-50%, -50%)',
color : 'rgba(255, 255, 255, 0.5)',
fontSize : '7em',
zIndex : 30,
margin : 0,
opacity : 0,
transition : 'opacity 0.1s ease-in-out',
'&.hover' :
{
opacity : 1
},
'&.smallContainer' :
{
fontSize : '3em'
}
},
controls : controls :
{ {
position : 'absolute', position : 'absolute',
@ -78,36 +118,29 @@ const styles = (theme) =>
zIndex : 21, zIndex : 21,
touchAction : 'none', touchAction : 'none',
pointerEvents : 'none', pointerEvents : 'none',
'& p' : '&.hide' :
{ {
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', transition : 'opacity 0.1s ease-in-out',
opacity : 0
},
'&.hover' : '&.hover' :
{ {
opacity : 1 opacity : 1
} }
}
}, },
ptt : ptt :
{ {
position : 'absolute', position : 'absolute',
float : 'left', float : 'left',
top : '10%', top : '25%',
left : '50%', left : '50%',
transform : 'translate(-50%, 0%)', transform : 'translate(-50%, 0%)',
color : 'rgba(255, 255, 255, 0.7)', color : 'rgba(255, 255, 255, 0.7)',
fontSize : '2vs', fontSize : '1.3em',
backgroundColor : 'rgba(255, 0, 0, 0.5)', backgroundColor : 'rgba(255, 0, 0, 0.9)',
margin : '4px', margin : '4px',
padding : '15px', padding : theme.spacing(2),
zIndex : 31,
borderRadius : '20px', borderRadius : '20px',
textAlign : 'center', textAlign : 'center',
opacity : 0, opacity : 0,
@ -135,11 +168,12 @@ const Me = (props) =>
activeSpeaker, activeSpeaker,
spacing, spacing,
style, style,
smallButtons, smallContainer,
advancedMode, advancedMode,
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer, screenProducer,
extraVideoProducers,
canShareScreen, canShareScreen,
classes, classes,
transports transports
@ -257,6 +291,28 @@ const Me = (props) =>
'margin' : spacing 'margin' : spacing
}; };
let audioScore = null;
if (micProducer && micProducer.score)
{
audioScore =
micProducer.score.reduce(
(prev, curr) =>
(prev.score < curr.score ? prev : curr)
);
}
let videoScore = null;
if (webcamProducer && webcamProducer.score)
{
videoScore =
webcamProducer.score.reduce(
(prev, curr) =>
(prev.score < curr.score ? prev : curr)
);
}
return ( return (
<React.Fragment> <React.Fragment>
<div <div
@ -289,9 +345,39 @@ const Me = (props) =>
}} }}
style={spacingStyle} style={spacingStyle}
> >
{ me.browser.platform !== 'mobile' &&
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
}
<div className={classes.viewContainer} style={style}> <div className={classes.viewContainer} style={style}>
<p className={
classnames(
classes.meTag,
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
{ !settings.buttonControlBar &&
<div <div
className={classes.controls} className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -312,26 +398,37 @@ const Me = (props) =>
}, 2000); }, 2000);
}} }}
> >
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
<React.Fragment> <React.Fragment>
<Tooltip title={micTip} placement='left'> <Tooltip title={micTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.smallContainer}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</IconButton>
</div>
:
<div> <div>
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@ -341,7 +438,7 @@ const Me = (props) =>
className={classes.fab} className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress} disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'} color={micState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
if (micState === 'off') if (micState === 'off')
@ -359,8 +456,35 @@ const Me = (props) =>
} }
</Fab> </Fab>
</div> </div>
}
</Tooltip> </Tooltip>
<Tooltip title={webcamTip} placement='left'> <Tooltip title={webcamTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</IconButton>
</div>
:
<div> <div>
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@ -370,7 +494,7 @@ const Me = (props) =>
className={classes.fab} className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress} disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'} color={webcamState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
webcamState === 'on' ? webcamState === 'on' ?
@ -385,9 +509,56 @@ const Me = (props) =>
} }
</Fab> </Fab>
</div> </div>
}
</Tooltip> </Tooltip>
{ me.browser.platform !== 'mobile' && { me.browser.platform !== 'mobile' &&
<Tooltip title={screenTip} placement='left'> <Tooltip title={screenTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.smallContainer}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color='primary'
size='small'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</IconButton>
</div>
:
<div> <div>
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@ -401,7 +572,7 @@ const Me = (props) =>
me.screenShareInProgress me.screenShareInProgress
} }
color={screenState === 'on' ? 'primary' : 'default'} color={screenState === 'on' ? 'primary' : 'default'}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
switch (screenState) switch (screenState)
@ -431,13 +602,16 @@ const Me = (props) =>
} }
</Fab> </Fab>
</div> </div>
}
</Tooltip> </Tooltip>
} }
</React.Fragment> </React.Fragment>
</div> </div>
}
<VideoView <VideoView
isMe isMe
VideoView
advancedMode={advancedMode} advancedMode={advancedMode}
peer={me} peer={me}
displayName={settings.displayName} displayName={settings.displayName}
@ -447,6 +621,8 @@ const Me = (props) =>
audioCodec={micProducer && micProducer.codec} audioCodec={micProducer && micProducer.codec}
videoCodec={webcamProducer && webcamProducer.codec} videoCodec={webcamProducer && webcamProducer.codec}
netInfo={transports && transports} netInfo={transports && transports}
audioScore={audioScore}
videoScore={videoScore}
onChangeDisplayName={(displayName) => onChangeDisplayName={(displayName) =>
{ {
roomClient.changeDisplayName(displayName); roomClient.changeDisplayName(displayName);
@ -456,6 +632,139 @@ const Me = (props) =>
</VideoView> </VideoView>
</div> </div>
</div> </div>
{ extraVideoProducers.map((producer) =>
{
return (
<div key={producer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<p className={
classnames(
classes.meTag,
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<Tooltip title={webcamTip} placement='left'>
{ smallContainer ?
<div>
<IconButton
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
size='small'
color='primary'
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</IconButton>
</div>
:
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</Fab>
</div>
}
</Tooltip>
</div>
<VideoView
isMe
advancedMode={advancedMode}
peer={me}
displayName={settings.displayName}
showPeerInfo
videoTrack={producer && producer.track}
videoVisible={videoVisible}
videoCodec={producer && producer.codec}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/>
</div>
</div>
);
})}
{ screenProducer && { screenProducer &&
<div <div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -481,36 +790,18 @@ const Me = (props) =>
style={spacingStyle} style={spacingStyle}
> >
<div className={classes.viewContainer} style={style}> <div className={classes.viewContainer} style={style}>
<div <p className={
className={classes.controls} classnames(
onMouseOver={() => setHover(true)} classes.meTag,
onMouseOut={() => setHover(false)} hover ? 'hover' : null,
onTouchStart={() => smallContainer ? 'smallContainer' : null
{ )}
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
> >
<p className={hover ? 'hover' : null}>
<FormattedMessage <FormattedMessage
id='room.me' id='room.me'
defaultMessage='ME' defaultMessage='ME'
/> />
</p> </p>
</div>
<VideoView <VideoView
isMe isMe
@ -538,16 +829,20 @@ Me.propTypes =
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
spacing : PropTypes.number, spacing : PropTypes.number,
style : PropTypes.object, style : PropTypes.object,
smallButtons : PropTypes.bool, smallContainer : PropTypes.bool,
canShareScreen : PropTypes.bool.isRequired, canShareScreen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired, theme : PropTypes.object.isRequired,
transports : PropTypes.object.isRequired transports : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
@ -555,26 +850,27 @@ const mapStateToProps = (state) =>
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId, activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen : canShareScreen : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_SCREEN.includes(role)),
transports : state.transports transports : state.transports
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me === next.me && prev.me === next.me &&
prev.peers === next.peers &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings && prev.settings === next.settings,
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.transports === next.transports prev.transports === next.transports
); );
} }

View File

@ -12,8 +12,9 @@ import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
import MicIcon from '@material-ui/icons/Mic'; import IconButton from '@material-ui/core/IconButton';
import MicOffIcon from '@material-ui/icons/MicOff'; import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import NewWindowIcon from '@material-ui/icons/OpenInNew'; import NewWindowIcon from '@material-ui/icons/OpenInNew';
import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen';
import Volume from './Volume'; import Volume from './Volume';
@ -59,6 +60,19 @@ const styles = (theme) =>
{ {
margin : theme.spacing(1) 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 : viewContainer :
{ {
position : 'relative', position : 'relative',
@ -125,11 +139,12 @@ const Peer = (props) =>
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer, screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen, toggleConsumerFullscreen,
toggleConsumerWindow, toggleConsumerWindow,
spacing, spacing,
style, style,
smallButtons, smallContainer,
windowConsumer, windowConsumer,
classes, classes,
theme theme
@ -234,16 +249,16 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
<Fab <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'device.muteAudio', id : 'device.muteAudio',
defaultMessage : 'Mute audio' defaultMessage : 'Mute audio'
})} })}
className={classes.fab} className={classes.smallContainer}
disabled={!micConsumer} disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'} color='primary'
size={smallButtons ? 'small' : 'large'} size='small'
onClick={() => onClick={() =>
{ {
micEnabled ? micEnabled ?
@ -252,12 +267,35 @@ const Peer = (props) =>
}} }}
> >
{ micEnabled ? { micEnabled ?
<MicIcon /> <VolumeUpIcon />
: :
<MicOffIcon /> <VolumeOffIcon />
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
} }
</Fab> </Fab>
</div> }
</Tooltip> </Tooltip>
{ browser.platform !== 'mobile' && { browser.platform !== 'mobile' &&
@ -268,7 +306,27 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.newWindow', id : 'label.newWindow',
@ -279,7 +337,7 @@ const Peer = (props) =>
!videoVisible || !videoVisible ||
(windowConsumer === webcamConsumer.id) (windowConsumer === webcamConsumer.id)
} }
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
toggleConsumerWindow(webcamConsumer); toggleConsumerWindow(webcamConsumer);
@ -287,7 +345,7 @@ const Peer = (props) =>
> >
<NewWindowIcon /> <NewWindowIcon />
</Fab> </Fab>
</div> }
</Tooltip> </Tooltip>
} }
@ -298,7 +356,24 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> { smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.fullscreen', id : 'label.fullscreen',
@ -306,7 +381,7 @@ const Peer = (props) =>
})} })}
className={classes.fab} className={classes.fab}
disabled={!videoVisible} disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
toggleConsumerFullscreen(webcamConsumer); toggleConsumerFullscreen(webcamConsumer);
@ -314,11 +389,12 @@ const Peer = (props) =>
> >
<FullScreenIcon /> <FullScreenIcon />
</Fab> </Fab>
</div> }
</Tooltip> </Tooltip>
</div> </div>
<VideoView <VideoView
showQuality
advancedMode={advancedMode} advancedMode={advancedMode}
peer={peer} peer={peer}
displayName={peer.displayName} displayName={peer.displayName}
@ -340,6 +416,7 @@ const Peer = (props) =>
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'} videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
videoTrack={webcamConsumer && webcamConsumer.track} videoTrack={webcamConsumer && webcamConsumer.track}
videoVisible={videoVisible} videoVisible={videoVisible}
audioTrack={micConsumer && micConsumer.track}
audioCodec={micConsumer && micConsumer.codec} audioCodec={micConsumer && micConsumer.codec}
videoCodec={webcamConsumer && webcamConsumer.codec} videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null} audioScore={micConsumer ? micConsumer.score : null}
@ -350,6 +427,199 @@ const Peer = (props) =>
</div> </div>
</div> </div>
{ extraVideoConsumers.map((consumer) =>
{
return (
<div key={consumer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ browser.platform !== 'mobile' &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
}
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
}
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
showPeerInfo
consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={videoVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</div>
</div>
);
})}
{ screenConsumer && { screenConsumer &&
<div <div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -416,7 +686,6 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div>
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.newWindow', id : 'label.newWindow',
@ -427,7 +696,7 @@ const Peer = (props) =>
!screenVisible || !screenVisible ||
(windowConsumer === screenConsumer.id) (windowConsumer === screenConsumer.id)
} }
size={smallButtons ? 'small' : 'large'} size={smallContainer ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerWindow(screenConsumer); toggleConsumerWindow(screenConsumer);
@ -435,7 +704,6 @@ const Peer = (props) =>
> >
<NewWindowIcon /> <NewWindowIcon />
</Fab> </Fab>
</div>
</Tooltip> </Tooltip>
} }
@ -446,7 +714,6 @@ const Peer = (props) =>
})} })}
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div>
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.fullscreen', id : 'label.fullscreen',
@ -454,7 +721,7 @@ const Peer = (props) =>
})} })}
className={classes.fab} className={classes.fab}
disabled={!screenVisible} disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'} size={smallContainer ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerFullscreen(screenConsumer); toggleConsumerFullscreen(screenConsumer);
@ -462,10 +729,10 @@ const Peer = (props) =>
> >
<FullScreenIcon /> <FullScreenIcon />
</Fab> </Fab>
</div>
</Tooltip> </Tooltip>
</div> </div>
<VideoView <VideoView
showQuality
advancedMode={advancedMode} advancedMode={advancedMode}
videoContain videoContain
consumerSpatialLayers={ consumerSpatialLayers={
@ -507,12 +774,13 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string, windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
browser : PropTypes.object.isRequired, browser : PropTypes.object.isRequired,
spacing : PropTypes.number, spacing : PropTypes.number,
style : PropTypes.object, style : PropTypes.object,
smallButtons : PropTypes.bool, smallContainer : PropTypes.bool,
toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,

View File

@ -91,16 +91,6 @@ const SpeakerPeer = (props) =>
!screenConsumer.remotelyPaused !screenConsumer.remotelyPaused
); );
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
const spacingStyle = const spacingStyle =
{ {
'margin' : spacing 'margin' : spacing
@ -134,11 +124,27 @@ const SpeakerPeer = (props) =>
peer={peer} peer={peer}
displayName={peer.displayName} displayName={peer.displayName}
showPeerInfo showPeerInfo
videoTrack={webcamConsumer ? webcamConsumer.track : null} consumerSpatialLayers={webcamConsumer ? webcamConsumer.spatialLayers : null}
consumerTemporalLayers={webcamConsumer ? webcamConsumer.temporalLayers : null}
consumerCurrentSpatialLayer={
webcamConsumer ? webcamConsumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
webcamConsumer ? webcamConsumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
webcamConsumer ? webcamConsumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
webcamConsumer ? webcamConsumer.preferredTemporalLayer : null
}
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
videoTrack={webcamConsumer && webcamConsumer.track}
videoVisible={videoVisible} videoVisible={videoVisible}
videoProfile={videoProfile} audioCodec={micConsumer && micConsumer.codec}
audioCodec={micConsumer ? micConsumer.codec : null} videoCodec={webcamConsumer && webcamConsumer.codec}
videoCodec={webcamConsumer ? webcamConsumer.codec : null} audioScore={micConsumer ? micConsumer.score : null}
videoScore={webcamConsumer ? webcamConsumer.score : null}
> >
<Volume id={peer.id} /> <Volume id={peer.id} />
</VideoView> </VideoView>
@ -165,10 +171,29 @@ const SpeakerPeer = (props) =>
<VideoView <VideoView
advancedMode={advancedMode} advancedMode={advancedMode}
videoContain videoContain
videoTrack={screenConsumer ? screenConsumer.track : null} consumerSpatialLayers={
screenConsumer ? screenConsumer.spatialLayers : null
}
consumerTemporalLayers={
screenConsumer ? screenConsumer.temporalLayers : null
}
consumerCurrentSpatialLayer={
screenConsumer ? screenConsumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
screenConsumer ? screenConsumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
screenConsumer ? screenConsumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
screenConsumer ? screenConsumer.preferredTemporalLayer : null
}
videoMultiLayer={screenConsumer && screenConsumer.type !== 'simple'}
videoTrack={screenConsumer && screenConsumer.track}
videoVisible={screenVisible} videoVisible={screenVisible}
videoProfile={screenProfile} videoCodec={screenConsumer && screenConsumer.codec}
videoCodec={screenConsumer ? screenConsumer.codec : null} videoScore={screenConsumer ? screenConsumer.score : null}
/> />
</div> </div>
} }

View File

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

View File

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

View File

@ -0,0 +1,167 @@
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 Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
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'
}
},
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const ExtraVideo = ({
roomClient,
extraVideoOpen,
webcamDevices,
handleCloseExtraVideo,
classes
}) =>
{
const intl = useIntl();
const [ videoDevice, setVideoDevice ] = React.useState('');
const handleChange = (event) =>
{
setVideoDevice(event.target.value);
};
let videoDevices;
if (webcamDevices)
videoDevices = Object.values(webcamDevices);
else
videoDevices = [];
return (
<Dialog
open={extraVideoOpen}
onClose={() => handleCloseExtraVideo(false)}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.extraVideo'
defaultMessage='Extra video'
/>
</DialogTitle>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={videoDevice}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={videoDevices.length === 0}
onChange={handleChange}
>
{ videoDevices.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ videoDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
</form>
<DialogActions>
<Button onClick={() => roomClient.addExtraVideo(videoDevice)} color='primary'>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</Button>
</DialogActions>
</Dialog>
);
};
ExtraVideo.propTypes =
{
roomClient : PropTypes.object.isRequired,
extraVideoOpen : PropTypes.bool.isRequired,
webcamDevices : PropTypes.object,
handleCloseExtraVideo : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
webcamDevices : state.me.webcamDevices,
extraVideoOpen : state.room.extraVideoOpen
});
const mapDispatchToProps = {
handleCloseExtraVideo : roomActions.setExtraVideoOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.webcamDevices === next.me.webcamDevices &&
prev.room.extraVideoOpen === next.room.extraVideoOpen
);
}
}
)(withStyles(styles)(ExtraVideo)));

View File

@ -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 (
<Dialog
open={helpOpen}
onClose={() => { handleCloseHelp(false); }}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.help'
defaultMessage='Help'
/>
</DialogTitle>
<Tabs
className={classes.tabsHeader}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
>
<Tab
label={
intl.formatMessage({
id : 'room.shortcutKeys',
defaultMessage : 'Shortcut keys'
})
}
/>
</Tabs>
<DialogContent dividers='true'>
<DialogContentText>
{shortcuts.map((value, index) =>
{
return (
<div key={index} className={classes.shortcuts}>
<Paper className={classes.paper}>
{value.key}
</Paper>
<FormattedMessage
id={value.label}
defaultMessage={value.defaultMessage}
/>
</div>
);
})}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => { handleCloseHelp(false); }} color='primary'>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>
);
};
Help.propTypes =
{
roomClient : PropTypes.object.isRequired,
helpOpen : PropTypes.bool.isRequired,
handleCloseHelp : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
helpOpen : state.room.helpOpen
});
const mapDispatchToProps = {
handleCloseHelp : roomActions.setHelpOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.helpOpen === next.room.helpOpen
);
}
}
)(withStyles(styles)(Help)));

View File

@ -1,23 +1,32 @@
import React from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
lobbyPeersKeySelector, lobbyPeersKeySelector,
peersLengthSelector peersLengthSelector,
raisedHandsSelector,
makePermissionSelector
} from '../Selectors'; } from '../Selectors';
import { permissions } from '../../permissions';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions'; import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions'; import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Popover from '@material-ui/core/Popover';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge'; 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 AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
@ -26,11 +35,40 @@ import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People'; import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock'; import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen'; import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import MoreIcon from '@material-ui/icons/MoreVert';
import HelpIcon from '@material-ui/icons/Help';
import InfoIcon from '@material-ui/icons/Info';
const styles = (theme) => const styles = (theme) =>
({ ({
persistentDrawerOpen :
{
width : 'calc(100% - 30vw)',
marginLeft : '30vw',
[theme.breakpoints.down('lg')] :
{
width : 'calc(100% - 40vw)',
marginLeft : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : 'calc(100% - 50vw)',
marginLeft : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : 'calc(100% - 70vw)',
marginLeft : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : 'calc(100% - 90vw)',
marginLeft : '90vw'
}
},
menuButton : menuButton :
{ {
margin : 0, margin : 0,
@ -72,14 +110,34 @@ const styles = (theme) =>
display : 'block' display : 'block'
} }
}, },
actionButtons : sectionDesktop : {
{ display : 'none',
[theme.breakpoints.up('md')] : {
display : 'flex' display : 'flex'
}
},
sectionMobile : {
display : 'flex',
[theme.breakpoints.up('md')] : {
display : 'none'
}
}, },
actionButton : actionButton :
{ {
margin : theme.spacing(1, 0), margin : theme.spacing(1, 0),
padding : theme.spacing(0, 1) padding : theme.spacing(0, 1)
},
disabledButton :
{
margin : theme.spacing(1, 0)
},
green :
{
color : 'rgba(0, 153, 0, 1)'
},
moreAction :
{
margin : theme.spacing(0.5, 0, 0.5, 1.5)
} }
}); });
@ -118,12 +176,47 @@ const TopBar = (props) =>
{ {
const intl = useIntl(); const intl = useIntl();
const [ mobileMoreAnchorEl, setMobileMoreAnchorEl ] = useState(null);
const [ anchorEl, setAnchorEl ] = useState(null);
const [ currentMenu, setCurrentMenu ] = useState(null);
const handleExited = () =>
{
setCurrentMenu(null);
};
const handleMobileMenuOpen = (event) =>
{
setMobileMoreAnchorEl(event.currentTarget);
};
const handleMobileMenuClose = () =>
{
setMobileMoreAnchorEl(null);
};
const handleMenuOpen = (event, menu) =>
{
setAnchorEl(event.currentTarget);
setCurrentMenu(menu);
};
const handleMenuClose = () =>
{
setAnchorEl(null);
handleMobileMenuClose();
};
const { const {
roomClient, roomClient,
room, room,
peersLength, peersLength,
lobbyPeers, lobbyPeers,
permanentTopBar, permanentTopBar,
drawerOverlayed,
toolAreaOpen,
isMobile,
myPicture, myPicture,
loggedIn, loggedIn,
loginEnabled, loginEnabled,
@ -131,14 +224,22 @@ const TopBar = (props) =>
fullscreen, fullscreen,
onFullscreen, onFullscreen,
setSettingsOpen, setSettingsOpen,
setExtraVideoOpen,
setHelpOpen,
setAboutOpen,
setLockDialogOpen, setLockDialogOpen,
toggleToolArea, toggleToolArea,
openUsersTab, openUsersTab,
unread, unread,
canProduceExtraVideo,
canLock, canLock,
canPromote,
classes classes
} = props; } = props;
const isMenuOpen = Boolean(anchorEl);
const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
const lockTooltip = room.locked ? const lockTooltip = room.locked ?
intl.formatMessage({ intl.formatMessage({
id : 'tooltip.unLockRoom', id : 'tooltip.unLockRoom',
@ -173,9 +274,15 @@ const TopBar = (props) =>
}); });
return ( return (
<React.Fragment>
<AppBar <AppBar
position='fixed' position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide} className={classnames(
room.toolbarsVisible || permanentTopBar ?
classes.show : classes.hide,
!(isMobile || drawerOverlayed) && toolAreaOpen ?
classes.persistentDrawerOpen : null
)}
> >
<Toolbar> <Toolbar>
<PulsingBadge <PulsingBadge
@ -194,17 +301,36 @@ const TopBar = (props) =>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
</PulsingBadge> </PulsingBadge>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography <Typography
className={classes.title} className={classes.title}
variant='h6' variant='h6'
color='inherit' color='inherit'
noWrap noWrap
> >
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography> </Typography>
<div className={classes.grow} /> <div className={classes.grow} />
<div className={classes.actionButtons}> <div className={classes.sectionDesktop}>
<Tooltip
title={intl.formatMessage({
id : 'label.moreActions',
defaultMessage : 'More actions'
})}
>
<IconButton
aria-owns={
isMenuOpen &&
currentMenu === 'moreActions' ?
'material-appbar' : undefined
}
aria-haspopup='true'
onClick={(event) => handleMenuOpen(event, 'moreActions')}
color='inherit'
>
<ExtensionIcon />
</IconButton>
</Tooltip>
{ fullscreenEnabled && { fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}> <Tooltip title={fullscreenTooltip}>
<IconButton <IconButton
@ -265,6 +391,7 @@ const TopBar = (props) =>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={lockTooltip}> <Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.lockRoom', id : 'tooltip.lockRoom',
@ -291,6 +418,7 @@ const TopBar = (props) =>
<LockOpenIcon /> <LockOpenIcon />
} }
</IconButton> </IconButton>
</span>
</Tooltip> </Tooltip>
{ lobbyPeers.length > 0 && { lobbyPeers.length > 0 &&
<Tooltip <Tooltip
@ -299,12 +427,15 @@ const TopBar = (props) =>
defaultMessage : 'Show lobby' defaultMessage : 'Show lobby'
})} })}
> >
<span className={classes.disabledButton}>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.lobby', id : 'tooltip.lobby',
defaultMessage : 'Show lobby' defaultMessage : 'Show lobby'
})} })}
className={classes.actionButton}
color='inherit' color='inherit'
disabled={!canPromote}
onClick={() => setLockDialogOpen(!room.lockDialogOpen)} onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
> >
<PulsingBadge <PulsingBadge
@ -314,6 +445,7 @@ const TopBar = (props) =>
<SecurityIcon /> <SecurityIcon />
</PulsingBadge> </PulsingBadge>
</IconButton> </IconButton>
</span>
</Tooltip> </Tooltip>
} }
{ loginEnabled && { loginEnabled &&
@ -333,11 +465,21 @@ const TopBar = (props) =>
{ myPicture ? { myPicture ?
<Avatar src={myPicture} /> <Avatar src={myPicture} />
: :
<AccountCircle /> <AccountCircle className={loggedIn ? classes.green : null} />
} }
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} }
</div>
<div className={classes.sectionMobile}>
<IconButton
aria-haspopup='true'
onClick={handleMobileMenuOpen}
color='inherit'
>
<MoreIcon />
</IconButton>
</div>
<div className={classes.divider} /> <div className={classes.divider} />
<Button <Button
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@ -354,9 +496,272 @@ const TopBar = (props) =>
defaultMessage='Leave' defaultMessage='Leave'
/> />
</Button> </Button>
</div>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Popover
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
open={isMenuOpen}
onClose={handleMenuClose}
onExited={handleExited}
getContentAnchorEl={null}
>
{ currentMenu === 'moreActions' &&
<Paper>
<MenuItem
disabled={!canProduceExtraVideo}
onClick={() =>
{
handleMenuClose();
setExtraVideoOpen(!room.extraVideoOpen);
}}
>
<VideoCallIcon
aria-label={intl.formatMessage({
id : 'label.addVideo',
defaultMessage : 'Add video'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</p>
</MenuItem>
<MenuItem
onClick={() =>
{
handleMenuClose();
setHelpOpen(!room.helpOpen);
}}
>
<HelpIcon
aria-label={intl.formatMessage({
id : 'room.help',
defaultMessage : 'Help'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='room.help'
defaultMessage='Help'
/>
</p>
</MenuItem>
<MenuItem
onClick={() =>
{
handleMenuClose();
setAboutOpen(!room.aboutOpen);
}}
>
<InfoIcon
aria-label={intl.formatMessage({
id : 'room.about',
defaultMessage : 'About'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='room.about'
defaultMessage='About'
/>
</p>
</MenuItem>
</Paper>
}
</Popover>
<Menu
anchorEl={mobileMoreAnchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'bottom', horizontal: 'right' }}
open={isMobileMenuOpen}
onClose={handleMenuClose}
getContentAnchorEl={null}
>
{ loginEnabled &&
<MenuItem
aria-label={loginTooltip}
onClick={() =>
{
handleMenuClose();
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
{ loggedIn ?
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.logout'
defaultMessage='Log out'
/>
</p>
:
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.login'
defaultMessage='Log in'
/>
</p>
}
</MenuItem>
}
<MenuItem
aria-label={lockTooltip}
disabled={!canLock}
onClick={() =>
{
handleMenuClose();
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
{ room.locked ?
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.unLockRoom'
defaultMessage='Unlock room'
/>
</p>
:
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.lockRoom'
defaultMessage='Lock room'
/>
</p>
}
</MenuItem>
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
onClick={() =>
{
handleMenuClose();
setSettingsOpen(!room.settingsOpen);
}}
>
<SettingsIcon />
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.settings'
defaultMessage='Show settings'
/>
</p>
</MenuItem>
{ lobbyPeers.length > 0 &&
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
disabled={!canPromote}
onClick={() =>
{
handleMenuClose();
setLockDialogOpen(!room.lockDialogOpen);
}}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.lobby'
defaultMessage='Show lobby'
/>
</p>
</MenuItem>
}
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
onClick={() =>
{
handleMenuClose();
openUsersTab();
}}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.participants'
defaultMessage='Show participants'
/>
</p>
</MenuItem>
{ fullscreenEnabled &&
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
onClick={() =>
{
handleMenuClose();
onFullscreen();
}}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.enterFullscreen'
defaultMessage='Enter fullscreen'
/>
</p>
</MenuItem>
}
<MenuItem
aria-label={intl.formatMessage({
id : 'label.moreActions',
defaultMessage : 'Add video'
})}
onClick={(event) => handleMenuOpen(event, 'moreActions')}
>
<ExtensionIcon />
<p className={classes.moreAction}>
<FormattedMessage
id='label.moreActions'
defaultMessage='More actions'
/>
</p>
</MenuItem>
</Menu>
</React.Fragment>
); );
}; };
@ -364,9 +769,12 @@ TopBar.propTypes =
{ {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
isMobile : PropTypes.bool.isRequired,
peersLength : PropTypes.number, peersLength : PropTypes.number,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool, permanentTopBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
myPicture : PropTypes.string, myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired, loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired,
@ -375,31 +783,53 @@ TopBar.propTypes =
onFullscreen : PropTypes.func.isRequired, onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired, setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired,
setHelpOpen : PropTypes.func.isRequired,
setAboutOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired, openUsersTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired, unread : PropTypes.number.isRequired,
canProduceExtraVideo : PropTypes.bool.isRequired,
canLock : PropTypes.bool.isRequired, canLock : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasExtraVideoPermission =
makePermissionSelector(permissions.EXTRA_VIDEO);
const hasLockPermission =
makePermissionSelector(permissions.CHANGE_ROOM_LOCK);
const hasPromotionPermission =
makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
room : state.room, room : state.room,
isMobile : state.me.browser.platform === 'mobile',
peersLength : peersLengthSelector(state), peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state), lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar, permanentTopBar : state.settings.permanentTopBar,
drawerOverlayed : state.settings.drawerOverlayed,
toolAreaOpen : state.toolarea.toolAreaOpen,
loggedIn : state.me.loggedIn, loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled, loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture, myPicture : state.me.picture,
unread : state.toolarea.unreadMessages + unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles, state.toolarea.unreadFiles + raisedHandsSelector(state),
canLock : canProduceExtraVideo : hasExtraVideoPermission(state),
state.me.roles.some((role) => canLock : hasLockPermission(state),
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)) canPromote : hasPromotionPermission(state)
}); });
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
({ ({
setToolbarsVisible : (visible) => setToolbarsVisible : (visible) =>
@ -408,11 +838,23 @@ const mapDispatchToProps = (dispatch) =>
}, },
setSettingsOpen : (settingsOpen) => setSettingsOpen : (settingsOpen) =>
{ {
dispatch(roomActions.setSettingsOpen({ settingsOpen })); dispatch(roomActions.setSettingsOpen(settingsOpen));
},
setExtraVideoOpen : (extraVideoOpen) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
},
setHelpOpen : (helpOpen) =>
{
dispatch(roomActions.setHelpOpen(helpOpen));
},
setAboutOpen : (aboutOpen) =>
{
dispatch(roomActions.setAboutOpen(aboutOpen));
}, },
setLockDialogOpen : (lockDialogOpen) => setLockDialogOpen : (lockDialogOpen) =>
{ {
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen })); dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
}, },
toggleToolArea : () => toggleToolArea : () =>
{ {
@ -426,7 +868,7 @@ const mapDispatchToProps = (dispatch) =>
}); });
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
{ {
@ -437,12 +879,15 @@ export default withRoomContext(connect(
prev.peers === next.peers && prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers && prev.lobbyPeers === next.lobbyPeers &&
prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.drawerOverlayed === next.settings.drawerOverlayed &&
prev.me.loggedIn === next.me.loggedIn && prev.me.loggedIn === next.me.loggedIn &&
prev.me.browser === next.me.browser &&
prev.me.loginEnabled === next.me.loginEnabled && prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture && prev.me.picture === next.me.picture &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages && prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
); );
} }
} }

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext'; import { withRoomContext } from '../RoomContext';
import classnames from 'classnames';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import * as settingsActions from '../actions/settingsActions'; import * as settingsActions from '../actions/settingsActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -82,6 +83,10 @@ const styles = (theme) =>
green : green :
{ {
color : 'rgba(0, 153, 0, 1)' color : 'rgba(0, 153, 0, 1)'
},
red :
{
color : 'rgba(153, 0, 0, 1)'
} }
}); });
@ -128,9 +133,9 @@ const DialogTitle = withStyles(styles)((props) =>
return ( return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}> <MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography> <Typography variant='h5'>{children}</Typography>
{ window.config && window.config.loginEnabled && { window.config.loginEnabled &&
<Tooltip <Tooltip
onClose={handleTooltipClose} onClose={handleTooltipClose}
onOpen={handleTooltipOpen} onOpen={handleTooltipOpen}
@ -147,7 +152,9 @@ const DialogTitle = withStyles(styles)((props) =>
{ myPicture ? { myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} /> <Avatar src={myPicture} className={classes.largeAvatar} />
: :
<AccountCircle className={classes.largeIcon} /> <AccountCircle
className={classnames(classes.largeIcon, loggedIn ? classes.green : null)}
/>
} }
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -217,11 +224,11 @@ const JoinDialog = ({
myPicture={myPicture} myPicture={myPicture}
onLogin={() => onLogin={() =>
{ {
loggedIn ? roomClient.logout() : roomClient.login(); loggedIn ? roomClient.logout(roomId) : roomClient.login(roomId);
}} }}
loggedIn={loggedIn} loggedIn={loggedIn}
> >
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config.title ? window.config.title : 'Multiparty meeting' }
<hr /> <hr />
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
@ -278,6 +285,16 @@ const JoinDialog = ({
}} }}
fullWidth fullWidth
/> />
{!room.inLobby && room.overRoomLimit &&
<DialogContentText className={classes.red} variant='h6' gutterBottom>
<FormattedMessage
id='room.overRoomLimit'
defaultMessage={
'The room is full, retry after some time.'
}
/>
</DialogContentText>
}
</DialogContent> </DialogContent>
@ -316,6 +333,7 @@ const JoinDialog = ({
className={classes.green} className={classes.green}
gutterBottom gutterBottom
variant='h6' variant='h6'
style={{ fontWeight: '600' }}
align='center' align='center'
> >
<FormattedMessage <FormattedMessage
@ -324,7 +342,11 @@ const JoinDialog = ({
/> />
</DialogContentText> </DialogContentText>
{ room.signInRequired ? { room.signInRequired ?
<DialogContentText gutterBottom> <DialogContentText
gutterBottom
variant='h5'
style={{ fontWeight: '600' }}
>
<FormattedMessage <FormattedMessage
id='room.emptyRequireLogin' id='room.emptyRequireLogin'
defaultMessage={ defaultMessage={
@ -334,7 +356,11 @@ const JoinDialog = ({
/> />
</DialogContentText> </DialogContentText>
: :
<DialogContentText gutterBottom> <DialogContentText
gutterBottom
variant='h5'
style={{ fontWeight: '600' }}
>
<FormattedMessage <FormattedMessage
id='room.locketWait' id='room.locketWait'
defaultMessage='The room is locked - hang on until somebody lets you in ...' defaultMessage='The room is locked - hang on until somebody lets you in ...'
@ -407,6 +433,7 @@ export default withRoomContext(connect(
return ( return (
prev.room.inLobby === next.room.inLobby && prev.room.inLobby === next.room.inLobby &&
prev.room.signInRequired === next.room.signInRequired && prev.room.signInRequired === next.room.signInRequired &&
prev.room.overRoomLimit === next.room.overRoomLimit &&
prev.settings.displayName === next.settings.displayName && prev.settings.displayName === next.settings.displayName &&
prev.me.displayNameInProgress === next.me.displayNameInProgress && prev.me.displayNameInProgress === next.me.displayNameInProgress &&
prev.me.loginEnabled === next.me.loginEnabled && prev.me.loginEnabled === next.me.loginEnabled &&

View File

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

View File

@ -4,18 +4,16 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex', display : 'flex',
listStyleType : 'none', padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)', boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)' backgroundColor : 'rgba(255, 255, 255, 1)'
}, },
@ -80,16 +78,21 @@ ChatModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
isChatModerator : isChatModerator : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)),
room : state.room room : state.room
}); });
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -97,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me === next.me prev.me === next.me &&
prev.peers === next.peers
); );
} }
} }

View File

@ -4,6 +4,8 @@ import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import FileList from './FileList'; import FileList from './FileList';
import FileSharingModerator from './FileSharingModerator'; import FileSharingModerator from './FileSharingModerator';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
@ -25,6 +27,10 @@ const styles = (theme) =>
button : button :
{ {
margin : theme.spacing(1) margin : theme.spacing(1)
},
shareButtonsWrapper :
{
display : 'flex'
} }
}); });
@ -36,12 +42,13 @@ const FileSharing = (props) =>
{ {
if (event.target.files.length > 0) if (event.target.files.length > 0)
{ {
props.roomClient.shareFiles(event.target.files); await props.roomClient.shareFiles(event.target.files);
} }
}; };
const { const {
canShareFiles, canShareFiles,
browser,
canShare, canShare,
classes classes
} = props; } = props;
@ -57,16 +64,38 @@ const FileSharing = (props) =>
defaultMessage : 'File sharing not supported' 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 ( return (
<Paper className={classes.root}> <Paper className={classes.root}>
<FileSharingModerator /> <FileSharingModerator />
<div className={classes.shareButtonsWrapper} >
<input <input
className={classes.input} className={classes.input}
type='file' type='file'
disabled={!canShare} disabled={!canShare}
onChange={handleFileChange} onChange={handleFileChange}
// Need to reset to be able to share same file twice
onClick={(e) => (e.target.value = null)}
id='share-files-button' id='share-files-button'
/> />
<input
className={classes.input}
type='file'
disabled={!canShare}
onChange={handleFileChange}
accept='image/*'
id='share-files-gallery-button'
/>
<label htmlFor='share-files-button'> <label htmlFor='share-files-button'>
<Button <Button
variant='contained' variant='contained'
@ -77,7 +106,19 @@ const FileSharing = (props) =>
{buttonDescription} {buttonDescription}
</Button> </Button>
</label> </label>
{
(browser.platform === 'mobile') && canShareFiles && canShare && <label htmlFor='share-files-gallery-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!canShareFiles || !canShare}
>
{buttonGalleryDescription}
</Button>
</label>
}
</div>
<FileList /> <FileList />
</Paper> </Paper>
); );
@ -85,34 +126,43 @@ const FileSharing = (props) =>
FileSharing.propTypes = { FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
browser : PropTypes.object.isRequired,
canShareFiles : PropTypes.bool.isRequired, canShareFiles : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired, tabOpen : PropTypes.bool.isRequired,
canShare : PropTypes.bool.isRequired, canShare : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SHARE_FILE);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
canShareFiles : state.me.canShareFiles, canShareFiles : state.me.canShareFiles,
browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files', tabOpen : state.toolarea.currentToolTab === 'files',
canShare : canShare : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_FILE.includes(role))
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.browser === next.me.browser &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.me.canShareFiles === next.me.canShareFiles && prev.me.canShareFiles === next.me.canShareFiles &&
prev.peers === next.peers &&
prev.toolarea.currentToolTab === next.toolarea.currentToolTab prev.toolarea.currentToolTab === next.toolarea.currentToolTab
); );
} }

View File

@ -4,18 +4,16 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex', display : 'flex',
listStyleType : 'none', padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)', boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)' backgroundColor : 'rgba(255, 255, 255, 1)'
}, },
@ -80,16 +78,21 @@ FileSharingModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_FILES);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
isFileSharingModerator : isFileSharingModerator : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_FILES.includes(role)),
room : state.room room : state.room
}); });
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -97,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me === next.me prev.me === next.me &&
prev.peers === next.peers
); );
} }
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { raisedHandsSelector } from '../Selectors';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import * as toolareaActions from '../../actions/toolareaActions'; import * as toolareaActions from '../../actions/toolareaActions';
@ -51,6 +52,7 @@ const MeetingDrawer = (props) =>
currentToolTab, currentToolTab,
unreadMessages, unreadMessages,
unreadFiles, unreadFiles,
raisedHands,
closeDrawer, closeDrawer,
setToolTab, setToolTab,
classes, classes,
@ -93,10 +95,14 @@ const MeetingDrawer = (props) =>
} }
/> />
<Tab <Tab
label={intl.formatMessage({ label={
<Badge color='secondary' badgeContent={raisedHands}>
{intl.formatMessage({
id : 'label.participants', id : 'label.participants',
defaultMessage : 'Participants' defaultMessage : 'Participants'
})} })}
</Badge>
}
/> />
</Tabs> </Tabs>
<IconButton onClick={closeDrawer}> <IconButton onClick={closeDrawer}>
@ -116,16 +122,21 @@ MeetingDrawer.propTypes =
setToolTab : PropTypes.func.isRequired, setToolTab : PropTypes.func.isRequired,
unreadMessages : PropTypes.number.isRequired, unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired, unreadFiles : PropTypes.number.isRequired,
raisedHands : PropTypes.number.isRequired,
closeDrawer : PropTypes.func.isRequired, closeDrawer : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) =>
{
return {
currentToolTab : state.toolarea.currentToolTab, currentToolTab : state.toolarea.currentToolTab,
unreadMessages : state.toolarea.unreadMessages, unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles unreadFiles : state.toolarea.unreadFiles,
}); raisedHands : raisedHandsSelector(state)
};
};
const mapDispatchToProps = { const mapDispatchToProps = {
setToolTab : toolareaActions.setToolTab setToolTab : toolareaActions.setToolTab
@ -141,7 +152,8 @@ export default connect(
return ( return (
prev.toolarea.currentToolTab === next.toolarea.currentToolTab && prev.toolarea.currentToolTab === next.toolarea.currentToolTab &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages && prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
prev.peers === next.peers
); );
} }
} }

View File

@ -1,79 +1,55 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl';
import 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'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex'
}, },
listPeer :
{
display : 'flex'
},
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(0.5)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
alignItems : 'center' alignItems : 'center'
}, },
indicators : buttons :
{ {
left : 0, padding : theme.spacing(1)
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
}, },
icon : green :
{ {
flex : '0 0 auto', color : 'rgba(0, 153, 0, 1)'
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`,
opacity : 1
}
} }
}); });
const ListMe = (props) => const ListMe = (props) =>
{ {
const intl = useIntl();
const { const {
roomClient,
me, me,
settings, settings,
classes classes
@ -82,26 +58,46 @@ const ListMe = (props) =>
const picture = me.picture || EmptyAvatar; const picture = me.picture || EmptyAvatar;
return ( return (
<li className={classes.root}> <div className={classes.root}>
<div className={classes.listPeer}>
<img alt='My avatar' className={classes.avatar} src={picture} /> <img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}> <div className={classes.peerInfo}>
{settings.displayName} {settings.displayName}
</div> </div>
<Tooltip
<div className={classes.indicators}> title={intl.formatMessage({
{ me.raisedHand && id : 'tooltip.raisedHand',
<div className={classnames(classes.icon, 'raise-hand')} /> defaultMessage : 'Raise hand'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={
classnames(me.raisedHand ? classes.green : null, classes.buttons)
} }
disabled={me.raisedHandInProgress}
color='primary'
onClick={(e) =>
{
e.stopPropagation();
roomClient.setRaisedHand(!me.raisedHand);
}}
>
<PanIcon />
</IconButton>
</Tooltip>
</div> </div>
</div>
</li>
); );
}; };
ListMe.propTypes = ListMe.propTypes =
{ {
roomClient : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired, settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
@ -112,7 +108,7 @@ const mapStateToProps = (state) => ({
settings : state.settings settings : state.settings
}); });
export default connect( export default withRoomContext(connect(
mapStateToProps, mapStateToProps,
null, null,
null, null,
@ -125,4 +121,4 @@ export default connect(
); );
} }
} }
)(withStyles(styles)(ListMe)); )(withStyles(styles)(ListMe)));

View File

@ -11,9 +11,6 @@ const styles = (theme) =>
root : root :
{ {
padding : theme.spacing(1), padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex' display : 'flex'
}, },
divider : divider :

View File

@ -3,42 +3,43 @@ import { connect } from 'react-redux';
import { makePeerConsumerSelector } from '../../Selectors'; import { makePeerConsumerSelector } from '../../Selectors';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { green } from '@material-ui/core/colors';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import VideocamIcon from '@material-ui/icons/Videocam';
import VideocamOffIcon from '@material-ui/icons/VideocamOff';
import MicIcon from '@material-ui/icons/Mic'; import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff'; 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'; import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp'; import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg'; import PanIcon from '@material-ui/icons/PanTool';
import RecordVoiceOverIcon from '@material-ui/icons/RecordVoiceOver';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex'
}, },
listPeer :
{
display : 'flex'
},
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(0.5)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
@ -46,52 +47,17 @@ const styles = (theme) =>
}, },
indicators : indicators :
{ {
left : 0,
top : 0,
display : 'flex', display : 'flex',
flexDirection : 'row', padding : theme.spacing(1)
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
}, },
icon : buttons :
{ {
flex : '0 0 auto', padding : theme.spacing(1)
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
}, },
'&.on' : green :
{ {
opacity : 1 color : 'rgba(0, 153, 0, 1)',
}, marginLeft : theme.spacing(2)
'&.off' :
{
opacity : 0.2
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`
}
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
} }
}); });
@ -102,13 +68,21 @@ const ListPeer = (props) =>
const { const {
roomClient, roomClient,
isModerator, isModerator,
spotlight,
peer, peer,
micConsumer, micConsumer,
webcamConsumer,
screenConsumer, screenConsumer,
children, children,
classes classes
} = props; } = props;
const webcamEnabled = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const micEnabled = ( const micEnabled = (
Boolean(micConsumer) && Boolean(micConsumer) &&
!micConsumer.locallyPaused && !micConsumer.locallyPaused &&
@ -130,22 +104,38 @@ const ListPeer = (props) =>
<div className={classes.peerInfo}> <div className={classes.peerInfo}>
{peer.displayName} {peer.displayName}
</div> </div>
<div className={classes.indicators}> { peer.raisedHand &&
{ peer.raiseHandState && <IconButton
<div className={ className={classes.buttons}
classnames( style={{ color: green[500] }}
classes.icon, 'raise-hand', { disabled={!isModerator || peer.raisedHandInProgress}
on : peer.raiseHandState, onClick={(e) =>
off : !peer.raiseHandState {
e.stopPropagation();
roomClient.lowerPeerHand(peer.id);
}}
>
<PanIcon />
</IconButton>
} }
) { spotlight &&
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled
>
<RecordVoiceOverIcon />
</IconButton>
} }
/> { screenConsumer && spotlight &&
} <Tooltip
</div> title={intl.formatMessage({
{children} id : 'tooltip.muteScreenSharing',
<div className={classes.controls}> defaultMessage : 'Mute participant share'
{ screenConsumer && })}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.muteScreenSharing', id : 'tooltip.muteScreenSharing',
@ -153,8 +143,11 @@ const ListPeer = (props) =>
})} })}
color={screenVisible ? 'primary' : 'secondary'} color={screenVisible ? 'primary' : 'secondary'}
disabled={peer.peerScreenInProgress} disabled={peer.peerScreenInProgress}
onClick={() => className={classes.buttons}
onClick={(e) =>
{ {
e.stopPropagation();
screenVisible ? screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) : roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false); roomClient.modifyPeerConsumer(peer.id, 'screen', false);
@ -166,7 +159,48 @@ const ListPeer = (props) =>
<ScreenOffIcon /> <ScreenOffIcon />
} }
</IconButton> </IconButton>
</Tooltip>
} }
{ spotlight &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
className={classes.buttons}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute participant'
})}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.muteParticipant', id : 'tooltip.muteParticipant',
@ -174,35 +208,105 @@ const ListPeer = (props) =>
})} })}
color={micEnabled ? 'primary' : 'secondary'} color={micEnabled ? 'primary' : 'secondary'}
disabled={peer.peerAudioInProgress} disabled={peer.peerAudioInProgress}
onClick={() => className={classes.buttons}
onClick={(e) =>
{ {
e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}} }}
> >
{ micEnabled ? { micEnabled ?
<MicIcon /> <VolumeUpIcon />
: :
<MicOffIcon /> <VolumeOffIcon />
} }
</IconButton> </IconButton>
</Tooltip>
{ isModerator && { isModerator &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant', id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant' defaultMessage : 'Kick out participant'
})} })}
disabled={peer.peerKickInProgress} disabled={peer.peerKickInProgress}
onClick={() => className={classes.buttons}
color='secondary'
onClick={(e) =>
{ {
e.stopPropagation();
roomClient.kickPeer(peer.id); roomClient.kickPeer(peer.id);
}} }}
> >
<ExitIcon /> <ExitIcon />
</IconButton> </IconButton>
</Tooltip>
} }
</div> { isModerator && micConsumer &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute globally participant mic'
})}
placement='bottom'
>
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled={!isModerator || peer.stopPeerAudioInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.mutePeer(peer.id);
}}
>
{ !micConsumer.remotelyPaused ?
<MicIcon />
:
<MicOffIcon />
}
</IconButton>
</Tooltip>
}
{ isModerator && webcamConsumer &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute globally participant video'
})}
placement='bottom'
>
<IconButton
className={classes.buttons}
style={{ color: green[500] }}
disabled={!isModerator || peer.stopPeerVideoInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.stopPeerVideo(peer.id);
}}
>
{ !webcamConsumer.remotelyPaused ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
}
{children}
</div> </div>
); );
}; };
@ -212,6 +316,7 @@ ListPeer.propTypes =
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool, isModerator : PropTypes.bool,
spotlight : PropTypes.bool,
peer : appPropTypes.Peer.isRequired, peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,

View File

@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
passivePeersSelector, participantListSelector,
spotlightSortedPeersSelector makePermissionSelector
} from '../../Selectors'; } from '../../Selectors';
import classNames from 'classnames'; import { permissions } from '../../../permissions';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Flipper, Flipped } from 'react-flip-toolkit';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer'; import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
@ -31,12 +33,10 @@ const styles = (theme) =>
}, },
listheader : listheader :
{ {
padding : theme.spacing(1),
fontWeight : 'bolder' fontWeight : 'bolder'
}, },
listItem : listItem :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'pointer', cursor : 'pointer',
@ -78,9 +78,9 @@ class ParticipantList extends React.PureComponent
roomClient, roomClient,
advancedMode, advancedMode,
isModerator, isModerator,
passivePeers, participants,
spotlights,
selectedPeerId, selectedPeerId,
spotlightPeers,
classes classes
} = this.props; } = this.props;
@ -109,50 +109,42 @@ class ParticipantList extends React.PureComponent
<ul className={classes.list}> <ul className={classes.list}>
<li className={classes.listheader}> <li className={classes.listheader}>
<FormattedMessage <FormattedMessage
id='room.spotlights' id='label.participants'
defaultMessage='Participants in Spotlight' defaultMessage='Participants'
/> />
</li> </li>
{ spotlightPeers.map((peer) => ( <Flipper
flipKey={participants}
>
{ participants.map((peer) => (
<Flipped key={peer.id} flipId={peer.id}>
<li <li
key={peer.id} key={peer.id}
className={classNames(classes.listItem, { className={classnames(classes.listItem, {
selected : peer.id === selectedPeerId selected : peer.id === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peer.id)} onClick={() => roomClient.setSelectedPeer(peer.id)}
> >
{ spotlights.includes(peer.id) ?
<ListPeer <ListPeer
id={peer.id} id={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
isModerator={isModerator} isModerator={isModerator}
spotlight
> >
<Volume small id={peer.id} /> <Volume small id={peer.id} />
</ListPeer> </ListPeer>
</li> :
))}
</ul>
<ul className={classes.list}>
<li className={classes.listheader}>
<FormattedMessage
id='room.passive'
defaultMessage='Passive Participants'
/>
</li>
{ passivePeers.map((peer) => (
<li
key={peer.id}
className={classNames(classes.listItem, {
selected : peer.id === selectedPeerId
})}
onClick={() => roomClient.setSelectedPeer(peer.id)}
>
<ListPeer <ListPeer
id={peer.id} id={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
isModerator={isModerator} isModerator={isModerator}
/> />
}
</li> </li>
</Flipped>
))} ))}
</Flipper>
</ul> </ul>
</div> </div>
); );
@ -164,37 +156,40 @@ ParticipantList.propTypes =
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
isModerator : PropTypes.bool, isModerator : PropTypes.bool,
passivePeers : PropTypes.array, participants : PropTypes.array,
spotlights : PropTypes.array,
selectedPeerId : PropTypes.string, selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
isModerator : isModerator : hasPermission(state),
state.me.roles.some((role) => participants : participantListSelector(state),
state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), spotlights : state.room.spotlights,
passivePeers : passivePeersSelector(state), selectedPeerId : state.room.selectedPeerId
selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightSortedPeersSelector(state)
}; };
}; };
return mapStateToProps;
};
const ParticipantListContainer = withRoomContext(connect( const ParticipantListContainer = withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers && prev.peers === next.peers
prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerId === next.room.selectedPeerId
); );
} }
} }

View File

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

View File

@ -12,6 +12,12 @@ import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer'; import SpeakerPeer from '../Containers/SpeakerPeer';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
const RATIO = 1.334;
const PADDING_V = 40;
const PADDING_H = 0;
const FILMSTRING_PADDING_V = 10;
const FILMSTRING_PADDING_H = 0;
const styles = () => const styles = () =>
({ ({
root : root :
@ -19,25 +25,24 @@ const styles = () =>
height : '100%', height : '100%',
width : '100%', width : '100%',
display : 'grid', display : 'grid',
overflow : 'hidden',
gridTemplateColumns : '1fr', gridTemplateColumns : '1fr',
gridTemplateRows : '1.6fr minmax(0, 0.4fr)' gridTemplateRows : '1fr 0.25fr'
}, },
speaker : speaker :
{ {
gridArea : '1 / 1 / 2 / 2', gridArea : '1 / 1 / 1 / 1',
display : 'flex', display : 'flex',
justifyContent : 'center', justifyContent : 'center',
alignItems : 'center', alignItems : 'center'
paddingTop : 40
}, },
filmStrip : filmStrip :
{ {
gridArea : '2 / 1 / 3 / 2' gridArea : '2 / 1 / 2 / 1'
}, },
filmItem : filmItem :
{ {
display : 'flex', display : 'flex',
marginLeft : '6px',
border : 'var(--peer-border)', border : 'var(--peer-border)',
'&.selected' : '&.selected' :
{ {
@ -45,8 +50,18 @@ const styles = () =>
}, },
'&.active' : '&.active' :
{ {
opacity : '0.6' borderColor : 'var(--selected-peer-border-color)'
} }
},
hiddenToolBar :
{
paddingTop : 0,
transition : 'padding .5s'
},
showingToolBar :
{
paddingTop : 60,
transition : 'padding .5s'
} }
}); });
@ -58,6 +73,8 @@ class Filmstrip extends React.PureComponent
this.resizeTimeout = null; this.resizeTimeout = null;
this.rootContainer = React.createRef();
this.activePeerContainer = React.createRef(); this.activePeerContainer = React.createRef();
this.filmStripContainer = React.createRef(); this.filmStripContainer = React.createRef();
@ -105,24 +122,38 @@ class Filmstrip extends React.PureComponent
{ {
const newState = {}; const newState = {};
const root = this.rootContainer.current;
if (!root)
return;
const availableWidth = root.clientWidth;
// Grid is:
// 4/5 speaker
// 1/5 filmstrip
const availableSpeakerHeight = (root.clientHeight * 0.8) -
(this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H);
const availableFilmstripHeight = root.clientHeight * 0.2;
const speaker = this.activePeerContainer.current; const speaker = this.activePeerContainer.current;
if (speaker) if (speaker)
{ {
let speakerWidth = (speaker.clientWidth - 100); let speakerWidth = (availableWidth - PADDING_H);
let speakerHeight = (speakerWidth / 4) * 3; let speakerHeight = speakerWidth / RATIO;
if (this.isSharingCamera(this.getActivePeerId())) if (this.isSharingCamera(this.getActivePeerId()))
{ {
speakerWidth /= 2; speakerWidth /= 2;
speakerHeight = (speakerWidth / 4) * 3; speakerHeight = speakerWidth / RATIO;
} }
if (speakerHeight > (speaker.clientHeight - 60)) if (speakerHeight > (availableSpeakerHeight - PADDING_V))
{ {
speakerHeight = (speaker.clientHeight - 60); speakerHeight = (availableSpeakerHeight - PADDING_V);
speakerWidth = (speakerHeight / 3) * 4; speakerWidth = speakerHeight * RATIO;
} }
newState.speakerWidth = speakerWidth; newState.speakerWidth = speakerWidth;
@ -133,14 +164,18 @@ class Filmstrip extends React.PureComponent
if (filmStrip) if (filmStrip)
{ {
let filmStripHeight = filmStrip.clientHeight - 10; let filmStripHeight = availableFilmstripHeight - FILMSTRING_PADDING_V;
let filmStripWidth = (filmStripHeight / 3) * 4; let filmStripWidth = filmStripHeight * RATIO;
if (filmStripWidth * this.props.boxes > (filmStrip.clientWidth - 50)) if (
(filmStripWidth * this.props.boxes) >
(availableWidth - FILMSTRING_PADDING_H)
)
{ {
filmStripWidth = (filmStrip.clientWidth - 50) / this.props.boxes; filmStripWidth = (availableWidth - FILMSTRING_PADDING_H) /
filmStripHeight = (filmStripWidth / 4) * 3; this.props.boxes;
filmStripHeight = filmStripWidth / RATIO;
} }
newState.filmStripWidth = filmStripWidth; newState.filmStripWidth = filmStripWidth;
@ -172,27 +207,21 @@ class Filmstrip extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions); window.removeEventListener('resize', this.updateDimensions);
} }
componentWillUpdate(nextProps)
{
if (nextProps !== this.props)
{
if (
nextProps.activeSpeakerId != null &&
nextProps.activeSpeakerId !== this.props.myId
)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : nextProps.activeSpeakerId
});
}
}
}
componentDidUpdate(prevProps) componentDidUpdate(prevProps)
{ {
if (prevProps !== this.props) if (prevProps !== this.props)
{ {
if (
this.props.activeSpeakerId != null &&
this.props.activeSpeakerId !== this.props.myId
)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : this.props.activeSpeakerId
});
}
this.updateDimensions(); this.updateDimensions();
} }
} }
@ -205,6 +234,8 @@ class Filmstrip extends React.PureComponent
myId, myId,
advancedMode, advancedMode,
spotlights, spotlights,
toolbarsVisible,
permanentTopBar,
classes classes
} = this.props; } = this.props;
@ -223,7 +254,14 @@ class Filmstrip extends React.PureComponent
}; };
return ( return (
<div className={classes.root}> <div
className={classnames(
classes.root,
toolbarsVisible || permanentTopBar ?
classes.showingToolBar : classes.hiddenToolBar
)}
ref={this.rootContainer}
>
<div className={classes.speaker} ref={this.activePeerContainer}> <div className={classes.speaker} ref={this.activePeerContainer}>
{ peers[activePeerId] && { peers[activePeerId] &&
<SpeakerPeer <SpeakerPeer
@ -245,7 +283,7 @@ class Filmstrip extends React.PureComponent
<Me <Me
advancedMode={advancedMode} advancedMode={advancedMode}
style={peerStyle} style={peerStyle}
smallButtons smallContainer
/> />
</div> </div>
</Grid> </Grid>
@ -268,7 +306,7 @@ class Filmstrip extends React.PureComponent
advancedMode={advancedMode} advancedMode={advancedMode}
id={peerId} id={peerId}
style={peerStyle} style={peerStyle}
smallButtons smallContainer
/> />
</div> </div>
</Grid> </Grid>
@ -296,6 +334,9 @@ Filmstrip.propTypes = {
selectedPeerId : PropTypes.string, selectedPeerId : PropTypes.string,
spotlights : PropTypes.array.isRequired, spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
toolbarsVisible : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
@ -308,7 +349,10 @@ const mapStateToProps = (state) =>
consumers : state.consumers, consumers : state.consumers,
myId : state.me.id, myId : state.me.id,
spotlights : state.room.spotlights, spotlights : state.room.spotlights,
boxes : videoBoxesSelector(state) boxes : videoBoxesSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
toolAreaOpen : state.toolarea.toolAreaOpen,
permanentTopBar : state.settings.permanentTopBar
}; };
}; };
@ -322,6 +366,9 @@ export default withRoomContext(connect(
return ( return (
prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.selectedPeerId === next.room.selectedPeerId && prev.room.selectedPeerId === next.room.selectedPeerId &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights && prev.room.spotlights === next.room.spotlights &&

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { micConsumerSelector } from '../Selectors'; import { passiveMicConsumerSelector } from '../Selectors';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import PeerAudio from './PeerAudio'; import PeerAudio from './PeerAudio';
@ -37,7 +37,7 @@ AudioPeers.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
micConsumers : micConsumerSelector(state), micConsumers : passiveMicConsumerSelector(state),
audioOutputDevice : state.settings.selectedAudioOutputDevice audioOutputDevice : state.settings.selectedAudioOutputDevice
}); });
@ -50,7 +50,9 @@ const AudioPeersContainer = connect(
{ {
return ( return (
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.settings.selectedAudioOutputDevice === next.settings.selectedAudioOutputDevice prev.room.spotlights === next.room.spotlights &&
prev.settings.selectedAudioOutputDevice ===
next.settings.selectedAudioOutputDevice
); );
} }
} }

View File

@ -31,14 +31,16 @@ export default class PeerAudio extends React.PureComponent
this._setOutputDevice(audioOutputDevice); this._setOutputDevice(audioOutputDevice);
} }
// eslint-disable-next-line camelcase componentDidUpdate(prevProps)
UNSAFE_componentWillReceiveProps(nextProps)
{ {
const { audioTrack, audioOutputDevice } = nextProps; if (prevProps !== this.props)
{
const { audioTrack, audioOutputDevice } = this.props;
this._setTrack(audioTrack); this._setTrack(audioTrack);
this._setOutputDevice(audioOutputDevice); this._setOutputDevice(audioOutputDevice);
} }
}
_setTrack(audioTrack) _setTrack(audioTrack)
{ {

View File

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

View File

@ -1,5 +1,8 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
const meRolesSelect = (state) => state.me.roles;
const roomPermissionsSelect = (state) => state.room.roomPermissions;
const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing;
const producersSelect = (state) => state.producers; const producersSelect = (state) => state.producers;
const consumersSelect = (state) => state.consumers; const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights; const spotlightsSelector = (state) => state.room.spotlights;
@ -12,7 +15,8 @@ const peersKeySelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.keys(peers) (peers) => Object.keys(peers)
); );
const peersValueSelector = createSelector(
export const peersValueSelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.values(peers) (peers) => Object.values(peers)
); );
@ -37,6 +41,11 @@ export const screenProducersSelector = createSelector(
(producers) => Object.values(producers).filter((producer) => producer.source === 'screen') (producers) => Object.values(producers).filter((producer) => producer.source === 'screen')
); );
export const extraVideoProducersSelector = createSelector(
producersSelect,
(producers) => Object.values(producers).filter((producer) => producer.source === 'extravideo')
);
export const micProducerSelector = createSelector( export const micProducerSelector = createSelector(
producersSelect, producersSelect,
(producers) => Object.values(producers).find((producer) => producer.source === 'mic') (producers) => Object.values(producers).find((producer) => producer.source === 'mic')
@ -67,6 +76,33 @@ export const screenConsumerSelector = createSelector(
(consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen') (consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen')
); );
export const spotlightScreenConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'screen' && spotlights.includes(consumer.peerId)
)
);
export const spotlightExtraVideoConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'extravideo' && spotlights.includes(consumer.peerId)
)
);
export const passiveMicConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'mic' && !spotlights.includes(consumer.peerId)
)
);
export const spotlightsLengthSelector = createSelector( export const spotlightsLengthSelector = createSelector(
spotlightsSelector, spotlightsSelector,
(spotlights) => spotlights.length (spotlights) => spotlights.length
@ -81,10 +117,33 @@ export const spotlightPeersSelector = createSelector(
export const spotlightSortedPeersSelector = createSelector( export const spotlightSortedPeersSelector = createSelector(
spotlightsSelector, spotlightsSelector,
peersValueSelector, peersValueSelector,
(spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id)) (spotlights, peers) =>
peers.filter((peer) => spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
); );
const raisedHandSortedPeers = createSelector(
peersValueSelector,
(peers) => peers.filter((peer) => peer.raisedHand)
.sort((a, b) => a.raisedHandTimestamp - b.raisedHandTimestamp)
);
const peersSortedSelector = createSelector(
spotlightsSelector,
peersValueSelector,
(spotlights, peers) =>
peers.filter((peer) => !spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
export const participantListSelector = createSelector(
raisedHandSortedPeers,
spotlightSortedPeersSelector,
peersSortedSelector,
(raisedHands, spotlights, peers) =>
[ ...raisedHands, ...spotlights, ...peers ]
);
export const peersLengthSelector = createSelector( export const peersLengthSelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.values(peers).length (peers) => Object.values(peers).length
@ -97,24 +156,41 @@ export const passivePeersSelector = createSelector(
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
); );
export const raisedHandsSelector = createSelector(
peersValueSelector,
(peers) => peers.reduce((a, b) => (a + (b.raisedHand ? 1 : 0)), 0)
);
export const videoBoxesSelector = createSelector( export const videoBoxesSelector = createSelector(
spotlightsLengthSelector, spotlightsLengthSelector,
screenProducersSelector, screenProducersSelector,
screenConsumerSelector, spotlightScreenConsumerSelector,
(spotlightsLength, screenProducers, screenConsumers) => extraVideoProducersSelector,
spotlightsLength + 1 + screenProducers.length + screenConsumers.length spotlightExtraVideoConsumerSelector,
(
spotlightsLength,
screenProducers,
screenConsumers,
extraVideoProducers,
extraVideoConsumers
) =>
spotlightsLength + 1 + screenProducers.length +
screenConsumers.length + extraVideoProducers.length +
extraVideoConsumers.length
); );
export const meProducersSelector = createSelector( export const meProducersSelector = createSelector(
micProducerSelector, micProducerSelector,
webcamProducerSelector, webcamProducerSelector,
screenProducerSelector, screenProducerSelector,
(micProducer, webcamProducer, screenProducer) => extraVideoProducersSelector,
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
{ {
return { return {
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer screenProducer,
extraVideoProducers
}; };
} }
); );
@ -137,8 +213,60 @@ export const makePeerConsumerSelector = () =>
consumersArray.find((consumer) => consumer.source === 'webcam'); consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer = const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen'); consumersArray.find((consumer) => consumer.source === 'screen');
const extraVideoConsumers =
consumersArray.filter((consumer) => consumer.source === 'extravideo');
return { micConsumer, webcamConsumer, screenConsumer }; return { micConsumer, webcamConsumer, screenConsumer, extraVideoConsumers };
}
);
};
// Very important that the Components that use this
// selector need to check at least these state changes:
//
// areStatesEqual : (next, prev) =>
// {
// return (
// prev.room.roomPermissions === next.room.roomPermissions &&
// prev.room.allowWhenRoleMissing === next.room.allowWhenRoleMissing &&
// prev.peers === next.peers &&
// prev.me.roles === next.me.roles
// );
// }
export const makePermissionSelector = (permission) =>
{
return createSelector(
meRolesSelect,
roomPermissionsSelect,
roomAllowWhenRoleMissing,
peersValueSelector,
(roles, roomPermissions, allowWhenRoleMissing, peers) =>
{
if (!roomPermissions)
return false;
const permitted = roles.some((role) =>
roomPermissions[permission].includes(role)
);
if (permitted)
return true;
if (!allowWhenRoleMissing)
return false;
// Allow if config is set, and no one is present
if (allowWhenRoleMissing.includes(permission) &&
peers.filter(
(peer) =>
peer.roles.some(
(role) => roomPermissions[permission].includes(role)
)
).length === 0
)
return true;
return false;
} }
); );
}; };

View File

@ -0,0 +1,125 @@
import React from 'react';
import { connect } from 'react-redux';
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) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const AdvancedSettings = ({
roomClient,
settings,
onToggleAdvancedMode,
onToggleNotificationSounds,
classes
}) =>
{
const intl = useIntl();
return (
<React.Fragment>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
label={intl.formatMessage({
id : 'settings.advancedMode',
defaultMessage : 'Advanced mode'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.notificationSounds} onChange={onToggleNotificationSounds} value='notificationSounds' />}
label={intl.formatMessage({
id : 'settings.notificationSounds',
defaultMessage : 'Notification sounds'
})}
/>
{ !window.config.lockLastN &&
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.lastN || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeMaxSpotlights(event.target.value);
}}
name='Last N'
autoWidth
className={classes.selectEmpty}
>
{ Array.from(
{ length: window.config.maxLastN || 10 },
(_, i) => i + 1
).map((lastN) =>
{
return (
<MenuItem key={lastN} value={lastN}>
{lastN}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.lastn'
defaultMessage='Number of visible videos'
/>
</FormHelperText>
</FormControl>
</form>
}
</React.Fragment>
);
};
AdvancedSettings.propTypes =
{
roomClient : PropTypes.any.isRequired,
settings : PropTypes.object.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
onToggleNotificationSounds : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
settings : state.settings
});
const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
onToggleNotificationSounds : settingsActions.toggleNotificationSounds
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.settings === next.settings
);
}
}
)(withStyles(styles)(AdvancedSettings)));

View File

@ -0,0 +1,182 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions';
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) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const AppearenceSettings = ({
isMobile,
room,
settings,
onTogglePermanentTopBar,
onToggleHiddenControls,
onToggleButtonControlBar,
onToggleShowNotifications,
onToggleDrawerOverlayed,
handleChangeMode,
classes
}) =>
{
const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
return (
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={room.mode || ''}
onChange={(event) =>
{
if (event.target.value)
handleChangeMode(event.target.value);
}}
name={intl.formatMessage({
id : 'settings.layout',
defaultMessage : 'Room layout'
})}
autoWidth
className={classes.selectEmpty}
>
{ modes.map((mode, index) =>
{
return (
<MenuItem key={index} value={mode.value}>
{mode.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.selectRoomLayout'
defaultMessage='Select room layout'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />}
label={intl.formatMessage({
id : 'settings.permanentTopBar',
defaultMessage : 'Permanent top bar'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.hiddenControls} onChange={onToggleHiddenControls} value='hiddenControls' />}
label={intl.formatMessage({
id : 'settings.hiddenControls',
defaultMessage : 'Hidden media controls'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.buttonControlBar} onChange={onToggleButtonControlBar} value='buttonControlBar' />}
label={intl.formatMessage({
id : 'settings.buttonControlBar',
defaultMessage : 'Separate media controls'
})}
/>
{ !isMobile &&
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.drawerOverlayed} onChange={onToggleDrawerOverlayed} value='drawerOverlayed' />}
label={intl.formatMessage({
id : 'settings.drawerOverlayed',
defaultMessage : 'Side drawer over content'
})}
/>
}
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.showNotifications} onChange={onToggleShowNotifications} value='showNotifications' />}
label={intl.formatMessage({
id : 'settings.showNotifications',
defaultMessage : 'Show notifications'
})}
/>
</React.Fragment>
);
};
AppearenceSettings.propTypes =
{
isMobile : PropTypes.bool.isRequired,
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
onToggleHiddenControls : PropTypes.func.isRequired,
onToggleButtonControlBar : PropTypes.func.isRequired,
onToggleShowNotifications : PropTypes.func.isRequired,
onToggleDrawerOverlayed : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isMobile : state.me.browser.platform === 'mobile',
room : state.room,
settings : state.settings
});
const mapDispatchToProps = {
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
onToggleHiddenControls : settingsActions.toggleHiddenControls,
onToggleShowNotifications : settingsActions.toggleShowNotifications,
onToggleButtonControlBar : settingsActions.toggleButtonControlBar,
onToggleDrawerOverlayed : settingsActions.toggleDrawerOverlayed,
handleChangeMode : roomActions.setDisplayMode
};
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.browser === next.me.browser &&
prev.room === next.room &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(AppearenceSettings));

View File

@ -0,0 +1,344 @@
import React from 'react';
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) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const MediaSettings = ({
setEchoCancellation,
setAutoGainControl,
setNoiseSuppression,
roomClient,
me,
settings,
classes
}) =>
{
const intl = useIntl();
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
webcams = Object.values(me.webcamDevices);
else
webcams = [];
let audioDevices;
if (me.audioDevices)
audioDevices = Object.values(me.audioDevices);
else
audioDevices = [];
let audioOutputDevices;
if (me.audioOutputDevices)
audioOutputDevices = Object.values(me.audioOutputDevices);
else
audioOutputDevices = [];
return (
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedWebcam || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeWebcam(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={webcams.length === 0 || me.webcamInProgress}
>
{ webcams.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ webcams.length > 0 ?
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
</form>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audio',
defaultMessage : 'Audio device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioDevices.length === 0 || me.audioInProgress}
>
{ audioDevices.map((audio, index) =>
{
return (
<MenuItem key={index} value={audio.deviceId}>{audio.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudio',
defaultMessage : 'Select audio device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device'
})
}
</FormHelperText>
</FormControl>
</form>
{ 'audioOutputSupportedBrowsers' in window.config &&
window.config.audioOutputSupportedBrowsers.includes(me.browser.name) &&
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioOutputDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioOutputDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audioOutput',
defaultMessage : 'Audio output device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioOutputDevices.length === 0 || me.audioOutputInProgress}
>
{ audioOutputDevices.map((audioOutput, index) =>
{
return (
<MenuItem
key={index}
value={audioOutput.deviceId}
>
{audioOutput.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioOutputDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudioOutput',
defaultMessage : 'Select audio output device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudioOutput',
defaultMessage : 'Unable to select audio output device'
})
}
</FormHelperText>
</FormControl>
</form>
}
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeVideoResolution(event.target.value);
}}
name='Video resolution'
autoWidth
className={classes.selectEmpty}
>
{ resolutions.map((resolution, index) =>
{
return (
<MenuItem key={index} value={resolution.value}>
{resolution.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.echoCancellation} onChange={
(event) =>
{
setEchoCancellation(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.echoCancellation',
defaultMessage : 'Echo cancellation'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.autoGainControl} onChange={
(event) =>
{
setAutoGainControl(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.autoGainControl',
defaultMessage : 'Auto gain control'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.noiseSuppression} onChange={
(event) =>
{
setNoiseSuppression(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.noiseSuppression',
defaultMessage : 'Noise suppression'
})}
/>
</form>
</React.Fragment>
);
};
MediaSettings.propTypes =
{
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) =>
{
return {
me : state.me,
settings : state.settings
};
};
const mapDispatchToProps = {
setEchoCancellation : settingsActions.setEchoCancellation,
setAutoGainControl : settingsActions.toggleAutoGainControl,
setNoiseSuppression : settingsActions.toggleNoiseSuppression
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me === next.me &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(MediaSettings)));

View File

@ -1,22 +1,25 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions'; import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import MediaSettings from './MediaSettings';
import AppearenceSettings from './AppearenceSettings';
import AdvancedSettings from './AdvancedSettings';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText'; const tabs =
import FormControl from '@material-ui/core/FormControl'; [
import FormControlLabel from '@material-ui/core/FormControlLabel'; 'media',
import Select from '@material-ui/core/Select'; 'appearence',
import Checkbox from '@material-ui/core/Checkbox'; 'advanced'
];
const styles = (theme) => const styles = (theme) =>
({ ({
@ -43,106 +46,27 @@ const styles = (theme) =>
width : '90vw' width : '90vw'
} }
}, },
setting : tabsHeader :
{ {
padding : theme.spacing(2) flexGrow : 1
},
formControl :
{
display : 'flex'
} }
}); });
const Settings = ({ const Settings = ({
roomClient, currentSettingsTab,
room, settingsOpen,
me,
settings,
onToggleAdvancedMode,
onTogglePermanentTopBar,
handleCloseSettings, handleCloseSettings,
handleChangeMode, setSettingsTab,
classes classes
}) => }) =>
{ {
const intl = useIntl(); const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
webcams = Object.values(me.webcamDevices);
else
webcams = [];
let audioDevices;
if (me.audioDevices)
audioDevices = Object.values(me.audioDevices);
else
audioDevices = [];
let audioOutputDevices;
if (me.audioOutputDevices)
audioOutputDevices = Object.values(me.audioOutputDevices);
else
audioOutputDevices = [];
return ( return (
<Dialog <Dialog
className={classes.root} className={classes.root}
open={room.settingsOpen} open={settingsOpen}
onClose={() => handleCloseSettings({ settingsOpen: false })} onClose={() => handleCloseSettings(false)}
classes={{ classes={{
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
@ -153,250 +77,40 @@ const Settings = ({
defaultMessage='Settings' defaultMessage='Settings'
/> />
</DialogTitle> </DialogTitle>
<form className={classes.setting} autoComplete='off'> <Tabs
<FormControl className={classes.formControl}> className={classes.tabsHeader}
<Select value={tabs.indexOf(currentSettingsTab)}
value={settings.selectedWebcam || ''} onChange={(event, value) => setSettingsTab(tabs[value])}
onChange={(event) => indicatorColor='primary'
{ textColor='primary'
if (event.target.value) variant='fullWidth'
roomClient.changeWebcam(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={webcams.length === 0 || me.webcamInProgress}
> >
{ webcams.map((webcam, index) => <Tab
{ label={
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ webcams.length > 0 ?
intl.formatMessage({ intl.formatMessage({
id : 'settings.selectCamera', id : 'label.media',
defaultMessage : 'Select video device' defaultMessage : 'Media'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
}) })
} }
</FormHelperText>
</FormControl>
</form>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audio',
defaultMessage : 'Audio device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioDevices.length === 0 || me.audioInProgress}
>
{ audioDevices.map((audio, index) =>
{
return (
<MenuItem key={index} value={audio.deviceId}>{audio.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudio',
defaultMessage : 'Select audio device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device'
})
}
</FormHelperText>
</FormControl>
</form>
{
'audioOutputSupportedBrowsers' in window.config &&
window.config.audioOutputSupportedBrowsers.includes(me.browser.name) &&
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioOutputDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioOutputDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audioOutput',
defaultMessage : 'Audio output device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioOutputDevices.length === 0 || me.audioOutputInProgress}
>
{ audioOutputDevices.map((audioOutput, index) =>
{
return (
<MenuItem
key={index}
value={audioOutput.deviceId}
>
{audioOutput.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioOutputDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudioOutput',
defaultMessage : 'Select audio output device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudioOutput',
defaultMessage : 'Unable to select audio output device'
})
}
</FormHelperText>
</FormControl>
</form>
}
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeVideoResolution(event.target.value);
}}
name='Video resolution'
autoWidth
className={classes.selectEmpty}
>
{ resolutions.map((resolution, index) =>
{
return (
<MenuItem key={index} value={resolution.value}>
{resolution.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/> />
</FormHelperText> <Tab
</FormControl>
</form>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={room.mode || ''}
onChange={(event) =>
{
if (event.target.value)
handleChangeMode(event.target.value);
}}
name={intl.formatMessage({
id : 'settings.layout',
defaultMessage : 'Room layout'
})}
autoWidth
className={classes.selectEmpty}
>
{ modes.map((mode, index) =>
{
return (
<MenuItem key={index} value={mode.value}>
{mode.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.selectRoomLayout'
defaultMessage='Select room layout'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
label={intl.formatMessage({ label={intl.formatMessage({
id : 'settings.advancedMode', id : 'label.appearence',
defaultMessage : 'Advanced mode' defaultMessage : 'Appearence'
})} })}
/> />
{ settings.advancedMode && <Tab
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.lastN || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeMaxSpotlights(event.target.value);
}}
name='Last N'
autoWidth
className={classes.selectEmpty}
>
{ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map((lastN) =>
{
return (
<MenuItem key={lastN} value={lastN}>
{lastN}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.lastn'
defaultMessage='Number of visible videos'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />}
label={intl.formatMessage({ label={intl.formatMessage({
id : 'settings.permanentTopBar', id : 'label.advanced',
defaultMessage : 'Permanent top bar' defaultMessage : 'Advanced'
})} })}
/> />
</React.Fragment> </Tabs>
} {currentSettingsTab === 'media' && <MediaSettings />}
{currentSettingsTab === 'appearence' && <AppearenceSettings />}
{currentSettingsTab === 'advanced' && <AdvancedSettings />}
<DialogActions> <DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'> <Button onClick={() => handleCloseSettings(false)} color='primary'>
<FormattedMessage <FormattedMessage
id='label.close' id='label.close'
defaultMessage='Close' defaultMessage='Close'
@ -409,34 +123,25 @@ const Settings = ({
Settings.propTypes = Settings.propTypes =
{ {
roomClient : PropTypes.any.isRequired, currentSettingsTab : PropTypes.string.isRequired,
me : appPropTypes.Me.isRequired, settingsOpen : PropTypes.bool.isRequired,
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
handleCloseSettings : PropTypes.func.isRequired, handleCloseSettings : PropTypes.func.isRequired,
setSettingsTab : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ ({
return { currentSettingsTab : state.room.currentSettingsTab,
me : state.me, settingsOpen : state.room.settingsOpen
room : state.room, });
settings : state.settings
};
};
const mapDispatchToProps = { const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode, handleCloseSettings : roomActions.setSettingsOpen,
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, setSettingsTab : roomActions.setSettingsTab
handleChangeMode : roomActions.setDisplayMode,
handleCloseSettings : roomActions.setSettingsOpen
}; };
export default withRoomContext(connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
@ -444,10 +149,9 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.me === next.me && prev.room.currentSettingsTab === next.room.currentSettingsTab &&
prev.room === next.room && prev.room.settingsOpen === next.room.settingsOpen
prev.settings === next.settings
); );
} }
} }
)(withStyles(styles)(Settings))); )(withStyles(styles)(Settings));

View File

@ -96,11 +96,6 @@ const FullScreenView = (props) =>
!consumer.remotelyPaused !consumer.remotelyPaused
); );
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return ( return (
<div className={classes.root}> <div className={classes.root}>
<div className={classes.controls}> <div className={classes.controls}>
@ -121,9 +116,25 @@ const FullScreenView = (props) =>
<VideoView <VideoView
advancedMode={advancedMode} advancedMode={advancedMode}
videoContain videoContain
videoTrack={consumer ? consumer.track : null} consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={consumerVisible} videoVisible={consumerVisible}
videoProfile={consumerProfile} videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/> />
</div> </div>
); );

View File

@ -81,12 +81,15 @@ class FullView extends React.PureComponent
this._setTracks(videoTrack); this._setTracks(videoTrack);
} }
componentDidUpdate() componentDidUpdate(prevProps)
{
if (prevProps !== this.props)
{ {
const { videoTrack } = this.props; const { videoTrack } = this.props;
this._setTracks(videoTrack); this._setTracks(videoTrack);
} }
}
_setTracks(videoTrack) _setTracks(videoTrack)
{ {

View File

@ -3,13 +3,15 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import EditableInput from '../Controls/EditableInput'; import EditableInput from '../Controls/EditableInput';
import { green, yellow, orange, red } from '@material-ui/core/colors'; import Logger from '../../Logger';
import { yellow, orange, red } from '@material-ui/core/colors';
import SignalCellularOffIcon from '@material-ui/icons/SignalCellularOff'; import SignalCellularOffIcon from '@material-ui/icons/SignalCellularOff';
import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar'; import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar';
import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar'; import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar';
import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar'; import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar';
import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar'; import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar';
import SignalCellularAltIcon from '@material-ui/icons/SignalCellularAlt';
const logger = new Logger('VideoView');
const styles = (theme) => const styles = (theme) =>
({ ({
@ -167,6 +169,10 @@ class VideoView extends React.PureComponent
videoHeight : null videoHeight : null
}; };
// Latest received audio track
// @type {MediaStreamTrack}
this._audioTrack = null;
// Latest received video track. // Latest received video track.
// @type {MediaStreamTrack} // @type {MediaStreamTrack}
this._videoTrack = null; this._videoTrack = null;
@ -179,6 +185,7 @@ class VideoView extends React.PureComponent
{ {
const { const {
isMe, isMe,
showQuality,
isScreen, isScreen,
displayName, displayName,
showPeerInfo, showPeerInfo,
@ -188,8 +195,6 @@ class VideoView extends React.PureComponent
videoMultiLayer, videoMultiLayer,
audioScore, audioScore,
videoScore, videoScore,
// consumerSpatialLayers,
// consumerTemporalLayers,
consumerCurrentSpatialLayer, consumerCurrentSpatialLayer,
consumerCurrentTemporalLayer, consumerCurrentTemporalLayer,
consumerPreferredSpatialLayer, consumerPreferredSpatialLayer,
@ -207,13 +212,17 @@ class VideoView extends React.PureComponent
videoHeight videoHeight
} = this.state; } = this.state;
let quality = <SignalCellularOffIcon style={{ color: red[500] }}/>; let quality = null;
if (showQuality)
{
quality = <SignalCellularOffIcon style={{ color: red[500] }}/>;
if (videoScore || audioScore) if (videoScore || audioScore)
{ {
const score = videoScore ? videoScore : audioScore; const score = videoScore ? videoScore : audioScore;
switch (score.producerScore) switch (isMe ? score.score : score.producerScore)
{ {
case 0: case 0:
case 1: case 1:
@ -242,16 +251,16 @@ class VideoView extends React.PureComponent
case 7: case 7:
case 8: case 8:
case 9:
{ {
quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>; quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>;
break; break;
} }
case 9:
case 10: case 10:
{ {
quality = <SignalCellularAltIcon style={{ color: green[500] }}/>; quality = null;
break; break;
} }
@ -262,6 +271,7 @@ class VideoView extends React.PureComponent
} }
} }
} }
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
@ -337,7 +347,7 @@ class VideoView extends React.PureComponent
} }
</div> </div>
{ !isMe && { showQuality &&
<div className={classnames(classes.box, 'right')}> <div className={classnames(classes.box, 'right')}>
{ {
quality quality
@ -379,7 +389,7 @@ class VideoView extends React.PureComponent
</div> </div>
<video <video
ref='video' ref='videoElement'
className={classnames(classes.video, { className={classnames(classes.video, {
hidden : !videoVisible, hidden : !videoVisible,
'isMe' : isMe && !isScreen, 'isMe' : isMe && !isScreen,
@ -387,6 +397,16 @@ class VideoView extends React.PureComponent
})} })}
autoPlay autoPlay
playsInline playsInline
muted
controls={false}
/>
<audio
ref='audioElement'
autoPlay
playsInline
muted={isMe}
controls={false}
/> />
{children} {children}
@ -396,52 +416,87 @@ class VideoView extends React.PureComponent
componentDidMount() componentDidMount()
{ {
const { videoTrack } = this.props; const { videoTrack, audioTrack } = this.props;
this._setTracks(videoTrack); this._setTracks(videoTrack, audioTrack);
} }
componentWillUnmount() componentWillUnmount()
{ {
clearInterval(this._videoResolutionTimer); clearInterval(this._videoResolutionTimer);
const { videoElement } = this.refs;
if (videoElement)
{
videoElement.oncanplay = null;
videoElement.onplay = null;
videoElement.onpause = null;
}
} }
// eslint-disable-next-line camelcase componentDidUpdate(prevProps)
UNSAFE_componentWillReceiveProps(nextProps)
{ {
const { videoTrack } = nextProps; if (prevProps !== this.props)
{
this._setTracks(videoTrack); const { videoTrack, audioTrack } = this.props;
this._setTracks(videoTrack, audioTrack);
}
} }
_setTracks(videoTrack) _setTracks(videoTrack, audioTrack)
{ {
if (this._videoTrack === videoTrack) if (this._videoTrack === videoTrack && this._audioTrack === audioTrack)
return; return;
this._videoTrack = videoTrack; this._videoTrack = videoTrack;
this._audioTrack = audioTrack;
clearInterval(this._videoResolutionTimer); clearInterval(this._videoResolutionTimer);
this._hideVideoResolution(); this._hideVideoResolution();
const { video } = this.refs; const { videoElement, audioElement } = this.refs;
if (videoTrack) if (videoTrack)
{ {
const stream = new MediaStream(); const stream = new MediaStream();
if (videoTrack)
stream.addTrack(videoTrack); stream.addTrack(videoTrack);
video.srcObject = stream; videoElement.srcObject = stream;
videoElement.oncanplay = () => this.setState({ videoCanPlay: true });
videoElement.onplay = () =>
{
audioElement.play()
.catch((error) => logger.warn('audioElement.play() [error:"%o]', error));
};
videoElement.play()
.catch((error) => logger.warn('videoElement.play() [error:"%o]', error));
if (videoTrack)
this._showVideoResolution(); this._showVideoResolution();
} }
else else
{ {
video.srcObject = null; videoElement.srcObject = null;
}
if (audioTrack)
{
const stream = new MediaStream();
stream.addTrack(audioTrack);
audioElement.srcObject = stream;
audioElement.play()
.catch((error) => logger.warn('audioElement.play() [error:"%o]', error));
}
else
{
audioElement.srcObject = null;
} }
} }
@ -450,16 +505,19 @@ class VideoView extends React.PureComponent
this._videoResolutionTimer = setInterval(() => this._videoResolutionTimer = setInterval(() =>
{ {
const { videoWidth, videoHeight } = this.state; const { videoWidth, videoHeight } = this.state;
const { video } = this.refs; const { videoElement } = this.refs;
// Don't re-render if nothing changed. // Don't re-render if nothing changed.
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight) if (
videoElement.videoWidth === videoWidth &&
videoElement.videoHeight === videoHeight
)
return; return;
this.setState( this.setState(
{ {
videoWidth : video.videoWidth, videoWidth : videoElement.videoWidth,
videoHeight : video.videoHeight videoHeight : videoElement.videoHeight
}); });
}, 1000); }, 1000);
} }
@ -473,12 +531,14 @@ class VideoView extends React.PureComponent
VideoView.propTypes = VideoView.propTypes =
{ {
isMe : PropTypes.bool, isMe : PropTypes.bool,
showQuality : PropTypes.bool,
isScreen : PropTypes.bool, isScreen : PropTypes.bool,
displayName : PropTypes.string, displayName : PropTypes.string,
showPeerInfo : PropTypes.bool, showPeerInfo : PropTypes.bool,
videoContain : PropTypes.bool, videoContain : PropTypes.bool,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
videoTrack : PropTypes.any, videoTrack : PropTypes.any,
audioTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired, videoVisible : PropTypes.bool.isRequired,
consumerSpatialLayers : PropTypes.number, consumerSpatialLayers : PropTypes.number,
consumerTemporalLayers : PropTypes.number, consumerTemporalLayers : PropTypes.number,

View File

@ -23,18 +23,29 @@ const VideoWindow = (props) =>
!consumer.remotelyPaused !consumer.remotelyPaused
); );
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return ( return (
<NewWindow onUnload={toggleConsumerWindow}> <NewWindow onUnload={toggleConsumerWindow}>
<FullView <FullView
advancedMode={advancedMode} advancedMode={advancedMode}
videoTrack={consumer ? consumer.track : null} consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={consumerVisible} videoVisible={consumerVisible}
videoProfile={consumerProfile} videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/> />
</NewWindow> </NewWindow>
); );

View File

@ -18,9 +18,9 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape( export const Producer = PropTypes.shape(
{ {
id : PropTypes.string.isRequired, id : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired, source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
deviceLabel : PropTypes.string, deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]), type : PropTypes.oneOf([ 'front', 'back', 'screen', 'extravideo' ]),
paused : PropTypes.bool.isRequired, paused : PropTypes.bool.isRequired,
track : PropTypes.any, track : PropTypes.any,
codec : PropTypes.string.isRequired codec : PropTypes.string.isRequired
@ -37,7 +37,7 @@ export const Consumer = PropTypes.shape(
{ {
id : PropTypes.string.isRequired, id : PropTypes.string.isRequired,
peerId : PropTypes.string.isRequired, peerId : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired, source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
locallyPaused : PropTypes.bool.isRequired, locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired, remotelyPaused : PropTypes.bool.isRequired,
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]), profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve">
<metadata
id="metadata11"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<defs
id="defs9" />
<path
style="fill:#000000;stroke-width:0.40677965"
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
id="path3710"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 96 96"
style="enable-background:new 0 0 96 96;"
xml:space="preserve">
<metadata
id="metadata11"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<defs
id="defs9" />
<path
style="fill:#ffffff;stroke-width:0.40677965"
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
id="path3710"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -37,6 +37,8 @@ import messagesCroatian from './translations/hr';
import messagesCzech from './translations/cs'; import messagesCzech from './translations/cs';
import messagesItalian from './translations/it'; import messagesItalian from './translations/it';
import messagesUkrainian from './translations/uk'; import messagesUkrainian from './translations/uk';
import messagesTurkish from './translations/tr';
import messagesLatvian from './translations/lv';
import './index.css'; import './index.css';
@ -61,7 +63,9 @@ const messages =
'hr' : messagesCroatian, 'hr' : messagesCroatian,
'cs' : messagesCzech, 'cs' : messagesCzech,
'it' : messagesItalian, 'it' : messagesItalian,
'uk' : messagesUkrainian 'uk' : messagesUkrainian,
'tr' : messagesTurkish,
'lv' : messagesLatvian
}; };
const locale = navigator.language.split(/[-_]/)[0]; // language without region code const locale = navigator.language.split(/[-_]/)[0]; // language without region code
@ -112,6 +116,13 @@ function run()
const displayName = parameters.get('displayName'); const displayName = parameters.get('displayName');
const muted = parameters.get('muted') === 'true'; const muted = parameters.get('muted') === 'true';
const { pathname } = window.location;
let basePath = pathname.substring(0, pathname.lastIndexOf('/'));
if (!basePath)
basePath = '/';
// Get current device. // Get current device.
const device = deviceInfo(); const device = deviceInfo();
@ -130,7 +141,8 @@ function run()
produce, produce,
forceTcp, forceTcp,
displayName, displayName,
muted muted,
basePath
}); });
global.CLIENT = roomClient; global.CLIENT = roomClient;
@ -142,7 +154,7 @@ function run()
<PersistGate loading={<LoadingView />} persistor={persistor}> <PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}> <RoomContext.Provider value={roomClient}>
<SnackbarProvider> <SnackbarProvider>
<Router> <Router basename={basePath}>
<Suspense fallback={<LoadingView />}> <Suspense fallback={<LoadingView />}>
<React.Fragment> <React.Fragment>
<Route exact path='/' component={ChooseRoom} /> <Route exact path='/' component={ChooseRoom} />

View File

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

View File

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

View File

@ -15,8 +15,8 @@ const initialState =
screenShareInProgress : false, screenShareInProgress : false,
displayNameInProgress : false, displayNameInProgress : false,
loginEnabled : false, loginEnabled : false,
raiseHand : false, raisedHand : false,
raiseHandInProgress : false, raisedHandInProgress : false,
loggedIn : false, loggedIn : false,
isSpeaking : false isSpeaking : false
}; };
@ -134,18 +134,18 @@ const me = (state = initialState, action) =>
return { ...state, screenShareInProgress: flag }; return { ...state, screenShareInProgress: flag };
} }
case 'SET_MY_RAISE_HAND_STATE': case 'SET_RAISED_HAND':
{ {
const { flag } = action.payload; const { flag } = action.payload;
return { ...state, raiseHand: flag }; return { ...state, raisedHand: flag };
} }
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS': case 'SET_RAISED_HAND_IN_PROGRESS':
{ {
const { flag } = action.payload; const { flag } = action.payload;
return { ...state, raiseHandInProgress: flag }; return { ...state, raisedHandInProgress: flag };
} }
case 'SET_DISPLAY_NAME_IN_PROGRESS': case 'SET_DISPLAY_NAME_IN_PROGRESS':

View File

@ -1,4 +1,6 @@
const peer = (state = {}, action) => const initialState = {};
const peer = (state = initialState, action) =>
{ {
switch (action.type) switch (action.type)
{ {
@ -20,8 +22,18 @@ const peer = (state = {}, action) =>
case 'SET_PEER_KICK_IN_PROGRESS': case 'SET_PEER_KICK_IN_PROGRESS':
return { ...state, peerKickInProgress: action.payload.flag }; return { ...state, peerKickInProgress: action.payload.flag };
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISED_HAND':
return { ...state, raiseHandState: action.payload.raiseHandState }; return {
...state,
raisedHand : action.payload.raisedHand,
raisedHandTimestamp : action.payload.raisedHandTimestamp
};
case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
return {
...state,
raisedHandInProgress : action.payload.flag
};
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
{ {
@ -58,12 +70,24 @@ const peer = (state = {}, action) =>
return { ...state, roles }; return { ...state, roles };
} }
case 'STOP_PEER_AUDIO_IN_PROGRESS':
return {
...state,
stopPeerAudioInProgress : action.payload.flag
};
case 'STOP_PEER_VIDEO_IN_PROGRESS':
return {
...state,
stopPeerVideoInProgress : action.payload.flag
};
default: default:
return state; return state;
} }
}; };
const peers = (state = {}, action) => const peers = (state = initialState, action) =>
{ {
switch (action.type) switch (action.type)
{ {
@ -86,11 +110,14 @@ const peers = (state = {}, action) =>
case 'SET_PEER_VIDEO_IN_PROGRESS': case 'SET_PEER_VIDEO_IN_PROGRESS':
case 'SET_PEER_AUDIO_IN_PROGRESS': case 'SET_PEER_AUDIO_IN_PROGRESS':
case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS':
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISED_HAND':
case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
case 'SET_PEER_PICTURE': case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE': case 'ADD_PEER_ROLE':
case 'REMOVE_PEER_ROLE': case 'REMOVE_PEER_ROLE':
case 'STOP_PEER_AUDIO_IN_PROGRESS':
case 'STOP_PEER_VIDEO_IN_PROGRESS':
{ {
const oldPeer = state[action.payload.peerId]; const oldPeer = state[action.payload.peerId];
@ -114,6 +141,11 @@ const peers = (state = {}, action) =>
return { ...state, [oldPeer.id]: peer(oldPeer, action) }; return { ...state, [oldPeer.id]: peer(oldPeer, action) };
} }
case 'CLEAR_PEERS':
{
return initialState;
}
default: default:
return state; return state;
} }

View File

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

View File

@ -1,40 +1,40 @@
const initialState = const initialState =
{ {
name : '', name : '',
state : 'new', // new/connecting/connected/disconnected/closed, // new/connecting/connected/disconnected/closed,
state : 'new',
locked : false, locked : false,
inLobby : false, inLobby : false,
signInRequired : false, signInRequired : false,
accessCode : '', // access code to the room if locked and joinByAccessCode == true overRoomLimit : false,
joinByAccessCode : true, // if true: accessCode is a possibility to open the room // access code to the room if locked and joinByAccessCode == true
accessCode : '',
// if true: accessCode is a possibility to open the room
joinByAccessCode : true,
activeSpeakerId : null, activeSpeakerId : null,
torrentSupport : false, torrentSupport : false,
showSettings : false, showSettings : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
windowConsumer : null, // ConsumerID windowConsumer : null, // ConsumerID
toolbarsVisible : true, toolbarsVisible : true,
mode : 'democratic', mode : window.config.defaultLayout || 'democratic',
selectedPeerId : null, selectedPeerId : null,
spotlights : [], spotlights : [],
settingsOpen : false, settingsOpen : false,
extraVideoOpen : false,
helpOpen : false,
aboutOpen : false,
currentSettingsTab : 'media', // media, appearence, advanced
lockDialogOpen : false, lockDialogOpen : false,
joined : false, joined : false,
muteAllInProgress : false, muteAllInProgress : false,
lobbyPeersPromotionInProgress : false,
stopAllVideoInProgress : false, stopAllVideoInProgress : false,
closeMeetingInProgress : false, closeMeetingInProgress : false,
clearChatInProgress : false, clearChatInProgress : false,
clearFileSharingInProgress : false, clearFileSharingInProgress : false,
userRoles : { NORMAL: 'normal' }, // Default role roomPermissions : null,
permissionsFromRoles : { allowWhenRoleMissing : null
CHANGE_ROOM_LOCK : [],
PROMOTE_PEER : [],
SEND_CHAT : [],
MODERATE_CHAT : [],
SHARE_SCREEN : [],
SHARE_FILE : [],
MODERATE_FILES : [],
MODERATE_ROOM : []
}
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -81,7 +81,12 @@ const room = (state = initialState, action) =>
return { ...state, signInRequired }; return { ...state, signInRequired };
} }
case 'SET_OVER_ROOM_LIMIT':
{
const { overRoomLimit } = action.payload;
return { ...state, overRoomLimit };
}
case 'SET_ACCESS_CODE': case 'SET_ACCESS_CODE':
{ {
const { accessCode } = action.payload; const { accessCode } = action.payload;
@ -110,6 +115,34 @@ const room = (state = initialState, action) =>
return { ...state, settingsOpen }; return { ...state, settingsOpen };
} }
case 'SET_EXTRA_VIDEO_OPEN':
{
const { extraVideoOpen } = action.payload;
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;
return { ...state, currentSettingsTab: tab };
}
case 'SET_ROOM_ACTIVE_SPEAKER': case 'SET_ROOM_ACTIVE_SPEAKER':
{ {
const { peerId } = action.payload; const { peerId } = action.payload;
@ -179,6 +212,14 @@ const room = (state = initialState, action) =>
return { ...state, spotlights }; return { ...state, spotlights };
} }
case 'CLEAR_SPOTLIGHTS':
{
return { ...state, spotlights: [] };
}
case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS':
return { ...state, lobbyPeersPromotionInProgress: action.payload.flag };
case 'MUTE_ALL_IN_PROGRESS': case 'MUTE_ALL_IN_PROGRESS':
return { ...state, muteAllInProgress: action.payload.flag }; return { ...state, muteAllInProgress: action.payload.flag };
@ -194,18 +235,18 @@ const room = (state = initialState, action) =>
case 'CLEAR_FILE_SHARING_IN_PROGRESS': case 'CLEAR_FILE_SHARING_IN_PROGRESS':
return { ...state, clearFileSharingInProgress: action.payload.flag }; return { ...state, clearFileSharingInProgress: action.payload.flag };
case 'SET_USER_ROLES': case 'SET_ROOM_PERMISSIONS':
{ {
const { userRoles } = action.payload; const { roomPermissions } = action.payload;
return { ...state, userRoles }; return { ...state, roomPermissions };
} }
case 'SET_PERMISSIONS_FROM_ROLES': case 'SET_ALLOW_WHEN_ROLE_MISSING':
{ {
const { permissionsFromRoles } = action.payload; const { allowWhenRoleMissing } = action.payload;
return { ...state, permissionsFromRoles }; return { ...state, allowWhenRoleMissing };
} }
default: default:

View File

@ -4,9 +4,23 @@ const initialState =
selectedWebcam : null, selectedWebcam : null,
selectedAudioDevice : null, selectedAudioDevice : null,
advancedMode : false, advancedMode : false,
resolution : 'medium', // low, medium, high, veryhigh, ultra 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, lastN : 4,
permanentTopBar : true permanentTopBar : true,
hiddenControls : false,
showNotifications : true,
notificationSounds : true,
buttonControlBar : window.config.buttonControlBar || false,
drawerOverlayed : window.config.drawerOverlayed || true,
...window.config.defaultAudio
}; };
const settings = (state = initialState, action) => const settings = (state = initialState, action) =>
@ -42,6 +56,83 @@ const settings = (state = initialState, action) =>
return { ...state, advancedMode }; 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': case 'SET_LAST_N':
{ {
const { lastN } = action.payload; const { lastN } = action.payload;
@ -56,6 +147,41 @@ const settings = (state = initialState, action) =>
return { ...state, permanentTopBar }; return { ...state, permanentTopBar };
} }
case 'TOGGLE_BUTTON_CONTROL_BAR':
{
const buttonControlBar = !state.buttonControlBar;
return { ...state, buttonControlBar };
}
case 'TOGGLE_DRAWER_OVERLAYED':
{
const drawerOverlayed = !state.drawerOverlayed;
return { ...state, drawerOverlayed };
}
case 'TOGGLE_HIDDEN_CONTROLS':
{
const hiddenControls = !state.hiddenControls;
return { ...state, hiddenControls };
}
case 'TOGGLE_NOTIFICATION_SOUNDS':
{
const notificationSounds = !state.notificationSounds;
return { ...state, notificationSounds };
}
case 'TOGGLE_SHOW_NOTIFICATIONS':
{
const showNotifications = !state.showNotifications;
return { ...state, showNotifications };
}
case 'SET_VIDEO_RESOLUTION': case 'SET_VIDEO_RESOLUTION':
{ {
const { resolution } = action.payload; const { resolution } = action.payload;

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "登录", "tooltip.login": "登录",
"tooltip.logout": "注销", "tooltip.logout": "注销",
"tooltip.admitFromLobby": "从大厅允许", "tooltip.admitFromLobby": "从大厅允许",
@ -69,6 +80,10 @@
"tooltip.settings": "显示设置", "tooltip.settings": "显示设置",
"tooltip.participants": "显示参加者", "tooltip.participants": "显示参加者",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "房间名称", "label.roomName": "房间名称",
"label.chooseRoomButton": "继续", "label.chooseRoomButton": "继续",
@ -82,6 +97,7 @@
"label.filesharing": "文件共享", "label.filesharing": "文件共享",
"label.participants": "参与者", "label.participants": "参与者",
"label.shareFile": "共享文件", "label.shareFile": "共享文件",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "不支持文件共享", "label.fileSharingUnsupported": "不支持文件共享",
"label.unknown": "未知", "label.unknown": "未知",
"label.democratic": "民主视图", "label.democratic": "民主视图",
@ -92,6 +108,12 @@
"label.veryHigh": "非常高 (FHD)", "label.veryHigh": "非常高 (FHD)",
"label.ultra": "超高 (UHD)", "label.ultra": "超高 (UHD)",
"label.close": "关闭", "label.close": "关闭",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "设置", "settings.settings": "设置",
"settings.camera": "视频设备", "settings.camera": "视频设备",
@ -109,6 +131,14 @@
"settings.advancedMode": "高级模式", "settings.advancedMode": "高级模式",
"settings.permanentTopBar": "永久顶吧", "settings.permanentTopBar": "永久顶吧",
"settings.lastn": "可见视频数量", "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.saveFileError": "无法保存文件",
"filesharing.startingFileShare": "正在尝试共享文件", "filesharing.startingFileShare": "正在尝试共享文件",

View File

@ -54,9 +54,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Přihlášení", "tooltip.login": "Přihlášení",
"tooltip.logout": "Odhlášení", "tooltip.logout": "Odhlášení",
"tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti", "tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti",
@ -66,6 +77,12 @@
"tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)", "tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)",
"tooltip.lobby": "Ukázat Přijímací místnost", "tooltip.lobby": "Ukázat Přijímací místnost",
"tooltip.settings": "Zobrazit nastavení", "tooltip.settings": "Zobrazit nastavení",
"tooltip.participants": null,
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Jméno místnosti", "label.roomName": "Jméno místnosti",
"label.chooseRoomButton": "Pokračovat", "label.chooseRoomButton": "Pokračovat",
@ -79,6 +96,7 @@
"label.filesharing": "Sdílení souborů", "label.filesharing": "Sdílení souborů",
"label.participants": "Účastníci", "label.participants": "Účastníci",
"label.shareFile": "Sdílet soubor", "label.shareFile": "Sdílet soubor",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Sdílení souborů není podporováno", "label.fileSharingUnsupported": "Sdílení souborů není podporováno",
"label.unknown": "Neznámý", "label.unknown": "Neznámý",
"label.democratic": "Rozvržení: Demokratické", "label.democratic": "Rozvržení: Demokratické",
@ -89,6 +107,12 @@
"label.veryHigh": "Velmi vysoké (FHD)", "label.veryHigh": "Velmi vysoké (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Zavřít", "label.close": "Zavřít",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Nastavení", "settings.settings": "Nastavení",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -104,6 +128,16 @@
"settings.layout": "Rozvržení místnosti", "settings.layout": "Rozvržení místnosti",
"settings.selectRoomLayout": "Vyberte rozvržení místnosti", "settings.selectRoomLayout": "Vyberte rozvržení místnosti",
"settings.advancedMode": "Pokočilý mód", "settings.advancedMode": "Pokočilý mód",
"settings.permanentTopBar": null,
"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.saveFileError": "Není možné uložit soubor",
"filesharing.startingFileShare": "Pokouším se sdílet soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor",

View File

@ -52,12 +52,23 @@
"room.muteAll": "Alle stummschalten", "room.muteAll": "Alle stummschalten",
"room.stopAllVideo": "Alle Videos stoppen", "room.stopAllVideo": "Alle Videos stoppen",
"room.closeMeeting": "Meeting schließen", "room.closeMeeting": "Meeting schließen",
"room.clearChat": null, "room.clearChat": "Liste löschen",
"room.clearFileSharing": null, "room.clearFileSharing": "Liste löschen",
"room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung", "room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung",
"room.moderatoractions": "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": "Impressum",
"room.shortcutKeys": "Tastaturkürzel",
"me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen", "me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
"roles.gotRole": "Rolle erhalten: {role}",
"roles.lostRole": "Rolle entzogen: {role}",
"tooltip.login": "Anmelden", "tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden", "tooltip.logout": "Abmelden",
"tooltip.admitFromLobby": "Teilnehmer reinlassen", "tooltip.admitFromLobby": "Teilnehmer reinlassen",
@ -68,7 +79,11 @@
"tooltip.lobby": "Warteraum", "tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen", "tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer", "tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": "Teilnehmer rauswerfen", "tooltip.kickParticipant": "Rauswerfen",
"tooltip.muteParticipant": "Stummschalten",
"tooltip.muteParticipantVideo": "Video stoppen",
"tooltip.raisedHand": "Hand heben",
"tooltip.muteScreenSharing": "Stoppe Bildschirmfreigabe",
"label.roomName": "Name des Raums", "label.roomName": "Name des Raums",
"label.chooseRoomButton": "Weiter", "label.chooseRoomButton": "Weiter",
@ -82,6 +97,7 @@
"label.filesharing": "Dateien", "label.filesharing": "Dateien",
"label.participants": "Teilnehmer", "label.participants": "Teilnehmer",
"label.shareFile": "Datei hochladen", "label.shareFile": "Datei hochladen",
"label.shareGalleryFile": "Bild teilen",
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt", "label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt", "label.unknown": "Unbekannt",
"label.democratic": "Demokratisch", "label.democratic": "Demokratisch",
@ -92,6 +108,12 @@
"label.veryHigh": "Sehr hoch (FHD)", "label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Schließen", "label.close": "Schließen",
"label.media": "Audio / Video",
"label.appearence": "Erscheinung",
"label.advanced": "Erweiter",
"label.addVideo": "Video hinzufügen",
"label.promoteAllPeers": "Alle Teinehmer einlassen",
"label.moreActions": "Weitere Aktionen",
"settings.settings": "Einstellungen", "settings.settings": "Einstellungen",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Erweiterter Modus", "settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste", "settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos", "settings.lastn": "Anzahl der sichtbaren Videos",
"settings.hiddenControls": "Medienwerkzeugleiste automatisch ausblenden",
"settings.notificationSounds": "Audiosignal bei Benachrichtigungen",
"settings.showNotifications": "Zeige Benachrichtigungen",
"settings.buttonControlBar": "Seperate seitliche Medienwerkzeugleiste",
"settings.echoCancellation": "Echounterdrückung",
"settings.autoGainControl": "Automatische Pegelregelung (Audioeingang)",
"settings.noiseSuppression": "Rauschunterdrückung",
"settings.drawerOverlayed": "Seitenpanel verdeckt Hauptinhalt",
"filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "Kamera getrennt", "devices.cameraDisconnected": "Kamera getrennt",
"devices.cameraError": "Fehler mit deiner Kamera", "devices.cameraError": "Fehler mit deiner Kamera",
"moderator.clearChat": null, "moderator.clearChat": "Moderator hat Chat gelöscht",
"moderator.clearFiles": null, "moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht",
"moderator.muteAudio": null, "moderator.muteAudio": "Moderator hat dich stummgeschaltet",
"moderator.muteVideo": null "moderator.muteVideo": "Moderator hat dein Video gestoppt"
} }

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Log ind", "tooltip.login": "Log ind",
"tooltip.logout": "Log ud", "tooltip.logout": "Log ud",
"tooltip.admitFromLobby": "Giv adgang fra lobbyen", "tooltip.admitFromLobby": "Giv adgang fra lobbyen",
@ -69,6 +80,10 @@
"tooltip.settings": "Vis indstillinger", "tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere", "tooltip.participants": "Vis deltagere",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Værelsesnavn", "label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt", "label.chooseRoomButton": "Fortsæt",
@ -82,6 +97,7 @@
"label.filesharing": "Fildeling", "label.filesharing": "Fildeling",
"label.participants": "Deltagere", "label.participants": "Deltagere",
"label.shareFile": "Del fil", "label.shareFile": "Del fil",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Fildeling er ikke understøttet", "label.fileSharingUnsupported": "Fildeling er ikke understøttet",
"label.unknown": "Ukendt", "label.unknown": "Ukendt",
"label.democracy": "Galleri visning", "label.democracy": "Galleri visning",
@ -92,6 +108,12 @@
"label.veryHigh": "Meget høj (FHD)", "label.veryHigh": "Meget høj (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Luk", "label.close": "Luk",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Indstillinger", "settings.settings": "Indstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -103,13 +125,20 @@
"settings.audioOutput": "Audio output enhed", "settings.audioOutput": "Audio output enhed",
"settings.selectAudioOutput": "Vælg lydudgangsenhed", "settings.selectAudioOutput": "Vælg lydudgangsenhed",
"settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed", "settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed",
"settings.resolution": "Vælg din videoopløsning", "settings.resolution": "Vælg din videoopløsning",
"settings.layout": "Møde visning", "settings.layout": "Møde visning",
"settings.selectRoomLayout": "Vælg møde visning", "settings.selectRoomLayout": "Vælg møde visning",
"settings.advancedMode": "Avanceret tilstand", "settings.advancedMode": "Avanceret tilstand",
"settings.permanentTopBar": "Permanent øverste linje", "settings.permanentTopBar": "Permanent øverste linje",
"settings.lastn": "Antal synlige videoer", "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.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen", "filesharing.startingFileShare": "Forsøger at dele filen",

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Σύνδεση", "tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση", "tooltip.logout": "Αποσύνδεση",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -69,6 +80,10 @@
"tooltip.settings": "Εμφάνιση ρυθμίσεων", "tooltip.settings": "Εμφάνιση ρυθμίσεων",
"tooltip.participants": "Εμφάνιση συμμετεχόντων", "tooltip.participants": "Εμφάνιση συμμετεχόντων",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Όνομα δωματίου", "label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια", "label.chooseRoomButton": "Συνέχεια",
@ -82,6 +97,7 @@
"label.filesharing": "Διαμοιρασμοός αρχείου", "label.filesharing": "Διαμοιρασμοός αρχείου",
"label.participants": "Συμμετέχοντες", "label.participants": "Συμμετέχοντες",
"label.shareFile": "Διαμοιραστείτε ένα αρχείο", "label.shareFile": "Διαμοιραστείτε ένα αρχείο",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται", "label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
"label.unknown": "Άγνωστο", "label.unknown": "Άγνωστο",
"label.democratic": null, "label.democratic": null,
@ -92,6 +108,12 @@
"label.veryHigh": "Πολύ υψηλή (FHD)", "label.veryHigh": "Πολύ υψηλή (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Κλείσιμο", "label.close": "Κλείσιμο",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ρυθμίσεις", "settings.settings": "Ρυθμίσεις",
"settings.camera": "Κάμερα", "settings.camera": "Κάμερα",
@ -109,6 +131,14 @@
"settings.advancedMode": "Προηγμένη λειτουργία", "settings.advancedMode": "Προηγμένη λειτουργία",
"settings.permanentTopBar": "Μόνιμη μπάρα κορυφής", "settings.permanentTopBar": "Μόνιμη μπάρα κορυφής",
"settings.lastn": "Αριθμός ορατών βίντεο", "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.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",

View File

@ -55,9 +55,20 @@
"room.clearChat": "Clear chat", "room.clearChat": "Clear chat",
"room.clearFileSharing": "Clear files", "room.clearFileSharing": "Clear files",
"room.speechUnsupported": "Your browser does not support speech recognition", "room.speechUnsupported": "Your browser does not support speech recognition",
"room.moderatoractions": "Moderator actions",
"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", "me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
"roles.gotRole": "You got the role: {role}",
"roles.lostRole": "You lost the role: {role}",
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -69,6 +80,10 @@
"tooltip.settings": "Show settings", "tooltip.settings": "Show settings",
"tooltip.participants": "Show participants", "tooltip.participants": "Show participants",
"tooltip.kickParticipant": "Kick out participant", "tooltip.kickParticipant": "Kick out participant",
"tooltip.muteParticipant": "Mute participant",
"tooltip.muteParticipantVideo": "Mute participant video",
"tooltip.raisedHand": "Raise hand",
"tooltip.muteScreenSharing": "Mute participant share",
"label.roomName": "Room name", "label.roomName": "Room name",
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": "Continue",
@ -82,6 +97,7 @@
"label.filesharing": "File sharing", "label.filesharing": "File sharing",
"label.participants": "Participants", "label.participants": "Participants",
"label.shareFile": "Share file", "label.shareFile": "Share file",
"label.shareGalleryFile": "Share image",
"label.fileSharingUnsupported": "File sharing not supported", "label.fileSharingUnsupported": "File sharing not supported",
"label.unknown": "Unknown", "label.unknown": "Unknown",
"label.democratic": "Democratic view", "label.democratic": "Democratic view",
@ -92,6 +108,12 @@
"label.veryHigh": "Very high (FHD)", "label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Close", "label.close": "Close",
"label.media": "Media",
"label.appearence": "Appearence",
"label.advanced": "Advanced",
"label.addVideo": "Add video",
"label.promoteAllPeers": "Promote all",
"label.moreActions": "More actions",
"settings.settings": "Settings", "settings.settings": "Settings",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Advanced mode", "settings.advancedMode": "Advanced mode",
"settings.permanentTopBar": "Permanent top bar", "settings.permanentTopBar": "Permanent top bar",
"settings.lastn": "Number of visible videos", "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.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file", "filesharing.startingFileShare": "Attempting to share file",

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Salir", "tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera", "tooltip.admitFromLobby": "Admitir desde la sala de espera",
@ -69,6 +80,10 @@
"tooltip.settings": "Mostrar ajustes", "tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes", "tooltip.participants": "Mostrar participantes",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nombre de la sala", "label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -82,6 +97,7 @@
"label.filesharing": "Compartir ficheros", "label.filesharing": "Compartir ficheros",
"label.participants": "Participantes", "label.participants": "Participantes",
"label.shareFile": "Compartir fichero", "label.shareFile": "Compartir fichero",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Compartir ficheros no está disponible", "label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido", "label.unknown": "Desconocido",
"label.democratic": "Vista democrática", "label.democratic": "Vista democrática",
@ -92,6 +108,12 @@
"label.veryHigh": "Muy alta (FHD)", "label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Cerrar", "label.close": "Cerrar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ajustes", "settings.settings": "Ajustes",
"settings.camera": "Cámara", "settings.camera": "Cámara",
@ -109,6 +131,14 @@
"settings.advancedMode": "Modo avanzado", "settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles", "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.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero",

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Connexion", "tooltip.login": "Connexion",
"tooltip.logout": "Déconnexion", "tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente", "tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
@ -69,6 +80,10 @@
"tooltip.settings": "Afficher les paramètres", "tooltip.settings": "Afficher les paramètres",
"tooltip.participants": "Afficher les participants", "tooltip.participants": "Afficher les participants",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nom de la salle", "label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer", "label.chooseRoomButton": "Continuer",
@ -82,6 +97,7 @@
"label.filesharing": "Partage de fichier", "label.filesharing": "Partage de fichier",
"label.participants": "Participants", "label.participants": "Participants",
"label.shareFile": "Partager un fichier", "label.shareFile": "Partager un fichier",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partage de fichier non supporté", "label.fileSharingUnsupported": "Partage de fichier non supporté",
"label.unknown": "Inconnu", "label.unknown": "Inconnu",
"label.democratic": "Vue démocratique", "label.democratic": "Vue démocratique",
@ -92,6 +108,12 @@
"label.veryHigh": "Très Haute Définition (FHD)", "label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra": "Ultra Haute Définition", "label.ultra": "Ultra Haute Définition",
"label.close": "Fermer", "label.close": "Fermer",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Paramètres", "settings.settings": "Paramètres",
"settings.camera": "Caméra", "settings.camera": "Caméra",
@ -109,6 +131,14 @@
"settings.advancedMode": "Mode avancé", "settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente", "settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles", "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.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier", "filesharing.startingFileShare": "Début du transfert de fichier",

View File

@ -52,12 +52,23 @@
"room.muteAll": "Utišaj sve", "room.muteAll": "Utišaj sve",
"room.stopAllVideo": "Ugasi sve kamere", "room.stopAllVideo": "Ugasi sve kamere",
"room.closeMeeting": "Završi sastanak", "room.closeMeeting": "Završi sastanak",
"room.clearChat": null, "room.clearChat": "Izbriši razgovor",
"room.clearFileSharing": null, "room.clearFileSharing": "Izbriši dijeljene datoteke",
"room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora", "room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora",
"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", "me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
"roles.gotRole": "Dodijeljena vam je uloga: {role}",
"roles.lostRole": "Uloga: {role} je povučena",
"tooltip.login": "Prijava", "tooltip.login": "Prijava",
"tooltip.logout": "Odjava", "tooltip.logout": "Odjava",
"tooltip.admitFromLobby": "Pusti iz predvorja", "tooltip.admitFromLobby": "Pusti iz predvorja",
@ -67,8 +78,12 @@
"tooltip.leaveFullscreen": "Izađi iz punog ekrana", "tooltip.leaveFullscreen": "Izađi iz punog ekrana",
"tooltip.lobby": "Prikaži predvorje", "tooltip.lobby": "Prikaži predvorje",
"tooltip.settings": "Prikaži postavke", "tooltip.settings": "Prikaži postavke",
"tooltip.participants": "Pokažite sudionike", "tooltip.participants": "Prikaži sudionike",
"tooltip.kickParticipant": "Izbaci sudionika", "tooltip.kickParticipant": "Izbaci sudionika",
"tooltip.muteParticipant": "Utišaj sudionika",
"tooltip.muteParticipantVideo": "Ne primaj video sudionika",
"tooltip.raisedHand": "Podigni ruku",
"tooltip.muteScreenSharing": null,
"label.roomName": "Naziv sobe", "label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi", "label.chooseRoomButton": "Nastavi",
@ -82,6 +97,7 @@
"label.filesharing": "Dijeljenje datoteka", "label.filesharing": "Dijeljenje datoteka",
"label.participants": "Sudionici", "label.participants": "Sudionici",
"label.shareFile": "Dijeli datoteku", "label.shareFile": "Dijeli datoteku",
"label.shareGalleryFile": "Dijeli sliku",
"label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano", "label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano",
"label.unknown": "Nepoznato", "label.unknown": "Nepoznato",
"label.democratic":"Demokratski prikaz", "label.democratic":"Demokratski prikaz",
@ -92,6 +108,12 @@
"label.veryHigh": "Vrlo visoka (FHD)", "label.veryHigh": "Vrlo visoka (FHD)",
"label.ultra": "Ultra visoka (UHD)", "label.ultra": "Ultra visoka (UHD)",
"label.close": "Zatvori", "label.close": "Zatvori",
"label.media": "Medij",
"label.appearence": "Prikaz",
"label.advanced": "Napredno",
"label.addVideo": "Dodaj video",
"label.promoteAllPeers": "Promoviraj sve",
"label.moreActions": null,
"settings.settings": "Postavke", "settings.settings": "Postavke",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -101,14 +123,22 @@
"settings.selectAudio": "Odaberi uređaj za zvuk", "settings.selectAudio": "Odaberi uređaj za zvuk",
"settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk", "settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk",
"settings.audioOutput": "Uređaj za izlaz zvuka", "settings.audioOutput": "Uređaj za izlaz zvuka",
"settings.selectAudioOutput": "Odaberite audio izlazni uređaj", "settings.selectAudioOutput": "Odaberite izlazni uređaj za zvuk",
"settings.cantSelectAudioOutput": "Nije moguće odabrati audio izlazni uređaj", "settings.cantSelectAudioOutput": "Nije moguće odabrati izlazni uređaj za zvuk",
"settings.resolution": "Odaberi video rezoluciju", "settings.resolution": "Odaberi video rezoluciju",
"settings.layout": "Način prikaza", "settings.layout": "Način prikaza",
"settings.selectRoomLayout": "Odaberi način prikaza", "settings.selectRoomLayout": "Odaberi način prikaza",
"settings.advancedMode": "Napredne mogućnosti", "settings.advancedMode": "Napredne mogućnosti",
"settings.permanentTopBar": "Stalna gornja šipka", "settings.permanentTopBar": "Stalna gornja šipka",
"settings.lastn": "Broj vidljivih videozapisa", "settings.lastn": "Broj vidljivih videozapisa",
"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.saveFileError": "Nije moguće spremiti datoteku",
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "Kamera odspojena", "devices.cameraDisconnected": "Kamera odspojena",
"devices.cameraError": "Greška prilikom pristupa kameri", "devices.cameraError": "Greška prilikom pristupa kameri",
"moderator.clearChat": null, "moderator.clearChat": "Moderator je izbrisao razgovor",
"moderator.clearFiles": null, "moderator.clearFiles": "Moderator je izbrisao datoteke",
"moderator.muteAudio": null, "moderator.muteAudio": "Moderator je utišao tvoj zvuk",
"moderator.muteVideo": null "moderator.muteVideo": "Moderator je zaustavio tvoj video"
} }

View File

@ -1,24 +1,24 @@
{ {
"socket.disconnected": "A kapcsolat lebomlott", "socket.disconnected": "A kapcsolat lebomlott",
"socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás", "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", "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.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "Megértettem",
"room.joined": "Csatlakozátál a konferenciához", "room.joined": "Csatlakoztál a konferenciához",
"room.cantJoin": "Sikertelen csatlakozás a konferenciához", "room.cantJoin": "Sikertelen csatlakozás a konferenciához",
"room.youLocked": "A konferenciába való belépés letiltva", "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.youUnLocked": "A konferenciába való belépés engedélyezve",
"room.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése", "room.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése",
"room.locked": "A konferenciába való belépés letiltva", "room.locked": "A konferenciába való belépés letiltva",
"room.unlocked": "A konferenciába való belépés engedélyezve", "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.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.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.lobbyPeerChangedDisplayName": "Az előszobai résztvevő megváltoztatta a nevét: {displayName}",
"room.lobbyPeerChangedPicture": "Az előszobai résztvevő meváltoztatta a képét", "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.setAccessCode": "A konferencia hozzáférési kódja megváltozott",
"room.accessCodeOn": "A konferencia hozzáférési kódja aktiválva", "room.accessCodeOn": "A konferencia hozzáférési kódja aktiválva",
"room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva", "room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva",
@ -39,8 +39,8 @@
"room.audioOnly": "csak Hang", "room.audioOnly": "csak Hang",
"room.audioVideo": "Hang és Videó", "room.audioVideo": "Hang és Videó",
"room.youAreReady": "Ok, kész vagy", "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.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": "A konferencia szobába a a belépés tilos - Várj amíg valaki be nem enged ...", "room.locketWait": "Az automatikus belépés tiltva van - Várj amíg valaki beenged ...",
"room.lobbyAdministration": "Előszoba adminisztráció", "room.lobbyAdministration": "Előszoba adminisztráció",
"room.peersInLobby": "Résztvevők az előszobában", "room.peersInLobby": "Résztvevők az előszobában",
"room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában", "room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában",
@ -49,18 +49,29 @@
"room.spotlights": "Látható résztvevők", "room.spotlights": "Látható résztvevők",
"room.passive": "Passzív résztvevők", "room.passive": "Passzív résztvevők",
"room.videoPaused": "Ez a videóstream szünetel", "room.videoPaused": "Ez a videóstream szünetel",
"room.muteAll": null, "room.muteAll": "Mindenki némítása",
"room.stopAllVideo": null, "room.stopAllVideo": "Mindenki video némítása",
"room.closeMeeting": null, "room.closeMeeting": "Konferencia lebontása",
"room.clearChat": null, "room.clearChat": "Chat történelem kiürítése",
"room.clearFileSharing": null, "room.clearFileSharing": "File megosztás kiürítése",
"room.speechUnsupported": null, "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": "{role} szerepet kaptál",
"roles.lostRole": "Elvesztetted a {role} szerepet",
"tooltip.login": "Belépés", "tooltip.login": "Belépés",
"tooltip.logout": "Kilé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.lockRoom": "A konferenciába való belépés letiltása",
"tooltip.unLockRoom": "konferenciába való belépés engedélyezése", "tooltip.unLockRoom": "konferenciába való belépés engedélyezése",
"tooltip.enterFullscreen": "Teljes képernyős mód", "tooltip.enterFullscreen": "Teljes képernyős mód",
@ -68,7 +79,11 @@
"tooltip.lobby": "Az előszobában várakozók listája", "tooltip.lobby": "Az előszobában várakozók listája",
"tooltip.settings": "Beállítások", "tooltip.settings": "Beállítások",
"tooltip.participants": "Résztvevők", "tooltip.participants": "Résztvevők",
"tooltip.kickParticipant": 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",
"label.roomName": "Konferencia", "label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább", "label.chooseRoomButton": "Tovább",
@ -82,6 +97,7 @@
"label.filesharing": "Fájl megosztás", "label.filesharing": "Fájl megosztás",
"label.participants": "Résztvevők", "label.participants": "Résztvevők",
"label.shareFile": "Fájl megosztása", "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.fileSharingUnsupported": "Fájl megosztás nem támogatott",
"label.unknown": "Ismeretlen", "label.unknown": "Ismeretlen",
"label.democratic": "Egyforma képméretű képkiosztás", "label.democratic": "Egyforma képméretű képkiosztás",
@ -92,6 +108,12 @@
"label.veryHigh": "Nagyon magas (FHD)", "label.veryHigh": "Nagyon magas (FHD)",
"label.ultra": "Ultra magas (UHD)", "label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár", "label.close": "Bezár",
"label.media": "Média",
"label.appearence": "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.settings": "Beállítások",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -109,11 +131,19 @@
"settings.advancedMode": "Részletes információk", "settings.advancedMode": "Részletes információk",
"settings.permanentTopBar": "Állandó felső sáv", "settings.permanentTopBar": "Állandó felső sáv",
"settings.lastn": "A látható videók száma", "settings.lastn": "A látható videók száma",
"settings.hiddenControls": "Média Gombok automatikus elrejtése",
"settings.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": null,
"filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása", "filesharing.startingFileShare": "Fájl megosztása",
"filesharing.successfulFileShare": "A fájl sikeresen megosztva", "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.error": "Hiba a fájlmegosztás során",
"filesharing.finished": "A fájl letöltés befejeződött", "filesharing.finished": "A fájl letöltés befejeződött",
"filesharing.save": "Mentés", "filesharing.save": "Mentés",
@ -123,7 +153,7 @@
"devices.devicesChanged": "Az eszközei megváltoztak, konfiguráld őket be a beállítások menüben", "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.activateAudio": "Hang aktiválása",
"device.muteAudio": "Hang némítása", "device.muteAudio": "Hang némítása",
"device.unMuteAudio": "Hang némítás kikapcsolása", "device.unMuteAudio": "Hang némítás kikapcsolása",
@ -134,9 +164,9 @@
"device.screenSharingUnsupported": "A képernyő megosztás nem támogatott", "device.screenSharingUnsupported": "A képernyő megosztás nem támogatott",
"device.startScreenSharing": "Képernyőmegosztás indítása", "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.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben",
"devices.microphoneMute": "A mikrofon némítva lett", "devices.microphoneMute": "A mikrofon némítva lett",
"devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva", "devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "A kamera kapcsolata lebomlott", "devices.cameraDisconnected": "A kamera kapcsolata lebomlott",
"devices.cameraError": "Hiba történt a kamera elérése során", "devices.cameraError": "Hiba történt a kamera elérése során",
"moderator.clearChat": null, "moderator.clearChat": "A moderátor kiürítette a chat történelmet",
"moderator.clearFiles": null, "moderator.clearFiles": "A moderátor kiürítette a file megosztás történelmet",
"moderator.muteAudio": null, "moderator.muteAudio": "A moderátor elnémította a hangod",
"moderator.muteVideo": null "moderator.muteVideo": "A moderátor elnémította a videód"
} }

View File

@ -49,18 +49,29 @@
"room.spotlights": "Partecipanti in Evidenza", "room.spotlights": "Partecipanti in Evidenza",
"room.passive": "Participanti Passivi", "room.passive": "Participanti Passivi",
"room.videoPaused": "Il video è in pausa", "room.videoPaused": "Il video è in pausa",
"room.muteAll": null, "room.muteAll": "Muta tutti",
"room.stopAllVideo": null, "room.stopAllVideo": "Ferma tutti i video",
"room.closeMeeting": null, "room.closeMeeting": "Termina meeting",
"room.clearChat": null, "room.clearChat": "Pulisci chat",
"room.clearFileSharing": null, "room.clearFileSharing": "Pulisci file sharing",
"room.speechUnsupported": null, "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",
"me.mutedPTT": 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.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Ammetti dalla lobby", "tooltip.admitFromLobby": "Accetta partecipante dalla lobby",
"tooltip.lockRoom": "Blocca stanza", "tooltip.lockRoom": "Blocca stanza",
"tooltip.unLockRoom": "Sblocca stanza", "tooltip.unLockRoom": "Sblocca stanza",
"tooltip.enterFullscreen": "Modalità schermo intero", "tooltip.enterFullscreen": "Modalità schermo intero",
@ -68,6 +79,11 @@
"tooltip.lobby": "Mostra lobby", "tooltip.lobby": "Mostra lobby",
"tooltip.settings": "Mostra impostazioni", "tooltip.settings": "Mostra impostazioni",
"tooltip.participants": "Mostra partecipanti", "tooltip.participants": "Mostra partecipanti",
"tooltip.kickParticipant": "Espelli partecipante",
"tooltip.muteParticipant": "Muta partecipante",
"tooltip.muteParticipantVideo": "Ferma video partecipante",
"tooltip.raisedHand": "Mano alzata",
"tooltip.muteScreenSharing": "Ferma condivisione schermo partecipante",
"label.roomName": "Nome della stanza", "label.roomName": "Nome della stanza",
"label.chooseRoomButton": "Continua", "label.chooseRoomButton": "Continua",
@ -81,6 +97,7 @@
"label.filesharing": "Condivisione file", "label.filesharing": "Condivisione file",
"label.participants": "Partecipanti", "label.participants": "Partecipanti",
"label.shareFile": "Condividi file", "label.shareFile": "Condividi file",
"label.shareGalleryFile": "Condividi immagine",
"label.fileSharingUnsupported": "Condivisione file non supportata", "label.fileSharingUnsupported": "Condivisione file non supportata",
"label.unknown": "Sconosciuto", "label.unknown": "Sconosciuto",
"label.democratic": "Vista Democratica", "label.democratic": "Vista Democratica",
@ -91,6 +108,12 @@
"label.veryHigh": "Molto alta (FHD)", "label.veryHigh": "Molto alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Chiudi", "label.close": "Chiudi",
"label.media": "Media",
"label.appearence": "Aspetto",
"label.advanced": "Avanzate",
"label.addVideo": "Aggiungi video",
"label.promoteAllPeers": "Promuovi tutti",
"label.moreActions": "Altre azioni",
"settings.settings": "Impostazioni", "settings.settings": "Impostazioni",
"settings.camera": "Videocamera", "settings.camera": "Videocamera",
@ -108,6 +131,14 @@
"settings.advancedMode": "Modalità avanzata", "settings.advancedMode": "Modalità avanzata",
"settings.permanentTopBar": "Barra superiore permanente", "settings.permanentTopBar": "Barra superiore permanente",
"settings.lastn": "Numero di video visibili", "settings.lastn": "Numero di video visibili",
"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.saveFileError": "Impossibile salvare file",
"filesharing.startingFileShare": "Tentativo di condivisione file", "filesharing.startingFileShare": "Tentativo di condivisione file",
@ -149,8 +180,8 @@
"devices.cameraDisconnected": "Videocamera scollegata", "devices.cameraDisconnected": "Videocamera scollegata",
"devices.cameraError": "Errore con l'accesso alla videocamera", "devices.cameraError": "Errore con l'accesso alla videocamera",
"moderator.clearChat": null, "moderator.clearChat": "Il moderatore ha pulito la chat",
"moderator.clearFiles": null, "moderator.clearFiles": "Il moderatore ha pulito i file",
"moderator.muteAudio": null, "moderator.muteAudio": "Il moderatore ha mutato il tuo audio",
"moderator.muteVideo": null "moderator.muteVideo": "Il moderatore ha fermato il tuo video"
} }

View File

@ -0,0 +1,181 @@
{
"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,
"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.appearence": "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"
}

View File

@ -55,9 +55,20 @@
"room.clearChat": "Tøm chat", "room.clearChat": "Tøm chat",
"room.clearFileSharing": "Fjern filer", "room.clearFileSharing": "Fjern filer",
"room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning", "room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning",
"room.moderatoractions": "Moderatorhandlinger",
"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", "me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke",
"roles.gotRole": "Du fikk rollen: {role}",
"roles.lostRole": "Du mistet rollen: {role}",
"tooltip.login": "Logg in", "tooltip.login": "Logg in",
"tooltip.logout": "Logg ut", "tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby", "tooltip.admitFromLobby": "Slipp inn fra lobby",
@ -69,6 +80,10 @@
"tooltip.settings": "Vis innstillinger", "tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere", "tooltip.participants": "Vis deltakere",
"tooltip.kickParticipant": "Spark ut deltaker", "tooltip.kickParticipant": "Spark ut deltaker",
"tooltip.muteParticipant": "Demp deltaker",
"tooltip.muteParticipantVideo": "Demp deltakervideo",
"tooltip.raisedHand": "Rekk opp hånden",
"tooltip.muteScreenSharing": "Demp deltaker skjermdeling",
"label.roomName": "Møtenavn", "label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett", "label.chooseRoomButton": "Fortsett",
@ -82,6 +97,7 @@
"label.filesharing": "Fildeling", "label.filesharing": "Fildeling",
"label.participants": "Deltakere", "label.participants": "Deltakere",
"label.shareFile": "Del fil", "label.shareFile": "Del fil",
"label.shareGalleryFile": "Del bilde",
"label.fileSharingUnsupported": "Fildeling ikke støttet", "label.fileSharingUnsupported": "Fildeling ikke støttet",
"label.unknown": "Ukjent", "label.unknown": "Ukjent",
"label.democratic": "Demokratisk", "label.democratic": "Demokratisk",
@ -92,6 +108,12 @@
"label.veryHigh": "Veldig høy (FHD)", "label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Lukk", "label.close": "Lukk",
"label.media": "Media",
"label.appearence": "Utseende",
"label.advanced": "Avansert",
"label.addVideo": "Legg til video",
"label.promoteAllPeers": "Slipp inn alle",
"label.moreActions": "Flere handlinger",
"settings.settings": "Innstillinger", "settings.settings": "Innstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Avansert modus", "settings.advancedMode": "Avansert modus",
"settings.permanentTopBar": "Permanent topplinje", "settings.permanentTopBar": "Permanent topplinje",
"settings.lastn": "Antall videoer synlig", "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.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling", "filesharing.startingFileShare": "Starter fildeling",

View File

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

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Sair", "tooltip.logout": "Sair",
"tooltip.admitFromLobby": "Admitir da sala de espera", "tooltip.admitFromLobby": "Admitir da sala de espera",
@ -69,6 +80,10 @@
"tooltip.settings": "Apresentar definições", "tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes", "tooltip.participants": "Apresentar participantes",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nome da sala", "label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -82,6 +97,7 @@
"label.filesharing": "Partilha de ficheiro", "label.filesharing": "Partilha de ficheiro",
"label.participants": "Participantes", "label.participants": "Participantes",
"label.shareFile": "Partilhar ficheiro", "label.shareFile": "Partilhar ficheiro",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partilha de ficheiro não disponível", "label.fileSharingUnsupported": "Partilha de ficheiro não disponível",
"label.unknown": "Desconhecido", "label.unknown": "Desconhecido",
"label.democratic": "Vista democrática", "label.democratic": "Vista democrática",
@ -92,6 +108,12 @@
"label.veryHigh": "Muito alta (FHD)", "label.veryHigh": "Muito alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Fechar", "label.close": "Fechar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Definições", "settings.settings": "Definições",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Modo avançado", "settings.advancedMode": "Modo avançado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Número de vídeos visíveis", "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.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro",

View File

@ -55,9 +55,20 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Intră în cont", "tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare", "tooltip.logout": "Deconectare",
"tooltip.admitFromLobby": "Admite din hol", "tooltip.admitFromLobby": "Admite din hol",
@ -69,6 +80,10 @@
"tooltip.settings": "Arată setăile", "tooltip.settings": "Arată setăile",
"tooltip.participants": null, "tooltip.participants": null,
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Numele camerei", "label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare", "label.chooseRoomButton": "Continuare",
@ -82,6 +97,7 @@
"label.filesharing": "Partajarea fișierelor", "label.filesharing": "Partajarea fișierelor",
"label.participants": "Participanți", "label.participants": "Participanți",
"label.shareFile": "Partajează fișierul", "label.shareFile": "Partajează fișierul",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată", "label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată",
"label.unknown": "Necunoscut", "label.unknown": "Necunoscut",
"label.democratic": "Distribuție egală a dimensiunii imaginii", "label.democratic": "Distribuție egală a dimensiunii imaginii",
@ -92,6 +108,12 @@
"label.veryHigh": "Rezoluție foarte înaltă (FHD)", "label.veryHigh": "Rezoluție foarte înaltă (FHD)",
"label.ultra": "Rezoluție ultra înaltă (UHD)", "label.ultra": "Rezoluție ultra înaltă (UHD)",
"label.close": "Închide", "label.close": "Închide",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Setări", "settings.settings": "Setări",
"settings.camera": "Cameră video", "settings.camera": "Cameră video",
@ -109,6 +131,14 @@
"settings.advancedMode": "Mod avansat", "settings.advancedMode": "Mod avansat",
"settings.permanentTopBar": "Bara de sus permanentă", "settings.permanentTopBar": "Bara de sus permanentă",
"settings.lastn": "Numărul de videoclipuri vizibile", "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.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului", "filesharing.startingFileShare": "Partajarea fișierului",

View File

@ -0,0 +1,179 @@
{
"socket.disconnected": "Bağlantınız Kesildi",
"socket.reconnecting": "Bağlantınız kesildi, yeniden bağlanmaya çalışılıyor",
"socket.reconnected": "Yeniden bağlandınız",
"socket.requestError": "Sunucu isteğinde hata",
"room.chooseRoom": "Katılmak istediğiniz odanın adını seçin",
"room.cookieConsent": "Bu web sayfası kullanıcı deneyimini geliştirmek için çerezleri kullanmaktadır",
"room.consentUnderstand": "Anladım",
"room.joined": "Odaya katıldın",
"room.cantJoin": "Odaya katılamadın",
"room.youLocked": "Odayı kilitledin",
"room.cantLock": "Oda kilitlenemiyor",
"room.youUnLocked": "Odanın kilidini açtın",
"room.cantUnLock": "Odanın kilidi açılamıyor",
"room.locked": "Oda kilitlendi",
"room.unlocked": "Oda kilidi açıldı",
"room.newLobbyPeer": "Lobiye yeni katılımcı girdi",
"room.lobbyPeerLeft": "Lobiden katılımcı ayrıldı",
"room.lobbyPeerChangedDisplayName": "Lobideki katılımcı adını {displayName} olarak değiştirdi",
"room.lobbyPeerChangedPicture": "Lobideki katılımcı resim değiştirdi",
"room.setAccessCode": "Oda için erişim kodu güncellendi",
"room.accessCodeOn": "Oda erişim kodu etkinleştirildi",
"room.accessCodeOff": "Oda erişim kodu devre dışı",
"room.peerChangedDisplayName": "{oldDisplayName}, {displayName} olarak değiştirildi",
"room.newPeer": "{displayName} odaya katıldı",
"room.newFile": "Yeni dosya mevcut",
"room.toggleAdvancedMode": "Gelişmiş moda geçiş",
"room.setDemocraticView": "Demokratik görünüme geçtiniz",
"room.setFilmStripView": "Filmşeridi görünümüne geçtiniz",
"room.loggedIn": "Giriş yaptınız",
"room.loggedOut": ıkış yaptınız",
"room.changedDisplayName": "Adınız {displayName} olarak değiştirildi",
"room.changeDisplayNameError": "Adınız değiştirilirken bir hata oluştu",
"room.chatError": "Sohbet mesajı gönderilemiyor",
"room.aboutToJoin": "Toplantıya katılmak üzeresiniz",
"room.roomId": "Oda ID: {roomName}",
"room.setYourName": "Katılım için adınızı belirleyin ve nasıl katılmak istediğinizi seçin:",
"room.audioOnly": "Sadece ses",
"room.audioVideo": "Ses ve Video",
"room.youAreReady": "Tamam, hazırsın",
"room.emptyRequireLogin": "Oda boş! Toplantıyı başlatmak için oturum açabilirsiniz veya toplantı sahibi katılana kadar bekleyebilirsiniz",
"room.locketWait": "Oda kilitli - birisi içeri alana kadar bekleyiniz ...",
"room.lobbyAdministration": "Lobi Yöneticisi",
"room.peersInLobby": "Lobideki katılımcılar",
"room.lobbyEmpty": "Lobide katılımcı yok",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Ben",
"room.spotlights": "Gündemdeki Katılımcılar",
"room.passive": "Pasif Katılımcılar",
"room.videoPaused": "Video duraklatıldı",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"room.help": null,
"room.about": null,
"room.shortcutKeys": null,
"me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Giriş",
"tooltip.logout": ıkış",
"tooltip.admitFromLobby": "Lobiden içeri al",
"tooltip.lockRoom": "Oda kilitle",
"tooltip.unLockRoom": "Oda kilidini aç",
"tooltip.enterFullscreen": "Tam Ekrana Geç",
"tooltip.leaveFullscreen": "Tam Ekrandan Çık",
"tooltip.lobby": "Lobiyi göster",
"tooltip.settings": "Ayarları göster",
"tooltip.participants": "Katılımcıları göster",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Oda adı",
"label.chooseRoomButton": "Devam",
"label.yourName": "Adınız",
"label.newWindow": "Yeni pencere",
"label.fullscreen": "Tam Ekran",
"label.openDrawer": "Çiziciyi aç",
"label.leave": "Ayrıl",
"label.chatInput": "Sohbet mesajı gir...",
"label.chat": "Sohbet",
"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",
"label.filmstrip": "Filmşeridi görünüm",
"label.low": "Düşük",
"label.medium": "Orta",
"label.high": "Yüksek (HD)",
"label.veryHigh": "Çok Yüksek (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Kapat",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ayarlar",
"settings.camera": "Kamera",
"settings.selectCamera": "Video aygıtını seç",
"settings.cantSelectCamera": "Video aygıtı seçilemiyor",
"settings.audio": "Ses aygıtı",
"settings.selectAudio": "Ses aygıtını seç",
"settings.cantSelectAudio": "Ses aygıtı seçilemiyor",
"settings.resolution": "Video çözünürlüğü ayarla",
"settings.layout": "Oda düzeni",
"settings.selectRoomLayout": "Oda düzeni seç",
"settings.advancedMode": "Detaylı mod",
"settings.permanentTopBar": "Üst barı kalıcı yap",
"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",
"filesharing.successfulFileShare": "Dosya başarıyla paylaşıldı",
"filesharing.unableToShare": "Dosya paylaşılamıyor",
"filesharing.error": "Dosya paylaşım hatası",
"filesharing.finished": "Dosya indirilmesi tamamlandı",
"filesharing.save": "Kaydet",
"filesharing.sharedFile": "{displayName} bir dosya paylaştı",
"filesharing.download": "İndir",
"filesharing.missingSeeds": "İşlem uzun zaman alıyorsa, bu torrent'i paylaşan kimse olmayabilir. İlgili dosyayı yeniden yüklemesini isteyin.",
"devices.devicesChanged": "Cihazlarınız değişti, ayarlar kutusundan cihazlarınızı yapılandırın",
"device.audioUnsupported": "Ses desteklenmiyor",
"device.activateAudio": "Sesi aktif et",
"device.muteAudio": "Sesi kıs",
"device.unMuteAudio": "Sesi aç",
"device.videoUnsupported": "Video desteklenmiyor",
"device.startVideo": "Video başlat",
"device.stopVideo": "Video durdur",
"device.screenSharingUnsupported": "Ekran paylaşımı desteklenmiyor",
"device.startScreenSharing": "Ekran paylaşımını başlat",
"device.stopScreenSharing": "Ekran paylaşımını durdur",
"devices.microphoneDisconnected": "Mikrofon bağlı değil",
"devices.microphoneError": "Mikrofononuza erişilirken bir hata oluştu",
"devices.microPhoneMute": "Mikrofonumu kıs",
"devices.micophoneUnMute": "Mikrofonumu aç",
"devices.microphoneEnable": "Mikrofonumu aktif et",
"devices.microphoneMuteError": "Mikrofonunuz kısılamıyor",
"devices.microphoneUnMuteError": "Mikrofonunuz açılamıyor",
"devices.screenSharingDisconnected" : "Ekran paylaşımı bağlı değil",
"devices.screenSharingError": "Ekranınıza erişilirken bir hata oluştu",
"devices.cameraDisconnected": "Kamera bağlı değil",
"devices.cameraError": "Kameranıza erişilirken bir hata oluştu"
}

View File

@ -55,6 +55,19 @@
"room.clearChat": null, "room.clearChat": null,
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"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,
"roles.lostRole": null,
"tooltip.login": "Увійти", "tooltip.login": "Увійти",
"tooltip.logout": "Вихід", "tooltip.logout": "Вихід",
@ -66,6 +79,11 @@
"tooltip.lobby": "Показати зал очікувань", "tooltip.lobby": "Показати зал очікувань",
"tooltip.settings": "Показати налаштування", "tooltip.settings": "Показати налаштування",
"tooltip.participants": "Показати учасників", "tooltip.participants": "Показати учасників",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Назва кімнати", "label.roomName": "Назва кімнати",
"label.chooseRoomButton": "Продовжити", "label.chooseRoomButton": "Продовжити",
@ -79,6 +97,7 @@
"label.filesharing": "Обмін файлами", "label.filesharing": "Обмін файлами",
"label.participants": "Учасники", "label.participants": "Учасники",
"label.shareFile": "Надіслати файл", "label.shareFile": "Надіслати файл",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Обмін файлами не підтримується", "label.fileSharingUnsupported": "Обмін файлами не підтримується",
"label.unknown": "Невідомо", "label.unknown": "Невідомо",
"label.democrat": "Демократичний вигляд", "label.democrat": "Демократичний вигляд",
@ -89,6 +108,12 @@
"label.veryHigh": "Дуже високий (FHD)", "label.veryHigh": "Дуже високий (FHD)",
"label.ultra": "Ультра (UHD)", "label.ultra": "Ультра (UHD)",
"label.close": "Закрити", "label.close": "Закрити",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Налаштування", "settings.settings": "Налаштування",
"settings.camera": "Камера", "settings.camera": "Камера",
@ -106,6 +131,14 @@
"settings.advancedMode": "Розширений режим", "settings.advancedMode": "Розширений режим",
"settings.permanentTopBar": "Постійний верхній рядок", "settings.permanentTopBar": "Постійний верхній рядок",
"settings.lastn": "Кількість видимих ​​відео", "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.saveFileError": "Неможливо зберегти файл",
"filesharing.startingFileShare": "Спроба поділитися файлом", "filesharing.startingFileShare": "Спроба поділитися файлом",

55
prom.md 100644
View File

@ -0,0 +1,55 @@
# Prometheus exporter
The goal of this version is to offer a few basic metrics for
initial testing. The set of supported metrics can be extended.
The current implementation is partly
[unconventional](https://prometheus.io/docs/instrumenting/writing_exporters)
in that it creates new metrics each time but does not register a
custom collector. Reasons are that the exporter should
[clear out metrics](https://github.com/prometheus/client_python/issues/182)
for closed connections but that `prom-client`
[does not yet support](https://github.com/siimon/prom-client/issues/241)
custom collectors.
This version has been ported from an earlier Python version that was not part
of `multiparty-meeting` but connected as an interactive client.
## Configuration
See `prometheus` in `server/config/config.example.js` for options and
applicable defaults.
If `multiparty-meeting` was installed with
[`mm-absible`](https://github.com/misi/mm-ansible)
it may be necessary to open the `iptables` firewall for incoming TCP traffic
on the allocated port (see `/etc/ferm/ferm.conf`).
## Metrics
| metric | value |
|--------|-------|
| `edumeet_peers`| |
| `edumeet_rooms`| |
| `mediasoup_consumer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
| `mediasoup_consumer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
| `mediasoup_producer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
| `mediasoup_producer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
## Architecture
```
+-----------+ +---------------------------------------------+
| workers | | server observer API |
| | sock | +------o------+----o-----+
| +------+ | int. server | exporter |
| | | | | |
| mediasoup | | express socket.io | net | express |
+-----+-----+ +----+---------+-----+-----+-------+-----+----+
^ min-max ^ 443 ^ 443 ^ sock ^ PROM_PORT
| RTP | HTTPS | ws | | HTTP
| | | | |
| +-+---------+-+ +------+------+ +---+--------+
+---------------+ app | | int. client | | Prometheus |
+-------------+ +-------------+ +------------+
```

11
server/access.js 100644
View File

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

View File

@ -1,5 +1,23 @@
const os = require('os'); const os = require('os');
const userRoles = require('../userRoles'); const userRoles = require('../userRoles');
const {
BYPASS_ROOM_LOCK,
BYPASS_LOBBY
} = require('../access');
const {
CHANGE_ROOM_LOCK,
PROMOTE_PEER,
SEND_CHAT,
MODERATE_CHAT,
SHARE_SCREEN,
EXTRA_VIDEO,
SHARE_FILE,
MODERATE_FILES,
MODERATE_ROOM
} = require('../permissions');
// const AwaitQueue = require('awaitqueue'); // const AwaitQueue = require('awaitqueue');
// const axios = require('axios'); // const axios = require('axios');
@ -60,9 +78,11 @@ module.exports =
// session cookie secret // session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e', cookieSecret : 'T0P-S3cR3t_cook!e',
cookieName : 'multiparty-meeting.sid', cookieName : 'multiparty-meeting.sid',
// if you use encrypted private key the set the passphrase
tls : tls :
{ {
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
// passphrase: 'key_password'
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
}, },
// listening Host or IP // listening Host or IP
@ -77,6 +97,12 @@ module.exports =
// listeningRedirectPort disabled // listeningRedirectPort disabled
// use case: loadbalancer backend // use case: loadbalancer backend
httpOnly : false, httpOnly : false,
// WebServer/Express trust proxy config for httpOnly mode
// You can find more info:
// - https://expressjs.com/en/guide/behind-proxies.html
// - https://www.npmjs.com/package/proxy-addr
// use case: loadbalancer backend
trustProxy : '',
// This logger class will have the log function // This logger class will have the log function
// called every time there is a room created or destroyed, // called every time there is a room created or destroyed,
// or peer created or destroyed. This would then be able // or peer created or destroyed. This would then be able
@ -88,8 +114,8 @@ module.exports =
this._queue = new AwaitQueue(); this._queue = new AwaitQueue();
} }
// rooms: number of rooms // rooms: rooms object
// peers: number of peers // peers: peers object
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
async log({ rooms, peers }) async log({ rooms, peers })
{ {
@ -98,9 +124,9 @@ module.exports =
// Do your logging in here, use queue to keep correct order // Do your logging in here, use queue to keep correct order
// eslint-disable-next-line no-console // 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 // eslint-disable-next-line no-console
console.log('Number of peers: ', peers); console.log('Number of peers: ', peers.size);
}) })
.catch((error) => .catch((error) =>
{ {
@ -109,12 +135,6 @@ module.exports =
}); });
} }
}, */ }, */
// WebServer/Express trust proxy config for httpOnly mode
// You can find more info:
// - https://expressjs.com/en/guide/behind-proxies.html
// - https://www.npmjs.com/package/proxy-addr
// use case: loadbalancer backend
trustProxy : '',
// This function will be called on successful login through oidc. // This function will be called on successful login through oidc.
// Use this function to map your oidc userinfo to the Peer object. // Use this function to map your oidc userinfo to the Peer object.
// The roomId is equal to the room name. // The roomId is equal to the room name.
@ -214,35 +234,48 @@ module.exports =
accessFromRoles : { accessFromRoles : {
// The role(s) will gain access to the room // The role(s) will gain access to the room
// even if it is locked (!) // even if it is locked (!)
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
// The role(s) will gain access to the room without // The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your // going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could // server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping // add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ] [BYPASS_LOBBY] : [ userRoles.NORMAL ]
}, },
permissionsFromRoles : { permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room // The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ],
// The role(s) have permission to promote a peer from the lobby // The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : [ userRoles.NORMAL ], [PROMOTE_PEER] : [ userRoles.NORMAL ],
// The role(s) have permission to send chat messages // The role(s) have permission to send chat messages
SEND_CHAT : [ userRoles.NORMAL ], [SEND_CHAT] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate chat // The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ], [MODERATE_CHAT] : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen // The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ], [SHARE_SCREEN] : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
[EXTRA_VIDEO] : [ userRoles.NORMAL ],
// The role(s) have permission to share files // The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ], [SHARE_FILE] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files // The role(s) have permission to moderate files
MODERATE_FILES : [ userRoles.MODERATOR ], [MODERATE_FILES] : [ userRoles.MODERATOR ],
// The role(s) have permission to moderate room (e.g. kick user) // The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : [ userRoles.MODERATOR ] [MODERATE_ROOM] : [ userRoles.MODERATOR ]
}, },
// Array of permissions. If no peer with the permission in question
// is in the room, all peers are permitted to do the action. The peers
// that are allowed because of this rule will not be able to do this
// action as soon as a peer with the permission joins. In this example
// everyone will be able to lock/unlock room until a MODERATOR joins.
allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ],
// When truthy, the room will be open to all users when as long as there // When truthy, the room will be open to all users when as long as there
// are allready users in the room // are allready users in the room
activateOnHostJoin : true, activateOnHostJoin : true,
// When set, maxUsersPerRoom defines how many users can join
// a single room. If not set, there is no limit.
// maxUsersPerRoom : 20,
// Room size before spreading to new router
routerScaleSize : 40,
// Mediasoup settings // Mediasoup settings
mediasoup : mediasoup :
{ {
@ -325,11 +358,12 @@ module.exports =
{ {
listenIps : listenIps :
[ [
// change ip to your servers IP address! // change 192.0.2.1 IPv4 to your server's IPv4 address!!
{ ip: '0.0.0.0', announcedIp: null } { ip: '192.0.2.1', announcedIp: null }
// Can have multiple listening interfaces // Can have multiple listening interfaces
// { ip: '::/0', announcedIp: null } // change 2001:DB8::1 IPv6 to your server's IPv6 address!!
// { ip: '2001:DB8::1', announcedIp: null }
], ],
initialAvailableOutgoingBitrate : 1000000, initialAvailableOutgoingBitrate : 1000000,
minimumAvailableOutgoingBitrate : 600000, minimumAvailableOutgoingBitrate : 600000,
@ -337,4 +371,13 @@ module.exports =
maxIncomingBitrate : 1500000 maxIncomingBitrate : 1500000
} }
} }
// Prometheus exporter
/*
prometheus: {
deidentify: false, // deidentify IP addresses
numeric: false, // show numeric IP addresses
port: 8889, // allocated port
quiet: false // include fewer labels
}
*/
}; };

View File

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

View File

@ -23,10 +23,14 @@ class Peer extends EventEmitter
this._joined = false; this._joined = false;
this._joinedTimestamp = null;
this._inLobby = false; this._inLobby = false;
this._authenticated = false; this._authenticated = false;
this._authenticatedTimestamp = null;
this._roles = [ userRoles.NORMAL ]; this._roles = [ userRoles.NORMAL ];
this._displayName = false; this._displayName = false;
@ -35,10 +39,14 @@ class Peer extends EventEmitter
this._email = null; this._email = null;
this._routerId = null;
this._rtpCapabilities = null; this._rtpCapabilities = null;
this._raisedHand = false; this._raisedHand = false;
this._raisedHandTimestamp = null;
this._transports = new Map(); this._transports = new Map();
this._producers = new Map(); this._producers = new Map();
@ -56,10 +64,10 @@ class Peer extends EventEmitter
// Iterate and close all mediasoup Transport associated to this Peer, so all // Iterate and close all mediasoup Transport associated to this Peer, so all
// its Producers and Consumers will also be closed. // its Producers and Consumers will also be closed.
this.transports.forEach((transport) => for (const transport of this.transports.values())
{ {
transport.close(); transport.close();
}); }
if (this.socket) if (this.socket)
this.socket.disconnect(true); this.socket.disconnect(true);
@ -135,9 +143,18 @@ class Peer extends EventEmitter
set joined(joined) set joined(joined)
{ {
joined ?
this._joinedTimestamp = Date.now() :
this._joinedTimestamp = null;
this._joined = joined; this._joined = joined;
} }
get joinedTimestamp()
{
return this._joinedTimestamp;
}
get inLobby() get inLobby()
{ {
return this._inLobby; return this._inLobby;
@ -157,6 +174,10 @@ class Peer extends EventEmitter
{ {
if (authenticated !== this._authenticated) if (authenticated !== this._authenticated)
{ {
authenticated ?
this._authenticatedTimestamp = Date.now() :
this._authenticatedTimestamp = null;
const oldAuthenticated = this._authenticated; const oldAuthenticated = this._authenticated;
this._authenticated = authenticated; this._authenticated = authenticated;
@ -165,6 +186,11 @@ class Peer extends EventEmitter
} }
} }
get authenticatedTimestamp()
{
return this._authenticatedTimestamp;
}
get roles() get roles()
{ {
return this._roles; return this._roles;
@ -214,6 +240,16 @@ class Peer extends EventEmitter
this._email = email; this._email = email;
} }
get routerId()
{
return this._routerId;
}
set routerId(routerId)
{
this._routerId = routerId;
}
get rtpCapabilities() get rtpCapabilities()
{ {
return this._rtpCapabilities; return this._rtpCapabilities;
@ -231,9 +267,18 @@ class Peer extends EventEmitter
set raisedHand(raisedHand) set raisedHand(raisedHand)
{ {
raisedHand ?
this._raisedHandTimestamp = Date.now() :
this._raisedHandTimestamp = null;
this._raisedHand = raisedHand; this._raisedHand = raisedHand;
} }
get raisedHandTimestamp()
{
return this._raisedHandTimestamp;
}
get transports() get transports()
{ {
return this._transports; return this._transports;
@ -336,7 +381,9 @@ class Peer extends EventEmitter
id : this.id, id : this.id,
displayName : this.displayName, displayName : this.displayName,
picture : this.picture, picture : this.picture,
roles : this.roles roles : this.roles,
raisedHand : this.raisedHand,
raisedHandTimestamp : this.raisedHandTimestamp
}; };
return peerInfo; return peerInfo;

View File

@ -1,14 +1,59 @@
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const AwaitQueue = require('awaitqueue');
const axios = require('axios'); const axios = require('axios');
const Logger = require('./Logger'); const Logger = require('./Logger');
const Lobby = require('./Lobby'); const Lobby = require('./Lobby');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const userRoles = require('../userRoles'); const userRoles = require('../userRoles');
const {
BYPASS_ROOM_LOCK,
BYPASS_LOBBY
} = require('../access');
const permissions = require('../permissions'), {
CHANGE_ROOM_LOCK,
PROMOTE_PEER,
SEND_CHAT,
MODERATE_CHAT,
SHARE_SCREEN,
EXTRA_VIDEO,
SHARE_FILE,
MODERATE_FILES,
MODERATE_ROOM
} = permissions;
const config = require('../config/config'); const config = require('../config/config');
const logger = new Logger('Room'); const logger = new Logger('Room');
// In case they are not configured properly
const roomAccess =
{
[BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
[BYPASS_LOBBY] : [ userRoles.NORMAL ],
...config.accessFromRoles
};
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 ],
...config.permissionsFromRoles
};
const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || [];
const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
class Room extends EventEmitter class Room extends EventEmitter
{ {
/** /**
@ -16,32 +61,49 @@ class Room extends EventEmitter
* *
* @async * @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. * mediasoup Router must be created.
* @param {String} roomId - Id of the Room instance. * @param {String} roomId - Id of the Room instance.
*/ */
static async create({ mediasoupWorker, roomId }) static async create({ mediasoupWorkers, roomId })
{ {
logger.info('create() [roomId:"%s"]', roomId); logger.info('create() [roomId:"%s"]', roomId);
// Shuffle workers to get random cores
let shuffledWorkers = mediasoupWorkers.sort(() => Math.random() - 0.5);
// Router media codecs. // Router media codecs.
const mediaCodecs = config.mediasoup.router.mediaCodecs; const mediaCodecs = config.mediasoup.router.mediaCodecs;
// Create a mediasoup Router. const mediasoupRouters = new Map();
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
// Create a mediasoup AudioLevelObserver. let firstRouter = null;
const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
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, maxEntries : 1,
threshold : -80, threshold : -80,
interval : 800 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); logger.info('constructor() [roomId:"%s"]', roomId);
@ -56,6 +118,9 @@ class Room extends EventEmitter
// Closed flag. // Closed flag.
this._closed = false; this._closed = false;
// Joining queue
this._queue = new AwaitQueue();
// Locked flag. // Locked flag.
this._locked = false; this._locked = false;
@ -76,8 +141,15 @@ class Room extends EventEmitter
this._peers = {}; this._peers = {};
// mediasoup Router instance. this._selfDestructTimeout = null;
this._mediasoupRouter = mediasoupRouter;
// 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. // mediasoup AudioLevelObserver.
this._audioLevelObserver = audioLevelObserver; this._audioLevelObserver = audioLevelObserver;
@ -100,8 +172,23 @@ class Room extends EventEmitter
this._closed = true; 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.close();
this._lobby = null;
// Close the peers. // Close the peers.
for (const peer in this._peers) for (const peer in this._peers)
{ {
@ -111,8 +198,19 @@ class Room extends EventEmitter
this._peers = null; this._peers = null;
// Close the mediasoup Router. // Close the mediasoup Routers.
this._mediasoupRouter.close(); for (const router of this._mediasoupRouters.values())
{
router.close();
}
this._routerIterator = null;
this._currentRouter = null;
this._mediasoupRouters.clear();
this._audioLevelObserver = null;
// Emit 'close' event. // Emit 'close' event.
this.emit('close'); this.emit('close');
@ -151,21 +249,34 @@ class Room extends EventEmitter
// Returning user // Returning user
if (returning) if (returning)
this._peerJoining(peer, true); this._peerJoining(peer, true);
else if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => config.accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) else if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
this._peerJoining(peer); 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) else if (this._locked)
this._parkPeer(peer); this._parkPeer(peer);
else else
{ {
// Has a role that is allowed to bypass lobby // Has a role that is allowed to bypass lobby
peer.roles.some((role) => config.accessFromRoles.BYPASS_LOBBY.includes(role)) ? this._hasAccess(peer, BYPASS_LOBBY) ?
this._peerJoining(peer) : this._peerJoining(peer) :
this._handleGuest(peer); this._handleGuest(peer);
} }
} }
_handleOverRoomLimit(peer)
{
this._notification(peer.socket, 'overRoomLimit');
}
_handleGuest(peer) _handleGuest(peer)
{ {
if (config.activateOnHostJoin && !this.checkEmpty()) if (config.activateOnHostJoin && !this.checkEmpty())
@ -187,7 +298,7 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer); this._peerJoining(promotedPeer);
for (const peer of this._getJoinedPeers()) for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{ {
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
} }
@ -195,9 +306,8 @@ class Room extends EventEmitter
this._lobby.on('peerRolesChanged', (peer) => this._lobby.on('peerRolesChanged', (peer) =>
{ {
if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => config.accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -206,7 +316,7 @@ class Room extends EventEmitter
if ( // Has a role that is allowed to bypass lobby if ( // Has a role that is allowed to bypass lobby
!this._locked && !this._locked &&
peer.roles.some((role) => config.accessFromRoles.BYPASS_LOBBY.includes(role)) this._hasAccess(peer, BYPASS_LOBBY)
) )
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -219,7 +329,7 @@ class Room extends EventEmitter
{ {
const { id, displayName } = changedPeer; const { id, displayName } = changedPeer;
for (const peer of this._getJoinedPeers()) for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{ {
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
} }
@ -229,7 +339,7 @@ class Room extends EventEmitter
{ {
const { id, picture } = changedPeer; const { id, picture } = changedPeer;
for (const peer of this._getJoinedPeers()) for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{ {
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
} }
@ -241,7 +351,7 @@ class Room extends EventEmitter
const { id } = closedPeer; const { id } = closedPeer;
for (const peer of this._getJoinedPeers()) for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{ {
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
} }
@ -301,7 +411,7 @@ class Room extends EventEmitter
); );
} }
async dump() dump()
{ {
return { return {
roomId : this._roomId, roomId : this._roomId,
@ -318,7 +428,10 @@ class Room extends EventEmitter
{ {
logger.debug('selfDestructCountdown() started'); logger.debug('selfDestructCountdown() started');
setTimeout(() => if (this._selfDestructTimeout)
clearTimeout(this._selfDestructTimeout);
this._selfDestructTimeout = setTimeout(() =>
{ {
if (this._closed) if (this._closed)
return; return;
@ -344,13 +457,15 @@ class Room extends EventEmitter
{ {
this._lobby.parkPeer(parkPeer); this._lobby.parkPeer(parkPeer);
for (const peer of this._getJoinedPeers()) for (const peer of this._getAllowedPeers(PROMOTE_PEER))
{ {
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
} }
} }
async _peerJoining(peer, returning = false) _peerJoining(peer, returning = false)
{
this._queue.push(async () =>
{ {
peer.socket.join(this._roomId); peer.socket.join(this._roomId);
@ -359,6 +474,9 @@ class Room extends EventEmitter
this._peers[peer.id] = peer; this._peers[peer.id] = peer;
// Assign routerId
peer.routerId = await this._getRouterId();
this._handlePeer(peer); this._handlePeer(peer);
if (returning) if (returning)
@ -410,45 +528,20 @@ class Room extends EventEmitter
this._notification(peer.socket, 'roomReady', { turnServers }); this._notification(peer.socket, 'roomReady', { turnServers });
} }
})
.catch((error) =>
{
logger.error('_peerJoining() [error:"%o"]', error);
});
} }
_handlePeer(peer) _handlePeer(peer)
{ {
logger.debug('_handlePeer() [peer:"%s"]', peer.id); logger.debug('_handlePeer() [peer:"%s"]', peer.id);
peer.socket.on('request', (request, cb) =>
{
logger.debug(
'Peer "request" event [method:"%s", peerId:"%s"]',
request.method, peer.id);
this._handleSocketRequest(peer, request, cb)
.catch((error) =>
{
logger.error('"request" failed [error:"%o"]', error);
cb(error);
});
});
peer.on('close', () => peer.on('close', () =>
{ {
if (this._closed) this._handlePeerClose(peer);
return;
// If the Peer was joined, notify all Peers.
if (peer.joined)
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
// Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
delete this._peers[peer.id];
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty())
this.selfDestructCountdown();
}); });
peer.on('displayNameChanged', ({ oldDisplayName }) => peer.on('displayNameChanged', ({ oldDisplayName }) =>
@ -489,6 +582,17 @@ class Room extends EventEmitter
peerId : peer.id, peerId : peer.id,
role : newRole role : newRole
}, true, true); }, true, true);
// Got permission to promote peers, notify peer of
// peers in lobby
if (roomPermissions.PROMOTE_PEER.includes(newRole))
{
const lobbyPeers = this._lobby.peerList();
lobbyPeers.length > 0 && this._notification(peer.socket, 'parkedPeers', {
lobbyPeers
});
}
}); });
peer.on('lostRole', ({ oldRole }) => peer.on('lostRole', ({ oldRole }) =>
@ -503,15 +607,81 @@ class Room extends EventEmitter
role : oldRole role : oldRole
}, true, true); }, true, true);
}); });
peer.socket.on('request', (request, cb) =>
{
logger.debug(
'Peer "request" event [method:"%s", peerId:"%s"]',
request.method, peer.id);
this._handleSocketRequest(peer, request, cb)
.catch((error) =>
{
logger.error('"request" failed [error:"%o"]', error);
cb(error);
});
});
// Peer left before we were done joining
if (peer.closed)
this._handlePeerClose(peer);
}
_handlePeerClose(peer)
{
logger.debug('_handlePeerClose() [peer:"%s"]', peer.id);
if (this._closed)
return;
// If the Peer was joined, notify all Peers.
if (peer.joined)
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
// Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
// Need this to know if this peer was the last with PROMOTE_PEER
const hasPromotePeer = peer.roles.some((role) =>
roomPermissions[PROMOTE_PEER].includes(role)
);
delete this._peers[peer.id];
// No peers left with PROMOTE_PEER, might need to give
// lobbyPeers to peers that are left.
if (
hasPromotePeer &&
!this._lobby.checkEmpty() &&
roomAllowWhenRoleMissing.includes(PROMOTE_PEER) &&
this._getPeersWithPermission(PROMOTE_PEER).length === 0
)
{
const lobbyPeers = this._lobby.peerList();
for (const allowedPeer of this._getAllowedPeers(PROMOTE_PEER))
{
this._notification(allowedPeer.socket, 'parkedPeers', { lobbyPeers });
}
}
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty())
this.selfDestructCountdown();
} }
async _handleSocketRequest(peer, request, cb) async _handleSocketRequest(peer, request, cb)
{ {
const router =
this._mediasoupRouters.get(peer.routerId);
switch (request.method) switch (request.method)
{ {
case 'getRouterRtpCapabilities': case 'getRouterRtpCapabilities':
{ {
cb(null, this._mediasoupRouter.rtpCapabilities); cb(null, router.rtpCapabilities);
break; break;
} }
@ -536,22 +706,31 @@ class Room extends EventEmitter
// Tell the new Peer about already joined Peers. // Tell the new Peer about already joined Peers.
// And also create Consumers for existing Producers. // And also create Consumers for existing Producers.
const joinedPeers = const joinedPeers = this._getJoinedPeers(peer);
[
...this._getJoinedPeers()
];
const peerInfos = joinedPeers const peerInfos = joinedPeers
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo)); .map((joinedPeer) => (joinedPeer.peerInfo));
let lobbyPeers = [];
// Allowed to promote peers, notify about lobbypeers
if (this._hasPermission(peer, PROMOTE_PEER))
lobbyPeers = this._lobby.peerList();
cb(null, { cb(null, {
roles : peer.roles, roles : peer.roles,
peers : peerInfos, peers : peerInfos,
tracker : config.fileTracker, tracker : config.fileTracker,
authenticated : peer.authenticated, authenticated : peer.authenticated,
permissionsFromRoles : config.permissionsFromRoles, roomPermissions : roomPermissions,
userRoles : userRoles userRoles : userRoles,
allowWhenRoleMissing : roomAllowWhenRoleMissing,
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}); });
// Mark the new Peer as joined. // Mark the new Peer as joined.
@ -572,7 +751,7 @@ class Room extends EventEmitter
} }
// Notify the new Peer to all other Peers. // Notify the new Peer to all other Peers.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) for (const otherPeer of this._getJoinedPeers(peer))
{ {
this._notification( this._notification(
otherPeer.socket, otherPeer.socket,
@ -612,7 +791,7 @@ class Room extends EventEmitter
webRtcTransportOptions.enableTcp = true; webRtcTransportOptions.enableTcp = true;
} }
const transport = await this._mediasoupRouter.createWebRtcTransport( const transport = await router.createWebRtcTransport(
webRtcTransportOptions webRtcTransportOptions
); );
@ -682,7 +861,13 @@ class Room extends EventEmitter
if ( if (
appData.source === 'screen' && appData.source === 'screen' &&
!peer.roles.some((role) => config.permissionsFromRoles.SHARE_SCREEN.includes(role)) !this._hasPermission(peer, SHARE_SCREEN)
)
throw new Error('peer not authorized');
if (
appData.source === 'extravideo' &&
!this._hasPermission(peer, EXTRA_VIDEO)
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
@ -703,6 +888,19 @@ class Room extends EventEmitter
const producer = const producer =
await transport.produce({ kind, rtpParameters, appData }); 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. // Store the Producer into the Peer data Object.
peer.addProducer(producer.id, producer); peer.addProducer(producer.id, producer);
@ -722,7 +920,7 @@ class Room extends EventEmitter
cb(null, { id: producer.id }); cb(null, { id: producer.id });
// Optimization: Create a server-side Consumer for each Peer. // Optimization: Create a server-side Consumer for each Peer.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) for (const otherPeer of this._getJoinedPeers(peer))
{ {
this._createConsumer( this._createConsumer(
{ {
@ -984,9 +1182,7 @@ class Room extends EventEmitter
case 'chatMessage': case 'chatMessage':
{ {
if ( if (!this._hasPermission(peer, SEND_CHAT))
!peer.roles.some((role) => config.permissionsFromRoles.SEND_CHAT.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { chatMessage } = request.data; const { chatMessage } = request.data;
@ -1007,9 +1203,7 @@ class Room extends EventEmitter
case 'moderator:clearChat': case 'moderator:clearChat':
{ {
if ( if (!this._hasPermission(peer, MODERATE_CHAT))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_CHAT.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._chatHistory = []; this._chatHistory = [];
@ -1023,31 +1217,9 @@ class Room extends EventEmitter
break; break;
} }
case 'serverHistory':
{
// Return to sender
const lobbyPeers = this._lobby.peerList();
cb(
null,
{
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}
);
break;
}
case 'lockRoom': case 'lockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some((role) => config.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = true; this._locked = true;
@ -1065,9 +1237,7 @@ class Room extends EventEmitter
case 'unlockRoom': case 'unlockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some((role) => config.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = false; this._locked = false;
@ -1125,9 +1295,7 @@ class Room extends EventEmitter
case 'promotePeer': case 'promotePeer':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some((role) => config.permissionsFromRoles.PROMOTE_PEER.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1142,9 +1310,7 @@ class Room extends EventEmitter
case 'promoteAllPeers': case 'promoteAllPeers':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some((role) => config.permissionsFromRoles.PROMOTE_PEER.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._lobby.promoteAllPeers(); this._lobby.promoteAllPeers();
@ -1157,9 +1323,7 @@ class Room extends EventEmitter
case 'sendFile': case 'sendFile':
{ {
if ( if (!this._hasPermission(peer, SHARE_FILE))
!peer.roles.some((role) => config.permissionsFromRoles.SHARE_FILE.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { magnetUri } = request.data; const { magnetUri } = request.data;
@ -1180,9 +1344,7 @@ class Room extends EventEmitter
case 'moderator:clearFileSharing': case 'moderator:clearFileSharing':
{ {
if ( if (!this._hasPermission(peer, MODERATE_FILES))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_FILES.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._fileHistory = []; this._fileHistory = [];
@ -1196,16 +1358,17 @@ class Room extends EventEmitter
break; break;
} }
case 'raiseHand': case 'raisedHand':
{ {
const { raisedHand } = request.data; const { raisedHand } = request.data;
peer.raisedHand = raisedHand; peer.raisedHand = raisedHand;
// Spread to others // Spread to others
this._notification(peer.socket, 'raiseHand', { this._notification(peer.socket, 'raisedHand', {
peerId : peer.id, peerId : peer.id,
raisedHand : raisedHand raisedHand : raisedHand,
raisedHandTimestamp : peer.raisedHandTimestamp
}, true); }, true);
// Return no error // Return no error
@ -1214,11 +1377,28 @@ class Room extends EventEmitter
break; break;
} }
case 'moderator:mute':
{
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
const mutePeer = this._peers[peerId];
if (!mutePeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(mutePeer.socket, 'moderator:mute');
cb();
break;
}
case 'moderator:muteAll': case 'moderator:muteAll':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1229,11 +1409,28 @@ class Room extends EventEmitter
break; break;
} }
case 'moderator:stopVideo':
{
if (!this._hasPermission(peer, MODERATE_ROOM))
throw new Error('peer not authorized');
const { peerId } = request.data;
const stopVideoPeer = this._peers[peerId];
if (!stopVideoPeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(stopVideoPeer.socket, 'moderator:stopVideo');
cb();
break;
}
case 'moderator:stopAllVideo': case 'moderator:stopAllVideo':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1246,9 +1443,7 @@ class Room extends EventEmitter
case 'moderator:closeMeeting': case 'moderator:closeMeeting':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._notification(peer.socket, 'moderator:kick', null, true); this._notification(peer.socket, 'moderator:kick', null, true);
@ -1263,9 +1458,7 @@ class Room extends EventEmitter
case 'moderator:kickPeer': case 'moderator:kickPeer':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1284,6 +1477,25 @@ class Room extends EventEmitter
break; 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: default:
{ {
logger.error('unknown request.method "%s"', request.method); logger.error('unknown request.method "%s"', request.method);
@ -1307,6 +1519,8 @@ class Room extends EventEmitter
producer.id producer.id
); );
const router = this._mediasoupRouters.get(producerPeer.routerId);
// Optimization: // Optimization:
// - Create the server-side Consumer. If video, do it paused. // - Create the server-side Consumer. If video, do it paused.
// - Tell its Peer about it and wait for its response. // - Tell its Peer about it and wait for its response.
@ -1317,7 +1531,7 @@ class Room extends EventEmitter
// NOTE: Don't create the Consumer if the remote Peer cannot consume it. // NOTE: Don't create the Consumer if the remote Peer cannot consume it.
if ( if (
!consumerPeer.rtpCapabilities || !consumerPeer.rtpCapabilities ||
!this._mediasoupRouter.canConsume( !router.canConsume(
{ {
producerId : producer.id, producerId : producer.id,
rtpCapabilities : consumerPeer.rtpCapabilities rtpCapabilities : consumerPeer.rtpCapabilities
@ -1443,15 +1657,66 @@ class Room extends EventEmitter
} }
} }
_hasPermission(peer, permission)
{
const hasPermission = peer.roles.some((role) =>
roomPermissions[permission].includes(role)
);
if (hasPermission)
return true;
// Allow if config is set, and no one is present
if (
roomAllowWhenRoleMissing.includes(permission) &&
this._getPeersWithPermission(permission).length === 0
)
return true;
return false;
}
_hasAccess(peer, access)
{
return peer.roles.some((role) => roomAccess[access].includes(role));
}
/** /**
* Helper to get the list of joined peers. * Helper to get the list of joined peers.
*/ */
_getJoinedPeers({ excludePeer = undefined } = {}) _getJoinedPeers(excludePeer = undefined)
{ {
return Object.values(this._peers) return Object.values(this._peers)
.filter((peer) => peer.joined && peer !== excludePeer); .filter((peer) => peer.joined && peer !== excludePeer);
} }
_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(
(peer) =>
peer.joined === joined &&
peer !== excludePeer &&
peer.roles.some(
(role) => roomPermissions[permission].includes(role)
)
);
}
_timeoutCallback(callback) _timeoutCallback(callback)
{ {
let called = false; let called = false;
@ -1516,6 +1781,84 @@ class Room extends EventEmitter
socket.emit('notification', { method, data }); 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; module.exports = Room;

View File

@ -0,0 +1,284 @@
const { Resolver } = require('dns').promises;
const express = require('express');
const mediasoup = require('mediasoup');
const prom = require('prom-client');
const Logger = require('./Logger');
const logger = new Logger('prom');
const resolver = new Resolver();
const workers = new Map();
const labelNames = [
'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id',
'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type'
];
const metadata = {
'byteCount' : { metricType: prom.Counter, unit: 'bytes' },
'score' : { metricType: prom.Gauge }
};
module.exports = async function(rooms, peers, config)
{
const collect = async function(registry)
{
const newMetrics = function(subsystem)
{
const namespace = 'mediasoup';
const metrics = new Map();
for (const key in metadata)
{
if (Object.prototype.hasOwnProperty.call(metadata, key))
{
const value = metadata[key];
const name = key.split(/(?=[A-Z])/).join('_')
.toLowerCase();
const unit = value.unit;
const metricType = value.metricType;
let s = `${namespace}_${subsystem}_${name}`;
if (unit)
{
s += `_${unit}`;
}
const m = new metricType({
name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] });
metrics.set(key, m);
}
}
return metrics;
};
const commonLabels = function(both, fn)
{
for (const roomId of rooms.keys())
{
for (const [ peerId, peer ] of peers)
{
if (fn(peer))
{
const displayName = peer._displayName;
const userAgent = peer._socket.client.request.headers['user-agent'];
const kind = both.kind;
const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1];
return { roomId, peerId, displayName, userAgent, kind, codec };
}
}
}
throw new Error('cannot find common labels');
};
const addr = async function(ip, port)
{
if (config.deidentify)
{
const a = ip.split('.');
for (let i = 0; i < a.length - 2; i++)
{
a[i] = 'xx';
}
return `${a.join('.')}:${port}`;
}
else if (config.numeric)
{
return `${ip}:${port}`;
}
else
{
try
{
const a = await resolver.reverse(ip);
ip = a[0];
}
catch (err)
{
logger.error(`reverse DNS query failed: ${ip} ${err.code}`);
}
return `${ip}:${port}`;
}
};
const quiet = function(s)
{
return config.quiet ? '' : s;
};
const setValue = function(key, m, labels, v)
{
logger.debug(`setValue key=${key} v=${v}`);
switch (metadata[key].metricType)
{
case prom.Counter:
m.inc(labels, v);
break;
case prom.Gauge:
m.set(labels, v);
break;
default:
throw new Error(`unexpected metric: ${m}`);
}
};
logger.debug('collect');
const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] });
mRooms.set(rooms.size);
const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] });
for (const [ roomId, room ] of rooms)
{
mPeers.labels(roomId).set(Object.keys(room._peers).length);
}
const mConsumer = newMetrics('consumer');
const mProducer = newMetrics('producer');
for (const [ pid, worker ] of workers)
{
logger.debug(`visiting worker ${pid}`);
for (const router of worker._routers)
{
logger.debug(`visiting router ${router.id}`);
for (const [ transportId, transport ] of router._transports)
{
logger.debug(`visiting transport ${transportId}`);
const transportJson = await transport.dump();
if (transportJson.iceState != 'completed')
{
logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`);
continue;
}
const iceSelectedTuple = transportJson.iceSelectedTuple;
const proto = iceSelectedTuple.protocol;
const localAddr = await addr(iceSelectedTuple.localIp,
iceSelectedTuple.localPort);
const remoteAddr = await addr(iceSelectedTuple.remoteIp,
iceSelectedTuple.remotePort);
for (const [ producerId, producer ] of transport._producers)
{
logger.debug(`visiting producer ${producerId}`);
const { roomId, peerId, displayName, userAgent, kind, codec } =
commonLabels(producer, (peer) => peer._producers.has(producerId));
const a = await producer.getStats();
for (const x of a)
{
const type = x.type;
const labels = {
'pid' : pid,
'room_id' : roomId,
'peer_id' : peerId,
'display_name' : displayName,
'user_agent' : userAgent,
'transport_id' : quiet(transportId),
'proto' : proto,
'local_addr' : localAddr,
'remote_addr' : remoteAddr,
'id' : quiet(producerId),
'kind' : kind,
'codec' : codec,
'type' : type
};
for (const [ key, m ] of mProducer)
{
setValue(key, m, labels, x[key]);
}
}
}
for (const [ consumerId, consumer ] of transport._consumers)
{
logger.debug(`visiting consumer ${consumerId}`);
const { roomId, peerId, displayName, userAgent, kind, codec } =
commonLabels(consumer, (peer) => peer._consumers.has(consumerId));
const a = await consumer.getStats();
for (const x of a)
{
if (x.type == 'inbound-rtp')
{
continue;
}
const type = x.type;
const labels =
{
'pid' : pid,
'room_id' : roomId,
'peer_id' : peerId,
'display_name' : displayName,
'user_agent' : userAgent,
'transport_id' : quiet(transportId),
'proto' : proto,
'local_addr' : localAddr,
'remote_addr' : remoteAddr,
'id' : quiet(consumerId),
'kind' : kind,
'codec' : codec,
'type' : type
};
for (const [ key, m ] of mConsumer)
{
setValue(key, m, labels, x[key]);
}
}
}
}
}
}
};
try
{
logger.debug(`config.deidentify=${config.deidentify}`);
logger.debug(`config.numeric=${config.numeric}`);
logger.debug(`config.port=${config.port}`);
logger.debug(`config.quiet=${config.quiet}`);
mediasoup.observer.on('newworker', (worker) =>
{
logger.debug(`observing newworker ${worker.pid} #${workers.size}`);
workers.set(worker.pid, worker);
worker.observer.on('close', () =>
{
logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`);
workers.delete(worker.pid);
});
});
const app = express();
app.get('/', async (req, res) =>
{
logger.debug(`GET ${req.originalUrl}`);
const registry = new prom.Registry();
await collect(registry);
res.set('Content-Type', registry.contentType);
const data = registry.metrics();
res.end(data);
});
const server = app.listen(config.port || 8889, () =>
{
const address = server.address();
logger.info(`listening ${address.address}:${address.port}`);
});
}
catch (err)
{
logger.error(err);
}
};

View File

@ -1,14 +1,15 @@
{ {
"name": "multiparty-meeting-server", "name": "multiparty-meeting-server",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting server", "description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js", "start": "node server.js",
"connect": "node connect.js" "connect": "node connect.js",
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
}, },
"dependencies": { "dependencies": {
"awaitqueue": "^1.0.0", "awaitqueue": "^1.0.0",
@ -31,9 +32,13 @@
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-lti": "0.0.7", "passport-lti": "0.0.7",
"pidusage": "^2.0.17", "pidusage": "^2.0.17",
"prom-client": ">=12.0.0",
"redis": "^2.8.0", "redis": "^2.8.0",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"spdy": "^4.0.1", "spdy": "^4.0.1",
"uuid": "^7.0.2" "uuid": "^7.0.2"
},
"devDependencies": {
"eslint": "6.8.0"
} }
} }

View File

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

View File

@ -34,6 +34,8 @@ const expressSession = require('express-session');
const RedisStore = require('connect-redis')(expressSession); const RedisStore = require('connect-redis')(expressSession);
const sharedSession = require('express-socket.io-session'); const sharedSession = require('express-socket.io-session');
const interactiveServer = require('./lib/interactiveServer'); const interactiveServer = require('./lib/interactiveServer');
const promExporter = require('./lib/promExporter');
const { v4: uuidv4 } = require('uuid');
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG); console.log('- process.env.DEBUG:', process.env.DEBUG);
@ -54,10 +56,6 @@ if ('StatusLogger' in config)
// @type {Array<mediasoup.Worker>} // @type {Array<mediasoup.Worker>}
const mediasoupWorkers = []; const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
// Map of Room instances indexed by roomId. // Map of Room instances indexed by roomId.
const rooms = new Map(); const rooms = new Map();
@ -128,10 +126,18 @@ let oidcClient;
let oidcStrategy; let oidcStrategy;
async function run() async function run()
{
try
{ {
// Open the interactive server. // Open the interactive server.
await interactiveServer(rooms, peers); await interactiveServer(rooms, peers);
// start Prometheus exporter
if (config.prometheus)
{
await promExporter(rooms, peers, config.prometheus);
}
if (typeof(config.auth) === 'undefined') if (typeof(config.auth) === 'undefined')
{ {
logger.warn('Auth is not configured properly!'); logger.warn('Auth is not configured properly!');
@ -150,23 +156,29 @@ async function run()
// Run WebSocketServer. // Run WebSocketServer.
await runWebSocketServer(); await runWebSocketServer();
// Log rooms status every 30 seconds. const errorHandler = (err, req, res, next) =>
setInterval(() =>
{ {
for (const room of rooms.values()) const trackingId = uuidv4();
{
room.logStatus();
}
}, 120000);
// check for deserted rooms res.status(500).send(
setInterval(() => `<h1>Internal Server Error</h1>
{ <p>If you report this error, please also report this
for (const room of rooms.values()) <i>tracking ID</i> which makes it possible to locate your session
{ in the logs which are available to the system administrator:
room.checkEmpty(); <b>${trackingId}</b></p>`
);
logger.error(
'Express error handler dump with tracking ID: %s, error dump: %o',
trackingId, err);
};
// eslint-disable-next-line no-unused-vars
app.use(errorHandler);
}
catch (error)
{
logger.error('run() [error:"%o"]', error);
} }
}, 10000);
} }
function statusLog() function statusLog()
@ -174,8 +186,8 @@ function statusLog()
if (statusLogger) if (statusLogger)
{ {
statusLogger.log({ statusLogger.log({
rooms : rooms.size, rooms : rooms,
peers : peers.size peers : peers
}); });
} }
} }
@ -199,7 +211,7 @@ function setupLTI(ltiConfig)
if (lti.user_id && lti.custom_room) if (lti.user_id && lti.custom_room)
{ {
user.id = lti.user_id; user.id = lti.user_id;
user._userinfo = { "lti" : lti }; user._userinfo = { 'lti': lti };
} }
if (lti.custom_room) if (lti.custom_room)
@ -240,8 +252,18 @@ function setupOIDC(oidcIssuer)
// redirect_uri defaults to client.redirect_uris[0] // redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code' // response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid' // scope defaults to 'openid'
const params = (({clinet_id, redirect_uri, scope})=>({clinet_id, redirect_uri, scope}))(config.auth.oidc.clientOptions);
/* eslint-disable camelcase */
const params = (({
client_id,
redirect_uri,
scope
}) => ({
client_id,
redirect_uri,
scope
}))(config.auth.oidc.clientOptions);
/* eslint-enable camelcase */
// optional, defaults to false, when true req is passed as a first // optional, defaults to false, when true req is passed as a first
// argument to verify fn // argument to verify fn
@ -256,7 +278,9 @@ function setupOIDC(oidcIssuer)
{ client: oidcClient, params, passReqToCallback, usePKCE }, { client: oidcClient, params, passReqToCallback, usePKCE },
(tokenset, userinfo, done) => (tokenset, userinfo, done) =>
{ {
if (userinfo && tokenset) { if (userinfo && tokenset)
{
// eslint-disable-next-line camelcase
userinfo._tokenset_claims = tokenset.claims(); userinfo._tokenset_claims = tokenset.claims();
} }
@ -344,7 +368,9 @@ async function setupAuth()
app.get( app.get(
'/auth/callback', '/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }), passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
async (req, res) => async (req, res, next) =>
{
try
{ {
const state = JSON.parse(base64.decode(req.query.state)); const state = JSON.parse(base64.decode(req.query.state));
@ -377,6 +403,11 @@ async function setupAuth()
picture : peer.picture picture : peer.picture
})); }));
} }
catch (error)
{
return next(error);
}
}
); );
} }
@ -562,6 +593,7 @@ async function runWebSocketServer()
{ {
logger.error('room creation or room joining failed [error:"%o"]', error); logger.error('room creation or room joining failed [error:"%o"]', error);
if (socket)
socket.disconnect(true); socket.disconnect(true);
return; return;
@ -600,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). * Get a Room instance (or create one if it does not exist).
*/ */
@ -625,9 +644,9 @@ async function getOrCreateRoom({ roomId })
{ {
logger.info('creating a new Room [roomId:"%s"]', 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); rooms.set(roomId, room);