Merge branch 'feat-user-roles' into develop

auto_join_3.3
Håvar Aambø Fosstveit 2020-03-26 21:31:57 +01:00
commit 5071282d40
31 changed files with 1244 additions and 669 deletions

View File

@ -417,7 +417,7 @@ export default class RoomClient
login()
{
const url = `/auth/login?id=${this._peerId}`;
const url = `/auth/login?peerId=${this._peerId}&roomId=${this._roomId}`;
window.open(url, 'loginWindow');
}
@ -433,16 +433,8 @@ export default class RoomClient
const { displayName, picture } = data;
if (store.getState().room.state === 'connected')
{
this.changeDisplayName(displayName);
this.changePicture(picture);
}
else
{
store.dispatch(settingsActions.setDisplayName(displayName));
store.dispatch(meActions.setPicture(picture));
}
store.dispatch(settingsActions.setDisplayName(displayName));
store.dispatch(meActions.setPicture(picture));
store.dispatch(meActions.loggedIn(true));
@ -1155,6 +1147,86 @@ export default class RoomClient
lobbyPeerActions.setLobbyPeerPromotionInProgress(peerId, false));
}
async kickPeer(peerId)
{
logger.debug('kickPeer() [peerId:"%s"]', peerId);
store.dispatch(
peerActions.setPeerKickInProgress(peerId, true));
try
{
await this.sendRequest('moderator:kickPeer', { peerId });
}
catch (error)
{
logger.error('kickPeer() failed: %o', error);
}
store.dispatch(
peerActions.setPeerKickInProgress(peerId, false));
}
async muteAllPeers()
{
logger.debug('muteAllPeers()');
store.dispatch(
roomActions.setMuteAllInProgress(true));
try
{
await this.sendRequest('moderator:muteAll');
}
catch (error)
{
logger.error('muteAllPeers() failed: %o', error);
}
store.dispatch(
roomActions.setMuteAllInProgress(false));
}
async stopAllPeerVideo()
{
logger.debug('stopAllPeerVideo()');
store.dispatch(
roomActions.setStopAllVideoInProgress(true));
try
{
await this.sendRequest('moderator:stopAllVideo');
}
catch (error)
{
logger.error('stopAllPeerVideo() failed: %o', error);
}
store.dispatch(
roomActions.setStopAllVideoInProgress(false));
}
async closeMeeting()
{
logger.debug('closeMeeting()');
store.dispatch(
roomActions.setCloseMeetingInProgress(true));
try
{
await this.sendRequest('moderator:closeMeeting');
}
catch (error)
{
logger.error('closeMeeting() failed: %o', error);
}
store.dispatch(
roomActions.setCloseMeetingInProgress(false));
}
// type: mic/webcam/screen
// mute: true/false
async modifyPeerConsumer(peerId, type, mute)
@ -1914,10 +1986,10 @@ export default class RoomClient
case 'newPeer':
{
const { id, displayName, picture } = notification.data;
const { id, displayName, picture, roles } = notification.data;
store.dispatch(
peerActions.addPeer({ id, displayName, picture, consumers: [] }));
peerActions.addPeer({ id, displayName, picture, roles, consumers: [] }));
store.dispatch(requestActions.notify(
{
@ -2016,6 +2088,96 @@ export default class RoomClient
break;
}
case 'moderator:mute':
{
// const { peerId } = notification.data;
if (this._micProducer && !this._micProducer.paused)
{
this.muteMic();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'moderator.mute',
defaultMessage : 'Moderator muted your microphone'
})
}));
}
break;
}
case 'moderator:stopVideo':
{
// const { peerId } = notification.data;
this.disableWebcam();
this.disableScreenSharing();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'moderator.mute',
defaultMessage : 'Moderator stopped your video'
})
}));
break;
}
case 'moderator:kick':
{
// Need some feedback
this.close();
break;
}
case 'gotRole':
{
const { peerId, role } = notification.data;
if (peerId === this._peerId)
{
store.dispatch(meActions.addRole(role));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.gotRole',
defaultMessage : `You got the role: ${role}`
})
}));
}
else
store.dispatch(peerActions.addPeerRole(peerId, role));
break;
}
case 'lostRole':
{
const { peerId, role } = notification.data;
if (peerId === this._peerId)
{
store.dispatch(meActions.removeRole(role));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.lostRole',
defaultMessage : `You lost the role: ${role}`
})
}));
}
else
store.dispatch(peerActions.removePeerRole(peerId, role));
break;
}
default:
{
@ -2195,7 +2357,7 @@ export default class RoomClient
canShareFiles : this._torrentSupport
}));
const { peers } = await this.sendRequest(
const { roles, peers } = await this.sendRequest(
'join',
{
displayName : displayName,
@ -2203,7 +2365,25 @@ export default class RoomClient
rtpCapabilities : this._mediasoupDevice.rtpCapabilities
});
logger.debug('_joinRoom() joined, got peers [peers:"%o"]', peers);
logger.debug('_joinRoom() joined [peers:"%o", roles:"%o"]', peers, roles);
const myRoles = store.getState().me.roles;
for (const role of roles)
{
if (!myRoles.includes(role))
{
store.dispatch(meActions.addRole(role));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'roles.gotRole',
defaultMessage : `You got the role: ${role}`
})
}));
}
}
for (const peer of peers)
{

View File

@ -15,6 +15,18 @@ export const loggedIn = (flag) =>
payload : { flag }
});
export const addRole = (role) =>
({
type : 'ADD_ROLE',
payload : { role }
});
export const removeRole = (role) =>
({
type : 'REMOVE_ROLE',
payload : { role }
});
export const setPicture = (picture) =>
({
type : 'SET_PICTURE',

View File

@ -45,3 +45,22 @@ export const setPeerPicture = (peerId, picture) =>
type : 'SET_PEER_PICTURE',
payload : { peerId, picture }
});
export const addPeerRole = (peerId, role) =>
({
type : 'ADD_PEER_ROLE',
payload : { peerId, role }
});
export const removePeerRole = (peerId, role) =>
({
type : 'REMOVE_PEER_ROLE',
payload : { peerId, role }
});
export const setPeerKickInProgress = (peerId, flag) =>
({
type : 'SET_PEER_KICK_IN_PROGRESS',
payload : { peerId, flag }
});

View File

@ -109,4 +109,22 @@ export const toggleConsumerFullscreen = (consumerId) =>
({
type : 'TOGGLE_FULLSCREEN_CONSUMER',
payload : { consumerId }
});
export const setMuteAllInProgress = (flag) =>
({
type : 'MUTE_ALL_IN_PROGRESS',
payload : { flag }
});
export const setStopAllVideoInProgress = (flag) =>
({
type : 'STOP_ALL_VIDEO_IN_PROGRESS',
payload : { flag }
});
export const setCloseMeetingInProgress = (flag) =>
({
type : 'CLOSE_MEETING_IN_PROGRESS',
payload : { flag }
});

View File

@ -1,21 +1,15 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import isElectron from 'is-electron';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import randomString from 'random-string';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import IconButton from '@material-ui/core/IconButton';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import CookieConsent from 'react-cookie-consent';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
@ -88,63 +82,12 @@ const styles = (theme) =>
const DialogTitle = withStyles(styles)((props) =>
{
const [ open, setOpen ] = useState(false);
const intl = useIntl();
useEffect(() =>
{
const openTimer = setTimeout(() => setOpen(true), 1000);
const closeTimer = setTimeout(() => setOpen(false), 4000);
return () =>
{
clearTimeout(openTimer);
clearTimeout(closeTimer);
};
}, []);
const { children, classes, myPicture, onLogin, ...other } = props;
const handleTooltipClose = () =>
{
setOpen(false);
};
const handleTooltipOpen = () =>
{
setOpen(true);
};
const { children, classes, ...other } = props;
return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography>
{ window.config && window.config.loginEnabled &&
<Tooltip
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
open={open}
title={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left'
>
<IconButton
aria-label='Account'
className={classes.loginButton}
color='inherit'
onClick={onLogin}
>
{ myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} />
:
<AccountCircle className={classes.largeIcon} />
}
</IconButton>
</Tooltip>
}
</MuiDialogTitle>
);
});
@ -165,9 +108,6 @@ const DialogActions = withStyles((theme) => ({
}))(MuiDialogActions);
const ChooseRoom = ({
roomClient,
loggedIn,
myPicture,
classes
}) =>
{
@ -184,13 +124,7 @@ const ChooseRoom = ({
paper : classes.dialogPaper
}}
>
<DialogTitle
myPicture={myPicture}
onLogin={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
<DialogTitle>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
<hr />
</DialogTitle>
@ -258,34 +192,7 @@ const ChooseRoom = ({
ChooseRoom.propTypes =
{
roomClient : PropTypes.any.isRequired,
loginEnabled : PropTypes.bool.isRequired,
loggedIn : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
loginEnabled : state.me.loginEnabled,
loggedIn : state.me.loggedIn,
myPicture : state.me.picture
};
};
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.picture === next.me.picture
);
}
}
)(withStyles(styles)(ChooseRoom)));
export default withStyles(styles)(ChooseRoom);

View File

