Merge remote-tracking branch 'upstream/develop' into mm-exporter

auto_join_3.3
christian2 2020-05-04 09:32:10 +02:00
commit 85c9062f86
61 changed files with 2456 additions and 1138 deletions

21
LICENSE.md 100644
View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "multiparty-meeting", "name": "multiparty-meeting",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting service", "description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",

View File

@ -233,6 +233,9 @@ export default class RoomClient
// Local webcam mediasoup Producer. // Local webcam mediasoup Producer.
this._webcamProducer = null; this._webcamProducer = null;
// Extra videos being produced
this._extraVideoProducers = new Map();
// Map of webcam MediaDeviceInfos indexed by deviceId. // Map of webcam MediaDeviceInfos indexed by deviceId.
// @type {Map<String, MediaDeviceInfos>} // @type {Map<String, MediaDeviceInfos>}
this._webcams = {}; this._webcams = {};
@ -516,16 +519,21 @@ export default class RoomClient
_soundNotification() _soundNotification()
{ {
const alertPromise = this._soundAlert.play(); const { notificationSounds } = store.getState().settings;
if (alertPromise !== undefined) if (notificationSounds)
{ {
alertPromise const alertPromise = this._soundAlert.play();
.then()
.catch((error) => if (alertPromise !== undefined)
{ {
logger.error('_soundAlert.play() | failed: %o', error); alertPromise
}); .then()
.catch((error) =>
{
logger.error('_soundAlert.play() | failed: %o', error);
});
}
} }
} }
@ -844,62 +852,6 @@ export default class RoomClient
} }
} }
async getServerHistory()
{
logger.debug('getServerHistory()');
try
{
const {
chatHistory,
fileHistory,
lastNHistory,
locked,
lobbyPeers,
accessCode
} = await this.sendRequest('serverHistory');
(chatHistory.length > 0) && store.dispatch(
chatActions.addChatHistory(chatHistory));
(fileHistory.length > 0) && store.dispatch(
fileActions.addFileHistory(fileHistory));
if (lastNHistory.length > 0)
{
logger.debug('Got lastNHistory');
// Remove our self from list
const index = lastNHistory.indexOf(this._peerId);
lastNHistory.splice(index, 1);
this._spotlights.addSpeakerList(lastNHistory);
}
locked ?
store.dispatch(roomActions.setRoomLocked()) :
store.dispatch(roomActions.setRoomUnLocked());
(lobbyPeers.length > 0) && lobbyPeers.forEach((peer) =>
{
store.dispatch(
lobbyPeerActions.addLobbyPeer(peer.peerId));
store.dispatch(
lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.peerId));
store.dispatch(
lobbyPeerActions.setLobbyPeerPicture(peer.picture));
});
(accessCode != null) && store.dispatch(
roomActions.setAccessCode(accessCode));
}
catch (error)
{
logger.error('getServerHistory() | failed: %o', error);
}
}
async muteMic() async muteMic()
{ {
logger.debug('muteMic()'); logger.debug('muteMic()');
@ -1561,30 +1513,30 @@ export default class RoomClient
} }
} }
async sendRaiseHandState(state) async setRaisedHand(raisedHand)
{ {
logger.debug('sendRaiseHandState: ', state); logger.debug('setRaisedHand: ', raisedHand);
store.dispatch( store.dispatch(
meActions.setMyRaiseHandStateInProgress(true)); meActions.setRaisedHandInProgress(true));
try try
{ {
await this.sendRequest('raiseHand', { raiseHandState: state }); await this.sendRequest('raisedHand', { raisedHand });
store.dispatch( store.dispatch(
meActions.setMyRaiseHandState(state)); meActions.setRaisedHand(raisedHand));
} }
catch (error) catch (error)
{ {
logger.error('sendRaiseHandState() | failed: %o', error); logger.error('setRaisedHand() | [error:"%o"]', error);
// We need to refresh the component for it to render changed state // We need to refresh the component for it to render changed state
store.dispatch(meActions.setMyRaiseHandState(!state)); store.dispatch(meActions.setRaisedHand(!raisedHand));
} }
store.dispatch( store.dispatch(
meActions.setMyRaiseHandStateInProgress(false)); meActions.setRaisedHandInProgress(false));
} }
async setMaxSendingSpatialLayer(spatialLayer) async setMaxSendingSpatialLayer(spatialLayer)
@ -2052,6 +2004,8 @@ export default class RoomClient
lobbyPeerActions.addLobbyPeer(peerId)); lobbyPeerActions.addLobbyPeer(peerId));
store.dispatch( store.dispatch(
roomActions.setToolbarsVisible(true)); roomActions.setToolbarsVisible(true));
this._soundNotification();
store.dispatch(requestActions.notify( store.dispatch(requestActions.notify(
{ {
@ -2063,6 +2017,43 @@ export default class RoomClient
break; break;
} }
case 'parkedPeers':
{
const { lobbyPeers } = notification.data;
if (lobbyPeers.length > 0)
{
lobbyPeers.forEach((peer) =>
{
store.dispatch(
lobbyPeerActions.addLobbyPeer(peer.peerId));
store.dispatch(
lobbyPeerActions.setLobbyPeerDisplayName(
peer.displayName,
peer.peerId
)
);
store.dispatch(
lobbyPeerActions.setLobbyPeerPicture(peer.picture));
});
store.dispatch(
roomActions.setToolbarsVisible(true));
this._soundNotification();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'room.newLobbyPeer',
defaultMessage : 'New participant entered the lobby'
})
}));
}
break;
}
case 'lobby:peerClosed': case 'lobby:peerClosed':
{ {
@ -2222,6 +2213,48 @@ export default class RoomClient
break; break;
} }
case 'raisedHand':
{
const { peerId, raisedHand } = notification.data;
store.dispatch(peerActions.setPeerRaisedHand(peerId, raisedHand));
const { displayName } = store.getState().peers[peerId];
let text;
if (raisedHand)
{
text = intl.formatMessage({
id : 'room.raisedHand',
defaultMessage : '{displayName} raised their hand'
}, {
displayName
});
}
else
{
text = intl.formatMessage({
id : 'room.loweredHand',
defaultMessage : '{displayName} put their hand down'
}, {
displayName
});
}
if (displayName)
{
store.dispatch(requestActions.notify(
{
text
}));
}
this._soundNotification();
break;
}
case 'chatMessage': case 'chatMessage':
{ {
const { peerId, chatMessage } = notification.data; const { peerId, chatMessage } = notification.data;
@ -2318,6 +2351,8 @@ export default class RoomClient
store.dispatch( store.dispatch(
peerActions.addPeer({ id, displayName, picture, roles, consumers: [] })); peerActions.addPeer({ id, displayName, picture, roles, consumers: [] }));
this._soundNotification();
store.dispatch(requestActions.notify( store.dispatch(requestActions.notify(
{ {
text : intl.formatMessage({ text : intl.formatMessage({
@ -2474,7 +2509,9 @@ export default class RoomClient
{ {
text : intl.formatMessage({ text : intl.formatMessage({
id : 'roles.gotRole', id : 'roles.gotRole',
defaultMessage : `You got the role: ${role}` defaultMessage : 'You got the role: {role}'
}, {
role
}) })
})); }));
} }
@ -2496,7 +2533,9 @@ export default class RoomClient
{ {
text : intl.formatMessage({ text : intl.formatMessage({
id : 'roles.lostRole', id : 'roles.lostRole',
defaultMessage : `You lost the role: ${role}` defaultMessage : 'You lost the role: {role}'
}, {
role
}) })
})); }));
} }
@ -2694,7 +2733,13 @@ export default class RoomClient
peers, peers,
tracker, tracker,
permissionsFromRoles, permissionsFromRoles,
userRoles userRoles,
chatHistory,
fileHistory,
lastNHistory,
locked,
lobbyPeers,
accessCode
} = await this.sendRequest( } = await this.sendRequest(
'join', 'join',
{ {
@ -2749,6 +2794,38 @@ export default class RoomClient
this.updateSpotlights(spotlights); this.updateSpotlights(spotlights);
}); });
(chatHistory.length > 0) && store.dispatch(
chatActions.addChatHistory(chatHistory));
(fileHistory.length > 0) && store.dispatch(
fileActions.addFileHistory(fileHistory));
if (lastNHistory.length > 0)
{
logger.debug('_joinRoom() | got lastN history');
this._spotlights.addSpeakerList(
lastNHistory.filter((peerId) => peerId !== this._peerId)
);
}
locked ?
store.dispatch(roomActions.setRoomLocked()) :
store.dispatch(roomActions.setRoomUnLocked());
(lobbyPeers.length > 0) && lobbyPeers.forEach((peer) =>
{
store.dispatch(
lobbyPeerActions.addLobbyPeer(peer.peerId));
store.dispatch(
lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.peerId));
store.dispatch(
lobbyPeerActions.setLobbyPeerPicture(peer.picture));
});
(accessCode != null) && store.dispatch(
roomActions.setAccessCode(accessCode));
// Don't produce if explicitly requested to not to do it. // Don't produce if explicitly requested to not to do it.
if (this._produce) if (this._produce)
{ {
@ -2782,8 +2859,6 @@ export default class RoomClient
// Clean all the existing notifications. // Clean all the existing notifications.
store.dispatch(notificationActions.removeAllNotifications()); store.dispatch(notificationActions.removeAllNotifications());
this.getServerHistory();
store.dispatch(requestActions.notify( store.dispatch(requestActions.notify(
{ {
text : intl.formatMessage({ text : intl.formatMessage({
@ -2933,6 +3008,159 @@ export default class RoomClient
} }
} }
async addExtraVideo(videoDeviceId)
{
logger.debug(
'addExtraVideo() [videoDeviceId:"%s"]',
videoDeviceId
);
store.dispatch(
roomActions.setExtraVideoOpen(false));
if (!this._mediasoupDevice.canProduce('video'))
{
logger.error('enableWebcam() | cannot produce video');
return;
}
let track;
store.dispatch(
meActions.setWebcamInProgress(true));
try
{
const device = this._webcams[videoDeviceId];
const resolution = store.getState().settings.resolution;
if (!device)
throw new Error('no webcam devices');
logger.debug(
'addExtraVideo() | new selected webcam [device:%o]',
device);
logger.debug('_setWebcamProducer() | calling getUserMedia()');
const stream = await navigator.mediaDevices.getUserMedia(
{
video :
{
deviceId : { ideal: videoDeviceId },
...VIDEO_CONSTRAINS[resolution]
}
});
track = stream.getVideoTracks()[0];
let producer;
if (this._useSimulcast)
{
// If VP9 is the only available video codec then use SVC.
const firstVideoCodec = this._mediasoupDevice
.rtpCapabilities
.codecs
.find((c) => c.kind === 'video');
let encodings;
if (firstVideoCodec.mimeType.toLowerCase() === 'video/vp9')
encodings = VIDEO_KSVC_ENCODINGS;
else if ('simulcastEncodings' in window.config)
encodings = window.config.simulcastEncodings;
else
encodings = VIDEO_SIMULCAST_ENCODINGS;
producer = await this._sendTransport.produce(
{
track,
encodings,
codecOptions :
{
videoGoogleStartBitrate : 1000
},
appData :
{
source : 'extravideo'
}
});
}
else
{
producer = await this._sendTransport.produce({
track,
appData :
{
source : 'extravideo'
}
});
}
this._extraVideoProducers.set(producer.id, producer);
store.dispatch(producerActions.addProducer(
{
id : producer.id,
deviceLabel : device.label,
source : 'extravideo',
paused : producer.paused,
track : producer.track,
rtpParameters : producer.rtpParameters,
codec : producer.rtpParameters.codecs[0].mimeType.split('/')[1]
}));
// store.dispatch(settingsActions.setSelectedWebcamDevice(deviceId));
await this._updateWebcams();
producer.on('transportclose', () =>
{
this._extraVideoProducers.delete(producer.id);
producer = null;
});
producer.on('trackended', () =>
{
store.dispatch(requestActions.notify(
{
type : 'error',
text : intl.formatMessage({
id : 'devices.cameraDisconnected',
defaultMessage : 'Camera disconnected'
})
}));
this.disableExtraVideo(producer.id)
.catch(() => {});
});
logger.debug('addExtraVideo() succeeded');
}
catch (error)
{
logger.error('addExtraVideo() failed:%o', error);
store.dispatch(requestActions.notify(
{
type : 'error',
text : intl.formatMessage({
id : 'devices.cameraError',
defaultMessage : 'An error occurred while accessing your camera'
})
}));
if (track)
track.stop();
}
store.dispatch(
meActions.setWebcamInProgress(false));
}
async enableMic() async enableMic()
{ {
if (this._micProducer) if (this._micProducer)
@ -3433,6 +3661,37 @@ export default class RoomClient
meActions.setWebcamInProgress(false)); meActions.setWebcamInProgress(false));
} }
async disableExtraVideo(id)
{
logger.debug('disableExtraVideo()');
const producer = this._extraVideoProducers.get(id);
if (!producer)
return;
store.dispatch(meActions.setWebcamInProgress(true));
producer.close();
store.dispatch(
producerActions.removeProducer(id));
try
{
await this.sendRequest(
'closeProducer', { producerId: id });
}
catch (error)
{
logger.error('disableWebcam() [error:"%o"]', error);
}
this._extraVideoProducers.delete(id);
store.dispatch(meActions.setWebcamInProgress(false));
}
async disableWebcam() async disableWebcam()
{ {
logger.debug('disableWebcam()'); logger.debug('disableWebcam()');

View File

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

View File

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

View File

@ -34,10 +34,10 @@ export const setPeerScreenInProgress = (peerId, flag) =>
payload : { peerId, flag } payload : { peerId, flag }
}); });
export const setPeerRaiseHandState = (peerId, raiseHandState) => export const setPeerRaisedHand = (peerId, raisedHand) =>
({ ({
type : 'SET_PEER_RAISE_HAND_STATE', type : 'SET_PEER_RAISED_HAND',
payload : { peerId, raiseHandState } payload : { peerId, raisedHand }
}); });
export const setPeerPicture = (peerId, picture) => export const setPeerPicture = (peerId, picture) =>

View File

@ -52,13 +52,25 @@ export const setJoinByAccessCode = (joinByAccessCode) =>
payload : { joinByAccessCode } payload : { joinByAccessCode }
}); });
export const setSettingsOpen = ({ settingsOpen }) => export const setSettingsOpen = (settingsOpen) =>
({ ({
type : 'SET_SETTINGS_OPEN', type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen } payload : { settingsOpen }
}); });
export const setLockDialogOpen = ({ lockDialogOpen }) => export const setExtraVideoOpen = (extraVideoOpen) =>
({
type : 'SET_EXTRA_VIDEO_OPEN',
payload : { extraVideoOpen }
});
export const setSettingsTab = (tab) =>
({
type : 'SET_SETTINGS_TAB',
payload : { tab }
});
export const setLockDialogOpen = (lockDialogOpen) =>
({ ({
type : 'SET_LOCK_DIALOG_OPEN', type : 'SET_LOCK_DIALOG_OPEN',
payload : { lockDialogOpen } payload : { lockDialogOpen }

View File

@ -38,6 +38,16 @@ export const togglePermanentTopBar = () =>
type : 'TOGGLE_PERMANENT_TOPBAR' type : 'TOGGLE_PERMANENT_TOPBAR'
}); });
export const toggleHiddenControls = () =>
({
type : 'TOGGLE_HIDDEN_CONTROLS'
});
export const toggleNotificationSounds = () =>
({
type : 'TOGGLE_NOTIFICATION_SOUNDS'
});
export const setLastN = (lastN) => export const setLastN = (lastN) =>
({ ({
type : 'SET_LAST_N', type : 'SET_LAST_N',

View File

@ -15,14 +15,6 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
// import FormLabel from '@material-ui/core/FormLabel';
// import FormControl from '@material-ui/core/FormControl';
// import FormGroup from '@material-ui/core/FormGroup';
// import FormControlLabel from '@material-ui/core/FormControlLabel';
// import Checkbox from '@material-ui/core/Checkbox';
// import InputLabel from '@material-ui/core/InputLabel';
// import OutlinedInput from '@material-ui/core/OutlinedInput';
// import Switch from '@material-ui/core/Switch';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader'; import ListSubheader from '@material-ui/core/ListSubheader';
import ListLobbyPeer from './ListLobbyPeer'; import ListLobbyPeer from './ListLobbyPeer';
@ -59,10 +51,8 @@ const styles = (theme) =>
}); });
const LockDialog = ({ const LockDialog = ({
// roomClient,
room, room,
handleCloseLockDialog, handleCloseLockDialog,
// handleAccessCode,
lobbyPeers, lobbyPeers,
classes classes
}) => }) =>
@ -71,7 +61,7 @@ const LockDialog = ({
<Dialog <Dialog
className={classes.root} className={classes.root}
open={room.lockDialogOpen} open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })} onClose={() => handleCloseLockDialog(false)}
classes={{ classes={{
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
@ -82,54 +72,6 @@ const LockDialog = ({
defaultMessage='Lobby administration' defaultMessage='Lobby administration'
/> />
</DialogTitle> </DialogTitle>
{/*
<FormControl component='fieldset' className={classes.formControl}>
<FormLabel component='legend'>Room lock</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Switch checked={room.locked} onChange={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
/>}
label='Lock'
/>
TODO: access code
<FormControlLabel disabled={ room.locked ? false : true }
control={
<Checkbox checked={room.joinByAccessCode}
onChange={
(event) => roomClient.setJoinByAccessCode(event.target.checked)
}
/>}
label='Join by Access code'
/>
<InputLabel htmlFor='access-code-input' />
<OutlinedInput
disabled={ room.locked ? false : true }
id='acces-code-input'
label='Access code'
labelWidth={0}
variant='outlined'
value={room.accessCode}
onChange={(event) => handleAccessCode(event.target.value)}
>
</OutlinedInput>
<Button onClick={() => roomClient.setAccessCode(room.accessCode)} color='primary'>
save
</Button>
</FormGroup>
</FormControl>
*/}
{ lobbyPeers.length > 0 ? { lobbyPeers.length > 0 ?
<List <List
dense dense
@ -160,7 +102,7 @@ const LockDialog = ({
</DialogContent> </DialogContent>
} }
<DialogActions> <DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'> <Button onClick={() => handleCloseLockDialog(false)} color='primary'>
<FormattedMessage <FormattedMessage
id='label.close' id='label.close'
defaultMessage='Close' defaultMessage='Close'
@ -173,7 +115,6 @@ const LockDialog = ({
LockDialog.propTypes = LockDialog.propTypes =
{ {
// roomClient : PropTypes.any.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
handleCloseLockDialog : PropTypes.func.isRequired, handleCloseLockDialog : PropTypes.func.isRequired,
handleAccessCode : PropTypes.func.isRequired, handleAccessCode : PropTypes.func.isRequired,
@ -202,12 +143,7 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.locked === next.room.locked && prev.room === next.room &&
prev.room.joinByAccessCode === next.room.joinByAccessCode &&
prev.room.accessCode === next.room.accessCode &&
prev.room.code === next.room.code &&
prev.room.lockDialogOpen === next.room.lockDialogOpen &&
prev.room.codeHidden === next.room.codeHidden &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );
} }

View File

@ -78,7 +78,16 @@ const styles = (theme) =>
zIndex : 21, zIndex : 21,
touchAction : 'none', touchAction : 'none',
pointerEvents : 'none', pointerEvents : 'none',
'& p' : '&.hide' :
{
transition : 'opacity 0.1s ease-in-out',
opacity : 0
},
'&.hover' :
{
opacity : 1
},
'& p' :
{ {
position : 'absolute', position : 'absolute',
float : 'left', float : 'left',
@ -107,7 +116,8 @@ const styles = (theme) =>
fontSize : '2vs', fontSize : '2vs',
backgroundColor : 'rgba(255, 0, 0, 0.5)', backgroundColor : 'rgba(255, 0, 0, 0.5)',
margin : '4px', margin : '4px',
padding : '15px', padding : theme.spacing(2),
zIndex : 31,
borderRadius : '20px', borderRadius : '20px',
textAlign : 'center', textAlign : 'center',
opacity : 0, opacity : 0,
@ -140,6 +150,7 @@ const Me = (props) =>
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer, screenProducer,
extraVideoProducers,
canShareScreen, canShareScreen,
classes classes
} = props; } = props;
@ -289,8 +300,22 @@ const Me = (props) =>
style={spacingStyle} style={spacingStyle}
> >
<div className={classes.viewContainer} style={style}> <div className={classes.viewContainer} style={style}>
<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 <div
className={classes.controls} className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -318,17 +343,6 @@ const Me = (props) =>
/> />
</p> </p>
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
<React.Fragment> <React.Fragment>
<Tooltip title={micTip} placement='left'> <Tooltip title={micTip} placement='left'>
<div> <div>
@ -454,6 +468,112 @@ const Me = (props) =>
</VideoView> </VideoView>
</div> </div>
</div> </div>
{ extraVideoProducers.map((producer) =>
{
return (
<div key={producer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={webcamTip} placement='left'>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
isMe
advancedMode={advancedMode}
peer={me}
displayName={settings.displayName}
showPeerInfo
videoTrack={producer && producer.track}
videoVisible={videoVisible}
videoCodec={producer && producer.codec}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/>
</div>
</div>
);
})}
{ screenProducer && { screenProducer &&
<div <div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -480,7 +600,11 @@ const Me = (props) =>
> >
<div className={classes.viewContainer} style={style}> <div className={classes.viewContainer} style={style}>
<div <div
className={classes.controls} className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -528,20 +652,21 @@ const Me = (props) =>
Me.propTypes = Me.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
settings : PropTypes.object, settings : PropTypes.object,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
spacing : PropTypes.number, extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
style : PropTypes.object, spacing : PropTypes.number,
smallButtons : PropTypes.bool, style : PropTypes.object,
canShareScreen : PropTypes.bool.isRequired, smallButtons : PropTypes.bool,
classes : PropTypes.object.isRequired, canShareScreen : PropTypes.bool.isRequired,
theme : PropTypes.object.isRequired classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>

View File

@ -125,6 +125,7 @@ const Peer = (props) =>
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer, screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen, toggleConsumerFullscreen,
toggleConsumerWindow, toggleConsumerWindow,
spacing, spacing,
@ -351,6 +352,161 @@ const Peer = (props) =>
</div> </div>
</div> </div>
{ extraVideoConsumers.map((consumer) =>
{
return (
<div key={consumer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ browser.platform !== 'mobile' &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
showPeerInfo
consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={videoVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</div>
</div>
);
})}
{ screenConsumer && { screenConsumer &&
<div <div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -508,6 +664,7 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string, windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
browser : PropTypes.object.isRequired, browser : PropTypes.object.isRequired,

View File

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

View File

@ -0,0 +1,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

@ -1,9 +1,10 @@
import React from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
lobbyPeersKeySelector, lobbyPeersKeySelector,
peersLengthSelector peersLengthSelector,
raisedHandsSelector
} from '../Selectors'; } from '../Selectors';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
@ -13,11 +14,14 @@ import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge'; import Badge from '@material-ui/core/Badge';
import ExtensionIcon from '@material-ui/icons/Extension';
import AccountCircle from '@material-ui/icons/AccountCircle'; import AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
@ -26,6 +30,7 @@ import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People'; import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock'; import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen'; import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
@ -81,9 +86,17 @@ const styles = (theme) =>
margin : theme.spacing(1, 0), margin : theme.spacing(1, 0),
padding : theme.spacing(0, 1) padding : theme.spacing(0, 1)
}, },
disabledButton :
{
margin : theme.spacing(1, 0)
},
green : green :
{ {
color : 'rgba(0, 153, 0, 1)' color : 'rgba(0, 153, 0, 1)'
},
moreAction :
{
margin : theme.spacing(0, 0, 0, 1)
} }
}); });
@ -122,6 +135,18 @@ const TopBar = (props) =>
{ {
const intl = useIntl(); const intl = useIntl();
const [ moreActionsElement, setMoreActionsElement ] = useState(null);
const handleMoreActionsOpen = (event) =>
{
setMoreActionsElement(event.currentTarget);
};
const handleMoreActionsClose = () =>
{
setMoreActionsElement(null);
};
const { const {
roomClient, roomClient,
room, room,
@ -135,15 +160,19 @@ const TopBar = (props) =>
fullscreen, fullscreen,
onFullscreen, onFullscreen,
setSettingsOpen, setSettingsOpen,
setExtraVideoOpen,
setLockDialogOpen, setLockDialogOpen,
toggleToolArea, toggleToolArea,
openUsersTab, openUsersTab,
unread, unread,
canProduceExtraVideo,
canLock, canLock,
canPromote, canPromote,
classes classes
} = props; } = props;
const isMoreActionsMenuOpen = Boolean(moreActionsElement);
const lockTooltip = room.locked ? const lockTooltip = room.locked ?
intl.formatMessage({ intl.formatMessage({
id : 'tooltip.unLockRoom', id : 'tooltip.unLockRoom',
@ -178,217 +207,264 @@ const TopBar = (props) =>
}); });
return ( return (
<AppBar <React.Fragment>
position='fixed' <AppBar
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide} position='fixed'
> className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
<Toolbar> >
<PulsingBadge <Toolbar>
color='secondary' <PulsingBadge
badgeContent={unread} color='secondary'
onClick={() => toggleToolArea()} 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.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'
})}
> >
<IconButton <IconButton
color='inherit'
aria-label={intl.formatMessage({ 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.actionButtons}>
<IconButton
aria-haspopup='true'
onClick={handleMoreActionsOpen}
color='inherit'
>
<ExtensionIcon />
</IconButton>
{ 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', id : 'tooltip.participants',
defaultMessage : 'Show participants' defaultMessage : 'Show participants'
})} })}
color='inherit'
onClick={() => openUsersTab()}
> >
<Badge <IconButton
color='primary' aria-label={intl.formatMessage({
badgeContent={peersLength + 1} id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
> >
<PeopleIcon /> <Badge
</Badge> color='primary'
</IconButton> badgeContent={peersLength + 1}
</Tooltip> >
<Tooltip <PeopleIcon />
title={intl.formatMessage({ </Badge>
id : 'tooltip.settings', </IconButton>
defaultMessage : 'Show settings' </Tooltip>
})} <Tooltip
> title={intl.formatMessage({
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings', id : 'tooltip.settings',
defaultMessage : 'Show 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 <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.lobby', id : 'tooltip.settings',
defaultMessage : 'Show lobby' defaultMessage : 'Show settings'
})}
color='inherit'
disabled={!canPromote}
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'
})} })}
className={classes.actionButton} className={classes.actionButton}
color='inherit' color='inherit'
onClick={() => onClick={() => setSettingsOpen(!room.settingsOpen)}
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
> >
{ myPicture ? <SettingsIcon />
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn && classes.green} />
}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} <Tooltip title={lockTooltip}>
<div className={classes.divider} /> <span className={classes.disabledButton}>
<Button <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 className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'label.leave',
defaultMessage : 'Leave'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
<FormattedMessage
id='label.leave'
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>
</AppBar>
<Menu
anchorEl={moreActionsElement}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
open={isMoreActionsMenuOpen}
onClose={handleMoreActionsClose}
getContentAnchorEl={null}
>
<MenuItem
dense
disabled={!canProduceExtraVideo}
onClick={() =>
{
handleMoreActionsClose();
setExtraVideoOpen(!room.extraVideoOpen);
}}
>
<VideoCallIcon
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.leave', id : 'label.addVideo',
defaultMessage : 'Leave' defaultMessage : 'Add video'
})} })}
className={classes.actionButton} />
variant='contained' <p className={classes.moreAction}>
color='secondary'
onClick={() => roomClient.close()}
>
<FormattedMessage <FormattedMessage
id='label.leave' id='label.addVideo'
defaultMessage='Leave' defaultMessage='Add video'
/> />
</Button> </p>
</div> </MenuItem>
</Toolbar> </Menu>
</AppBar> </React.Fragment>
); );
}; };
TopBar.propTypes = TopBar.propTypes =
{ {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
peersLength : PropTypes.number, peersLength : PropTypes.number,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool, permanentTopBar : PropTypes.bool,
myPicture : PropTypes.string, myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired, loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool, fullscreenEnabled : PropTypes.bool,
fullscreen : PropTypes.bool, fullscreen : PropTypes.bool,
onFullscreen : PropTypes.func.isRequired, onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired, setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired, setExtraVideoOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired, openUsersTab : PropTypes.func.isRequired,
canLock : PropTypes.bool.isRequired, unread : PropTypes.number.isRequired,
canPromote : PropTypes.bool.isRequired, canProduceExtraVideo : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired, canLock : PropTypes.bool.isRequired,
theme : PropTypes.object.isRequired canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -401,7 +477,10 @@ const mapStateToProps = (state) =>
loginEnabled : state.me.loginEnabled, loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture, myPicture : state.me.picture,
unread : state.toolarea.unreadMessages + unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles, state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)),
canLock : canLock :
state.me.roles.some((role) => state.me.roles.some((role) =>
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)), state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)),
@ -418,11 +497,15 @@ const mapDispatchToProps = (dispatch) =>
}, },
setSettingsOpen : (settingsOpen) => setSettingsOpen : (settingsOpen) =>
{ {
dispatch(roomActions.setSettingsOpen({ settingsOpen })); dispatch(roomActions.setSettingsOpen(settingsOpen));
},
setExtraVideoOpen : (extraVideoOpen) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
}, },
setLockDialogOpen : (lockDialogOpen) => setLockDialogOpen : (lockDialogOpen) =>
{ {
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen })); dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
}, },
toggleToolArea : () => toggleToolArea : () =>
{ {

View File

@ -149,7 +149,7 @@ const DialogTitle = withStyles(styles)((props) =>
<Avatar src={myPicture} className={classes.largeAvatar} /> <Avatar src={myPicture} className={classes.largeAvatar} />
: :
<AccountCircle <AccountCircle
className={classnames(classes.largeIcon, loggedIn && classes.green)} className={classnames(classes.largeIcon, loggedIn ? classes.green : null)}
/> />
} }
</IconButton> </IconButton>

View File

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

View File

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

View File

@ -1,79 +1,50 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames'; import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl';
import IconButton from '@material-ui/core/IconButton';
import PanIcon from '@material-ui/icons/PanTool';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex',
}, padding : theme.spacing(1)
listPeer :
{
display : 'flex'
}, },
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(1)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
alignItems : 'center' alignItems : 'center'
}, },
indicators : green :
{ {
left : 0, color : 'rgba(0, 153, 0, 1)'
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
},
icon :
{
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
}
} }
}); });
const ListMe = (props) => const ListMe = (props) =>
{ {
const intl = useIntl();
const { const {
roomClient,
me, me,
settings, settings,
classes classes
@ -82,29 +53,38 @@ const ListMe = (props) =>
const picture = me.picture || EmptyAvatar; const picture = me.picture || EmptyAvatar;
return ( return (
<li className={classes.root}> <div className={classes.root}>
<div className={classes.listPeer}> <img alt='My avatar' className={classes.avatar} src={picture} />
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}> <div className={classes.peerInfo}>
{settings.displayName} {settings.displayName}
</div>
<div className={classes.indicators}>
{ me.raisedHand &&
<div className={classnames(classes.icon, 'raise-hand')} />
}
</div>
</div> </div>
</li> <IconButton
aria-label={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={me.raisedHand ? classes.green : null}
disabled={me.raisedHandInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.setRaisedHand(!me.raisedHand);
}}
>
<PanIcon />
</IconButton>
</div>
); );
}; };
ListMe.propTypes = ListMe.propTypes =
{ {
me : appPropTypes.Me.isRequired, roomClient : PropTypes.object.isRequired,
settings : PropTypes.object.isRequired, me : appPropTypes.Me.isRequired,
classes : PropTypes.object.isRequired settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@ -112,7 +92,7 @@ const mapStateToProps = (state) => ({
settings : state.settings settings : state.settings
}); });
export default connect( export default withRoomContext(connect(
mapStateToProps, mapStateToProps,
null, null,
null, null,
@ -125,4 +105,4 @@ export default connect(
); );
} }
} }
)(withStyles(styles)(ListMe)); )(withStyles(styles)(ListMe)));

