From 08e2c425c66843d0c4492a0d630799a3ec14a4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 4 May 2020 00:16:23 +0200 Subject: [PATCH] Add the ability for a peer to have several video producers in a room. --- app/src/RoomClient.js | 187 ++++++++++ app/src/actions/roomActions.js | 6 + app/src/components/Containers/Me.js | 136 +++++++- app/src/components/Containers/Peer.js | 157 +++++++++ app/src/components/Controls/ExtraVideo.js | 167 +++++++++ app/src/components/Controls/TopBar.js | 398 +++++++++++++--------- app/src/components/Room.js | 5 + app/src/components/Selectors.js | 49 ++- app/src/components/appPropTypes.js | 6 +- app/src/reducers/room.js | 8 + app/src/translations/cn.json | 2 + app/src/translations/cs.json | 2 + app/src/translations/de.json | 2 + app/src/translations/dk.json | 2 + app/src/translations/el.json | 2 + app/src/translations/en.json | 2 + app/src/translations/es.json | 2 + app/src/translations/fr.json | 2 + app/src/translations/hr.json | 2 + app/src/translations/hu.json | 2 + app/src/translations/it.json | 2 + app/src/translations/nb.json | 2 + app/src/translations/pl.json | 2 + app/src/translations/pt.json | 2 + app/src/translations/ro.json | 2 + app/src/translations/tr.json | 2 + app/src/translations/uk.json | 2 + 27 files changed, 962 insertions(+), 191 deletions(-) create mode 100644 app/src/components/Controls/ExtraVideo.js diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 488ed24..d52631d 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -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} this._webcams = {}; @@ -3005,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) @@ -3505,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()'); diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index 4ac0912..ddcfcc4 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -58,6 +58,12 @@ export const setSettingsOpen = (settingsOpen) => payload : { settingsOpen } }); +export const setExtraVideoOpen = (extraVideoOpen) => + ({ + type : 'SET_EXTRA_VIDEO_OPEN', + payload : { extraVideoOpen } + }); + export const setSettingsTab = (tab) => ({ type : 'SET_SETTINGS_TAB', diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index b488952..5e8e0dc 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -150,6 +150,7 @@ const Me = (props) => micProducer, webcamProducer, screenProducer, + extraVideoProducers, canShareScreen, classes } = props; @@ -467,6 +468,112 @@ const Me = (props) => + { extraVideoProducers.map((producer) => + { + return ( +
setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + style={spacingStyle} + > +
+
setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + > +

+ +