@ -0,0 +1,118 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { useIntl, FormattedMessage } from 'react-intl';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
actionButtons :
{
display : 'flex'
},
divider :
{
marginLeft : theme.spacing(2)
}
});
const ListModerator = (props) =>
{
const intl = useIntl();
const {
roomClient,
room,
classes
} = props;
return (
<div className={classes.root}>
<Button
aria-label={intl.formatMessage({
id : 'room.muteAll',
defaultMessage : 'Mute all'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.muteAllInProgress}
onClick={() => roomClient.muteAllPeers()}
>
<FormattedMessage
id='room.muteAll'
defaultMessage='Mute all'
/>
</Button>
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'room.stopAllVideo',
defaultMessage : 'Stop all video'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.stopAllVideoInProgress}
onClick={() => roomClient.stopAllPeerVideo()}
>
<FormattedMessage
id='room.stopAllVideo'
defaultMessage='Stop all video'
/>
</Button>
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'room.closeMeeting',
defaultMessage : 'Close meeting'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.closeMeetingInProgress}
onClick={() => roomClient.closeMeeting()}
>
<FormattedMessage
id='room.closeMeeting'
defaultMessage='Close meeting'
/>
</Button>
</div>
);
};
ListModerator.propTypes =
{
roomClient : PropTypes.any.isRequired,
room : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
room : state.room
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room
);
}
}
)(withStyles(styles)(ListModerator)));

View File

@ -12,6 +12,7 @@ import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
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';
@ -91,40 +92,6 @@ const styles = (theme) =>
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
},
button :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.5)',
cursor : 'pointer',
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
},
'&.unsupported' :
{
pointerEvents : 'none'
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.on' :
{
backgroundColor : 'var(--media-control-botton-on)'
},
'&.off' :
{
backgroundColor : 'var(--media-control-botton-off)'
}
}
});
@ -134,6 +101,7 @@ const ListPeer = (props) =>
const {
roomClient,
isModerator,
peer,
micConsumer,
screenConsumer,
@ -185,9 +153,8 @@ const ListPeer = (props) =>
})}
color={ screenVisible ? 'primary' : 'secondary'}
disabled={ peer.peerScreenInProgress }
onClick={(e) =>
onClick={() =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
@ -207,9 +174,8 @@ const ListPeer = (props) =>
})}
color={ micEnabled ? 'primary' : 'secondary'}
disabled={ peer.peerAudioInProgress }
onClick={(e) =>
onClick={() =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
@ -221,6 +187,21 @@ const ListPeer = (props) =>
<MicOffIcon />
}
</IconButton>
{ isModerator &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
disabled={ peer.peerKickInProgress }
onClick={() =>
{
roomClient.kickPeer(peer.id);
}}
>
<ExitIcon />
</IconButton>
}
</div>
</div>
);
@ -230,6 +211,7 @@ ListPeer.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
isModerator : PropTypes.bool,
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,

View File