View File

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { makePeerConsumerSelector } from '../../Selectors'; import { makePeerConsumerSelector } from '../../Selectors';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -16,31 +15,26 @@ import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp'; import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg'; import PanIcon from '@material-ui/icons/PanTool';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex'
}, },
listPeer :
{
display : 'flex'
},
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(1)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
@ -48,52 +42,12 @@ const styles = (theme) =>
}, },
indicators : indicators :
{ {
left : 0, display : 'flex',
top : 0, padding : theme.spacing(1.5)
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
}, },
icon : green :
{ {
flex : '0 0 auto', color : 'rgba(0, 153, 0, 1)'
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.on' :
{
opacity : 1
},
'&.off' :
{
opacity : 0.2
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`
}
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
} }
}); });
@ -140,106 +94,96 @@ const ListPeer = (props) =>
{peer.displayName} {peer.displayName}
</div> </div>
<div className={classes.indicators}> <div className={classes.indicators}>
{ peer.raiseHandState && { peer.raisedHand &&
<div className={ <PanIcon className={classes.green} />
classnames(
classes.icon, 'raise-hand', {
on : peer.raiseHandState,
off : !peer.raiseHandState
}
)
}
/>
} }
</div> </div>
{ screenConsumer &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteScreenSharing',
defaultMessage : 'Mute participant share'
})}
color={screenVisible ? 'primary' : 'secondary'}
disabled={peer.peerScreenInProgress}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}}
>
{ screenVisible ?
<ScreenIcon />
:
<ScreenOffIcon />
}
</IconButton>
}
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute participant'
})}
color={micEnabled ? 'primary' : 'secondary'}
disabled={peer.peerAudioInProgress}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</IconButton>
{ isModerator &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
disabled={peer.peerKickInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.kickPeer(peer.id);
}}
>
<ExitIcon />
</IconButton>
}
{children} {children}
<div className={classes.controls}>
{ screenConsumer &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteScreenSharing',
defaultMessage : 'Mute participant share'
})}
color={screenVisible ? 'primary' : 'secondary'}
disabled={peer.peerScreenInProgress}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}}
>
{ screenVisible ?
<ScreenIcon />
:
<ScreenOffIcon />
}
</IconButton>
}
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute participant'
})}
color={micEnabled ? 'primary' : 'secondary'}
disabled={peer.peerAudioInProgress}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</IconButton>
{ isModerator &&
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
disabled={peer.peerKickInProgress}
onClick={(e) =>
{
e.stopPropagation();
roomClient.kickPeer(peer.id);
}}
>
<ExitIcon />
</IconButton>
}
</div>
</div> </div>
); );
}; };

View File

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

View File

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

View File

@ -24,6 +24,7 @@ import LockDialog from './AccessControl/LockDialog/LockDialog';
import Settings from './Settings/Settings'; import Settings from './Settings/Settings';
import TopBar from './Controls/TopBar'; import TopBar from './Controls/TopBar';
import WakeLock from 'react-wakelock-react16'; import WakeLock from 'react-wakelock-react16';
import ExtraVideo from './Controls/ExtraVideo';
const TIMEOUT = 5 * 1000; const TIMEOUT = 5 * 1000;
@ -217,6 +218,10 @@ class Room extends React.PureComponent
{ room.settingsOpen && { room.settingsOpen &&
<Settings /> <Settings />
} }
{ room.extraVideoOpen &&
<ExtraVideo />
}
</div> </div>
); );
} }

View File

@ -37,6 +37,11 @@ export const screenProducersSelector = createSelector(
(producers) => Object.values(producers).filter((producer) => producer.source === 'screen') (producers) => Object.values(producers).filter((producer) => producer.source === 'screen')
); );
export const extraVideoProducersSelector = createSelector(
producersSelect,
(producers) => Object.values(producers).filter((producer) => producer.source === 'extravideo')
);
export const micProducerSelector = createSelector( export const micProducerSelector = createSelector(
producersSelect, producersSelect,
(producers) => Object.values(producers).find((producer) => producer.source === 'mic') (producers) => Object.values(producers).find((producer) => producer.source === 'mic')
@ -67,6 +72,24 @@ export const screenConsumerSelector = createSelector(
(consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen') (consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen')
); );
export const spotlightScreenConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'screen' && spotlights.includes(consumer.peerId)
)
);
export const spotlightExtraVideoConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'extravideo' && spotlights.includes(consumer.peerId)
)
);
export const passiveMicConsumerSelector = createSelector( export const passiveMicConsumerSelector = createSelector(
spotlightsSelector, spotlightsSelector,
consumersSelect, consumersSelect,
@ -106,24 +129,41 @@ export const passivePeersSelector = createSelector(
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
); );
export const raisedHandsSelector = createSelector(
peersValueSelector,
(peers) => peers.reduce((a, b) => (a + (b.raisedHand ? 1 : 0)), 0)
);
export const videoBoxesSelector = createSelector( export const videoBoxesSelector = createSelector(
spotlightsLengthSelector, spotlightsLengthSelector,
screenProducersSelector, screenProducersSelector,
screenConsumerSelector, spotlightScreenConsumerSelector,
(spotlightsLength, screenProducers, screenConsumers) => extraVideoProducersSelector,
spotlightsLength + 1 + screenProducers.length + screenConsumers.length spotlightExtraVideoConsumerSelector,
(
spotlightsLength,
screenProducers,
screenConsumers,
extraVideoProducers,
extraVideoConsumers
) =>
spotlightsLength + 1 + screenProducers.length +
screenConsumers.length + extraVideoProducers.length +
extraVideoConsumers.length
); );
export const meProducersSelector = createSelector( export const meProducersSelector = createSelector(
micProducerSelector, micProducerSelector,
webcamProducerSelector, webcamProducerSelector,
screenProducerSelector, screenProducerSelector,
(micProducer, webcamProducer, screenProducer) => extraVideoProducersSelector,
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
{ {
return { return {
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer screenProducer,
extraVideoProducers
}; };
} }
); );
@ -146,8 +186,10 @@ export const makePeerConsumerSelector = () =>
consumersArray.find((consumer) => consumer.source === 'webcam'); consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer = const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen'); consumersArray.find((consumer) => consumer.source === 'screen');
const extraVideoConsumers =
consumersArray.filter((consumer) => consumer.source === 'extravideo');
return { micConsumer, webcamConsumer, screenConsumer }; return { micConsumer, webcamConsumer, screenConsumer, extraVideoConsumers };
} }
); );
}; };

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,143 @@
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 = ({
room,
settings,
onTogglePermanentTopBar,
onToggleHiddenControls,
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'
})}
/>
</React.Fragment>
);
};
AppearenceSettings.propTypes =
{
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
onToggleHiddenControls : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
room : state.room,
settings : state.settings
});
const mapDispatchToProps = {
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
onToggleHiddenControls : settingsActions.toggleHiddenControls,
handleChangeMode : roomActions.setDisplayMode
};
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(AppearenceSettings));

View File

@ -0,0 +1,284 @@
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 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 Select from '@material-ui/core/Select';
const styles = (theme) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const MediaSettings = ({
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>
</form>
</React.Fragment>
);
};
MediaSettings.propTypes =
{
roomClient : PropTypes.any.isRequired,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
settings : state.settings
};
};
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me === next.me &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(MediaSettings)));

View File

@ -1,22 +1,25 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions'; import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import MediaSettings from './MediaSettings';
import AppearenceSettings from './AppearenceSettings';
import AdvancedSettings from './AdvancedSettings';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText'; const tabs =
import FormControl from '@material-ui/core/FormControl'; [
import FormControlLabel from '@material-ui/core/FormControlLabel'; 'media',
import Select from '@material-ui/core/Select'; 'appearence',
import Checkbox from '@material-ui/core/Checkbox'; 'advanced'
];
const styles = (theme) => const styles = (theme) =>
({ ({
@ -43,106 +46,27 @@ const styles = (theme) =>
width : '90vw' width : '90vw'
} }
}, },
setting : tabsHeader :
{ {
padding : theme.spacing(2) flexGrow : 1
},
formControl :
{
display : 'flex'
} }
}); });
const Settings = ({ const Settings = ({
roomClient, currentSettingsTab,
room, settingsOpen,
me,
settings,
onToggleAdvancedMode,
onTogglePermanentTopBar,
handleCloseSettings, handleCloseSettings,
handleChangeMode, setSettingsTab,
classes classes
}) => }) =>
{ {
const intl = useIntl(); const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
webcams = Object.values(me.webcamDevices);
else
webcams = [];
let audioDevices;
if (me.audioDevices)
audioDevices = Object.values(me.audioDevices);
else
audioDevices = [];
let audioOutputDevices;
if (me.audioOutputDevices)
audioOutputDevices = Object.values(me.audioOutputDevices);
else
audioOutputDevices = [];
return ( return (
<Dialog <Dialog
className={classes.root} className={classes.root}
open={room.settingsOpen} open={settingsOpen}
onClose={() => handleCloseSettings({ settingsOpen: false })} onClose={() => handleCloseSettings(false)}
classes={{ classes={{
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
@ -153,254 +77,40 @@ const Settings = ({
defaultMessage='Settings' defaultMessage='Settings'
/> />
</DialogTitle> </DialogTitle>
<form className={classes.setting} autoComplete='off'> <Tabs
<FormControl className={classes.formControl}> className={classes.tabsHeader}
<Select value={tabs.indexOf(currentSettingsTab)}
value={settings.selectedWebcam || ''} onChange={(event, value) => setSettingsTab(tabs[value])}
onChange={(event) => indicatorColor='primary'
{ textColor='primary'
if (event.target.value) variant='fullWidth'
roomClient.changeWebcam(event.target.value); >
}} <Tab
displayEmpty label={
name={intl.formatMessage({ intl.formatMessage({
id : 'settings.camera', id : 'label.media',
defaultMessage : 'Camera' defaultMessage : 'Media'
})} })
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>
{ !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>
} }
<FormControlLabel />
className={classes.setting} <Tab
control={<Checkbox checked={settings.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />} label={intl.formatMessage({
label={intl.formatMessage({ id : 'label.appearence',
id : 'settings.permanentTopBar', defaultMessage : 'Appearence'
defaultMessage : 'Permanent top bar' })}
})} />
/> <Tab
</React.Fragment> label={intl.formatMessage({
} id : 'label.advanced',
defaultMessage : 'Advanced'
})}
/>
</Tabs>
{currentSettingsTab === 'media' && <MediaSettings />}
{currentSettingsTab === 'appearence' && <AppearenceSettings />}
{currentSettingsTab === 'advanced' && <AdvancedSettings />}
<DialogActions> <DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'> <Button onClick={() => handleCloseSettings(false)} color='primary'>
<FormattedMessage <FormattedMessage
id='label.close' id='label.close'
defaultMessage='Close' defaultMessage='Close'
@ -413,34 +123,25 @@ const Settings = ({
Settings.propTypes = Settings.propTypes =
{ {
roomClient : PropTypes.any.isRequired, currentSettingsTab : PropTypes.string.isRequired,
me : appPropTypes.Me.isRequired, settingsOpen : PropTypes.bool.isRequired,
room : appPropTypes.Room.isRequired, handleCloseSettings : PropTypes.func.isRequired,
settings : PropTypes.object.isRequired, setSettingsTab : PropTypes.func.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired, classes : PropTypes.object.isRequired
onTogglePermanentTopBar : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
handleCloseSettings : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ ({
return { currentSettingsTab : state.room.currentSettingsTab,
me : state.me, settingsOpen : state.room.settingsOpen
room : state.room, });
settings : state.settings
};
};
const mapDispatchToProps = { const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode, handleCloseSettings : roomActions.setSettingsOpen,
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, setSettingsTab : roomActions.setSettingsTab
handleChangeMode : roomActions.setDisplayMode,
handleCloseSettings : roomActions.setSettingsOpen
}; };
export default withRoomContext(connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
@ -448,10 +149,9 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.me === next.me && prev.room.currentSettingsTab === next.room.currentSettingsTab &&
prev.room === next.room && prev.room.settingsOpen === next.room.settingsOpen
prev.settings === next.settings
); );
} }
} }
)(withStyles(styles)(Settings))); )(withStyles(styles)(Settings));

View File

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

View File

@ -81,11 +81,14 @@ class FullView extends React.PureComponent
this._setTracks(videoTrack); 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) _setTracks(videoTrack)

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

@ -20,8 +20,8 @@ const peer = (state = {}, action) =>
case 'SET_PEER_KICK_IN_PROGRESS': case 'SET_PEER_KICK_IN_PROGRESS':
return { ...state, peerKickInProgress: action.payload.flag }; return { ...state, peerKickInProgress: action.payload.flag };
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISED_HAND':
return { ...state, raiseHandState: action.payload.raiseHandState }; return { ...state, raisedHand: action.payload.raisedHand };
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
{ {
@ -86,7 +86,7 @@ const peers = (state = {}, action) =>
case 'SET_PEER_VIDEO_IN_PROGRESS': case 'SET_PEER_VIDEO_IN_PROGRESS':
case 'SET_PEER_AUDIO_IN_PROGRESS': case 'SET_PEER_AUDIO_IN_PROGRESS':
case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS':
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISED_HAND':
case 'SET_PEER_PICTURE': case 'SET_PEER_PICTURE':
case 'ADD_CONSUMER': case 'ADD_CONSUMER':
case 'ADD_PEER_ROLE': case 'ADD_PEER_ROLE':

View File

@ -20,6 +20,8 @@ const initialState =
selectedPeerId : null, selectedPeerId : null,
spotlights : [], spotlights : [],
settingsOpen : false, settingsOpen : false,
extraVideoOpen : false,
currentSettingsTab : 'media', // media, appearence, advanced
lockDialogOpen : false, lockDialogOpen : false,
joined : false, joined : false,
muteAllInProgress : false, muteAllInProgress : false,
@ -34,6 +36,7 @@ const initialState =
SEND_CHAT : [], SEND_CHAT : [],
MODERATE_CHAT : [], MODERATE_CHAT : [],
SHARE_SCREEN : [], SHARE_SCREEN : [],
EXTRA_VIDEO : [],
SHARE_FILE : [], SHARE_FILE : [],
MODERATE_FILES : [], MODERATE_FILES : [],
MODERATE_ROOM : [] MODERATE_ROOM : []
@ -113,6 +116,20 @@ const room = (state = initialState, action) =>
return { ...state, settingsOpen }; return { ...state, settingsOpen };
} }
case 'SET_EXTRA_VIDEO_OPEN':
{
const { extraVideoOpen } = action.payload;
return { ...state, extraVideoOpen };
}
case 'SET_SETTINGS_TAB':
{
const { tab } = action.payload;
return { ...state, currentSettingsTab: tab };
}
case 'SET_ROOM_ACTIVE_SPEAKER': case 'SET_ROOM_ACTIVE_SPEAKER':
{ {
const { peerId } = action.payload; const { peerId } = action.payload;

View File

@ -7,7 +7,9 @@ const initialState =
// low, medium, high, veryhigh, ultra // low, medium, high, veryhigh, ultra
resolution : window.config.defaultResolution || 'medium', resolution : window.config.defaultResolution || 'medium',
lastN : 4, lastN : 4,
permanentTopBar : true permanentTopBar : true,
hiddenControls : false,
notificationSounds : true
}; };
const settings = (state = initialState, action) => const settings = (state = initialState, action) =>
@ -57,6 +59,20 @@ const settings = (state = initialState, action) =>
return { ...state, permanentTopBar }; return { ...state, permanentTopBar };
} }
case 'TOGGLE_HIDDEN_CONTROLS':
{
const hiddenControls = !state.hiddenControls;
return { ...state, hiddenControls };
}
case 'TOGGLE_NOTIFICATION_SOUNDS':
{
const notificationSounds = !state.notificationSounds;
return { ...state, notificationSounds };
}
case 'SET_VIDEO_RESOLUTION': case 'SET_VIDEO_RESOLUTION':
{ {
const { resolution } = action.payload; const { resolution } = action.payload;

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "登录", "tooltip.login": "登录",
"tooltip.logout": "注销", "tooltip.logout": "注销",
"tooltip.admitFromLobby": "从大厅允许", "tooltip.admitFromLobby": "从大厅允许",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "房间名称", "label.roomName": "房间名称",
"label.chooseRoomButton": "继续", "label.chooseRoomButton": "继续",
@ -95,6 +102,10 @@
"label.veryHigh": "非常高 (FHD)", "label.veryHigh": "非常高 (FHD)",
"label.ultra": "超高 (UHD)", "label.ultra": "超高 (UHD)",
"label.close": "关闭", "label.close": "关闭",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "设置", "settings.settings": "设置",
"settings.camera": "视频设备", "settings.camera": "视频设备",
@ -112,6 +123,8 @@
"settings.advancedMode": "高级模式", "settings.advancedMode": "高级模式",
"settings.permanentTopBar": "永久顶吧", "settings.permanentTopBar": "永久顶吧",
"settings.lastn": "可见视频数量", "settings.lastn": "可见视频数量",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "无法保存文件", "filesharing.saveFileError": "无法保存文件",
"filesharing.startingFileShare": "正在尝试共享文件", "filesharing.startingFileShare": "正在尝试共享文件",

View File

@ -55,9 +55,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Přihlášení", "tooltip.login": "Přihlášení",
"tooltip.logout": "Odhlášení", "tooltip.logout": "Odhlášení",
"tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti", "tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti",
@ -71,6 +77,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Jméno místnosti", "label.roomName": "Jméno místnosti",
"label.chooseRoomButton": "Pokračovat", "label.chooseRoomButton": "Pokračovat",
@ -94,6 +101,10 @@
"label.veryHigh": "Velmi vysoké (FHD)", "label.veryHigh": "Velmi vysoké (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Zavřít", "label.close": "Zavřít",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Nastavení", "settings.settings": "Nastavení",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -109,6 +120,10 @@
"settings.layout": "Rozvržení místnosti", "settings.layout": "Rozvržení místnosti",
"settings.selectRoomLayout": "Vyberte rozvržení místnosti", "settings.selectRoomLayout": "Vyberte rozvržení místnosti",
"settings.advancedMode": "Pokočilý mód", "settings.advancedMode": "Pokočilý mód",
"settings.permanentTopBar": null,
"settings.lastn": null,
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Není možné uložit soubor", "filesharing.saveFileError": "Není možné uložit soubor",
"filesharing.startingFileShare": "Pokouším se sdílet soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor",

View File

@ -56,8 +56,14 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung", "room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung",
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"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": null,
"roles.lostRole": null,
"tooltip.login": "Anmelden", "tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden", "tooltip.logout": "Abmelden",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": "Teilnehmer rauswerfen", "tooltip.kickParticipant": "Teilnehmer rauswerfen",
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Name des Raums", "label.roomName": "Name des Raums",
"label.chooseRoomButton": "Weiter", "label.chooseRoomButton": "Weiter",
@ -95,6 +102,10 @@
"label.veryHigh": "Sehr hoch (FHD)", "label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Schließen", "label.close": "Schließen",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Einstellungen", "settings.settings": "Einstellungen",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Erweiterter Modus", "settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste", "settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos", "settings.lastn": "Anzahl der sichtbaren Videos",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Log ind", "tooltip.login": "Log ind",
"tooltip.logout": "Log ud", "tooltip.logout": "Log ud",
"tooltip.admitFromLobby": "Giv adgang fra lobbyen", "tooltip.admitFromLobby": "Giv adgang fra lobbyen",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Værelsesnavn", "label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt", "label.chooseRoomButton": "Fortsæt",
@ -95,6 +102,10 @@
"label.veryHigh": "Meget høj (FHD)", "label.veryHigh": "Meget høj (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Luk", "label.close": "Luk",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Indstillinger", "settings.settings": "Indstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -106,13 +117,14 @@
"settings.audioOutput": "Audio output enhed", "settings.audioOutput": "Audio output enhed",
"settings.selectAudioOutput": "Vælg lydudgangsenhed", "settings.selectAudioOutput": "Vælg lydudgangsenhed",
"settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed", "settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed",
"settings.resolution": "Vælg din videoopløsning", "settings.resolution": "Vælg din videoopløsning",
"settings.layout": "Møde visning", "settings.layout": "Møde visning",
"settings.selectRoomLayout": "Vælg møde visning", "settings.selectRoomLayout": "Vælg møde visning",
"settings.advancedMode": "Avanceret tilstand", "settings.advancedMode": "Avanceret tilstand",
"settings.permanentTopBar": "Permanent øverste linje", "settings.permanentTopBar": "Permanent øverste linje",
"settings.lastn": "Antal synlige videoer", "settings.lastn": "Antal synlige videoer",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen", "filesharing.startingFileShare": "Forsøger at dele filen",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Σύνδεση", "tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση", "tooltip.logout": "Αποσύνδεση",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Όνομα δωματίου", "label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια", "label.chooseRoomButton": "Συνέχεια",
@ -95,6 +102,10 @@
"label.veryHigh": "Πολύ υψηλή (FHD)", "label.veryHigh": "Πολύ υψηλή (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Κλείσιμο", "label.close": "Κλείσιμο",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ρυθμίσεις", "settings.settings": "Ρυθμίσεις",
"settings.camera": "Κάμερα", "settings.camera": "Κάμερα",
@ -112,6 +123,8 @@
"settings.advancedMode": "Προηγμένη λειτουργία", "settings.advancedMode": "Προηγμένη λειτουργία",
"settings.permanentTopBar": "Μόνιμη μπάρα κορυφής", "settings.permanentTopBar": "Μόνιμη μπάρα κορυφής",
"settings.lastn": "Αριθμός ορατών βίντεο", "settings.lastn": "Αριθμός ορατών βίντεο",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": "Clear files", "room.clearFileSharing": "Clear files",
"room.speechUnsupported": "Your browser does not support speech recognition", "room.speechUnsupported": "Your browser does not support speech recognition",
"room.moderatoractions": "Moderator actions", "room.moderatoractions": "Moderator actions",
"room.raisedHand": "{displayName} raised their hand",
"room.loweredHand": "{displayName} put their hand down",
"room.extraVideo": "Extra video",
"me.mutedPTT": "You are muted, hold down SPACE-BAR to talk", "me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
"roles.gotRole": "You got the role: {role}",
"roles.lostRole": "You lost the role: {role}",
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": "Kick out participant", "tooltip.kickParticipant": "Kick out participant",
"tooltip.muteParticipant": "Mute participant", "tooltip.muteParticipant": "Mute participant",
"tooltip.muteParticipantVideo": "Mute participant video", "tooltip.muteParticipantVideo": "Mute participant video",
"tooltip.raisedHand": "Raise hand",
"label.roomName": "Room name", "label.roomName": "Room name",
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": "Continue",
@ -95,6 +102,10 @@
"label.veryHigh": "Very high (FHD)", "label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Close", "label.close": "Close",
"label.media": "Media",
"label.appearence": "Appearence",
"label.advanced": "Advanced",
"label.addVideo": "Add video",
"settings.settings": "Settings", "settings.settings": "Settings",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Advanced mode", "settings.advancedMode": "Advanced mode",
"settings.permanentTopBar": "Permanent top bar", "settings.permanentTopBar": "Permanent top bar",
"settings.lastn": "Number of visible videos", "settings.lastn": "Number of visible videos",
"settings.hiddenControls": "Hidden media controls",
"settings.notificationSounds": "Notification sounds",
"filesharing.saveFileError": "Unable to save file", "filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file", "filesharing.startingFileShare": "Attempting to share file",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Salir", "tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera", "tooltip.admitFromLobby": "Admitir desde la sala de espera",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Nombre de la sala", "label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -95,6 +102,10 @@
"label.veryHigh": "Muy alta (FHD)", "label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Cerrar", "label.close": "Cerrar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ajustes", "settings.settings": "Ajustes",
"settings.camera": "Cámara", "settings.camera": "Cámara",
@ -112,6 +123,8 @@
"settings.advancedMode": "Modo avanzado", "settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles", "settings.lastn": "Cantidad de videos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Connexion", "tooltip.login": "Connexion",
"tooltip.logout": "Déconnexion", "tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente", "tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Nom de la salle", "label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer", "label.chooseRoomButton": "Continuer",
@ -95,6 +102,10 @@
"label.veryHigh": "Très Haute Définition (FHD)", "label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra": "Ultra Haute Définition", "label.ultra": "Ultra Haute Définition",
"label.close": "Fermer", "label.close": "Fermer",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Paramètres", "settings.settings": "Paramètres",
"settings.camera": "Caméra", "settings.camera": "Caméra",
@ -112,6 +123,8 @@
"settings.advancedMode": "Mode avancé", "settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente", "settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles", "settings.lastn": "Nombre de vidéos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier", "filesharing.startingFileShare": "Début du transfert de fichier",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora", "room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora",
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor", "me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Prijava", "tooltip.login": "Prijava",
"tooltip.logout": "Odjava", "tooltip.logout": "Odjava",
"tooltip.admitFromLobby": "Pusti iz predvorja", "tooltip.admitFromLobby": "Pusti iz predvorja",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": "Izbaci sudionika", "tooltip.kickParticipant": "Izbaci sudionika",
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Naziv sobe", "label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi", "label.chooseRoomButton": "Nastavi",
@ -95,6 +102,10 @@
"label.veryHigh": "Vrlo visoka (FHD)", "label.veryHigh": "Vrlo visoka (FHD)",
"label.ultra": "Ultra visoka (UHD)", "label.ultra": "Ultra visoka (UHD)",
"label.close": "Zatvori", "label.close": "Zatvori",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Postavke", "settings.settings": "Postavke",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Napredne mogućnosti", "settings.advancedMode": "Napredne mogućnosti",
"settings.permanentTopBar": "Stalna gornja šipka", "settings.permanentTopBar": "Stalna gornja šipka",
"settings.lastn": "Broj vidljivih videozapisa", "settings.lastn": "Broj vidljivih videozapisa",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.saveFileError": "Nije moguće spremiti datoteku",
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Belépés", "tooltip.login": "Belépés",
"tooltip.logout": "Kilépés", "tooltip.logout": "Kilépés",
"tooltip.admitFromLobby": "Beenegdem az előszobából", "tooltip.admitFromLobby": "Beenegdem az előszobából",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Konferencia", "label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább", "label.chooseRoomButton": "Tovább",
@ -95,6 +102,10 @@
"label.veryHigh": "Nagyon magas (FHD)", "label.veryHigh": "Nagyon magas (FHD)",
"label.ultra": "Ultra magas (UHD)", "label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár", "label.close": "Bezár",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Beállítások", "settings.settings": "Beállítások",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Részletes információk", "settings.advancedMode": "Részletes információk",
"settings.permanentTopBar": "Állandó felső sáv", "settings.permanentTopBar": "Állandó felső sáv",
"settings.lastn": "A látható videók száma", "settings.lastn": "A látható videók száma",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása", "filesharing.startingFileShare": "Fájl megosztása",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Ammetti dalla lobby", "tooltip.admitFromLobby": "Ammetti dalla lobby",
@ -71,6 +77,7 @@
"tooltip.participants": "Mostra partecipanti", "tooltip.participants": "Mostra partecipanti",
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Nome della stanza", "label.roomName": "Nome della stanza",
"label.chooseRoomButton": "Continua", "label.chooseRoomButton": "Continua",
@ -94,6 +101,10 @@
"label.veryHigh": "Molto alta (FHD)", "label.veryHigh": "Molto alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Chiudi", "label.close": "Chiudi",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Impostazioni", "settings.settings": "Impostazioni",
"settings.camera": "Videocamera", "settings.camera": "Videocamera",
@ -111,6 +122,8 @@
"settings.advancedMode": "Modalità avanzata", "settings.advancedMode": "Modalità avanzata",
"settings.permanentTopBar": "Barra superiore permanente", "settings.permanentTopBar": "Barra superiore permanente",
"settings.lastn": "Numero di video visibili", "settings.lastn": "Numero di video visibili",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Impossibile salvare file", "filesharing.saveFileError": "Impossibile salvare file",
"filesharing.startingFileShare": "Tentativo di condivisione file", "filesharing.startingFileShare": "Tentativo di condivisione file",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": "Fjern filer", "room.clearFileSharing": "Fjern filer",
"room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning", "room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning",
"room.moderatoractions": "Moderatorhandlinger", "room.moderatoractions": "Moderatorhandlinger",
"room.raisedHand": "{displayName} rakk opp hånden",
"room.loweredHand": "{displayName} tok ned hånden",
"room.extraVideo": "Ekstra video",
"me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke", "me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke",
"roles.gotRole": "Du fikk rollen: {role}",
"roles.lostRole": "Du mistet rollen: {role}",
"tooltip.login": "Logg in", "tooltip.login": "Logg in",
"tooltip.logout": "Logg ut", "tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby", "tooltip.admitFromLobby": "Slipp inn fra lobby",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": "Spark ut deltaker", "tooltip.kickParticipant": "Spark ut deltaker",
"tooltip.muteParticipant": "Demp deltaker", "tooltip.muteParticipant": "Demp deltaker",
"tooltip.muteParticipantVideo": "Demp deltakervideo", "tooltip.muteParticipantVideo": "Demp deltakervideo",
"tooltip.raisedHand": "Rekk opp hånden",
"label.roomName": "Møtenavn", "label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett", "label.chooseRoomButton": "Fortsett",
@ -95,6 +102,10 @@
"label.veryHigh": "Veldig høy (FHD)", "label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Lukk", "label.close": "Lukk",
"label.media": "Media",
"label.appearence": "Utseende",
"label.advanced": "Avansert",
"label.addVideo": "Legg til video",
"settings.settings": "Innstillinger", "settings.settings": "Innstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Avansert modus", "settings.advancedMode": "Avansert modus",
"settings.permanentTopBar": "Permanent topplinje", "settings.permanentTopBar": "Permanent topplinje",
"settings.lastn": "Antall videoer synlig", "settings.lastn": "Antall videoer synlig",
"settings.hiddenControls": "Skjul media knapper",
"settings.notificationSounds": "Varslingslyder",
"filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling", "filesharing.startingFileShare": "Starter fildeling",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Zaloguj", "tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj", "tooltip.logout": "Wyloguj",
"tooltip.admitFromLobby": "Przejście z poczekalni", "tooltip.admitFromLobby": "Przejście z poczekalni",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Nazwa konferencji", "label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj", "label.chooseRoomButton": "Kontynuuj",
@ -95,6 +102,10 @@
"label.veryHigh": "Bardzo wysoka (FHD)", "label.veryHigh": "Bardzo wysoka (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Zamknij", "label.close": "Zamknij",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ustawienia", "settings.settings": "Ustawienia",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Tryb zaawansowany", "settings.advancedMode": "Tryb zaawansowany",
"settings.permanentTopBar": "Stały górny pasek", "settings.permanentTopBar": "Stały górny pasek",
"settings.lastn": "Liczba widocznych uczestników (zdalnych)", "settings.lastn": "Liczba widocznych uczestników (zdalnych)",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.saveFileError": "Nie można zapisać pliku",
"filesharing.startingFileShare": "Próba udostępnienia pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Sair", "tooltip.logout": "Sair",
"tooltip.admitFromLobby": "Admitir da sala de espera", "tooltip.admitFromLobby": "Admitir da sala de espera",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Nome da sala", "label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -95,6 +102,10 @@
"label.veryHigh": "Muito alta (FHD)", "label.veryHigh": "Muito alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Fechar", "label.close": "Fechar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Definições", "settings.settings": "Definições",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -112,6 +123,8 @@
"settings.advancedMode": "Modo avançado", "settings.advancedMode": "Modo avançado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Número de vídeos visíveis", "settings.lastn": "Número de vídeos visíveis",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro",

View File

@ -56,9 +56,15 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Intră în cont", "tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare", "tooltip.logout": "Deconectare",
"tooltip.admitFromLobby": "Admite din hol", "tooltip.admitFromLobby": "Admite din hol",
@ -72,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Numele camerei", "label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare", "label.chooseRoomButton": "Continuare",
@ -95,6 +102,10 @@
"label.veryHigh": "Rezoluție foarte înaltă (FHD)", "label.veryHigh": "Rezoluție foarte înaltă (FHD)",
"label.ultra": "Rezoluție ultra înaltă (UHD)", "label.ultra": "Rezoluție ultra înaltă (UHD)",
"label.close": "Închide", "label.close": "Închide",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Setări", "settings.settings": "Setări",
"settings.camera": "Cameră video", "settings.camera": "Cameră video",
@ -112,6 +123,8 @@
"settings.advancedMode": "Mod avansat", "settings.advancedMode": "Mod avansat",
"settings.permanentTopBar": "Bara de sus permanentă", "settings.permanentTopBar": "Bara de sus permanentă",
"settings.lastn": "Numărul de videoclipuri vizibile", "settings.lastn": "Numărul de videoclipuri vizibile",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului", "filesharing.startingFileShare": "Partajarea fișierului",

View File

@ -56,6 +56,14 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Giriş", "tooltip.login": "Giriş",
"tooltip.logout": ıkış", "tooltip.logout": ıkış",
@ -70,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Oda adı", "label.roomName": "Oda adı",
"label.chooseRoomButton": "Devam", "label.chooseRoomButton": "Devam",
@ -93,6 +102,10 @@
"label.veryHigh": "Çok Yüksek (FHD)", "label.veryHigh": "Çok Yüksek (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Kapat", "label.close": "Kapat",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Ayarlar", "settings.settings": "Ayarlar",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -107,6 +120,8 @@
"settings.advancedMode": "Detaylı mod", "settings.advancedMode": "Detaylı mod",
"settings.permanentTopBar": "Üst barı kalıcı yap", "settings.permanentTopBar": "Üst barı kalıcı yap",
"settings.lastn": "İzlenebilir video sayısı", "settings.lastn": "İzlenebilir video sayısı",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.saveFileError": "Dosya kaydedilemiyor",
"filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", "filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor",

View File

@ -56,6 +56,14 @@
"room.clearFileSharing": null, "room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null, "room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Увійти", "tooltip.login": "Увійти",
"tooltip.logout": "Вихід", "tooltip.logout": "Вихід",
@ -70,6 +78,7 @@
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null, "tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null, "tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"label.roomName": "Назва кімнати", "label.roomName": "Назва кімнати",
"label.chooseRoomButton": "Продовжити", "label.chooseRoomButton": "Продовжити",
@ -93,6 +102,10 @@
"label.veryHigh": "Дуже високий (FHD)", "label.veryHigh": "Дуже високий (FHD)",
"label.ultra": "Ультра (UHD)", "label.ultra": "Ультра (UHD)",
"label.close": "Закрити", "label.close": "Закрити",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"settings.settings": "Налаштування", "settings.settings": "Налаштування",
"settings.camera": "Камера", "settings.camera": "Камера",
@ -110,6 +123,8 @@
"settings.advancedMode": "Розширений режим", "settings.advancedMode": "Розширений режим",
"settings.permanentTopBar": "Постійний верхній рядок", "settings.permanentTopBar": "Постійний верхній рядок",
"settings.lastn": "Кількість видимих ​​відео", "settings.lastn": "Кількість видимих ​​відео",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.saveFileError": "Неможливо зберегти файл",
"filesharing.startingFileShare": "Спроба поділитися файлом", "filesharing.startingFileShare": "Спроба поділитися файлом",

View File

@ -235,6 +235,8 @@ module.exports =
MODERATE_CHAT : [ userRoles.MODERATOR ], MODERATE_CHAT : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen // The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ], SHARE_SCREEN : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
EXTRA_VIDEO : [ userRoles.NORMAL ],
// The role(s) have permission to share files // The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ], SHARE_FILE : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files // The role(s) have permission to moderate files

View File

@ -47,7 +47,8 @@ class Lobby extends EventEmitter
return Object.values(this._peers).map((peer) => return Object.values(this._peers).map((peer) =>
({ ({
peerId : peer.id, peerId : peer.id,
displayName : peer.displayName displayName : peer.displayName,
picture : peer.picture
})); }));
} }

View File

@ -336,7 +336,8 @@ class Peer extends EventEmitter
id : this.id, id : this.id,
displayName : this.displayName, displayName : this.displayName,
picture : this.picture, picture : this.picture,
roles : this.roles roles : this.roles,
raisedHand : this.raisedHand
}; };
return peerInfo; return peerInfo;

View File

@ -24,6 +24,7 @@ const permissionsFromRoles =
SEND_CHAT : [ userRoles.NORMAL ], SEND_CHAT : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ], MODERATE_CHAT : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ], SHARE_SCREEN : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ], SHARE_FILE : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ], MODERATE_FILES : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ], MODERATE_ROOM : [ userRoles.MODERATOR ],
@ -530,6 +531,17 @@ class Room extends EventEmitter
peerId : peer.id, peerId : peer.id,
role : newRole role : newRole
}, true, true); }, true, true);
// Got permission to promote peers, notify peer of
// peers in lobby
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole))
{
const lobbyPeers = this._lobby.peerList();
lobbyPeers.length > 0 && this._notification(peer.socket, 'parkedPeers', {
lobbyPeers
});
}
}); });
peer.on('lostRole', ({ oldRole }) => peer.on('lostRole', ({ oldRole }) =>
@ -586,13 +598,21 @@ class Room extends EventEmitter
.filter((joinedPeer) => joinedPeer.id !== peer.id) .filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo)); .map((joinedPeer) => (joinedPeer.peerInfo));
const lobbyPeers = this._lobby.peerList();
cb(null, { cb(null, {
roles : peer.roles, roles : peer.roles,
peers : peerInfos, peers : peerInfos,
tracker : config.fileTracker, tracker : config.fileTracker,
authenticated : peer.authenticated, authenticated : peer.authenticated,
permissionsFromRoles : permissionsFromRoles, permissionsFromRoles : permissionsFromRoles,
userRoles : userRoles userRoles : userRoles,
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}); });
// Mark the new Peer as joined. // Mark the new Peer as joined.
@ -728,6 +748,13 @@ class Room extends EventEmitter
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
if (
appData.source === 'extravideo' &&
!peer.roles.some(
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
)
throw new Error('peer not authorized');
// Ensure the Peer is joined. // Ensure the Peer is joined.
if (!peer.joined) if (!peer.joined)
throw new Error('Peer not yet joined'); throw new Error('Peer not yet joined');
@ -1067,26 +1094,6 @@ class Room extends EventEmitter
break; break;
} }
case 'serverHistory':
{
// Return to sender
const lobbyPeers = this._lobby.peerList();
cb(
null,
{
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}
);
break;
}
case 'lockRoom': case 'lockRoom':
{ {
if ( if (
@ -1252,14 +1259,14 @@ class Room extends EventEmitter
break; break;
} }
case 'raiseHand': case 'raisedHand':
{ {
const { raisedHand } = request.data; const { raisedHand } = request.data;
peer.raisedHand = raisedHand; peer.raisedHand = raisedHand;
// Spread to others // Spread to others
this._notification(peer.socket, 'raiseHand', { this._notification(peer.socket, 'raisedHand', {
peerId : peer.id, peerId : peer.id,
raisedHand : raisedHand raisedHand : raisedHand
}, true); }, true);

View File

@ -1,6 +1,6 @@
{ {
"name": "multiparty-meeting-server", "name": "multiparty-meeting-server",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting server", "description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",