+ + +
+ + { + roomClient.disableExtraVideo(producer.id); + }} + > + + +
+
+
+ + + { + roomClient.changeDisplayName(displayName); + }} + /> +
+
+ ); + })} { screenProducer &&
Me.propTypes = { - roomClient : PropTypes.any.isRequired, - advancedMode : PropTypes.bool, - me : appPropTypes.Me.isRequired, - settings : PropTypes.object, - activeSpeaker : PropTypes.bool, - micProducer : appPropTypes.Producer, - webcamProducer : appPropTypes.Producer, - screenProducer : appPropTypes.Producer, - spacing : PropTypes.number, - style : PropTypes.object, - smallButtons : PropTypes.bool, - canShareScreen : PropTypes.bool.isRequired, - classes : PropTypes.object.isRequired, - theme : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + advancedMode : PropTypes.bool, + me : appPropTypes.Me.isRequired, + settings : PropTypes.object, + activeSpeaker : PropTypes.bool, + micProducer : appPropTypes.Producer, + webcamProducer : appPropTypes.Producer, + screenProducer : appPropTypes.Producer, + extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer), + spacing : PropTypes.number, + style : PropTypes.object, + smallButtons : PropTypes.bool, + canShareScreen : PropTypes.bool.isRequired, + classes : PropTypes.object.isRequired, + theme : PropTypes.object.isRequired }; const mapStateToProps = (state) => diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 827550b..3e6e776 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -125,6 +125,7 @@ const Peer = (props) => micConsumer, webcamConsumer, screenConsumer, + extraVideoConsumers, toggleConsumerFullscreen, toggleConsumerWindow, spacing, @@ -351,6 +352,161 @@ const Peer = (props) =>
+ { extraVideoConsumers.map((consumer) => + { + return ( +
setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + style={rootStyle} + > +
+ { !videoVisible && +
+

+ +

+
+ } + +
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' && + +
+ + { + toggleConsumerWindow(consumer); + }} + > + + +
+
+ } + + +
+ + { + toggleConsumerFullscreen(consumer); + }} + > + + +
+
+
+ + +
+
+ ); + })} + { screenConsumer &&
+ ({ + 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 ( + handleCloseExtraVideo(false)} + classes={{ + paper : classes.dialogPaper + }} + > + + + +
+ + + + { videoDevices.length > 0 ? + intl.formatMessage({ + id : 'settings.selectCamera', + defaultMessage : 'Select video device' + }) + : + intl.formatMessage({ + id : 'settings.cantSelectCamera', + defaultMessage : 'Unable to select video device' + }) + } + + +
+ + + +
+ ); +}; + +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))); \ No newline at end of file diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 89bf385..64e56d5 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { @@ -14,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'; @@ -27,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'; @@ -89,6 +93,10 @@ const styles = (theme) => green : { color : 'rgba(0, 153, 0, 1)' + }, + moreAction : + { + margin : theme.spacing(0, 0, 0, 1) } }); @@ -127,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, @@ -140,6 +160,7 @@ const TopBar = (props) => fullscreen, onFullscreen, setSettingsOpen, + setExtraVideoOpen, setLockDialogOpen, toggleToolArea, openUsersTab, @@ -149,6 +170,8 @@ const TopBar = (props) => classes } = props; + const isMoreActionsMenuOpen = Boolean(moreActionsElement); + const lockTooltip = room.locked ? intl.formatMessage({ id : 'tooltip.unLockRoom', @@ -183,196 +206,230 @@ const TopBar = (props) => }); return ( - - - toggleToolArea()} - > - + + + toggleToolArea()} + > + + + + + { window.config.logo && Logo } + - - - - { window.config.logo && Logo } - - { window.config.title ? window.config.title : 'Multiparty meeting' } - -
-
- { fullscreenEnabled && - - - { fullscreen ? - - : - - } - - - } - + { window.config.title ? window.config.title : 'Multiparty meeting' } + +
+
openUsersTab()} > - - - + - - - setSettingsOpen(!room.settingsOpen)} - > - - - - - - - { - if (room.locked) - { - roomClient.unlockRoom(); - } - else - { - roomClient.lockRoom(); - } - }} - > - { room.locked ? - - : - - } - - - - { lobbyPeers.length > 0 && - - + { fullscreenEnabled && + setLockDialogOpen(!room.lockDialogOpen)} + onClick={onFullscreen} > - - - + { fullscreen ? + + : + + } - - - } - { loginEnabled && - + + } + openUsersTab()} + > + + + + + + + - { - loggedIn ? roomClient.logout() : roomClient.login(); - }} + onClick={() => setSettingsOpen(!room.settingsOpen)} > - { myPicture ? - - : - - } + - } -
- +
+ + + + + { + handleMoreActionsClose(); + setExtraVideoOpen(!room.extraVideoOpen); + }} + > + roomClient.close()} - > - - -
- - + /> +

Add video