@ -11,7 +11,9 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer';
import ListMe from './ListMe';
import ListModerator from './ListModerator';
import Volume from '../../Containers/Volume';
import * as userRoles from '../../../reducers/userRoles';
const styles = (theme) =>
({
@ -76,6 +78,7 @@ class ParticipantList extends React.PureComponent
const {
roomClient,
advancedMode,
isModerator,
passivePeers,
selectedPeerId,
spotlightPeers,
@ -84,6 +87,17 @@ class ParticipantList extends React.PureComponent
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
{ isModerator &&
<ul className={classes.list}>
<li className={classes.listheader}>
<FormattedMessage
id='room.moderatoractions'
defaultMessage='Moderator actions'
/>
</li>
<ListModerator />
</ul>
}
<ul className={classes.list}>
<li className={classes.listheader}>
<FormattedMessage
@ -108,7 +122,7 @@ class ParticipantList extends React.PureComponent
})}
onClick={() => roomClient.setSelectedPeer(peerId)}
>
<ListPeer id={peerId} advancedMode={advancedMode}>
<ListPeer id={peerId} advancedMode={advancedMode} isModerator={isModerator}>
<Volume small id={peerId} />
</ListPeer>
</li>
@ -129,7 +143,11 @@ class ParticipantList extends React.PureComponent
})}
onClick={() => roomClient.setSelectedPeer(peerId)}
>
<ListPeer id={peerId} advancedMode={advancedMode} />
<ListPeer
id={peerId}
advancedMode={advancedMode}
isModerator={isModerator}
/>
</li>
))}
</ul>
@ -142,6 +160,7 @@ ParticipantList.propTypes =
{
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool,
isModerator : PropTypes.bool,
passivePeers : PropTypes.array,
selectedPeerId : PropTypes.string,
spotlightPeers : PropTypes.array,
@ -151,6 +170,8 @@ ParticipantList.propTypes =
const mapStateToProps = (state) =>
{
return {
isModerator : state.me.roles.includes(userRoles.MODERATOR) ||
state.me.roles.includes(userRoles.ADMIN),
passivePeers : passivePeersSelector(state),
selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state)
@ -165,6 +186,7 @@ const ParticipantListContainer = withRoomContext(connect(
areStatesEqual : (next, prev) =>
{
return (
prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerId === next.room.selectedPeerId

View File

@ -1,8 +1,11 @@
import * as userRoles from './userRoles';
const initialState =
{
id : null,
picture : null,
isMobile : false,
roles : [ userRoles.ALL ],
canSendMic : false,
canSendWebcam : false,
canShareScreen : false,
@ -49,6 +52,24 @@ const me = (state = initialState, action) =>
return { ...state, loggedIn: flag };
}
case 'ADD_ROLE':
{
if (state.roles.includes(action.payload.role))
return state;
const roles = [ ...state.roles, action.payload.role ];
return { ...state, roles };
}
case 'REMOVE_ROLE':
{
const roles = state.roles.filter((role) =>
role !== action.payload.role);
return { ...state, roles };
}
case 'SET_PICTURE':
return { ...state, picture: action.payload.picture };

View File

@ -16,6 +16,9 @@ const peer = (state = {}, action) =>
case 'SET_PEER_SCREEN_IN_PROGRESS':
return { ...state, peerScreenInProgress: action.payload.flag };
case 'SET_PEER_KICK_IN_PROGRESS':
return { ...state, peerKickInProgress: action.payload.flag };
case 'SET_PEER_RAISE_HAND_STATE':
return { ...state, raiseHandState: action.payload.raiseHandState };
@ -40,6 +43,21 @@ const peer = (state = {}, action) =>
return { ...state, picture: action.payload.picture };
}
case 'ADD_PEER_ROLE':
{
const roles = [ ...state.roles, action.payload.role ];
return { ...state, roles };
}
case 'REMOVE_PEER_ROLE':
{
const roles = state.roles.filter((role) =>
role !== action.payload.role);
return { ...state, roles };
}
default:
return state;
}
@ -71,6 +89,8 @@ const peers = (state = {}, action) =>
case 'SET_PEER_RAISE_HAND_STATE':
case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE':
case 'REMOVE_PEER_ROLE':
{
const oldPeer = state[action.payload.peerId];
@ -82,6 +102,7 @@ const peers = (state = {}, action) =>
return { ...state, [oldPeer.id]: peer(oldPeer, action) };
}
case 'SET_PEER_KICK_IN_PROGRESS':
case 'REMOVE_CONSUMER':
{
const oldPeer = state[action.payload.peerId];

View File

@ -1,24 +1,27 @@
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
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
};
const room = (state = initialState, action) =>
@ -110,7 +113,7 @@ const room = (state = initialState, action) =>
case 'TOGGLE_JOINED':
{
const joined = !state.joined;
const joined = true;
return { ...state, joined };
}
@ -163,6 +166,15 @@ const room = (state = initialState, action) =>
return { ...state, spotlights };
}
case 'MUTE_ALL_IN_PROGRESS':
return { ...state, muteAllInProgress: action.payload.flag };
case 'STOP_ALL_VIDEO_IN_PROGRESS':
return { ...state, stopAllVideoInProgress: action.payload.flag };
case 'CLOSE_MEETING_IN_PROGRESS':
return { ...state, closeMeetingInProgress: action.payload.flag };
default:
return state;
}

View File

@ -0,0 +1,4 @@
export const ADMIN = 'admin';
export const MODERATOR = 'moderator';
export const AUTHENTICATED = 'authenticated';
export const ALL = 'normal';

View File

@ -49,6 +49,9 @@
"room.spotlights": "Spotlight中的参与者",
"room.passive": "被动参与者",
"room.videoPaused": "该视频已暂停",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "登录",
"tooltip.logout": "注销",
@ -60,6 +63,7 @@
"tooltip.lobby": "显示大厅",
"tooltip.settings": "显示设置",
"tooltip.participants": "显示参加者",
"tooltip.kickParticipant": null,
"label.roomName": "房间名称",
"label.chooseRoomButton": "继续",

View File

@ -4,7 +4,7 @@
"socket.reconnected": "Verbindung wieder herges|tellt",
"socket.requestError": "Fehler bei Serveranfrage",
"room.chooseRoom": "Choose the name of the room you would like to join",
"room.chooseRoom": null,
"room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen",
"room.consentUnderstand": "I understand",
"room.joined": "Konferenzraum betreten",
@ -49,6 +49,9 @@
"room.spotlights": "Aktive Teinehmer",
"room.passive": "Passive Teilnehmer",
"room.videoPaused": "Video gestoppt",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden",
@ -60,9 +63,10 @@
"tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": null,
"label.roomName": "Room name",
"label.chooseRoomButton": "Continue",
"label.roomName": null,
"label.chooseRoomButton": null,
"label.yourName": "Dein Name",
"label.newWindow": "In separatem Fenster öffnen",
"label.fullscreen": "Vollbild",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Deltagere i fokus",
"room.passive": "Passive deltagere",
"room.videoPaused": "Denne video er sat på pause",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Log ind",
"tooltip.logout": "Log ud",
@ -60,6 +63,7 @@
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere",
"tooltip.kickParticipant": null,
"label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt",
@ -106,7 +110,7 @@
"filesharing.finished": "Filen er færdig med at downloade",
"filesharing.save": "Gem",
"filesharing.sharedFile": "{displayName} delte en fil",
"filesharing.download": "Download",
"filesharing.download": null,
"filesharing.missingSeeds": "Hvis denne proces tager lang tid, er der muligvis ikke nogen, der seedede denne torrent. Prøv at bede nogen om at uploade den fil, du ønsker at hente.",
"device.devicesChanged": "Detekteret ndringer i dine enheder, konfigurer dine enheder i indstillingsdialogen",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Συμμετέχοντες στο Spotlight",
"room.passive": "Παθητικοί συμμετέχοντες",
"room.videoPaused": "Το βίντεο έχει σταματήσει",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση",
@ -60,6 +63,7 @@
"tooltip.lobby": "Εμφάνιση λόμπι",
"tooltip.settings": "Εμφάνιση ρυθμίσεων",
"tooltip.participants": "Εμφάνιση συμμετεχόντων",
"tooltip.kickParticipant": null,
"label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια",
@ -75,8 +79,8 @@
"label.shareFile": "Διαμοιραστείτε ένα αρχείο",
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
"label.unknown": "Άγνωστο",
"label.democratic": "Democratic view",
"label.filmstrip": "Filmstrip view",
"label.democratic": null,
"label.filmstrip": null,
"label.low": "Χαμηλή",
"label.medium": "Μέτρια",
"label.high": "Υψηλή (HD)",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participants in Spotlight",
"room.passive": "Passive Participants",
"room.videoPaused": "This video is paused",
"room.muteAll": "Mute all",
"room.stopAllVideo": "Stop all video",
"room.closeMeeting": "Close meeting",
"tooltip.login": "Log in",
"tooltip.logout": "Log out",
@ -60,6 +63,7 @@
"tooltip.lobby": "Show lobby",
"tooltip.settings": "Show settings",
"tooltip.participants": "Show participants",
"tooltip.kickParticipant": "Kick out participant",
"label.roomName": "Room name",
"label.chooseRoomButton": "Continue",

View File

@ -1,140 +1,144 @@
{
"socket.disconnected": "Desconectado",
"socket.reconnecting": "Desconectado, intentando reconectar",
"socket.reconnected": "Reconectado",
"socket.requestError": "Error en la petición al servidor",
"socket.disconnected": "Desconectado",
"socket.reconnecting": "Desconectado, intentando reconectar",
"socket.reconnected": "Reconectado",
"socket.requestError": "Error en la petición al servidor",
"room.chooseRoom": "Indique el nombre de la sala a la que le gustaría unirse",
"room.cookieConsent": "Esta web utiliza cookies para mejorar la experiencia de usuario",
"room.consentUnderstand": "I understand",
"room.joined": "Se ha unido a la sala",
"room.cantJoin": "No ha sido posible unirse a la sala",
"room.youLocked": "Ha cerrado la sala",
"room.cantLock": "No ha sido posible cerrar la sala",
"room.youUnLocked": "Ha abierto la sala",
"room.cantUnLock": "No ha sido posible abrir la sala",
"room.locked": "La sala ahora es privada",
"room.unlocked": "La sala ahora es pública",
"room.newLobbyPeer": "Nuevo participante en la sala de espera",
"room.lobbyPeerLeft": "Un participante en espera ha salido",
"room.lobbyPeerChangedDisplayName": "Participante en espera cambió su nombre a {displayName}",
"room.lobbyPeerChangedPicture": "Participante en espera cambió su foto",
"room.setAccessCode": "Código de acceso de la sala actualizado",
"room.accessCodeOn": "Código de acceso de la sala activado",
"room.accessCodeOff": "Código de acceso de la sala desactivado",
"room.peerChangedDisplayName": "{oldDisplayName} es ahora {displayName}",
"room.newPeer": "{displayName} se unió a la sala",
"room.newFile": "Nuevo fichero disponible",
"room.toggleAdvancedMode": "Cambiado a modo avanzado",
"room.setDemocraticView": "Cambiado a modo democrático",
"room.setFilmStripView": "Cambiado a modo viñeta",
"room.loggedIn": "Ha iniciado sesión",
"room.loggedOut": "Ha cerrado su sesión",
"room.changedDisplayName": "Ha cambiado su nombre a {displayName}",
"room.changeDisplayNameError": "Hubo un error al intentar cambiar su nombre",
"room.chatError": "No ha sido posible enviar su mensaje",
"room.aboutToJoin": "Está a punto de unirse a una reunión",
"room.roomId": "ID de la sala: {roomName}",
"room.setYourName": "Indique el nombre con el que quiere participar y cómo quiere unirse:",
"room.audioOnly": "Solo sonido",
"room.audioVideo": "Sonido y vídeo",
"room.youAreReady": "Ok, está preparado",
"room.emptyRequireLogin": "¡La sala está vacía! Puede iniciar sesión para comenzar la reunión o esperar hasta que el anfitrión se una",
"room.locketWait": "La sala es privada - espere hasta que alguien le invite ...",
"room.lobbyAdministration": "Administración de la sala de espera",
"room.peersInLobby": "Participantes en la sala de espera",
"room.lobbyEmpty": "La sala de espera está vacía",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}",
"room.me": "Yo",
"room.spotlights": "Participantes destacados",
"room.passive": "Participantes pasivos",
"room.videoPaused": "El vídeo está pausado",
"room.chooseRoom": "Indique el nombre de la sala a la que le gustaría unirse",
"room.cookieConsent": "Esta web utiliza cookies para mejorar la experiencia de usuario",
"room.consentUnderstand": "I understand",
"room.joined": "Se ha unido a la sala",
"room.cantJoin": "No ha sido posible unirse a la sala",
"room.youLocked": "Ha cerrado la sala",
"room.cantLock": "No ha sido posible cerrar la sala",
"room.youUnLocked": "Ha abierto la sala",
"room.cantUnLock": "No ha sido posible abrir la sala",
"room.locked": "La sala ahora es privada",
"room.unlocked": "La sala ahora es pública",
"room.newLobbyPeer": "Nuevo participante en la sala de espera",
"room.lobbyPeerLeft": "Un participante en espera ha salido",
"room.lobbyPeerChangedDisplayName": "Participante en espera cambió su nombre a {displayName}",
"room.lobbyPeerChangedPicture": "Participante en espera cambió su foto",
"room.setAccessCode": "Código de acceso de la sala actualizado",
"room.accessCodeOn": "Código de acceso de la sala activado",
"room.accessCodeOff": "Código de acceso de la sala desactivado",
"room.peerChangedDisplayName": "{oldDisplayName} es ahora {displayName}",
"room.newPeer": "{displayName} se unió a la sala",
"room.newFile": "Nuevo fichero disponible",
"room.toggleAdvancedMode": "Cambiado a modo avanzado",
"room.setDemocraticView": "Cambiado a modo democrático",
"room.setFilmStripView": "Cambiado a modo viñeta",
"room.loggedIn": "Ha iniciado sesión",
"room.loggedOut": "Ha cerrado su sesión",
"room.changedDisplayName": "Ha cambiado su nombre a {displayName}",
"room.changeDisplayNameError": "Hubo un error al intentar cambiar su nombre",
"room.chatError": "No ha sido posible enviar su mensaje",
"room.aboutToJoin": "Está a punto de unirse a una reunión",
"room.roomId": "ID de la sala: {roomName}",
"room.setYourName": "Indique el nombre con el que quiere participar y cómo quiere unirse:",
"room.audioOnly": "Solo sonido",
"room.audioVideo": "Sonido y vídeo",
"room.youAreReady": "Ok, está preparado",
"room.emptyRequireLogin": "¡La sala está vacía! Puede iniciar sesión para comenzar la reunión o esperar hasta que el anfitrión se una",
"room.locketWait": "La sala es privada - espere hasta que alguien le invite ...",
"room.lobbyAdministration": "Administración de la sala de espera",
"room.peersInLobby": "Participantes en la sala de espera",
"room.lobbyEmpty": "La sala de espera está vacía",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}",
"room.me": "Yo",
"room.spotlights": "Participantes destacados",
"room.passive": "Participantes pasivos",
"room.videoPaused": "El vídeo está pausado",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Entrar",
"tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera",
"tooltip.lockRoom": "Configurar sala como privada",
"tooltip.unLockRoom": "Configurar sala como pública",
"tooltip.enterFullscreen": "Presentar en pantalla completa",
"tooltip.leaveFullscreen": "Salir de la pantalla completa",
"tooltip.lobby": "Mostrar sala de espera",
"tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes",
"tooltip.login": "Entrar",
"tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera",
"tooltip.lockRoom": "Configurar sala como privada",
"tooltip.unLockRoom": "Configurar sala como pública",
"tooltip.enterFullscreen": "Presentar en pantalla completa",
"tooltip.leaveFullscreen": "Salir de la pantalla completa",
"tooltip.lobby": "Mostrar sala de espera",
"tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes",
"tooltip.kickParticipant": null,
"label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar",
"label.yourName": "Su nombre",
"label.newWindow": "Nueva ventana",
"label.fullscreen": "Pantalla completa",
"label.openDrawer": "Abrir panel",
"label.leave": "Salir",
"label.chatInput": "Escriba su mensaje...",
"label.chat": "Chat",
"label.filesharing": "Compartir ficheros",
"label.participants": "Participantes",
"label.shareFile": "Compartir fichero",
"label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido",
"label.democratic": "Vista democrática",
"label.filmstrip": "Vista en viñeta",
"label.low": "Baja",
"label.medium": "Media",
"label.high": "Alta (HD)",
"label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Cerrar",
"label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar",
"label.yourName": "Su nombre",
"label.newWindow": "Nueva ventana",
"label.fullscreen": "Pantalla completa",
"label.openDrawer": "Abrir panel",
"label.leave": "Salir",
"label.chatInput": "Escriba su mensaje...",
"label.chat": "Chat",
"label.filesharing": "Compartir ficheros",
"label.participants": "Participantes",
"label.shareFile": "Compartir fichero",
"label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido",
"label.democratic": "Vista democrática",
"label.filmstrip": "Vista en viñeta",
"label.low": "Baja",
"label.medium": "Media",
"label.high": "Alta (HD)",
"label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Cerrar",
"settings.settings": "Ajustes",
"settings.camera": "Cámara",
"settings.selectCamera": "Seleccionar dispositivo de vídeo",
"settings.cantSelectCamera": "No ha sido posible seleccionar el dispositivo de vídeo",
"settings.audio": "Dispositivo de sonido",
"settings.selectAudio": "Seleccione dispositivo de sonido",
"settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido",
"settings.resolution": "Seleccione su resolución de imagen",
"settings.layout": "Disposición de la sala",
"settings.selectRoomLayout": "Seleccione la disposición de la sala",
"settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles",
"settings.settings": "Ajustes",
"settings.camera": "Cámara",
"settings.selectCamera": "Seleccionar dispositivo de vídeo",
"settings.cantSelectCamera": "No ha sido posible seleccionar el dispositivo de vídeo",
"settings.audio": "Dispositivo de sonido",
"settings.selectAudio": "Seleccione dispositivo de sonido",
"settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido",
"settings.resolution": "Seleccione su resolución de imagen",
"settings.layout": "Disposición de la sala",
"settings.selectRoomLayout": "Seleccione la disposición de la sala",
"settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles",
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero",
"filesharing.successfulFileShare": "El fichero se compartió con éxito",
"filesharing.unableToShare": "No ha sido posible compartir el fichero",
"filesharing.error": "Hubo un error al compartir el fichero",
"filesharing.finished": "Descarga del fichero finalizada",
"filesharing.save": "Guardar",
"filesharing.sharedFile": "{displayName} compartió un fichero",
"filesharing.download": "Descargar",
"filesharing.missingSeeds": "Si este proceso demora en exceso, puede ocurrir que no haya nadie compartiendo el fichero. Pruebe a pedirle a alguien que vuelva a subir el fichero que busca.",
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero",
"filesharing.successfulFileShare": "El fichero se compartió con éxito",
"filesharing.unableToShare": "No ha sido posible compartir el fichero",
"filesharing.error": "Hubo un error al compartir el fichero",
"filesharing.finished": "Descarga del fichero finalizada",
"filesharing.save": "Guardar",
"filesharing.sharedFile": "{displayName} compartió un fichero",
"filesharing.download": "Descargar",
"filesharing.missingSeeds": "Si este proceso demora en exceso, puede ocurrir que no haya nadie compartiendo el fichero. Pruebe a pedirle a alguien que vuelva a subir el fichero que busca.",
"devices.devicesChanged": "Sus dispositivos han cambiado, vuelva a configurarlos en la ventana de ajustes",
"devices.devicesChanged": "Sus dispositivos han cambiado, vuelva a configurarlos en la ventana de ajustes",
"device.audioUnsupported": "Sonido no disponible",
"device.activateAudio": "Activar sonido",
"device.muteAudio": "Silenciar sonido",
"device.unMuteAudio": "Reactivar sonido",
"device.audioUnsupported": "Sonido no disponible",
"device.activateAudio": "Activar sonido",
"device.muteAudio": "Silenciar sonido",
"device.unMuteAudio": "Reactivar sonido",
"device.videoUnsupported": "Vídeo no disponible",
"device.startVideo": "Iniciar vídeo",
"device.stopVideo": "Detener vídeo",
"device.videoUnsupported": "Vídeo no disponible",
"device.startVideo": "Iniciar vídeo",
"device.stopVideo": "Detener vídeo",
"device.screenSharingUnsupported": "Compartir pantalla no disponible",
"device.startScreenSharing": "Iniciar compartir pantalla",
"device.stopScreenSharing": "Detener compartir pantalla",
"device.screenSharingUnsupported": "Compartir pantalla no disponible",
"device.startScreenSharing": "Iniciar compartir pantalla",
"device.stopScreenSharing": "Detener compartir pantalla",
"devices.microphoneDisconnected": "Micrófono desconectado",
"devices.microphoneError": "Hubo un error al acceder a su micrófono",
"devices.microPhoneMute": "Desactivar micrófono",
"devices.micophoneUnMute": "Activar micrófono",
"devices.microphoneEnable": "Micrófono activado",
"devices.microphoneMuteError": "No ha sido posible desactivar su micrófono",
"devices.microphoneUnMuteError": "No ha sido posible activar su micrófono",
"devices.microphoneDisconnected": "Micrófono desconectado",
"devices.microphoneError": "Hubo un error al acceder a su micrófono",
"devices.microPhoneMute": "Desactivar micrófono",
"devices.micophoneUnMute": "Activar micrófono",
"devices.microphoneEnable": "Micrófono activado",
"devices.microphoneMuteError": "No ha sido posible desactivar su micrófono",
"devices.microphoneUnMuteError": "No ha sido posible activar su micrófono",
"devices.screenSharingDisconnected": "Pantalla compartida desconectada",
"devices.screenSharingError": "Hubo un error al acceder a su pantalla",
"devices.screenSharingDisconnected": "Pantalla compartida desconectada",
"devices.screenSharingError": "Hubo un error al acceder a su pantalla",
"devices.cameraDisconnected": "Cámara desconectada",
"devices.cameraError": "Hubo un error al acceder a su cámara"
"devices.cameraDisconnected": "Cámara desconectada",
"devices.cameraError": "Hubo un error al acceder a su cámara"
}

