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
### App config
mm/configs/app/config.js
``` js
multipartyServer : 'meet.example.com',
```
### Server config
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
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.

View File

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

View File

@ -1,9 +1,9 @@
// eslint-disable-next-line
var config =
{
loginEnabled : false,
developmentPort : 3443,
productionPort : 443,
loginEnabled : false,
developmentPort : 3443,
productionPort : 443,
/**
* If defaultResolution is set, it will override user settings when joining:
@ -25,6 +25,7 @@ var config =
{ scaleResolutionDownBy: 2 },
{ scaleResolutionDownBy: 1 }
],
/**
* White listing browsers that support audio output device selection.
* It is not yet fully implemented in Firefox.
@ -41,13 +42,36 @@ var config =
{
tcp : true
},
lastN : 4,
mobileLastN : 1,
background : 'images/background.jpg',
defaultAudio :
{
sampleRate : 48000,
channelCount : 1,
volume : 1.0,
autoGainControl : true,
echoCancellation : true,
noiseSuppression : true,
sampleSize : 16
},
background : 'images/background.jpg',
defaultLayout : 'democratic', // democratic, filmstrip
// If true, will show media control buttons in separate
// control bar, not in the ME container.
buttonControlBar : false,
// If false, will push videos away to make room for side
// drawer. If true, will overlay side drawer over videos
drawerOverlayed : true,
// Timeout for autohiding topbar and button control bar
hideTimeout : 3000,
lastN : 4,
mobileLastN : 1,
// Highest number of speakers user can select
maxLastN : 5,
// If truthy, users can NOT change number of speakers visible
lockLastN : false,
// Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg',
title : 'Multiparty meeting',
theme :
title : 'Multiparty meeting',
theme :
{
palette :
{

View File

@ -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();
}
case 'chrome':
{
return new DisplayMediaScreenShare();
}
case 'msedge':
case 'edge':
{
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)
{
logger.debug(

View File

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

View File

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

View File

@ -63,9 +63,9 @@ export const setWebcamDevices = (devices) =>
payload : { devices }
});
export const setMyRaiseHandState = (flag) =>
export const setRaisedHand = (flag) =>
({
type : 'SET_MY_RAISE_HAND_STATE',
type : 'SET_RAISED_HAND',
payload : { flag }
});
@ -93,9 +93,9 @@ export const setScreenShareInProgress = (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 }
});

View File

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

View File

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

View File

@ -38,6 +38,70 @@ export const togglePermanentTopBar = () =>
type : 'TOGGLE_PERMANENT_TOPBAR'
});
export const toggleButtonControlBar = () =>
({
type : 'TOGGLE_BUTTON_CONTROL_BAR'
});
export const toggleDrawerOverlayed = () =>
({
type : 'TOGGLE_DRAWER_OVERLAYED'
});
export const toggleShowNotifications = () =>
({
type : 'TOGGLE_SHOW_NOTIFICATIONS'
});
export const setEchoCancellation = (echoCancellation) =>
({
type : 'SET_ECHO_CANCELLATION',
payload : { echoCancellation }
});
export const setAutoGainControl = (autoGainControl) =>
({
type : 'SET_AUTO_GAIN_CONTROL',
payload : { autoGainControl }
});
export const setNoiseSuppression = (noiseSuppression) =>
({
type : 'SET_NOISE_SUPPRESSION',
payload : { noiseSuppression }
});
export const setDefaultAudio = (audio) =>
({
type : 'SET_DEFAULT_AUDIO',
payload : { audio }
});
export const toggleEchoCancellation = () =>
({
type : 'TOGGLE_ECHO_CANCELLATION'
});
export const toggleAutoGainControl = () =>
({
type : 'TOGGLE_AUTO_GAIN_CONTROL'
});
export const toggleNoiseSuppression = () =>
({
type : 'TOGGLE_NOISE_SUPPRESSION'
});
export const toggleHiddenControls = () =>
({
type : 'TOGGLE_HIDDEN_CONTROLS'
});
export const toggleNotificationSounds = () =>
({
type : 'TOGGLE_NOTIFICATION_SOUNDS'
});
export const setLastN = (lastN) =>
({
type : 'SET_LAST_N',

View File

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

View File

@ -1,8 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import {
lobbyPeersKeySelector
lobbyPeersKeySelector,
makePermissionSelector
} from '../../Selectors';
import { permissions } from '../../../permissions';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
@ -15,14 +17,6 @@ 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 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 ListSubheader from '@material-ui/core/ListSubheader';
import ListLobbyPeer from './ListLobbyPeer';
@ -59,11 +53,11 @@ const styles = (theme) =>
});
const LockDialog = ({
// roomClient,
roomClient,
room,
handleCloseLockDialog,
// handleAccessCode,
lobbyPeers,
canPromote,
classes
}) =>
{
@ -71,7 +65,7 @@ const LockDialog = ({
<Dialog
className={classes.root}
open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })}
onClose={() => handleCloseLockDialog(false)}
classes={{
paper : classes.dialogPaper
}}
@ -82,54 +76,6 @@ const LockDialog = ({
defaultMessage='Lobby administration'
/>
</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 ?
<List
dense
@ -160,7 +106,21 @@ const LockDialog = ({
</DialogContent>
}
<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
id='label.close'
defaultMessage='Close'
@ -173,20 +133,29 @@ const LockDialog = ({
LockDialog.propTypes =
{
// roomClient : PropTypes.any.isRequired,
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
handleCloseLockDialog : PropTypes.func.isRequired,
handleAccessCode : PropTypes.func.isRequired,
lobbyPeers : PropTypes.array,
canPromote : PropTypes.bool,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
const makeMapStateToProps = () =>
{
return {
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state)
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
{
return {
room : state.room,
lobbyPeers : lobbyPeersKeySelector(state),
canPromote : hasPermission(state)
};
};
return mapStateToProps;
};
const mapDispatchToProps = {
@ -195,19 +164,16 @@ const mapDispatchToProps = {
};
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.locked === next.room.locked &&
prev.room.joinByAccessCode === next.room.joinByAccessCode &&
prev.room.accessCode === next.room.accessCode &&
prev.room.code === next.room.code &&
prev.room.lockDialogOpen === next.room.lockDialogOpen &&
prev.room.codeHidden === next.room.codeHidden &&
prev.room === next.room &&
prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers
);
}

View File

@ -86,7 +86,7 @@ const DialogTitle = withStyles(styles)((props) =>
return (
<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>
</MuiDialogTitle>
);
@ -125,7 +125,7 @@ const ChooseRoom = ({
}}
>
<DialogTitle>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
{ window.config.title ? window.config.title : 'Multiparty meeting' }
<hr />
</DialogTitle>
<DialogContent>

View File

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

View File

@ -12,8 +12,9 @@ import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Tooltip from '@material-ui/core/Tooltip';
import Fab from '@material-ui/core/Fab';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import IconButton from '@material-ui/core/IconButton';
import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import NewWindowIcon from '@material-ui/icons/OpenInNew';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import Volume from './Volume';
@ -59,6 +60,19 @@ const styles = (theme) =>
{
margin : theme.spacing(1)
},
smallContainer :
{
backgroundColor : 'rgba(255, 255, 255, 0.9)',
margin : '0.5vmin',
padding : '0.5vmin',
boxShadow : '0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)',
pointerEvents : 'auto',
transition : 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
'&:hover' :
{
backgroundColor : 'rgba(213, 213, 213, 1)'
}
},
viewContainer :
{
position : 'relative',
@ -125,11 +139,12 @@ const Peer = (props) =>
micConsumer,
webcamConsumer,
screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen,
toggleConsumerWindow,
spacing,
style,
smallButtons,
smallContainer,
windowConsumer,
classes,
theme
@ -234,16 +249,16 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
className={classes.smallContainer}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
color='primary'
size='small'
onClick={() =>
{
micEnabled ?
@ -252,12 +267,35 @@ const Peer = (props) =>
}}
>
{ 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>
</div>
}
</Tooltip>
{ browser.platform !== 'mobile' &&
@ -268,7 +306,27 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
@ -279,7 +337,7 @@ const Peer = (props) =>
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
size='large'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
@ -287,7 +345,7 @@ const Peer = (props) =>
>
<NewWindowIcon />
</Fab>
</div>
}
</Tooltip>
}
@ -298,7 +356,24 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
@ -306,7 +381,7 @@ const Peer = (props) =>
})}
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
@ -314,11 +389,12 @@ const Peer = (props) =>
>
<FullScreenIcon />
</Fab>
</div>
}
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
@ -340,6 +416,7 @@ const Peer = (props) =>
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
videoTrack={webcamConsumer && webcamConsumer.track}
videoVisible={videoVisible}
audioTrack={micConsumer && micConsumer.track}
audioCodec={micConsumer && micConsumer.codec}
videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null}
@ -350,6 +427,199 @@ const Peer = (props) =>
</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 &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -416,26 +686,24 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</Tooltip>
}
@ -446,26 +714,25 @@ const Peer = (props) =>
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</Tooltip>
</div>
<VideoView
showQuality
advancedMode={advancedMode}
videoContain
consumerSpatialLayers={
@ -507,12 +774,13 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool,
browser : PropTypes.object.isRequired,
spacing : PropTypes.number,
style : PropTypes.object,
smallButtons : PropTypes.bool,
smallContainer : PropTypes.bool,
toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired,

View File

@ -91,16 +91,6 @@ const SpeakerPeer = (props) =>
!screenConsumer.remotelyPaused
);
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
const spacingStyle =
{
'margin' : spacing
@ -134,11 +124,27 @@ const SpeakerPeer = (props) =>
peer={peer}
displayName={peer.displayName}
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}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
audioCodec={micConsumer && micConsumer.codec}
videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null}
videoScore={webcamConsumer ? webcamConsumer.score : null}
>
<Volume id={peer.id} />
</VideoView>
@ -165,10 +171,29 @@ const SpeakerPeer = (props) =>
<VideoView
advancedMode={advancedMode}
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}
videoProfile={screenProfile}
videoCodec={screenConsumer ? screenConsumer.codec : null}
videoCodec={screenConsumer && screenConsumer.codec}
videoScore={screenConsumer ? screenConsumer.score : null}
/>
</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 PropTypes from 'prop-types';
import {
lobbyPeersKeySelector,
peersLengthSelector
peersLengthSelector,
raisedHandsSelector,
makePermissionSelector
} from '../Selectors';
import { permissions } from '../../permissions';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Popover from '@material-ui/core/Popover';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge';
import Paper from '@material-ui/core/Paper';
import ExtensionIcon from '@material-ui/icons/Extension';
import AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
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 LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
import MoreIcon from '@material-ui/icons/MoreVert';
import HelpIcon from '@material-ui/icons/Help';
import InfoIcon from '@material-ui/icons/Info';
const styles = (theme) =>
({
persistentDrawerOpen :
{
width : 'calc(100% - 30vw)',
marginLeft : '30vw',
[theme.breakpoints.down('lg')] :
{
width : 'calc(100% - 40vw)',
marginLeft : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : 'calc(100% - 50vw)',
marginLeft : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : 'calc(100% - 70vw)',
marginLeft : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : 'calc(100% - 90vw)',
marginLeft : '90vw'
}
},
menuButton :
{
margin : 0,
@ -72,14 +110,34 @@ const styles = (theme) =>
display : 'block'
}
},
actionButtons :
{
display : 'flex'
sectionDesktop : {
display : 'none',
[theme.breakpoints.up('md')] : {
display : 'flex'
}
},
sectionMobile : {
display : 'flex',
[theme.breakpoints.up('md')] : {
display : 'none'
}
},
actionButton :
{
margin : theme.spacing(1, 0),
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 [ 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 {
roomClient,
room,
peersLength,
lobbyPeers,
permanentTopBar,
drawerOverlayed,
toolAreaOpen,
isMobile,
myPicture,
loggedIn,
loginEnabled,
@ -131,14 +224,22 @@ const TopBar = (props) =>
fullscreen,
onFullscreen,
setSettingsOpen,
setExtraVideoOpen,
setHelpOpen,
setAboutOpen,
setLockDialogOpen,
toggleToolArea,
openUsersTab,
unread,
canProduceExtraVideo,
canLock,
canPromote,
classes
} = props;
const isMenuOpen = Boolean(anchorEl);
const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
const lockTooltip = room.locked ?
intl.formatMessage({
id : 'tooltip.unLockRoom',
@ -173,171 +274,212 @@ const TopBar = (props) =>
});
return (
<AppBar
position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
onClick={() => toggleToolArea()}
>
<IconButton
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
<React.Fragment>
<AppBar
position='fixed'
className={classnames(
room.toolbarsVisible || permanentTopBar ?
classes.show : classes.hide,
!(isMobile || drawerOverlayed) && toolAreaOpen ?
classes.persistentDrawerOpen : null
)}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
onClick={() => toggleToolArea()}
>
<IconButton
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<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 &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
>
<PeopleIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
<Tooltip title={lockTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
disabled={!canLock}
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
color='inherit'
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle />
}
<SettingsIcon />
</IconButton>
</Tooltip>
}
<Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
disabled={!canLock}
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</span>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
className={classes.actionButton}
color='inherit'
disabled={!canPromote}
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</span>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
</IconButton>
</Tooltip>
}
</div>
<div className={classes.sectionMobile}>
<IconButton
aria-haspopup='true'
onClick={handleMobileMenuOpen}
color='inherit'
>
<MoreIcon />
</IconButton>
</div>
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
@ -354,51 +496,339 @@ const TopBar = (props) =>
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>
</AppBar>
</Toolbar>
</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>
);
};
TopBar.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
peersLength : PropTypes.number,
lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool,
fullscreen : PropTypes.bool,
onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
canLock : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
isMobile : PropTypes.bool.isRequired,
peersLength : PropTypes.number,
lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool,
fullscreen : PropTypes.bool,
onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired,
setHelpOpen : PropTypes.func.isRequired,
setAboutOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
canProduceExtraVideo : PropTypes.bool.isRequired,
canLock : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
room : state.room,
peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles,
canLock :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role))
});
const makeMapStateToProps = () =>
{
const hasExtraVideoPermission =
makePermissionSelector(permissions.EXTRA_VIDEO);
const hasLockPermission =
makePermissionSelector(permissions.CHANGE_ROOM_LOCK);
const hasPromotionPermission =
makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
({
room : state.room,
isMobile : state.me.browser.platform === 'mobile',
peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state),
permanentTopBar : state.settings.permanentTopBar,
drawerOverlayed : state.settings.drawerOverlayed,
toolAreaOpen : state.toolarea.toolAreaOpen,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo : hasExtraVideoPermission(state),
canLock : hasLockPermission(state),
canPromote : hasPromotionPermission(state)
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) =>
({
@ -408,11 +838,23 @@ const mapDispatchToProps = (dispatch) =>
},
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) =>
{
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen }));
dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
},
toggleToolArea : () =>
{
@ -426,7 +868,7 @@ const mapDispatchToProps = (dispatch) =>
});
export default withRoomContext(connect(
mapStateToProps,
makeMapStateToProps,
mapDispatchToProps,
null,
{
@ -437,12 +879,15 @@ export default withRoomContext(connect(
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.settings.drawerOverlayed === next.settings.drawerOverlayed &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.browser === next.me.browser &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture &&
prev.me.roles === next.me.roles &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles
prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}
}

View File

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

View File

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

View File

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

View File

@ -94,7 +94,7 @@ const Message = (props) =>
<Typography variant='caption'>
{ self ?
intl.formatMessage({
id : 'room.me',
id : 'room.me',
defaultMessage : 'Me'
})
:

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { raisedHandsSelector } from '../Selectors';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import * as toolareaActions from '../../actions/toolareaActions';
@ -51,6 +52,7 @@ const MeetingDrawer = (props) =>
currentToolTab,
unreadMessages,
unreadFiles,
raisedHands,
closeDrawer,
setToolTab,
classes,
@ -93,10 +95,14 @@ const MeetingDrawer = (props) =>
}
/>
<Tab
label={intl.formatMessage({
id : 'label.participants',
defaultMessage : 'Participants'
})}
label={
<Badge color='secondary' badgeContent={raisedHands}>
{intl.formatMessage({
id : 'label.participants',
defaultMessage : 'Participants'
})}
</Badge>
}
/>
</Tabs>
<IconButton onClick={closeDrawer}>
@ -116,16 +122,21 @@ MeetingDrawer.propTypes =
setToolTab : PropTypes.func.isRequired,
unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired,
raisedHands : PropTypes.number.isRequired,
closeDrawer : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
currentToolTab : state.toolarea.currentToolTab,
unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles
});
const mapStateToProps = (state) =>
{
return {
currentToolTab : state.toolarea.currentToolTab,
unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles,
raisedHands : raisedHandsSelector(state)
};
};
const mapDispatchToProps = {
setToolTab : toolareaActions.setToolTab
@ -141,7 +152,8 @@ export default connect(
return (
prev.toolarea.currentToolTab === next.toolarea.currentToolTab &&
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 { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import PanIcon from '@material-ui/icons/PanTool';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
listPeer :
{
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
height : '2rem',
marginTop : theme.spacing(0.5)
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},
indicators :
buttons :
{
left : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
padding : theme.spacing(1)
},
icon :
green :
{
flex : '0 0 auto',
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
}
color : 'rgba(0, 153, 0, 1)'
}
});
const ListMe = (props) =>
{
const intl = useIntl();
const {
roomClient,
me,
settings,
classes
@ -82,29 +58,49 @@ const ListMe = (props) =>
const picture = me.picture || EmptyAvatar;
return (
<li className={classes.root}>
<div className={classes.listPeer}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.root}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{settings.displayName}
</div>
<div className={classes.indicators}>
{ me.raisedHand &&
<div className={classnames(classes.icon, 'raise-hand')} />
}
</div>
<div className={classes.peerInfo}>
{settings.displayName}
</div>
</li>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.raisedHand',
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>
);
};
ListMe.propTypes =
{
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
roomClient : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
@ -112,7 +108,7 @@ const mapStateToProps = (state) => ({
settings : state.settings
});
export default connect(
export default withRoomContext(connect(
mapStateToProps,
null,
null,
@ -125,4 +121,4 @@ export default connect(
);
}
}
)(withStyles(styles)(ListMe));
)(withStyles(styles)(ListMe)));

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,12 @@ import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer';
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 = () =>
({
root :
@ -19,25 +25,24 @@ const styles = () =>
height : '100%',
width : '100%',
display : 'grid',
overflow : 'hidden',
gridTemplateColumns : '1fr',
gridTemplateRows : '1.6fr minmax(0, 0.4fr)'
gridTemplateRows : '1fr 0.25fr'
},
speaker :
{
gridArea : '1 / 1 / 2 / 2',
gridArea : '1 / 1 / 1 / 1',
display : 'flex',
justifyContent : 'center',
alignItems : 'center',
paddingTop : 40
alignItems : 'center'
},
filmStrip :
{
gridArea : '2 / 1 / 3 / 2'
gridArea : '2 / 1 / 2 / 1'
},
filmItem :
{
display : 'flex',
marginLeft : '6px',
border : 'var(--peer-border)',
'&.selected' :
{
@ -45,8 +50,18 @@ const styles = () =>
},
'&.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.rootContainer = React.createRef();
this.activePeerContainer = React.createRef();
this.filmStripContainer = React.createRef();
@ -105,24 +122,38 @@ class Filmstrip extends React.PureComponent
{
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;
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()))
{
speakerWidth /= 2;
speakerHeight = (speakerWidth / 4) * 3;
speakerHeight = speakerWidth / RATIO;
}
if (speakerHeight > (speaker.clientHeight - 60))
if (speakerHeight > (availableSpeakerHeight - PADDING_V))
{
speakerHeight = (speaker.clientHeight - 60);
speakerWidth = (speakerHeight / 3) * 4;
speakerHeight = (availableSpeakerHeight - PADDING_V);
speakerWidth = speakerHeight * RATIO;
}
newState.speakerWidth = speakerWidth;
@ -133,14 +164,18 @@ class Filmstrip extends React.PureComponent
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;
filmStripHeight = (filmStripWidth / 4) * 3;
filmStripWidth = (availableWidth - FILMSTRING_PADDING_H) /
this.props.boxes;
filmStripHeight = filmStripWidth / RATIO;
}
newState.filmStripWidth = filmStripWidth;
@ -172,27 +207,21 @@ class Filmstrip extends React.PureComponent
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)
{
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();
}
}
@ -205,6 +234,8 @@ class Filmstrip extends React.PureComponent
myId,
advancedMode,
spotlights,
toolbarsVisible,
permanentTopBar,
classes
} = this.props;
@ -223,7 +254,14 @@ class Filmstrip extends React.PureComponent
};
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}>
{ peers[activePeerId] &&
<SpeakerPeer
@ -245,7 +283,7 @@ class Filmstrip extends React.PureComponent
<Me
advancedMode={advancedMode}
style={peerStyle}
smallButtons
smallContainer
/>
</div>
</Grid>
@ -268,7 +306,7 @@ class Filmstrip extends React.PureComponent
advancedMode={advancedMode}
id={peerId}
style={peerStyle}
smallButtons
smallContainer
/>
</div>
</Grid>
@ -296,6 +334,9 @@ Filmstrip.propTypes = {
selectedPeerId : PropTypes.string,
spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number,
toolbarsVisible : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired
};
@ -308,7 +349,10 @@ const mapStateToProps = (state) =>
consumers : state.consumers,
myId : state.me.id,
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 (
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.selectedPeerId === next.room.selectedPeerId &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.peers === next.peers &&
prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights &&

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { micConsumerSelector } from '../Selectors';
import { passiveMicConsumerSelector } from '../Selectors';
import PropTypes from 'prop-types';
import PeerAudio from './PeerAudio';
@ -37,7 +37,7 @@ AudioPeers.propTypes =
const mapStateToProps = (state) =>
({
micConsumers : micConsumerSelector(state),
micConsumers : passiveMicConsumerSelector(state),
audioOutputDevice : state.settings.selectedAudioOutputDevice
});
@ -50,7 +50,9 @@ const AudioPeersContainer = connect(
{
return (
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,13 +31,15 @@ export default class PeerAudio extends React.PureComponent
this._setOutputDevice(audioOutputDevice);
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps)
componentDidUpdate(prevProps)
{
const { audioTrack, audioOutputDevice } = nextProps;
if (prevProps !== this.props)
{
const { audioTrack, audioOutputDevice } = this.props;
this._setTrack(audioTrack);
this._setOutputDevice(audioOutputDevice);
this._setTrack(audioTrack);
this._setOutputDevice(audioOutputDevice);
}
}
_setTrack(audioTrack)

View File

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

View File

@ -1,5 +1,8 @@
import { createSelector } from 'reselect';
const meRolesSelect = (state) => state.me.roles;
const roomPermissionsSelect = (state) => state.room.roomPermissions;
const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing;
const producersSelect = (state) => state.producers;
const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights;
@ -12,7 +15,8 @@ const peersKeySelector = createSelector(
peersSelector,
(peers) => Object.keys(peers)
);
const peersValueSelector = createSelector(
export const peersValueSelector = createSelector(
peersSelector,
(peers) => Object.values(peers)
);
@ -37,6 +41,11 @@ export const screenProducersSelector = createSelector(
(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(
producersSelect,
(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')
);
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(
spotlightsSelector,
(spotlights) => spotlights.length
@ -81,8 +117,31 @@ export const spotlightPeersSelector = createSelector(
export const spotlightSortedPeersSelector = createSelector(
spotlightsSelector,
peersValueSelector,
(spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id))
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
(spotlights, peers) =>
peers.filter((peer) => spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
const raisedHandSortedPeers = createSelector(
peersValueSelector,
(peers) => peers.filter((peer) => peer.raisedHand)
.sort((a, b) => a.raisedHandTimestamp - b.raisedHandTimestamp)
);
const peersSortedSelector = createSelector(
spotlightsSelector,
peersValueSelector,
(spotlights, peers) =>
peers.filter((peer) => !spotlights.includes(peer.id) && !peer.raisedHand)
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
export const participantListSelector = createSelector(
raisedHandSortedPeers,
spotlightSortedPeersSelector,
peersSortedSelector,
(raisedHands, spotlights, peers) =>
[ ...raisedHands, ...spotlights, ...peers ]
);
export const peersLengthSelector = createSelector(
@ -97,24 +156,41 @@ export const passivePeersSelector = createSelector(
.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(
spotlightsLengthSelector,
screenProducersSelector,
screenConsumerSelector,
(spotlightsLength, screenProducers, screenConsumers) =>
spotlightsLength + 1 + screenProducers.length + screenConsumers.length
spotlightScreenConsumerSelector,
extraVideoProducersSelector,
spotlightExtraVideoConsumerSelector,
(
spotlightsLength,
screenProducers,
screenConsumers,
extraVideoProducers,
extraVideoConsumers
) =>
spotlightsLength + 1 + screenProducers.length +
screenConsumers.length + extraVideoProducers.length +
extraVideoConsumers.length
);
export const meProducersSelector = createSelector(
micProducerSelector,
webcamProducerSelector,
screenProducerSelector,
(micProducer, webcamProducer, screenProducer) =>
extraVideoProducersSelector,
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
{
return {
micProducer,
webcamProducer,
screenProducer
screenProducer,
extraVideoProducers
};
}
);
@ -137,8 +213,60 @@ export const makePeerConsumerSelector = () =>
consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer =
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 { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
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 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 FormControlLabel from '@material-ui/core/FormControlLabel';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
const tabs =
[
'media',
'appearence',
'advanced'
];
const styles = (theme) =>
({
@ -43,106 +46,27 @@ const styles = (theme) =>
width : '90vw'
}
},
setting :
tabsHeader :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
flexGrow : 1
}
});
const Settings = ({
roomClient,
room,
me,
settings,
onToggleAdvancedMode,
onTogglePermanentTopBar,
currentSettingsTab,
settingsOpen,
handleCloseSettings,
handleChangeMode,
setSettingsTab,
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'
})
} ];
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 (
<Dialog
className={classes.root}
open={room.settingsOpen}
onClose={() => handleCloseSettings({ settingsOpen: false })}
open={settingsOpen}
onClose={() => handleCloseSettings(false)}
classes={{
paper : classes.dialogPaper
}}
@ -153,250 +77,40 @@ const Settings = ({
defaultMessage='Settings'
/>
</DialogTitle>
<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>
</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({
id : 'settings.advancedMode',
defaultMessage : 'Advanced mode'
})}
/>
{ settings.advancedMode &&
<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({
id : 'settings.permanentTopBar',
defaultMessage : 'Permanent top bar'
})}
/>
</React.Fragment>
}
<Tabs
className={classes.tabsHeader}
value={tabs.indexOf(currentSettingsTab)}
onChange={(event, value) => setSettingsTab(tabs[value])}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
>
<Tab
label={
intl.formatMessage({
id : 'label.media',
defaultMessage : 'Media'
})
}
/>
<Tab
label={intl.formatMessage({
id : 'label.appearence',
defaultMessage : 'Appearence'
})}
/>
<Tab
label={intl.formatMessage({
id : 'label.advanced',
defaultMessage : 'Advanced'
})}
/>
</Tabs>
{currentSettingsTab === 'media' && <MediaSettings />}
{currentSettingsTab === 'appearence' && <AppearenceSettings />}
{currentSettingsTab === 'advanced' && <AdvancedSettings />}
<DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
<Button onClick={() => handleCloseSettings(false)} color='primary'>
<FormattedMessage
id='label.close'
defaultMessage='Close'
@ -409,34 +123,25 @@ const Settings = ({
Settings.propTypes =
{
roomClient : PropTypes.any.isRequired,
me : appPropTypes.Me.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,
classes : PropTypes.object.isRequired
currentSettingsTab : PropTypes.string.isRequired,
settingsOpen : PropTypes.bool.isRequired,
handleCloseSettings : PropTypes.func.isRequired,
setSettingsTab : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
room : state.room,
settings : state.settings
};
};
({
currentSettingsTab : state.room.currentSettingsTab,
settingsOpen : state.room.settingsOpen
});
const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
handleChangeMode : roomActions.setDisplayMode,
handleCloseSettings : roomActions.setSettingsOpen
handleCloseSettings : roomActions.setSettingsOpen,
setSettingsTab : roomActions.setSettingsTab
};
export default withRoomContext(connect(
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
@ -444,10 +149,9 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) =>
{
return (
prev.me === next.me &&
prev.room === next.room &&
prev.settings === next.settings
prev.room.currentSettingsTab === next.room.currentSettingsTab &&
prev.room.settingsOpen === next.room.settingsOpen
);
}
}
)(withStyles(styles)(Settings)));
)(withStyles(styles)(Settings));

View File

@ -96,11 +96,6 @@ const FullScreenView = (props) =>
!consumer.remotelyPaused
);
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return (
<div className={classes.root}>
<div className={classes.controls}>
@ -121,9 +116,25 @@ const FullScreenView = (props) =>
<VideoView
advancedMode={advancedMode}
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}
videoProfile={consumerProfile}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</div>
);

View File

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

View File

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

View File

@ -23,18 +23,29 @@ const VideoWindow = (props) =>
!consumer.remotelyPaused
);
let consumerProfile;
if (consumer)
consumerProfile = consumer.profile;
return (
<NewWindow onUnload={toggleConsumerWindow}>
<FullView
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}
videoProfile={consumerProfile}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</NewWindow>
);

View File

@ -18,9 +18,9 @@ export const Me = PropTypes.shape(
export const Producer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
type : PropTypes.oneOf([ 'front', 'back', 'screen', 'extravideo' ]),
paused : PropTypes.bool.isRequired,
track : PropTypes.any,
codec : PropTypes.string.isRequired
@ -37,7 +37,7 @@ export const Consumer = PropTypes.shape(
{
id : 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,
remotelyPaused : PropTypes.bool.isRequired,
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 messagesItalian from './translations/it';
import messagesUkrainian from './translations/uk';
import messagesTurkish from './translations/tr';
import messagesLatvian from './translations/lv';
import './index.css';
@ -61,7 +63,9 @@ const messages =
'hr' : messagesCroatian,
'cs' : messagesCzech,
'it' : messagesItalian,
'uk' : messagesUkrainian
'uk' : messagesUkrainian,
'tr' : messagesTurkish,
'lv' : messagesLatvian
};
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
@ -112,6 +116,13 @@ function run()
const displayName = parameters.get('displayName');
const muted = parameters.get('muted') === 'true';
const { pathname } = window.location;
let basePath = pathname.substring(0, pathname.lastIndexOf('/'));
if (!basePath)
basePath = '/';
// Get current device.
const device = deviceInfo();
@ -130,7 +141,8 @@ function run()
produce,
forceTcp,
displayName,
muted
muted,
basePath
});
global.CLIENT = roomClient;
@ -142,7 +154,7 @@ function run()
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<Router>
<Router basename={basePath}>
<Suspense fallback={<LoadingView />}>
<React.Fragment>
<Route exact path='/' component={ChooseRoom} />

View File

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

View File

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

View File

@ -15,8 +15,8 @@ const initialState =
screenShareInProgress : false,
displayNameInProgress : false,
loginEnabled : false,
raiseHand : false,
raiseHandInProgress : false,
raisedHand : false,
raisedHandInProgress : false,
loggedIn : false,
isSpeaking : false
};
@ -134,18 +134,18 @@ const me = (state = initialState, action) =>
return { ...state, screenShareInProgress: flag };
}
case 'SET_MY_RAISE_HAND_STATE':
case 'SET_RAISED_HAND':
{
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;
return { ...state, raiseHandInProgress: flag };
return { ...state, raisedHandInProgress: flag };
}
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)
{
@ -20,8 +22,18 @@ const peer = (state = {}, action) =>
case 'SET_PEER_KICK_IN_PROGRESS':
return { ...state, peerKickInProgress: action.payload.flag };
case 'SET_PEER_RAISE_HAND_STATE':
return { ...state, raiseHandState: action.payload.raiseHandState };
case 'SET_PEER_RAISED_HAND':
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':
{
@ -58,12 +70,24 @@ const peer = (state = {}, action) =>
return { ...state, roles };
}
case 'STOP_PEER_AUDIO_IN_PROGRESS':
return {
...state,
stopPeerAudioInProgress : action.payload.flag
};
case 'STOP_PEER_VIDEO_IN_PROGRESS':
return {
...state,
stopPeerVideoInProgress : action.payload.flag
};
default:
return state;
}
};
const peers = (state = {}, action) =>
const peers = (state = initialState, action) =>
{
switch (action.type)
{
@ -86,11 +110,14 @@ const peers = (state = {}, action) =>
case 'SET_PEER_VIDEO_IN_PROGRESS':
case 'SET_PEER_AUDIO_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 'ADD_CONSUMER':
case 'ADD_PEER_ROLE':
case 'REMOVE_PEER_ROLE':
case 'STOP_PEER_AUDIO_IN_PROGRESS':
case 'STOP_PEER_VIDEO_IN_PROGRESS':
{
const oldPeer = state[action.payload.peerId];
@ -114,6 +141,11 @@ const peers = (state = {}, action) =>
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
}
case 'CLEAR_PEERS':
{
return initialState;
}
default:
return state;
}

View File

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

View File

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

View File

@ -4,9 +4,23 @@ const initialState =
selectedWebcam : null,
selectedAudioDevice : null,
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,
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) =>
@ -42,6 +56,83 @@ const settings = (state = initialState, action) =>
return { ...state, advancedMode };
}
case 'SET_SAMPLE_RATE':
{
const { sampleRate } = action.payload;
return { ...state, sampleRate };
}
case 'SET_CHANNEL_COUNT':
{
const { channelCount } = action.payload;
return { ...state, channelCount };
}
case 'SET_VOLUME':
{
const { volume } = action.payload;
return { ...state, volume };
}
case 'SET_AUTO_GAIN_CONTROL':
{
const { autoGainControl } = action.payload;
return { ...state, autoGainControl };
}
case 'SET_ECHO_CANCELLATION':
{
const { echoCancellation } = action.payload;
return { ...state, echoCancellation };
}
case 'SET_NOISE_SUPPRESSION':
{
const { noiseSuppression } = action.payload;
return { ...state, noiseSuppression };
}
case 'SET_DEFAULT_AUDIO':
{
const { audio } = action.payload;
return { ...state, audio };
}
case 'TOGGLE_AUTO_GAIN_CONTROL':
{
const autoGainControl = !state.autoGainControl;
return { ...state, autoGainControl };
}
case 'TOGGLE_ECHO_CANCELLATION':
{
const echoCancellation = !state.echoCancellation;
return { ...state, echoCancellation };
}
case 'TOGGLE_NOISE_SUPPRESSION':
{
const noiseSuppression = !state.noiseSuppression;
return { ...state, noiseSuppression };
}
case 'SET_SAMPLE_SIZE':
{
const { sampleSize } = action.payload;
return { ...state, sampleSize };
}
case 'SET_LAST_N':
{
const { lastN } = action.payload;
@ -56,6 +147,41 @@ const settings = (state = initialState, action) =>
return { ...state, permanentTopBar };
}
case 'TOGGLE_BUTTON_CONTROL_BAR':
{
const buttonControlBar = !state.buttonControlBar;
return { ...state, buttonControlBar };
}
case 'TOGGLE_DRAWER_OVERLAYED':
{
const drawerOverlayed = !state.drawerOverlayed;
return { ...state, drawerOverlayed };
}
case 'TOGGLE_HIDDEN_CONTROLS':
{
const hiddenControls = !state.hiddenControls;
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':
{
const { resolution } = action.payload;

View File

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

View File

@ -54,9 +54,20 @@
"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": "Přihlášení",
"tooltip.logout": "Odhlášení",
"tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti",
@ -66,6 +77,12 @@
"tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)",
"tooltip.lobby": "Ukázat Přijímací místnost",
"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.chooseRoomButton": "Pokračovat",
@ -79,6 +96,7 @@
"label.filesharing": "Sdílení souborů",
"label.participants": "Účastníci",
"label.shareFile": "Sdílet soubor",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Sdílení souborů není podporováno",
"label.unknown": "Neznámý",
"label.democratic": "Rozvržení: Demokratické",
@ -89,6 +107,12 @@
"label.veryHigh": "Velmi vysoké (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Kamera",
@ -104,6 +128,16 @@
"settings.layout": "Rozvržení místnosti",
"settings.selectRoomLayout": "Vyberte rozvržení místnosti",
"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.startingFileShare": "Pokouším se sdílet soubor",

View File

@ -52,11 +52,22 @@
"room.muteAll": "Alle stummschalten",
"room.stopAllVideo": "Alle Videos stoppen",
"room.closeMeeting": "Meeting schließen",
"room.clearChat": null,
"room.clearFileSharing": null,
"room.clearChat": "Liste löschen",
"room.clearFileSharing": "Liste löschen",
"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.logout": "Abmelden",
@ -68,7 +79,11 @@
"tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen",
"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.chooseRoomButton": "Weiter",
@ -82,6 +97,7 @@
"label.filesharing": "Dateien",
"label.participants": "Teilnehmer",
"label.shareFile": "Datei hochladen",
"label.shareGalleryFile": "Bild teilen",
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt",
"label.democratic": "Demokratisch",
@ -92,6 +108,12 @@
"label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Kamera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste",
"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.startingFileShare": "Starte Teilen der Datei",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "Kamera getrennt",
"devices.cameraError": "Fehler mit deiner Kamera",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "Moderator hat Chat gelöscht",
"moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht",
"moderator.muteAudio": "Moderator hat dich stummgeschaltet",
"moderator.muteVideo": "Moderator hat dein Video gestoppt"
}

View File

@ -55,9 +55,20 @@
"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": "Log ind",
"tooltip.logout": "Log ud",
"tooltip.admitFromLobby": "Giv adgang fra lobbyen",
@ -69,6 +80,10 @@
"tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt",
@ -82,6 +97,7 @@
"label.filesharing": "Fildeling",
"label.participants": "Deltagere",
"label.shareFile": "Del fil",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Fildeling er ikke understøttet",
"label.unknown": "Ukendt",
"label.democracy": "Galleri visning",
@ -92,6 +108,12 @@
"label.veryHigh": "Meget høj (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Kamera",
@ -103,13 +125,20 @@
"settings.audioOutput": "Audio output enhed",
"settings.selectAudioOutput": "Vælg lydudgangsenhed",
"settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed",
"settings.resolution": "Vælg din videoopløsning",
"settings.layout": "Møde visning",
"settings.selectRoomLayout": "Vælg møde visning",
"settings.advancedMode": "Avanceret tilstand",
"settings.permanentTopBar": "Permanent øverste linje",
"settings.lastn": "Antal synlige videoer",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen",

View File

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

View File

@ -55,9 +55,20 @@
"room.clearChat": "Clear chat",
"room.clearFileSharing": "Clear files",
"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",
"roles.gotRole": "You got the role: {role}",
"roles.lostRole": "You lost the role: {role}",
"tooltip.login": "Log in",
"tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby",
@ -69,6 +80,10 @@
"tooltip.settings": "Show settings",
"tooltip.participants": "Show participants",
"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.chooseRoomButton": "Continue",
@ -82,6 +97,7 @@
"label.filesharing": "File sharing",
"label.participants": "Participants",
"label.shareFile": "Share file",
"label.shareGalleryFile": "Share image",
"label.fileSharingUnsupported": "File sharing not supported",
"label.unknown": "Unknown",
"label.democratic": "Democratic view",
@ -92,6 +108,12 @@
"label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Camera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Advanced mode",
"settings.permanentTopBar": "Permanent top bar",
"settings.lastn": "Number of visible videos",
"settings.hiddenControls": "Hidden media controls",
"settings.notificationSounds": "Notification sounds",
"settings.showNotifications": "Show notifications",
"settings.buttonControlBar": "Separate media controls",
"settings.echoCancellation": "Echo cancellation",
"settings.autoGainControl": "Auto gain control",
"settings.noiseSuppression": "Noise suppression",
"settings.drawerOverlayed": "Side drawer over content",
"filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file",

View File

@ -55,9 +55,20 @@
"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": "Entrar",
"tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera",
@ -69,6 +80,10 @@
"tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar",
@ -82,6 +97,7 @@
"label.filesharing": "Compartir ficheros",
"label.participants": "Participantes",
"label.shareFile": "Compartir fichero",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido",
"label.democratic": "Vista democrática",
@ -92,6 +108,12 @@
"label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Cámara",
@ -109,6 +131,14 @@
"settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero",

View File

@ -55,9 +55,20 @@
"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": "Connexion",
"tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
@ -69,6 +80,10 @@
"tooltip.settings": "Afficher les paramètres",
"tooltip.participants": "Afficher les participants",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer",
@ -82,6 +97,7 @@
"label.filesharing": "Partage de fichier",
"label.participants": "Participants",
"label.shareFile": "Partager un fichier",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partage de fichier non supporté",
"label.unknown": "Inconnu",
"label.democratic": "Vue démocratique",
@ -92,6 +108,12 @@
"label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra": "Ultra Haute Définition",
"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.camera": "Caméra",
@ -109,6 +131,14 @@
"settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier",

View File

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

View File

@ -1,24 +1,24 @@
{
"socket.disconnected": "A kapcsolat lebomlott",
"socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás",
"socket.reconnected": "Sikeres újarkapcsolódás",
"socket.reconnected": "Sikeres újrakapcsolódás",
"socket.requestError": "Sikertelen szerver lekérés",
"room.chooseRoom": null,
"room.chooseRoom": "Válaszd ki a konferenciaszobát",
"room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ",
"room.consentUnderstand": "I understand",
"room.joined": "Csatlakozátál a konferenciához",
"room.consentUnderstand": "Megértettem",
"room.joined": "Csatlakoztál a konferenciához",
"room.cantJoin": "Sikertelen csatlakozás a konferenciához",
"room.youLocked": "A konferenciába való belépés letiltva",
"room.cantLock": "Sikertelen a konferenciaba való belépés letiltása",
"room.cantLock": "Sikertelen a konferenciába való belépés letiltása",
"room.youUnLocked": "A konferenciába való belépés engedélyezve",
"room.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése",
"room.locked": "A konferenciába való belépés letiltva",
"room.unlocked": "A konferenciába való belépés engedélyezve",
"room.newLobbyPeer": "Új részvevő lépett be a konferencia előszobájába",
"room.lobbyPeerLeft": "A konferencia előszobájából a részvevő távozott",
"room.lobbyPeerChangedDisplayName": "Az előszobai résztvevő meváltoztatta a nevét: {displayName}",
"room.lobbyPeerChangedPicture": "Az előszobai résztvevő meváltoztatta a képét",
"room.lobbyPeerChangedDisplayName": "Az előszobai résztvevő megváltoztatta a nevét: {displayName}",
"room.lobbyPeerChangedPicture": "Az előszobai résztvevő megváltoztatta a képét",
"room.setAccessCode": "A konferencia hozzáférési kódja megváltozott",
"room.accessCodeOn": "A konferencia hozzáférési kódja aktiválva",
"room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva",
@ -39,8 +39,8 @@
"room.audioOnly": "csak Hang",
"room.audioVideo": "Hang és Videó",
"room.youAreReady": "Ok, kész vagy",
"room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferecnia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.",
"room.locketWait": "A konferencia szobába a a belépés tilos - Várj amíg valaki be nem enged ...",
"room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferencia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.",
"room.locketWait": "Az automatikus belépés tiltva van - Várj amíg valaki beenged ...",
"room.lobbyAdministration": "Előszoba adminisztráció",
"room.peersInLobby": "Résztvevők az előszobában",
"room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában",
@ -49,18 +49,29 @@
"room.spotlights": "Látható résztvevők",
"room.passive": "Passzív résztvevők",
"room.videoPaused": "Ez a videóstream szünetel",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.muteAll": "Mindenki némítása",
"room.stopAllVideo": "Mindenki video némítása",
"room.closeMeeting": "Konferencia lebontása",
"room.clearChat": "Chat történelem kiürítése",
"room.clearFileSharing": "File megosztás kiürítése",
"room.speechUnsupported": "A böngésződ nem támogatja a hangfelismerést",
"room.moderatoractions": "Moderátor funkciók",
"room.raisedHand": "{displayName} jelentkezik",
"room.loweredHand": "{displayName} leeresztette a kezét",
"room.extraVideo": "Kiegészítő videó",
"room.overRoomLimit": "A konferenciaszoba betelt..",
"room.help": "Segítség",
"room.about": "Névjegy",
"room.shortcutKeys": "Billentyűparancsok",
"me.mutedPTT": null,
"me.mutedPTT": "Némítva vagy, ha beszélnél nyomd le a szóköz billentyűt",
"roles.gotRole": "{role} szerepet kaptál",
"roles.lostRole": "Elvesztetted a {role} szerepet",
"tooltip.login": "Belépés",
"tooltip.logout": "Kilépés",
"tooltip.admitFromLobby": "Beenegdem az előszobából",
"tooltip.admitFromLobby": "Beengedem az előszobából",
"tooltip.lockRoom": "A konferenciába való belépés letiltása",
"tooltip.unLockRoom": "konferenciába való belépés engedélyezése",
"tooltip.enterFullscreen": "Teljes képernyős mód",
@ -68,7 +79,11 @@
"tooltip.lobby": "Az előszobában várakozók listája",
"tooltip.settings": "Beállítások",
"tooltip.participants": "Résztvevők",
"tooltip.kickParticipant": null,
"tooltip.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.chooseRoomButton": "Tovább",
@ -82,6 +97,7 @@
"label.filesharing": "Fájl megosztás",
"label.participants": "Résztvevők",
"label.shareFile": "Fájl megosztása",
"label.shareGalleryFile": "Fájl megosztás galériából",
"label.fileSharingUnsupported": "Fájl megosztás nem támogatott",
"label.unknown": "Ismeretlen",
"label.democratic": "Egyforma képméretű képkiosztás",
@ -92,6 +108,12 @@
"label.veryHigh": "Nagyon magas (FHD)",
"label.ultra": "Ultra magas (UHD)",
"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.camera": "Kamera",
@ -109,11 +131,19 @@
"settings.advancedMode": "Részletes információk",
"settings.permanentTopBar": "Állandó felső sáv",
"settings.lastn": "A látható videók száma",
"settings.hiddenControls": "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.startingFileShare": "Fájl megosztása",
"filesharing.successfulFileShare": "A fájl sikeresen megosztva",
"filesharing.unableToShare": "Sikereteln fájl megosztás",
"filesharing.unableToShare": "Sikertelen fájl megosztás",
"filesharing.error": "Hiba a fájlmegosztás során",
"filesharing.finished": "A fájl letöltés befejeződött",
"filesharing.save": "Mentés",
@ -123,7 +153,7 @@
"devices.devicesChanged": "Az eszközei megváltoztak, konfiguráld őket be a beállítások menüben",
"device.audioUnsupported": "A hnag nem támogatott",
"device.audioUnsupported": "A hang nem támogatott",
"device.activateAudio": "Hang aktiválása",
"device.muteAudio": "Hang némítása",
"device.unMuteAudio": "Hang némítás kikapcsolása",
@ -134,9 +164,9 @@
"device.screenSharingUnsupported": "A képernyő megosztás nem támogatott",
"device.startScreenSharing": "Képernyőmegosztás indítása",
"device.stopScreenSharing": "Képernyőmegosztás leáłłítása",
"device.stopScreenSharing": "Képernyőmegosztás leállítása",
"devices.microphoneDisconnected": "Microphone kapcsolat bontva",
"devices.microphoneDisconnected": "Mikrofon kapcsolat bontva",
"devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben",
"devices.microphoneMute": "A mikrofon némítva lett",
"devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "A kamera kapcsolata lebomlott",
"devices.cameraError": "Hiba történt a kamera elérése során",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "A moderátor kiürítette a chat történelmet",
"moderator.clearFiles": "A moderátor kiürítette a file megosztás történelmet",
"moderator.muteAudio": "A moderátor elnémította a hangod",
"moderator.muteVideo": "A moderátor elnémította a videód"
}

View File

@ -49,18 +49,29 @@
"room.spotlights": "Partecipanti in Evidenza",
"room.passive": "Participanti Passivi",
"room.videoPaused": "Il video è in pausa",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.muteAll": "Muta tutti",
"room.stopAllVideo": "Ferma tutti i video",
"room.closeMeeting": "Termina meeting",
"room.clearChat": "Pulisci chat",
"room.clearFileSharing": "Pulisci file sharing",
"room.speechUnsupported": "Il tuo browser non supporta il riconoscimento vocale",
"room.moderatoractions": "Azioni moderatore",
"room.raisedHand": "{displayName} ha alzato la mano",
"room.loweredHand": "{displayName} ha abbassato la mano",
"room.extraVideo": "Video extra",
"room.overRoomLimit": "La stanza è piena, riprova più tardi.",
"room.help": "Aiuto",
"room.about": "Informazioni su",
"room.shortcutKeys": "Scorciatoie da tastiera",
"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.logout": "Log out",
"tooltip.admitFromLobby": "Ammetti dalla lobby",
"tooltip.admitFromLobby": "Accetta partecipante dalla lobby",
"tooltip.lockRoom": "Blocca stanza",
"tooltip.unLockRoom": "Sblocca stanza",
"tooltip.enterFullscreen": "Modalità schermo intero",
@ -68,6 +79,11 @@
"tooltip.lobby": "Mostra lobby",
"tooltip.settings": "Mostra impostazioni",
"tooltip.participants": "Mostra partecipanti",
"tooltip.kickParticipant": "Espelli partecipante",
"tooltip.muteParticipant": "Muta partecipante",
"tooltip.muteParticipantVideo": "Ferma video partecipante",
"tooltip.raisedHand": "Mano alzata",
"tooltip.muteScreenSharing": "Ferma condivisione schermo partecipante",
"label.roomName": "Nome della stanza",
"label.chooseRoomButton": "Continua",
@ -81,6 +97,7 @@
"label.filesharing": "Condivisione file",
"label.participants": "Partecipanti",
"label.shareFile": "Condividi file",
"label.shareGalleryFile": "Condividi immagine",
"label.fileSharingUnsupported": "Condivisione file non supportata",
"label.unknown": "Sconosciuto",
"label.democratic": "Vista Democratica",
@ -91,6 +108,12 @@
"label.veryHigh": "Molto alta (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Videocamera",
@ -108,6 +131,14 @@
"settings.advancedMode": "Modalità avanzata",
"settings.permanentTopBar": "Barra superiore permanente",
"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.startingFileShare": "Tentativo di condivisione file",
@ -123,7 +154,7 @@
"devices.devicesChanged": "Il tuo dispositivo è cambiato, configura i dispositivi nel menù di impostazioni",
"device.audioUnsupported": "Dispositivo audio non supportato",
"device.activateAudio": "Attiva audio",
"device.activateAudio": "Attiva audio",
"device.muteAudio": "Silenzia audio",
"device.unMuteAudio": "Riattiva audio",
@ -149,8 +180,8 @@
"devices.cameraDisconnected": "Videocamera scollegata",
"devices.cameraError": "Errore con l'accesso alla videocamera",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "Il moderatore ha pulito la chat",
"moderator.clearFiles": "Il moderatore ha pulito i file",
"moderator.muteAudio": "Il moderatore ha mutato il tuo audio",
"moderator.muteVideo": "Il moderatore ha fermato il tuo video"
}

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.clearFileSharing": "Fjern filer",
"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",
"roles.gotRole": "Du fikk rollen: {role}",
"roles.lostRole": "Du mistet rollen: {role}",
"tooltip.login": "Logg in",
"tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby",
@ -69,6 +80,10 @@
"tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere",
"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.chooseRoomButton": "Fortsett",
@ -82,6 +97,7 @@
"label.filesharing": "Fildeling",
"label.participants": "Deltakere",
"label.shareFile": "Del fil",
"label.shareGalleryFile": "Del bilde",
"label.fileSharingUnsupported": "Fildeling ikke støttet",
"label.unknown": "Ukjent",
"label.democratic": "Demokratisk",
@ -92,6 +108,12 @@
"label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Kamera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Avansert modus",
"settings.permanentTopBar": "Permanent topplinje",
"settings.lastn": "Antall videoer synlig",
"settings.hiddenControls": "Skjul media knapper",
"settings.notificationSounds": "Varslingslyder",
"settings.showNotifications": "Vis varslinger",
"settings.buttonControlBar": "Separate media knapper",
"settings.echoCancellation": "Echokansellering",
"settings.autoGainControl": "Auto gain kontroll",
"settings.noiseSuppression": "Støy reduksjon",
"settings.drawerOverlayed": "Sidemeny over innhold",
"filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling",

View File

@ -49,14 +49,25 @@
"room.spotlights": "Aktywni uczestnicy",
"room.passive": "Pasywni uczestnicy",
"room.videoPaused": "To wideo jest wstrzymane.",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.muteAll": "Wycisz wszystkich",
"room.stopAllVideo": "Zatrzymaj wszystkie Video",
"room.closeMeeting": "Zamknij spotkanie",
"room.clearChat": "Wyczyść Chat",
"room.clearFileSharing": "Wyczyść pliki",
"room.speechUnsupported": "Twoja przeglądarka nie rozpoznaje mowy",
"room.moderatoractions": "Akcje moderatora",
"room.raisedHand": "{displayName} podniósł rękę",
"room.loweredHand": "{displayName} opuścił rękę",
"room.extraVideo": "Dodatkowe Video",
"room.overRoomLimit": "Pokój jest pełny, spróbuj za jakiś czas.",
"room.help": "Pomoc",
"room.about": "O pogramie",
"room.shortcutKeys": "Skróty klawiaturowe",
"me.mutedPTT": null,
"me.mutedPTT": "Masz wyciszony mikrofon, przytrzymaj spację aby mówić",
"roles.gotRole": "Masz rolę {role}",
"roles.lostRole": "Nie masz już roli {role}",
"tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj",
@ -68,7 +79,11 @@
"tooltip.lobby": "Pokaż poczekalnię",
"tooltip.settings": "Pokaż ustawienia",
"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.chooseRoomButton": "Kontynuuj",
@ -82,6 +97,7 @@
"label.filesharing": "Udostępnianie plików",
"label.participants": "Uczestnicy",
"label.shareFile": "Udostępnij plik",
"label.shareGalleryFile": "Udostępnij obraz",
"label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane",
"label.unknown": "Nieznane",
"label.democratic": "Układ demokratyczny",
@ -92,6 +108,12 @@
"label.veryHigh": "Bardzo wysoka (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Kamera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Tryb zaawansowany",
"settings.permanentTopBar": "Stały górny pasek",
"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.startingFileShare": "Próba udostępnienia pliku",
@ -150,8 +180,8 @@
"devices.cameraDisconnected": "Kamera odłączona",
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
"moderator.clearChat": "Moderator wyczyścił chat",
"moderator.clearFiles": "Moderator wyczyścił pliki",
"moderator.muteAudio": "Moderator wyciszył audio",
"moderator.muteVideo": "Moderator wyciszył twoje video"
}

View File

@ -55,9 +55,20 @@
"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": "Entrar",
"tooltip.logout": "Sair",
"tooltip.admitFromLobby": "Admitir da sala de espera",
@ -69,6 +80,10 @@
"tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar",
@ -82,6 +97,7 @@
"label.filesharing": "Partilha de ficheiro",
"label.participants": "Participantes",
"label.shareFile": "Partilhar ficheiro",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partilha de ficheiro não disponível",
"label.unknown": "Desconhecido",
"label.democratic": "Vista democrática",
@ -92,6 +108,12 @@
"label.veryHigh": "Muito alta (FHD)",
"label.ultra": "Ultra (UHD)",
"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.camera": "Camera",
@ -109,6 +131,14 @@
"settings.advancedMode": "Modo avançado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Número de vídeos visíveis",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro",

View File

@ -55,9 +55,20 @@
"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": "Intră în cont",
"tooltip.logout": "Deconectare",
"tooltip.admitFromLobby": "Admite din hol",
@ -69,6 +80,10 @@
"tooltip.settings": "Arată setăile",
"tooltip.participants": null,
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare",
@ -82,6 +97,7 @@
"label.filesharing": "Partajarea fișierelor",
"label.participants": "Participanți",
"label.shareFile": "Partajează fișierul",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată",
"label.unknown": "Necunoscut",
"label.democratic": "Distribuție egală a dimensiunii imaginii",
@ -92,6 +108,12 @@
"label.veryHigh": "Rezoluție foarte înaltă (FHD)",
"label.ultra": "Rezoluție ultra înaltă (UHD)",
"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.camera": "Cameră video",
@ -109,6 +131,14 @@
"settings.advancedMode": "Mod avansat",
"settings.permanentTopBar": "Bara de sus permanentă",
"settings.lastn": "Numărul de videoclipuri vizibile",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"settings.showNotifications": null,
"settings.buttonControlBar": null,
"settings.echoCancellation": null,
"settings.autoGainControl": null,
"settings.noiseSuppression": null,
"settings.drawerOverlayed": null,
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului",

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

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 userRoles = require('../userRoles');
const {
BYPASS_ROOM_LOCK,
BYPASS_LOBBY
} = require('../access');
const {
CHANGE_ROOM_LOCK,
PROMOTE_PEER,
SEND_CHAT,
MODERATE_CHAT,
SHARE_SCREEN,
EXTRA_VIDEO,
SHARE_FILE,
MODERATE_FILES,
MODERATE_ROOM
} = require('../permissions');
// const AwaitQueue = require('awaitqueue');
// const axios = require('axios');
@ -36,14 +54,14 @@ module.exports =
},
*/
// URI and key for requesting geoip-based TURN server closest to the client
turnAPIKey : 'examplekey',
turnAPIURI : 'https://example.com/api/turn',
turnAPIparams : {
'uri_schema' : 'turn',
'transport' : 'tcp',
'ip_ver' : 'ipv4',
'servercount' : '2'
},
turnAPIKey : 'examplekey',
turnAPIURI : 'https://example.com/api/turn',
turnAPIparams : {
'uri_schema' : 'turn',
'transport' : 'tcp',
'ip_ver' : 'ipv4',
'servercount' : '2'
},
// Backup turnservers if REST fails or is not configured
backupTurnServers : [
@ -60,14 +78,16 @@ module.exports =
// session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e',
cookieName : 'multiparty-meeting.sid',
// if you use encrypted private key the set the passphrase
tls :
{
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
// passphrase: 'key_password'
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
},
// listening Host or IP
// If omitted listens on every IP. ("0.0.0.0" and "::")
//listeningHost: 'localhost',
// listeningHost: 'localhost',
// Listening port for https server.
listeningPort : 443,
// Any http request is redirected to https.
@ -77,6 +97,12 @@ module.exports =
// listeningRedirectPort disabled
// use case: loadbalancer backend
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
// called every time there is a room created or destroyed,
// or peer created or destroyed. This would then be able
@ -88,8 +114,8 @@ module.exports =
this._queue = new AwaitQueue();
}
// rooms: number of rooms
// peers: number of peers
// rooms: rooms object
// peers: peers object
// eslint-disable-next-line no-unused-vars
async log({ rooms, peers })
{
@ -98,9 +124,9 @@ module.exports =
// Do your logging in here, use queue to keep correct order
// eslint-disable-next-line no-console
console.log('Number of rooms: ', rooms);
console.log('Number of rooms: ', rooms.size);
// eslint-disable-next-line no-console
console.log('Number of peers: ', peers);
console.log('Number of peers: ', peers.size);
})
.catch((error) =>
{
@ -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.
// Use this function to map your oidc userinfo to the Peer object.
// The roomId is equal to the room name.
@ -211,40 +231,53 @@ module.exports =
//
// Example:
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
accessFromRoles : {
accessFromRoles : {
// The role(s) will gain access to the room
// even if it is locked (!)
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
[BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
// The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ]
[BYPASS_LOBBY] : [ userRoles.NORMAL ]
},
permissionsFromRoles : {
permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
[CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ],
// The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : [ userRoles.NORMAL ],
[PROMOTE_PEER] : [ userRoles.NORMAL ],
// The role(s) have permission to send chat messages
SEND_CHAT : [ userRoles.NORMAL ],
[SEND_CHAT] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ],
[MODERATE_CHAT] : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ],
[SHARE_SCREEN] : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
[EXTRA_VIDEO] : [ userRoles.NORMAL ],
// The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ],
[SHARE_FILE] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files
MODERATE_FILES : [ userRoles.MODERATOR ],
[MODERATE_FILES] : [ userRoles.MODERATOR ],
// The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : [ userRoles.MODERATOR ]
[MODERATE_ROOM] : [ userRoles.MODERATOR ]
},
// Array of permissions. If no peer with the permission in question
// is in the room, all peers are permitted to do the action. The peers
// that are allowed because of this rule will not be able to do this
// action as soon as a peer with the permission joins. In this example
// everyone will be able to lock/unlock room until a MODERATOR joins.
allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ],
// When truthy, the room will be open to all users when as long as there
// are allready users in the room
activateOnHostJoin : true,
activateOnHostJoin : true,
// When set, maxUsersPerRoom defines how many users can join
// a single room. If not set, there is no limit.
// maxUsersPerRoom : 20,
// Room size before spreading to new router
routerScaleSize : 40,
// Mediasoup settings
mediasoup :
mediasoup :
{
numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings.
@ -325,11 +358,12 @@ module.exports =
{
listenIps :
[
// change ip to your servers IP address!
{ ip: '0.0.0.0', announcedIp: null }
// change 192.0.2.1 IPv4 to your server's IPv4 address!!
{ ip: '192.0.2.1', announcedIp: null }
// 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,
minimumAvailableOutgoingBitrate : 600000,
@ -337,4 +371,13 @@ module.exports =
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) =>
({
peerId : peer.id,
displayName : peer.displayName
id : peer.id,
displayName : peer.displayName,
picture : peer.picture
}));
}
@ -62,8 +63,8 @@ class Lobby extends EventEmitter
for (const peer in this._peers)
{
if (peer.socket)
this.promotePeer(peer.id);
if (!this._peers[peer].closed)
this.promotePeer(peer);
}
}
@ -153,8 +154,6 @@ class Lobby extends EventEmitter
this.emit('lobbyEmpty');
};
this._notification(peer.socket, 'enteredLobby');
this._peers[peer.id] = peer;
peer.on('gotRole', peer.gotRoleHandler);
@ -164,6 +163,8 @@ class Lobby extends EventEmitter
peer.socket.on('request', peer.socketRequestHandler);
peer.on('close', peer.closeHandler);
this._notification(peer.socket, 'enteredLobby');
}
async _handleSocketRequest(peer, request, cb)
@ -189,6 +190,7 @@ class Lobby extends EventEmitter
break;
}
case 'changePicture':
{
const { picture } = request.data;

View File

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

File diff suppressed because it is too large Load Diff

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",
"version": "3.2.0",
"version": "3.3.0",
"private": true,
"description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"main": "lib/index.js",
"scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js",
"connect": "node connect.js"
"start": "node server.js",
"connect": "node connect.js",
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
},
"dependencies": {
"awaitqueue": "^1.0.0",
@ -31,9 +32,13 @@
"passport": "^0.4.0",
"passport-lti": "0.0.7",
"pidusage": "^2.0.17",
"prom-client": ">=12.0.0",
"redis": "^2.8.0",
"socket.io": "^2.3.0",
"spdy": "^4.0.1",
"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 sharedSession = require('express-socket.io-session');
const interactiveServer = require('./lib/interactiveServer');
const promExporter = require('./lib/promExporter');
const { v4: uuidv4 } = require('uuid');
/* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG);
@ -54,10 +56,6 @@ if ('StatusLogger' in config)
// @type {Array<mediasoup.Worker>}
const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
// Map of Room instances indexed by roomId.
const rooms = new Map();
@ -129,44 +127,58 @@ let oidcStrategy;
async function run()
{
// Open the interactive server.
await interactiveServer(rooms, peers);
if (typeof(config.auth) === 'undefined')
try
{
logger.warn('Auth is not configured properly!');
}
else
{
await setupAuth();
}
// Open the interactive server.
await interactiveServer(rooms, peers);
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
// Log rooms status every 30 seconds.
setInterval(() =>
{
for (const room of rooms.values())
// start Prometheus exporter
if (config.prometheus)
{
room.logStatus();
await promExporter(rooms, peers, config.prometheus);
}
}, 120000);
// check for deserted rooms
setInterval(() =>
{
for (const room of rooms.values())
if (typeof(config.auth) === 'undefined')
{
room.checkEmpty();
logger.warn('Auth is not configured properly!');
}
}, 10000);
else
{
await setupAuth();
}
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
const errorHandler = (err, req, res, next) =>
{
const trackingId = uuidv4();
res.status(500).send(
`<h1>Internal Server Error</h1>
<p>If you report this error, please also report this
<i>tracking ID</i> which makes it possible to locate your session
in the logs which are available to the system administrator:
<b>${trackingId}</b></p>`
);
logger.error(
'Express error handler dump with tracking ID: %s, error dump: %o',
trackingId, err);
};
// eslint-disable-next-line no-unused-vars
app.use(errorHandler);
}
catch (error)
{
logger.error('run() [error:"%o"]', error);
}
}
function statusLog()
@ -174,8 +186,8 @@ function statusLog()
if (statusLogger)
{
statusLogger.log({
rooms : rooms.size,
peers : peers.size
rooms : rooms,
peers : peers
});
}
}
@ -199,7 +211,7 @@ function setupLTI(ltiConfig)
if (lti.user_id && lti.custom_room)
{
user.id = lti.user_id;
user._userinfo = { "lti" : lti };
user._userinfo = { 'lti': lti };
}
if (lti.custom_room)
@ -240,8 +252,18 @@ function setupOIDC(oidcIssuer)
// redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code'
// 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
// argument to verify fn
@ -256,7 +278,9 @@ function setupOIDC(oidcIssuer)
{ client: oidcClient, params, passReqToCallback, usePKCE },
(tokenset, userinfo, done) =>
{
if (userinfo && tokenset) {
if (userinfo && tokenset)
{
// eslint-disable-next-line camelcase
userinfo._tokenset_claims = tokenset.claims();
}
@ -344,38 +368,45 @@ async function setupAuth()
app.get(
'/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
async (req, res) =>
async (req, res, next) =>
{
const state = JSON.parse(base64.decode(req.query.state));
const { peerId, roomId } = state;
req.session.peerId = peerId;
req.session.roomId = roomId;
let peer = peers.get(peerId);
if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId });
if (peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (typeof config.userMapping === 'function')
try
{
await config.userMapping({
peer,
roomId,
userinfo : req.user._userinfo
});
const state = JSON.parse(base64.decode(req.query.state));
const { peerId, roomId } = state;
req.session.peerId = peerId;
req.session.roomId = roomId;
let peer = peers.get(peerId);
if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId });
if (peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (typeof config.userMapping === 'function')
{
await config.userMapping({
peer,
roomId,
userinfo : req.user._userinfo
});
}
peer.authenticated = true;
res.send(loginHelper({
displayName : peer.displayName,
picture : peer.picture
}));
}
catch (error)
{
return next(error);
}
peer.authenticated = true;
res.send(loginHelper({
displayName : peer.displayName,
picture : peer.picture
}));
}
);
}
@ -431,14 +462,14 @@ async function runHttpsServer()
// http
const redirectListener = http.createServer(app);
if(config.listeningHost)
if (config.listeningHost)
redirectListener.listen(config.listeningRedirectPort, config.listeningHost);
else
redirectListener.listen(config.listeningRedirectPort);
}
// https or http
if(config.listeningHost)
if (config.listeningHost)
mainListener.listen(config.listeningPort, config.listeningHost);
else
mainListener.listen(config.listeningPort);
@ -562,7 +593,8 @@ async function runWebSocketServer()
{
logger.error('room creation or room joining failed [error:"%o"]', error);
socket.disconnect(true);
if (socket)
socket.disconnect(true);
return;
});
@ -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).
*/
@ -625,9 +644,9 @@ async function getOrCreateRoom({ roomId })
{
logger.info('creating a new Room [roomId:"%s"]', roomId);
const mediasoupWorker = getMediasoupWorker();
// const mediasoupWorker = getMediasoupWorker();
room = await Room.create({ mediasoupWorker, roomId });
room = await Room.create({ mediasoupWorkers, roomId });
rooms.set(roomId, room);

View File

@ -7,5 +7,5 @@ module.exports = {
// Don't change anything after this point
// All users have this role by default, do not change or remove this role
NORMAL : 'normal'
NORMAL : 'normal'
};