+ + + ); }; @@ -391,6 +448,7 @@ 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, @@ -430,6 +488,10 @@ const mapDispatchToProps = (dispatch) => { dispatch(roomActions.setSettingsOpen(settingsOpen)); }, + setExtraVideoOpen : (extraVideoOpen) => + { + dispatch(roomActions.setExtraVideoOpen(extraVideoOpen)); + }, setLockDialogOpen : (lockDialogOpen) => { dispatch(roomActions.setLockDialogOpen(lockDialogOpen)); diff --git a/app/src/components/Room.js b/app/src/components/Room.js index fbd4cc5..cdda66f 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -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 && } + + { room.extraVideoOpen && + + }
); } diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index a33c171..fd22aff 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -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, @@ -114,21 +137,33 @@ export const raisedHandsSelector = createSelector( 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 }; } ); @@ -151,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 }; } ); }; diff --git a/app/src/components/appPropTypes.js b/app/src/components/appPropTypes.js index 0969c7b..0ae4fc2 100644 --- a/app/src/components/appPropTypes.js +++ b/app/src/components/appPropTypes.js @@ -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' ]), diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 08fa664..35c1e12 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -20,6 +20,7 @@ const initialState = selectedPeerId : null, spotlights : [], settingsOpen : false, + extraVideoOpen : false, currentSettingsTab : 'media', // media, appearence, advanced lockDialogOpen : false, joined : false, @@ -114,6 +115,13 @@ 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; diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index 1dd2f75..77de748 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "设置", "settings.camera": "视频设备", diff --git a/app/src/translations/cs.json b/app/src/translations/cs.json index 7cf5ddd..a6a9af4 100644 --- a/app/src/translations/cs.json +++ b/app/src/translations/cs.json @@ -57,6 +57,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -103,6 +104,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Nastavení", "settings.camera": "Kamera", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index 5e69940..9820bcd 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen", @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Einstellungen", "settings.camera": "Kamera", diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index 096f14b..163b51d 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Indstillinger", "settings.camera": "Kamera", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index c5de505..6b6cdab 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Ρυθμίσεις", "settings.camera": "Κάμερα", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 88bb3be..38414b7 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -58,6 +58,7 @@ "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", @@ -104,6 +105,7 @@ "label.media": "Media", "label.appearence": "Appearence", "label.advanced": "Advanced", + "label.addVideo": "Add video", "settings.settings": "Settings", "settings.camera": "Camera", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index 032493d..b149a8a 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Ajustes", "settings.camera": "Cámara", diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index 67beebe..6d694e3 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Paramètres", "settings.camera": "Caméra", diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index 28c5c0f..c047ce4 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -58,6 +58,7 @@ "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", @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Postavke", "settings.camera": "Kamera", diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index e85ffa2..44e9931 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Beállítások", "settings.camera": "Kamera", diff --git a/app/src/translations/it.json b/app/src/translations/it.json index 3166480..302a8fb 100644 --- a/app/src/translations/it.json +++ b/app/src/translations/it.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -103,6 +104,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Impostazioni", "settings.camera": "Videocamera", diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index fb741b0..375ffae 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -58,6 +58,7 @@ "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", @@ -104,6 +105,7 @@ "label.media": "Media", "label.appearence": "Utseende", "label.advanced": "Avansert", + "label.addVideo": "Legg til video", "settings.settings": "Innstillinger", "settings.camera": "Kamera", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index dee0dcc..75a6b15 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Ustawienia", "settings.camera": "Kamera", diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index af6b783..a823925 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Definições", "settings.camera": "Camera", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index e37dda3..c63133f 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Setări", "settings.camera": "Cameră video", diff --git a/app/src/translations/tr.json b/app/src/translations/tr.json index 62a22ed..8502bb3 100644 --- a/app/src/translations/tr.json +++ b/app/src/translations/tr.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Ayarlar", "settings.camera": "Kamera", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json index de5b736..b783913 100644 --- a/app/src/translations/uk.json +++ b/app/src/translations/uk.json @@ -58,6 +58,7 @@ "room.moderatoractions": null, "room.raisedHand": null, "room.loweredHand": null, + "room.extraVideo": null, "me.mutedPTT": null, @@ -104,6 +105,7 @@ "label.media": null, "label.appearence": null, "label.advanced": null, + "label.addVideo": null, "settings.settings": "Налаштування", "settings.camera": "Камера",