View File

@ -1,139 +1,143 @@
{
"socket.disconnected" : " Vous avez été déconnecté",
"socket.reconnecting" : " Vous avez été déconnecté, reconnexion en cours",
"socket.reconnected" : " Vous êtes reconnecté",
"socket.requestError" : " Erreur sur une requête serveur",
"socket.disconnected": "Vous avez été déconnecté",
"socket.reconnecting": "Vous avez été déconnecté, reconnexion en cours",
"socket.reconnected": "Vous êtes reconnecté",
"socket.requestError": "Erreur sur une requête serveur",
"room.chooseRoom" : " Choisissez le nom de la réunion que vous souhaitez rejoindre",
"room.cookieConsent" : " Ce site utilise les cookies pour améliorer votre expérience utilisateur",
"room.chooseRoom": "Choisissez le nom de la réunion que vous souhaitez rejoindre",
"room.cookieConsent": "Ce site utilise les cookies pour améliorer votre expérience utilisateur",
"room.consentUnderstand": "I understand",
"room.joined" : " Vous avez rejoint la salle",
"room.cantJoin" : " Impossible de rejoindre la salle",
"room.youLocked" : " Vous avez privatisé la salle",
"room.cantLock" : " Impossible de privatiser la salle",
"room.youUnLocked" : " Vous avez dé-privatiser la salle",
"room.cantUnLock" : " Impossible de dé-privatiser la réunion",
"room.locked" : " La réunion est privée",
"room.unlocked" : " La réunion est publique",
"room.newLobbyPeer" : " Un nouveau participant est dans la salle dattente",
"room.lobbyPeerLeft" : " Un participant de la salle dattente vient de partir",
"room.lobbyPeerChangedDisplayName" : " Un participant dans la salle dattente a changé de nom pour {displayName}",
"room.lobbyPeerChangedPicture" : " Un participant dans le hall à changer de photo",
"room.setAccessCode" : " Code daccès à la réunion mis à jour",
"room.accessCodeOn" : " Code daccès à la réunion activée",
"room.accessCodeOff" : " Code daccès à la réunion désactivée",
"room.peerChangedDisplayName" : " {oldDisplayName} est maintenant {displayName}",
"room.newPeer" : " {displayName} a rejoint la salle",
"room.newFile" : " Nouveau fichier disponible",
"room.toggleAdvancedMode" : " Basculer en mode avancé",
"room.setDemocraticView" : " Passer en vue démocratique",
"room.setFilmStripView" : " Passer en vue vignette",
"room.loggedIn" : " Vous êtes connecté",
"room.loggedOut" : " Vous êtes déconnecté",
"room.changedDisplayName" : " Votre nom à changer pour {displayname}",
"room.changeDisplayNameError" : " Une erreur sest produite pour votre changement de nom",
"room.chatError" : " Impossible denvoyer un message",
"room.aboutToJoin" : " Vous allez rejoindre une réunion",
"room.roomId" : " Salle ID: {roomName}",
"room.setYourName" : " Choisissez votre nom de participant puis comment vous connecter :",
"room.audioOnly" : " Audio uniquement",
"room.audioVideo" : " Audio et Vidéo",
"room.youAreReady" : " Ok, vous êtes prêt",
"room.emptyRequireLogin" : " La réunion est vide ! Vous pouvez vous connecter pour commencer la réunion ou attendre qu'un hôte se connecte",
"room.locketWait" : " La réunion est privatisée - attendez que quelquun vous laisse entrer",
"room.lobbyAdministration" : " Administration de la salle dattente",
"room.peersInLobby" : " Participants dans la salle dattente",
"room.lobbyEmpty" : " Il n'y a actuellement aucun participant dans la salle d'attente",
"room.hiddenPeers" : " {hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me" : " Moi",
"room.spotlights" : " Participants actifs",
"room.passive" : " Participants passifs",
"room.videoPaused" : " La vidéo est en pause",
"room.joined": "Vous avez rejoint la salle",
"room.cantJoin": "Impossible de rejoindre la salle",
"room.youLocked": "Vous avez privatisé la salle",
"room.cantLock": "Impossible de privatiser la salle",
"room.youUnLocked": "Vous avez dé-privatiser la salle",
"room.cantUnLock": "Impossible de dé-privatiser la réunion",
"room.locked": "La réunion est privée",
"room.unlocked": "La réunion est publique",
"room.newLobbyPeer": "Un nouveau participant est dans la salle dattente",
"room.lobbyPeerLeft": "Un participant de la salle dattente vient de partir",
"room.lobbyPeerChangedDisplayName": "Un participant dans la salle dattente a changé de nom pour {displayName}",
"room.lobbyPeerChangedPicture": "Un participant dans le hall à changer de photo",
"room.setAccessCode": "Code daccès à la réunion mis à jour",
"room.accessCodeOn": "Code daccès à la réunion activée",
"room.accessCodeOff": "Code daccès à la réunion désactivée",
"room.peerChangedDisplayName": "{oldDisplayName} est maintenant {displayName}",
"room.newPeer": "{displayName} a rejoint la salle",
"room.newFile": "Nouveau fichier disponible",
"room.toggleAdvancedMode": "Basculer en mode avancé",
"room.setDemocraticView": "Passer en vue démocratique",
"room.setFilmStripView": "Passer en vue vignette",
"room.loggedIn": "Vous êtes connecté",
"room.loggedOut": "Vous êtes déconnecté",
"room.changedDisplayName": "Votre nom à changer pour {displayname}",
"room.changeDisplayNameError": "Une erreur sest produite pour votre changement de nom",
"room.chatError": "Impossible denvoyer un message",
"room.aboutToJoin": "Vous allez rejoindre une réunion",
"room.roomId": "Salle ID: {roomName}",
"room.setYourName": "Choisissez votre nom de participant puis comment vous connecter:",
"room.audioOnly": "Audio uniquement",
"room.audioVideo": "Audio et Vidéo",
"room.youAreReady": "Ok, vous êtes prêt",
"room.emptyRequireLogin": "La réunion est vide ! Vous pouvez vous connecter pour commencer la réunion ou attendre qu'un hôte se connecte",
"room.locketWait": "La réunion est privatisée - attendez que quelquun vous laisse entrer",
"room.lobbyAdministration": "Administration de la salle dattente",
"room.peersInLobby": "Participants dans la salle dattente",
"room.lobbyEmpty": "Il n'y a actuellement aucun participant dans la salle d'attente",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Moi",
"room.spotlights": "Participants actifs",
"room.passive": "Participants passifs",
"room.videoPaused": "La vidéo est en pause",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login" : " Connexion",
"tooltip.logout" : " Déconnexion",
"tooltip.admitFromLobby" : " Autorisé depuis la salle d'attente",
"tooltip.lockRoom" : " Privatisation de la salle",
"tooltip.unLockRoom" : " Dé-privatisation de la salle",
"tooltip.enterFullscreen" : " Afficher en plein écran",
"tooltip.leaveFullscreen" : " Quitter le plein écran",
"tooltip.lobby" : " Afficher la salle d'attente",
"tooltip.settings" : " Afficher les paramètres",
"tooltip.login": "Connexion",
"tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
"tooltip.lockRoom": "Privatisation de la salle",
"tooltip.unLockRoom": "Dé-privatisation de la salle",
"tooltip.enterFullscreen": "Afficher en plein écran",
"tooltip.leaveFullscreen": "Quitter le plein écran",
"tooltip.lobby": "Afficher la salle d'attente",
"tooltip.settings": "Afficher les paramètres",
"tooltip.participants": "Afficher les participants",
"tooltip.kickParticipant": null,
"label.roomName" : " Nom de la salle",
"label.chooseRoomButton" : " Continuer",
"label.yourName" : " Votre nom",
"label.newWindow" : " Nouvelle fenêtre",
"label.fullscreen" : " Plein écran",
"label.openDrawer" : " Ouvrir Drawer",
"label.leave" : " Quiter",
"label.chatInput" : " Entrer un message",
"label.chat" : " Chat",
"label.filesharing" : " Partage de fichier",
"label.participants" : " Participants",
"label.shareFile" : " Partager un fichier",
"label.fileSharingUnsupported" : " Partage de fichier non supporté",
"label.unknown" : " Inconnu",
"label.democratic" : " Vue démocratique",
"label.filmstrip" : " Vue avec miniature",
"label.low" : " Basse définition",
"label.medium" : " Définition normale",
"label.high" : " Haute Définition (HD)",
"label.veryHigh" : " Très Haute Définition (FHD)",
"label.ultra" : " Ultra Haute Définition",
"label.close" : " Fermer",
"label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer",
"label.yourName": "Votre nom",
"label.newWindow": "Nouvelle fenêtre",
"label.fullscreen": "Plein écran",
"label.openDrawer": "Ouvrir Drawer",
"label.leave": "Quiter",
"label.chatInput": "Entrer un message",
"label.chat": "Chat",
"label.filesharing": "Partage de fichier",
"label.participants": "Participants",
"label.shareFile": "Partager un fichier",
"label.fileSharingUnsupported": "Partage de fichier non supporté",
"label.unknown": "Inconnu",
"label.democratic": "Vue démocratique",
"label.filmstrip": "Vue avec miniature",
"label.low": "Basse définition",
"label.medium": "Définition normale",
"label.high": "Haute Définition (HD)",
"label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra": "Ultra Haute Définition",
"label.close": "Fermer",
"settings.settings" : " Paramètres",
"settings.camera" : " Caméra",
"settings.selectCamera" : " Sélectionner votre caméra",
"settings.cantSelectCamera" : " Impossible de sélectionner votre caméra",
"settings.audio" : " Microphone",
"settings.selectAudio" : " Sélectionner votre microphone",
"settings.cantSelectAudio" : " Impossible de sélectionner votre la caméra",
"settings.resolution" : " Sélection votre résolution",
"settings.layout" : " Mode d'affichage de la salle",
"settings.selectRoomLayout" : " Sélectionner l'affiche de la salle",
"settings.advancedMode" : " Mode avancé",
"settings.settings": "Paramètres",
"settings.camera": "Caméra",
"settings.selectCamera": "Sélectionner votre caméra",
"settings.cantSelectCamera": "Impossible de sélectionner votre caméra",
"settings.audio": "Microphone",
"settings.selectAudio": "Sélectionner votre microphone",
"settings.cantSelectAudio": "Impossible de sélectionner votre la caméra",
"settings.resolution": "Sélection votre résolution",
"settings.layout": "Mode d'affichage de la salle",
"settings.selectRoomLayout": "Sélectionner l'affiche de la salle",
"settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles",
"filesharing.saveFileError" : " Impossible d'enregistrer le fichier",
"filesharing.startingFileShare" : " Début du transfert de fichier",
"filesharing.successfulFileShare" : " Fichier transféré",
"filesharing.unableToShare" : " Impossible de transférer le fichier",
"filesharing.error" : " Erreur lors du transfert de fichier",
"filesharing.finished" : " Fin du transfert de fichier",
"filesharing.save" : " Sauver",
"filesharing.sharedFile" : " {displayName} a partagé un fichier",
"filesharing.download" : " Télécharger",
"filesharing.missingSeeds" : " Si le téléchargement prend trop de temps cest quil ny a peut-être plus personne qui partage ce torrent. Demander à quelquun de repartager le document.",
"devices.devicesChanged" : " Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre",
"filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier",
"filesharing.successfulFileShare": "Fichier transféré",
"filesharing.unableToShare": "Impossible de transférer le fichier",
"filesharing.error": "Erreur lors du transfert de fichier",
"filesharing.finished": "Fin du transfert de fichier",
"filesharing.save": "Sauver",
"filesharing.sharedFile": "{displayName} a partagé un fichier",
"filesharing.download": "Télécharger",
"filesharing.missingSeeds": "Si le téléchargement prend trop de temps cest quil ny a peut-être plus personne qui partage ce torrent. Demander à quelquun de repartager le document.",
"devices.devicesChanged": "Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre",
"device.audioUnsupported" : " Microphone non supporté",
"device.activateAudio" : " Activer l'audio",
"device.muteAudio" : " Désactiver l'audio",
"device.unMuteAudio" : " Réactiver l'audio",
"device.audioUnsupported": "Microphone non supporté",
"device.activateAudio": "Activer l'audio",
"device.muteAudio": "Désactiver l'audio",
"device.unMuteAudio": "Réactiver l'audio",
"device.videoUnsupported" : " Vidéo non supporté",
"device.startVideo" : " Démarrer la vidéo",
"device.stopVideo" : " Arrêter la vidéo",
"device.videoUnsupported": "Vidéo non supporté",
"device.startVideo": "Démarrer la vidéo",
"device.stopVideo": "Arrêter la vidéo",
"device.screenSharingUnsupported" : " Partage d'écran non supporté",
"device.startScreenSharing" : " Démarrer le partage d 'écran'",
"device.stopScreenSharing" : " Arrêter le partage d'écran",
"device.screenSharingUnsupported": "Partage d'écran non supporté",
"device.startScreenSharing": "Démarrer le partage d 'écran'",
"device.stopScreenSharing": "Arrêter le partage d'écran",
"devices.microphoneDisconnected" : " Microphone déconnecté",
"devices.microphoneError" : " Une erreur est apparue lors de l'accès à votre microphone",
"devices.microPhoneMute" : " Désactiver le microphone",
"devices.micophoneUnMute" : " Réactiver le microphone",
"devices.microphoneEnable" : " Activer le microphone",
"devices.microphoneMuteError" : " Impossible de désactiver le microphone",
"devices.microphoneUnMuteError" : " Impossible de réactiver le microphone",
"devices.microphoneDisconnected": "Microphone déconnecté",
"devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone",
"devices.microPhoneMute": "Désactiver le microphone",
"devices.micophoneUnMute": "Réactiver le microphone",
"devices.microphoneEnable": "Activer le microphone",
"devices.microphoneMuteError": "Impossible de désactiver le microphone",
"devices.microphoneUnMuteError": "Impossible de réactiver le microphone",
"devices.screenSharingDisconnected" : " Partage d'écran déconnecté",
"devices.screenSharingError" : " Une erreur est apparue lors de l'accès à votre partage d'écran",
"devices.screenSharingDisconnected": "Partage d'écran déconnecté",
"devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran",
"devices.cameraDisconnected" : " Caméra déconnectée",
"devices.cameraError" : " Une erreur est apparue lors de l'accès à votre caméra"
"devices.cameraDisconnected": "Caméra déconnectée",
"devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra"
}

