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
MIT
MIT License (see `LICENSE.md`)
Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project.

View File

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

View File

@ -233,6 +233,9 @@ export default class RoomClient
// Local webcam mediasoup Producer.
this._webcamProducer = null;
// Extra videos being produced
this._extraVideoProducers = new Map();
// Map of webcam MediaDeviceInfos indexed by deviceId.
// @type {Map<String, MediaDeviceInfos>}
this._webcams = {};
@ -515,6 +518,10 @@ export default class RoomClient
}
_soundNotification()
{
const { notificationSounds } = store.getState().settings;
if (notificationSounds)
{
const alertPromise = this._soundAlert.play();
@ -528,6 +535,7 @@ export default class RoomClient
});
}
}
}
timeoutCallback(callback)
{
@ -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()
{
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(
meActions.setMyRaiseHandStateInProgress(true));
meActions.setRaisedHandInProgress(true));
try
{
await this.sendRequest('raiseHand', { raiseHandState: state });
await this.sendRequest('raisedHand', { raisedHand });
store.dispatch(
meActions.setMyRaiseHandState(state));
meActions.setRaisedHand(raisedHand));
}
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
store.dispatch(meActions.setMyRaiseHandState(!state));
store.dispatch(meActions.setRaisedHand(!raisedHand));
}
store.dispatch(
meActions.setMyRaiseHandStateInProgress(false));
meActions.setRaisedHandInProgress(false));
}
async setMaxSendingSpatialLayer(spatialLayer)
@ -2053,6 +2005,8 @@ export default class RoomClient
store.dispatch(
roomActions.setToolbarsVisible(true));
this._soundNotification();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
@ -2064,6 +2018,43 @@ export default class RoomClient
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':
{
const { peerId } = notification.data;
@ -2222,6 +2213,48 @@ export default class RoomClient
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':
{
const { peerId, chatMessage } = notification.data;
@ -2318,6 +2351,8 @@ export default class RoomClient
store.dispatch(
peerActions.addPeer({ id, displayName, picture, roles, consumers: [] }));
this._soundNotification();
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
@ -2474,7 +2509,9 @@ export default class RoomClient
{
text : intl.formatMessage({
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({
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,
tracker,
permissionsFromRoles,
userRoles
userRoles,
chatHistory,
fileHistory,
lastNHistory,
locked,
lobbyPeers,
accessCode
} = await this.sendRequest(
'join',
{
@ -2749,6 +2794,38 @@ export default class RoomClient
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.
if (this._produce)
{
@ -2782,8 +2859,6 @@ export default class RoomClient
// Clean all the existing notifications.
store.dispatch(notificationActions.removeAllNotifications());
this.getServerHistory();
store.dispatch(requestActions.notify(
{
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()
{
if (this._micProducer)
@ -3433,6 +3661,37 @@ export default class RoomClient
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()
{
logger.debug('disableWebcam()');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,6 +78,15 @@ const styles = (theme) =>
zIndex : 21,
touchAction : 'none',
pointerEvents : 'none',
'&.hide' :
{
transition : 'opacity 0.1s ease-in-out',
opacity : 0
},
'&.hover' :
{
opacity : 1
},
'& p' :
{
position : 'absolute',
@ -107,7 +116,8 @@ const styles = (theme) =>
fontSize : '2vs',
backgroundColor : 'rgba(255, 0, 0, 0.5)',
margin : '4px',
padding : '15px',
padding : theme.spacing(2),
zIndex : 31,
borderRadius : '20px',
textAlign : 'center',
opacity : 0,
@ -140,6 +150,7 @@ const Me = (props) =>
micProducer,
webcamProducer,
screenProducer,
extraVideoProducers,
canShareScreen,
classes
} = props;
@ -289,8 +300,22 @@ const Me = (props) =>
style={spacingStyle}
>
<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
className={classes.controls}
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -318,17 +343,6 @@ const Me = (props) =>
/>
</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>
<Tooltip title={micTip} placement='left'>
<div>
@ -454,6 +468,112 @@ const Me = (props) =>
</VideoView>
</div>
</div>
{ extraVideoProducers.map((producer) =>
{
return (
<div key={producer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
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 &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -480,7 +600,11 @@ const Me = (props) =>
>
<div className={classes.viewContainer} style={style}>
<div
className={classes.controls}
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
@ -536,6 +660,7 @@ Me.propTypes =
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer,
extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
spacing : PropTypes.number,
style : PropTypes.object,
smallButtons : PropTypes.bool,

View File

@ -125,6 +125,7 @@ const Peer = (props) =>
micConsumer,
webcamConsumer,
screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen,
toggleConsumerWindow,
spacing,
@ -351,6 +352,161 @@ const Peer = (props) =>
</div>
</div>
{ extraVideoConsumers.map((consumer) =>
{
return (
<div key={consumer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ browser.platform !== 'mobile' &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<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 &&
<div
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
@ -508,6 +664,7 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool,
browser : PropTypes.object.isRequired,

View File

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

View File

@ -0,0 +1,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 PropTypes from 'prop-types';
import {
lobbyPeersKeySelector,
peersLengthSelector
peersLengthSelector,
raisedHandsSelector
} from '../Selectors';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
@ -13,11 +14,14 @@ import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge';
import ExtensionIcon from '@material-ui/icons/Extension';
import AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
@ -26,6 +30,7 @@ import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
@ -81,9 +86,17 @@ const styles = (theme) =>
margin : theme.spacing(1, 0),
padding : theme.spacing(0, 1)
},
disabledButton :
{
margin : theme.spacing(1, 0)
},
green :
{
color : 'rgba(0, 153, 0, 1)'
},
moreAction :
{
margin : theme.spacing(0, 0, 0, 1)
}
});
@ -122,6 +135,18 @@ const TopBar = (props) =>
{
const intl = useIntl();
const [ moreActionsElement, setMoreActionsElement ] = useState(null);
const handleMoreActionsOpen = (event) =>
{
setMoreActionsElement(event.currentTarget);
};
const handleMoreActionsClose = () =>
{
setMoreActionsElement(null);
};
const {
roomClient,
room,
@ -135,15 +160,19 @@ const TopBar = (props) =>
fullscreen,
onFullscreen,
setSettingsOpen,
setExtraVideoOpen,
setLockDialogOpen,
toggleToolArea,
openUsersTab,
unread,
canProduceExtraVideo,
canLock,
canPromote,
classes
} = props;
const isMoreActionsMenuOpen = Boolean(moreActionsElement);
const lockTooltip = room.locked ?
intl.formatMessage({
id : 'tooltip.unLockRoom',
@ -178,6 +207,7 @@ const TopBar = (props) =>
});
return (
<React.Fragment>
<AppBar
position='fixed'
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
@ -210,6 +240,13 @@ const TopBar = (props) =>
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<IconButton
aria-haspopup='true'
onClick={handleMoreActionsOpen}
color='inherit'
>
<ExtensionIcon />
</IconButton>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
@ -270,6 +307,7 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
<Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
@ -296,6 +334,7 @@ const TopBar = (props) =>
<LockOpenIcon />
}
</IconButton>
</span>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
@ -304,11 +343,13 @@ const TopBar = (props) =>
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)}
@ -320,6 +361,7 @@ const TopBar = (props) =>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</span>
</Tooltip>
}
{ loginEnabled &&
@ -339,7 +381,7 @@ const TopBar = (props) =>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn && classes.green} />
<AccountCircle className={loggedIn ? classes.green : null} />
}
</IconButton>
</Tooltip>
@ -363,6 +405,38 @@ const TopBar = (props) =>
</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({
id : 'label.addVideo',
defaultMessage : 'Add video'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</p>
</MenuItem>
</Menu>
</React.Fragment>
);
};
@ -381,10 +455,12 @@ TopBar.propTypes =
onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired,
setExtraVideoOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
canProduceExtraVideo : PropTypes.bool.isRequired,
canLock : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
@ -401,7 +477,10 @@ const mapStateToProps = (state) =>
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
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 :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)),
@ -418,11 +497,15 @@ const mapDispatchToProps = (dispatch) =>
},
setSettingsOpen : (settingsOpen) =>
{
dispatch(roomActions.setSettingsOpen({ settingsOpen }));
dispatch(roomActions.setSettingsOpen(settingsOpen));
},
setExtraVideoOpen : (extraVideoOpen) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
},
setLockDialogOpen : (lockDialogOpen) =>
{
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen }));
dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
},
toggleToolArea : () =>
{

View File

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

View File

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

View File

@ -1,79 +1,50 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types';
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 HandIcon from '../../../images/icon-hand-white.svg';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
listPeer :
{
display : 'flex'
display : 'flex',
padding : theme.spacing(1)
},
avatar :
{
borderRadius : '50%',
height : '2rem'
height : '2rem',
marginTop : theme.spacing(1)
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},
indicators :
green :
{
left : 0,
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
}
color : 'rgba(0, 153, 0, 1)'
}
});
const ListMe = (props) =>
{
const intl = useIntl();
const {
roomClient,
me,
settings,
classes
@ -82,26 +53,35 @@ const ListMe = (props) =>
const picture = me.picture || EmptyAvatar;
return (
<li className={classes.root}>
<div className={classes.listPeer}>
<div className={classes.root}>
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}>
{settings.displayName}
</div>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={me.raisedHand ? classes.green : null}
disabled={me.raisedHandInProgress}
onClick={(e) =>
{
e.stopPropagation();
<div className={classes.indicators}>
{ me.raisedHand &&
<div className={classnames(classes.icon, 'raise-hand')} />
}
roomClient.setRaisedHand(!me.raisedHand);
}}
>
<PanIcon />
</IconButton>
</div>
</div>
</li>
);
};
ListMe.propTypes =
{
roomClient : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
@ -112,7 +92,7 @@ const mapStateToProps = (state) => ({
settings : state.settings
});
export default connect(
export default withRoomContext(connect(
mapStateToProps,
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 { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
@ -16,31 +15,26 @@ import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
import PanIcon from '@material-ui/icons/PanTool';
const styles = (theme) =>
({
root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
listPeer :
{
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
height : '2rem',
marginTop : theme.spacing(1)
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : theme.spacing(1),
flexGrow : 1,
@ -48,52 +42,12 @@ const styles = (theme) =>
},
indicators :
{
left : 0,
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
padding : theme.spacing(1.5)
},
icon :
green :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.on' :
{
opacity : 1
},
'&.off' :
{
opacity : 0.2
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`
}
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
color : 'rgba(0, 153, 0, 1)'
}
});
@ -140,20 +94,10 @@ const ListPeer = (props) =>
{peer.displayName}
</div>
<div className={classes.indicators}>
{ peer.raiseHandState &&
<div className={
classnames(
classes.icon, 'raise-hand', {
on : peer.raiseHandState,
off : !peer.raiseHandState
}
)
}
/>
{ peer.raisedHand &&
<PanIcon className={classes.green} />
}
</div>
{children}
<div className={classes.controls}>
{ screenConsumer &&
<IconButton
aria-label={intl.formatMessage({
@ -239,7 +183,7 @@ const ListPeer = (props) =>
<ExitIcon />
</IconButton>
}
</div>
{children}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -235,6 +235,8 @@ module.exports =
MODERATE_CHAT : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
EXTRA_VIDEO : [ userRoles.NORMAL ],
// The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ],
// 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) =>
({
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,
displayName : this.displayName,
picture : this.picture,
roles : this.roles
roles : this.roles,
raisedHand : this.raisedHand
};
return peerInfo;

View File

@ -24,6 +24,7 @@ const permissionsFromRoles =
SEND_CHAT : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ],
@ -530,6 +531,17 @@ class Room extends EventEmitter
peerId : peer.id,
role : newRole
}, 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 }) =>
@ -586,13 +598,21 @@ class Room extends EventEmitter
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo));
const lobbyPeers = this._lobby.peerList();
cb(null, {
roles : peer.roles,
peers : peerInfos,
tracker : config.fileTracker,
authenticated : peer.authenticated,
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.
@ -728,6 +748,13 @@ class Room extends EventEmitter
)
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.
if (!peer.joined)
throw new Error('Peer not yet joined');
@ -1067,26 +1094,6 @@ class Room extends EventEmitter
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':
{
if (
@ -1252,14 +1259,14 @@ class Room extends EventEmitter
break;
}
case 'raiseHand':
case 'raisedHand':
{
const { raisedHand } = request.data;
peer.raisedHand = raisedHand;
// Spread to others
this._notification(peer.socket, 'raiseHand', {
this._notification(peer.socket, 'raisedHand', {
peerId : peer.id,
raisedHand : raisedHand
}, true);

View File

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