View File

@ -49,6 +49,9 @@
"room.spotlights": "Učesnici u fokusu",
"room.passive": "Pasivni učesnici",
"room.videoPaused": "Video pauziran",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Prijava",
"tooltip.logout": "Odjava",
@ -60,6 +63,7 @@
"tooltip.lobby": "Prikaži predvorje",
"tooltip.settings": "Prikaži postavke",
"tooltip.participants": "Pokažite sudionike",
"tooltip.kickParticipant": null,
"label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi",

View File

@ -4,7 +4,7 @@
"socket.reconnected": "Sikeres újarkapcsolódás",
"socket.requestError": "Sikertelen szerver lekérés",
"room.chooseRoom": "Choose the name of the room you would like to join",
"room.chooseRoom": null,
"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",
@ -49,6 +49,9 @@
"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,
"tooltip.login": "Belépés",
"tooltip.logout": "Kilépés",
@ -60,6 +63,7 @@
"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,
"label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Deltakere i fokus",
"room.passive": "Passive deltakere",
"room.videoPaused": "Denne videoen er inaktiv",
"room.muteAll": "Demp alle",
"room.stopAllVideo": "Stopp all video",
"room.closeMeeting": "Avslutt møte",
"tooltip.login": "Logg in",
"tooltip.logout": "Logg ut",
@ -60,6 +63,7 @@
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere",
"tooltip.kickParticipant": "Spark ut deltaker",
"label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Aktywni uczestnicy",
"room.passive": "Pasywni uczestnicy",
"room.videoPaused": "To wideo jest wstrzymane.",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj",
@ -60,6 +63,7 @@
"tooltip.lobby": "Pokaż poczekalnię",
"tooltip.settings": "Pokaż ustawienia",
"tooltip.participants": "Pokaż uczestników",
"tooltip.kickParticipant": null,
"label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participantes em foco",
"room.passive": "Participantes passivos",
"room.videoPaused": "Este vídeo está em pausa",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Entrar",
"tooltip.logout": "Sair",
@ -60,6 +63,7 @@
"tooltip.lobby": "Apresentar sala de espera",
"tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes",
"tooltip.kickParticipant": null,
"label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar",

View File

@ -49,6 +49,9 @@
"room.spotlights": "Participanți în Spotlight",
"room.passive": "Participanți pasivi",
"room.videoPaused": "Acest video este pus pe pauză",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare",
@ -58,7 +61,9 @@
"tooltip.enterFullscreen": "Modul ecran complet",
"tooltip.leaveFullscreen": "Ieșire din modul ecran complet",
"tooltip.lobby": "Arată holul",
"tooltip.settings": "Arată participanții",
"tooltip.settings": "Arată setăile",
"tooltip.participants": null,
"tooltip.kickParticipant": null,
"label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare",

View File

@ -1,4 +1,5 @@
const os = require('os');
const userRoles = require('../userRoles');
module.exports =
{
@ -63,18 +64,109 @@ module.exports =
// listeningRedirectPort disabled
// use case: loadbalancer backend
httpOnly : false,
// If this is set to true, only signed-in users will be able
// to join a room directly. Non-signed-in users (guests) will
// always be put in the lobby regardless of room lock status.
// If false, there is no difference between guests and signed-in
// users when joining.
requireSignInToAccess : true,
// This flag has no effect when requireSignInToAccess is false
// When truthy, the room will be open to all users when the first
// authenticated user has already joined the room.
activateOnHostJoin : true,
// 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.
// See examples below.
// Examples:
/*
// All authenicated users will be MODERATOR and AUTHENTICATED
userMapping : async ({ peer, roomId, userinfo }) =>
{
peer.addRole(userRoles.MODERATOR);
peer.addRole(userRoles.AUTHENTICATED);
},
// All authenicated users will be AUTHENTICATED,
// and those with the moderator role set in the userinfo
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (
Array.isArray(userinfo.meet_roles) &&
userinfo.meet_roles.includes('moderator')
)
{
peer.addRole(userRoles.MODERATOR);
}
if (
Array.isArray(userinfo.meet_roles) &&
userinfo.meet_roles.includes('meetingadmin')
)
{
peer.addRole(userRoles.ADMIN);
}
peer.addRole(userRoles.AUTHENTICATED);
},
// All authenicated users will be AUTHENTICATED,
// and those with email ending with @example.com
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.email && userinfo.email.endsWith('@example.com'))
{
peer.addRole(userRoles.MODERATOR);
}
peer.addRole(userRoles.AUTHENTICATED);
}
// All authenicated users will be AUTHENTICATED,
// and those with email ending with @example.com
// will also be MODERATOR
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.email && userinfo.email.endsWith('@example.com'))
{
peer.addRole(userRoles.MODERATOR);
}
peer.addRole(userRoles.AUTHENTICATED);
},
*/
userMapping : async ({ peer, roomId, userinfo }) =>
{
if (userinfo.picture != null)
{
if (!userinfo.picture.match(/^http/g))
{
peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`;
}
else
{
peer.picture = userinfo.picture;
}
}
if (userinfo.nickname != null)
{
peer.displayName = userinfo.nickname;
}
if (userinfo.name != null)
{
peer.displayName = userinfo.name;
}
if (userinfo.email != null)
{
peer.email = userinfo.email;
}
},
// Required roles for Access. All users have the role "ALL" by default.
// Other roles need to be added in the "userMapping" function. This
// is an Array of roles. userRoles.ADMIN have all priveleges and access
// always.
//
// Example:
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
// This will allow all MODERATOR and AUTHENTICATED users access.
requiredRolesForAccess : [ userRoles.ALL ],
// When truthy, the room will be open to all users when as long as there
// are allready users in the room
activateOnHostJoin : true,
// Mediasoup settings
mediasoup :
mediasoup :
{
numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings.

View File

@ -14,7 +14,7 @@ class Lobby extends EventEmitter
// Closed flag.
this._closed = false;
this._peers = new Map();
this._peers = {};
}
close()
@ -23,27 +23,28 @@ class Lobby extends EventEmitter
this._closed = true;
this._peers.forEach((peer) =>
// Close the peers.
for (const peer in this._peers)
{
if (!peer.closed)
peer.close();
});
}
this._peers.clear();
this._peers = null;
}
checkEmpty()
{
logger.info('checkEmpty()');
return this._peers.size === 0;
return Object.keys(this._peers).length === 0;
}
peerList()
{
logger.info('peerList()');
return Array.from(this._peers.values()).map((peer) =>
return Object.values(this._peers).map((peer) =>
({
peerId : peer.id,
displayName : peer.displayName
@ -52,38 +53,42 @@ class Lobby extends EventEmitter
hasPeer(peerId)
{
return this._peers.has(peerId);
return this._peers[peerId] != null;
}
promoteAllPeers()
{
logger.info('promoteAllPeers()');
this._peers.forEach((peer) =>
for (const peer in this._peers)
{
if (peer.socket)
this.promotePeer(peer.id);
});
}
}
promotePeer(peerId)
{
logger.info('promotePeer() [peer:"%s"]', peerId);
const peer = this._peers.get(peerId);
const peer = this._peers[peerId];
if (peer)
{
peer.socket.removeListener('request', peer.socketRequestHandler);
peer.removeListener('authenticationChanged', peer.authenticationHandler);
peer.removeListener('gotRole', peer.gotRoleHandler);
peer.removeListener('displayNameChanged', peer.displayNameChangeHandler);
peer.removeListener('pictureChanged', peer.pictureChangeHandler);
peer.removeListener('close', peer.closeHandler);
peer.socketRequestHandler = null;
peer.authenticationHandler = null;
peer.gotRoleHandler = null;
peer.displayNameChangeHandler = null;
peer.pictureChangeHandler = null;
peer.closeHandler = null;
this.emit('promotePeer', peer);
this._peers.delete(peerId);
delete this._peers[peerId];
}
}
@ -112,16 +117,25 @@ class Lobby extends EventEmitter
});
};
peer.authenticationHandler = () =>
peer.gotRoleHandler = () =>
{
logger.info('parkPeer() | authenticationChange [peer:"%s"]', peer.id);
logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id);
if (peer.authenticated)
{
this.emit('changeDisplayName', peer);
this.emit('changePicture', peer);
this.emit('peerAuthenticated', peer);
}
this.emit('peerRolesChanged', peer);
};
peer.displayNameChangeHandler = () =>
{
logger.info('parkPeer() | displayNameChange [peer:"%s"]', peer.id);
this.emit('changeDisplayName', peer);
};
peer.pictureChangeHandler = () =>
{
logger.info('parkPeer() | pictureChange [peer:"%s"]', peer.id);
this.emit('changePicture', peer);
};
peer.closeHandler = () =>
@ -133,7 +147,7 @@ class Lobby extends EventEmitter
this.emit('peerClosed', peer);
this._peers.delete(peer.id);
delete this._peers[peer.id];
if (this.checkEmpty())
this.emit('lobbyEmpty');
@ -141,9 +155,11 @@ class Lobby extends EventEmitter
this._notification(peer.socket, 'enteredLobby');
this._peers.set(peer.id, peer);
this._peers[peer.id] = peer;
peer.on('authenticationChanged', peer.authenticationHandler);
peer.on('gotRole', peer.gotRoleHandler);
peer.on('displayNameChanged', peer.displayNameChangeHandler);
peer.on('pictureChanged', peer.pictureChangeHandler);
peer.socket.on('request', peer.socketRequestHandler);
@ -169,8 +185,6 @@ class Lobby extends EventEmitter
peer.displayName = displayName;
this.emit('changeDisplayName', peer);
cb();
break;
@ -181,8 +195,6 @@ class Lobby extends EventEmitter
peer.picture = picture;
this.emit('changePicture', peer);
cb();
break;

View File

@ -1,17 +1,20 @@
const EventEmitter = require('events').EventEmitter;
const userRoles = require('../userRoles');
const Logger = require('./Logger');
const logger = new Logger('Peer');
class Peer extends EventEmitter
{
constructor({ id, socket })
constructor({ id, roomId, socket })
{
logger.info('constructor() [id:"%s", socket:"%s"]', id, socket.id);
logger.info('constructor() [id:"%s"]', id);
super();
this._id = id;
this._roomId = roomId;
this._authId = null;
this._socket = socket;
@ -22,7 +25,7 @@ class Peer extends EventEmitter
this._inLobby = false;
this._authenticated = false;
this._roles = [ userRoles.ALL ];
this._displayName = false;
@ -40,8 +43,6 @@ class Peer extends EventEmitter
this._consumers = new Map();
this._checkAuthentication();
this._handlePeer();
}
@ -58,56 +59,25 @@ class Peer extends EventEmitter
transport.close();
});
if (this._socket)
this._socket.disconnect(true);
if (this.socket)
this.socket.disconnect(true);
this.emit('close');
}
_handlePeer()
{
this.socket.use((packet, next) =>
if (this.socket)
{
this._checkAuthentication();
return next();
});
this.socket.on('disconnect', () =>
{
if (this.closed)
return;
logger.debug('"disconnect" event [id:%s]', this.id);
this.close();
});
}
_checkAuthentication()
{
if (
Boolean(this.socket.handshake.session.passport) &&
Boolean(this.socket.handshake.session.passport.user)
)
{
const {
id,
displayName,
picture,
email
} = this.socket.handshake.session.passport.user;
id && (this.authId = id);
displayName && (this.displayName = displayName);
picture && (this.picture = picture);
email && (this.email = email);
this.authenticated = true;
}
else
{
this.authenticated = false;
this.socket.on('disconnect', () =>
{
if (this.closed)
return;
logger.debug('"disconnect" event [id:%s]', this.id);
this.close();
});
}
}
@ -121,6 +91,16 @@ class Peer extends EventEmitter
this._id = id;
}
get roomId()
{
return this._roomId;
}
set roomId(roomId)
{
this._roomId = roomId;
}
get authId()
{
return this._authId;
@ -166,21 +146,9 @@ class Peer extends EventEmitter
this._inLobby = inLobby;
}
get authenticated()
get roles()
{
return this._authenticated;
}
set authenticated(authenticated)
{
if (authenticated !== this._authenticated)
{
const oldAuthenticated = this._authenticated;
this._authenticated = authenticated;
this.emit('authenticationChanged', { oldAuthenticated });
}
return this._roles;
}
get displayName()
@ -262,6 +230,35 @@ class Peer extends EventEmitter
return this._consumers;
}
addRole(newRole)
{
if (!this._roles.includes(newRole))
{
this._roles.push(newRole);
logger.info('addRole() | [newRole:"%s]"', newRole);
this.emit('gotRole', { newRole });
}
}
removeRole(oldRole)
{
if (this._roles.includes(oldRole))
{
this._roles = this._roles.filter((role) => role !== oldRole);
logger.info('removeRole() | [oldRole:"%s]"', oldRole);
this.emit('lostRole', { oldRole });
}
}
hasRole(role)
{
return this._roles.includes(role);
}
addTransport(id, transport)
{
this.transports.set(id, transport);
@ -319,7 +316,8 @@ class Peer extends EventEmitter
{
id : this.id,
displayName : this.displayName,
picture : this.picture
picture : this.picture,
roles : this.roles
};
return peerInfo;

View File

@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter;
const axios = require('axios');
const Logger = require('./Logger');
const Lobby = require('./Lobby');
const userRoles = require('../userRoles');
const config = require('../config/config');
const logger = new Logger('Room');
@ -54,6 +55,12 @@ class Room extends EventEmitter
// Locked flag.
this._locked = false;
// Required roles to access
this._requiredRoles = [ userRoles.ALL ];
if ('requiredRolesForAccess' in config)
this._requiredRoles = config.requiredRolesForAccess;
// if true: accessCode is a possibility to open the room
this._joinByAccesCode = true;
@ -100,11 +107,8 @@ class Room extends EventEmitter
// Close the peers.
for (const peer in this._peers)
{
if (Object.prototype.hasOwnProperty.call(this._peers, peer))
{
if (!peer.closed)
peer.close();
}
if (!peer.closed)
peer.close();
}
this._peers = null;
@ -118,26 +122,27 @@ class Room extends EventEmitter
handlePeer(peer)
{
logger.info('handlePeer() [peer:"%s"]', peer.id);
logger.info('handlePeer() [peer:"%s", roles:"%s"]', peer.id, peer.roles);
// This will allow reconnects to join despite lock
// Allow reconnections, remove old peer
if (this._peers[peer.id])
{
logger.warn(
'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
peer.id);
peer.close();
this._peers[peer.id].close();
}
return;
}
// Always let ADMIN in, even if locked
if (peer.roles.includes(userRoles.ADMIN))
this._peerJoining(peer);
else if (this._locked)
{
this._parkPeer(peer);
}
else
{
peer.authenticated ?
// If the user has a role in config.requiredRolesForAccess, let them in
peer.roles.some((role) => this._requiredRoles.includes(role)) ?
this._peerJoining(peer) :
this._handleGuest(peer);
}
@ -145,21 +150,12 @@ class Room extends EventEmitter
_handleGuest(peer)
{
if (config.requireSignInToAccess)
{
if (config.activateOnHostJoin && !this.checkEmpty())
{
this._peerJoining(peer);
}
else
{
this._parkPeer(peer);
this._notification(peer.socket, 'signInRequired');
}
}
if (config.activateOnHostJoin && !this.checkEmpty())
this._peerJoining(peer);
else
{
this._peerJoining(peer);
this._parkPeer(peer);
this._notification(peer.socket, 'signInRequired');
}
}
@ -179,9 +175,26 @@ class Room extends EventEmitter
}
});
this._lobby.on('peerAuthenticated', (peer) =>
this._lobby.on('peerRolesChanged', (peer) =>
{
!this._locked && this._lobby.promotePeer(peer.id);
// Always let admin in, even if locked
if (peer.roles.includes(userRoles.ADMIN))
{
this._lobby.promotePeer(peer.id);
return;
}
// If the user has a role in config.requiredRolesForAccess, let them in
if (
!this._locked &&
peer.roles.some((role) => this._requiredRoles.includes(role))
)
{
this._lobby.promotePeer(peer.id);
return;
}
});
this._lobby.on('changeDisplayName', (changedPeer) =>
@ -304,7 +317,6 @@ class Room extends EventEmitter
}, 10000);
}
// checks both room and lobby
checkEmpty()
{
return Object.keys(this._peers).length === 0;
@ -324,12 +336,8 @@ class Room extends EventEmitter
{
peer.socket.join(this._roomId);
const index = this._lastN.indexOf(peer.id);
if (index === -1) // We don't have this peer, add to end
{
this._lastN.push(peer.id);
}
// If we don't have this peer, add to end
!this._lastN.includes(peer.id) && this._lastN.push(peer.id);
this._peers[peer.id] = peer;
@ -402,25 +410,17 @@ class Room extends EventEmitter
// If the Peer was joined, notify all Peers.
if (peer.joined)
{
this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true);
}
const index = this._lastN.indexOf(peer.id);
if (index > -1) // We have this peer in the list, remove
{
this._lastN.splice(index, 1);
}
// Remove from lastN
this._lastN = this._lastN.filter((id) => id !== peer.id);
delete this._peers[peer.id];
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
if (this.checkEmpty() && this._lobby.checkEmpty())
{
this.selfDestructCountdown();
}
});
peer.on('displayNameChanged', ({ oldDisplayName }) =>
@ -449,6 +449,32 @@ class Room extends EventEmitter
picture : peer.picture
}, true);
});
peer.on('gotRole', ({ newRole }) =>
{
// Ensure the Peer is joined.
if (!peer.joined)
return;
// Spread to others
this._notification(peer.socket, 'gotRole', {
peerId : peer.id,
role : newRole
}, true, true);
});
peer.on('lostRole', ({ oldRole }) =>
{
// Ensure the Peer is joined.
if (!peer.joined)
return;
// Spread to others
this._notification(peer.socket, 'lostRole', {
peerId : peer.id,
role : oldRole
}, true, true);
});
}
async _handleSocketRequest(peer, request, cb)
@ -464,27 +490,6 @@ class Room extends EventEmitter
case 'join':
{
try
{
if (peer.socket.handshake.session.passport.user.displayName)
{
this._notification(
peer.socket,
'changeDisplayname',
{
peerId : peer.id,
displayName : peer.socket.handshake.session.passport.user.displayName,
oldDisplayName : ''
},
true
);
}
}
catch (error)
{
logger.error(error);
}
// Ensure the Peer is not already joined.
if (peer.joined)
throw new Error('Peer already joined');
@ -512,7 +517,10 @@ class Room extends EventEmitter
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo));
cb(null, { peers: peerInfos });
cb(null, {
roles : peer.roles,
peers : peerInfos
});
// Mark the new Peer as joined.
peer.joined = true;
@ -540,7 +548,8 @@ class Room extends EventEmitter
{
id : peer.id,
displayName : displayName,
picture : picture
picture : picture,
roles : peer.roles
}
);
}
@ -1106,6 +1115,92 @@ class Room extends EventEmitter
break;
}
case 'moderator:muteAll':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
// Spread to others
this._notification(peer.socket, 'moderator:mute', {
peerId : peer.id
}, true);
cb();
break;
}
case 'moderator:stopAllVideo':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
// Spread to others
this._notification(peer.socket, 'moderator:stopVideo', {
peerId : peer.id
}, true);
cb();
break;
}
case 'moderator:closeMeeting':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
this._notification(
peer.socket,
'moderator:kick',
null,
true
);
cb();
// Close the room
this.close();
break;
}
case 'moderator:kickPeer':
{
if (
!peer.hasRole(userRoles.MODERATOR) &&
!peer.hasRole(userRoles.ADMIN)
)
throw new Error('peer does not have moderator priveleges');
const { peerId } = request.data;
const kickPeer = this._peers[peerId];
if (!kickPeer)
throw new Error(`peer with id "${peerId}" not found`);
this._notification(
kickPeer.socket,
'moderator:kick'
);
kickPeer.close();
cb();
break;
}
default:
{
logger.error('unknown request.method "%s"', request.method);
@ -1319,13 +1414,16 @@ class Room extends EventEmitter
});
}
_notification(socket, method, data = {}, broadcast = false)
_notification(socket, method, data = {}, broadcast = false, includeSender = false)
{
if (broadcast)
{
socket.broadcast.to(this._roomId).emit(
'notification', { method, data }
);
if (includeSender)
socket.emit('notification', { method, data });
}
else
{

View File

@ -18,6 +18,7 @@ const Peer = require('./lib/Peer');
const base64 = require('base-64');
const helmet = require('helmet');
const userRoles = require('./userRoles');
const {
loginHelper,
logoutHelper
@ -190,7 +191,7 @@ function setupLTI(ltiConfig)
}
if (lti.lis_person_name_full)
{
user.displayName=lti.lis_person_name_full;
user.displayName = lti.lis_person_name_full;
}
// Perform local authentication if necessary
@ -241,51 +242,6 @@ function setupOIDC(oidcIssuer)
_claims : tokenset.claims
};
if (userinfo.picture != null)
{
if (!userinfo.picture.match(/^http/g))
{
user.picture = `data:image/jpeg;base64, ${userinfo.picture}`;
}
else
{
user.picture = userinfo.picture;
}
}
if (userinfo.nickname != null)
{
user.displayName = userinfo.nickname;
}
if (userinfo.name != null)
{
user.displayName = userinfo.name;
}
if (userinfo.email != null)
{
user.email = userinfo.email;
}
if (userinfo.given_name != null)
{
user.name={};
user.name.givenName = userinfo.given_name;
}
if (userinfo.family_name != null)
{
if (user.name == null) user.name={};
user.name.familyName = userinfo.family_name;
}
if (userinfo.middle_name != null)
{
if (user.name == null) user.name={};
user.name.middleName = userinfo.middle_name;
}
return done(null, user);
}
);
@ -324,7 +280,8 @@ async function setupAuth()
{
passport.authenticate('oidc', {
state : base64.encode(JSON.stringify({
id : req.query.id
peerId : req.query.peerId,
roomId : req.query.roomId
}))
})(req, res, next);
});
@ -341,6 +298,19 @@ async function setupAuth()
// logout
app.get('/auth/logout', (req, res) =>
{
const { peerId } = req.session;
const peer = peers.get(peerId);
if (peer)
{
for (const role of peer.roles)
{
if (role !== userRoles.ALL)
peer.removeRole(role);
}
}
req.logout();
res.send(logoutHelper());
});
@ -349,35 +319,35 @@ async function setupAuth()
app.get(
'/auth/callback',
passport.authenticate('oidc', { failureRedirect: '/auth/login' }),
(req, res) =>
async (req, res) =>
{
const state = JSON.parse(base64.decode(req.query.state));
let displayName;
let picture;
const { peerId, roomId } = state;
if (req.user != null)
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 && peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room');
if (peer && typeof config.userMapping === 'function')
{
if (req.user.displayName != null)
displayName = req.user.displayName;
else
displayName = '';
if (req.user.picture != null)
picture = req.user.picture;
else
picture = '/static/media/buddy.403cb9f6.svg';
await config.userMapping({
peer,
roomId,
userinfo : req.user._userinfo
});
}
const peer = peers.get(state.id);
peer && (peer.displayName = displayName);
peer && (peer.picture = picture);
peer && (peer.authenticated = true);
res.send(loginHelper({
displayName,
picture
displayName : peer.displayName,
picture : peer.picture
}));
}
);
@ -495,12 +465,36 @@ async function runWebSocketServer()
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId });
const peer = new Peer({ id: peerId, socket });
const peer = new Peer({ id: peerId, roomId, socket });
peers.set(peerId, peer);
peer.on('close', () => peers.delete(peerId));
if (
Boolean(socket.handshake.session.passport) &&
Boolean(socket.handshake.session.passport.user)
)
{
const {
id,
displayName,
picture,
email,
_userinfo
} = socket.handshake.session.passport.user;
peer.authId= id;
peer.displayName = displayName;
peer.picture = picture;
peer.email = email;
if (typeof config.userMapping === 'function')
{
await config.userMapping({ peer, roomId, userinfo: _userinfo });
}
}
room.handlePeer(peer);
})
.catch((error) =>

View File

@ -0,0 +1,12 @@
module.exports = {
// Allowed to enter locked rooms + all other priveleges
ADMIN : 'admin',
// Allowed to enter restricted rooms if configured.
// Allowed to moderate users in a room (mute all,
// spotlight video, kick users)
MODERATOR : 'moderator',
// Same as MODERATOR, but can't moderate users
AUTHENTICATED : 'authenticated',
// No priveleges
ALL : 'normal'
};