diff --git a/.gitignore b/.gitignore index 5c12d1eb..aaed6d85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,12 @@ node_modules/ /app/build/ /app/public/config/config.js /app/public/images/logo.* -!/app/public/images/logo.example.png +!/app/public/images/logo.edumeet.svg /server/config/ !/server/config/config.example.js /server/public/ /server/certs/ !/server/certs/mediasoup-demo.localhost.* .vscode -/app/public/config/*.pem \ No newline at end of file +/app/public/config/*.pem +yarn.lock \ No newline at end of file diff --git a/README.md b/README.md index 210557ce..1c6cde41 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ If you want the ansible approach, you can find ansible role [here](https://githu If you want to install it on the Debian & Ubuntu based operating systems. * Prerequisites: -edumeet will run on nodejs v10.x and later versions. (v12.x has a know issue for now, please until it will be fixed use the 10.x version) +edumeet will run on nodejs v14.x (tested with v14.15.4 version). To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall). * Download .deb package from [here](https://github.com/edumeet/edumeet/actions?query=workflow%3ADeployer+branch%3Amaster+is%3Asuccess) (job artifact) @@ -61,7 +61,7 @@ $ sudo systemctl start edumeet ## Manual installation * Prerequisites: -Currently edumeet will only run on nodejs v13.x +Currently edumeet will run on nodejs v14.x To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall). ```bash diff --git a/app/package.json b/app/package.json index 4a82bd0b..085c4025 100644 --- a/app/package.json +++ b/app/package.json @@ -18,6 +18,7 @@ "@material-ui/core": "^4.5.1", "@material-ui/icons": "^4.5.1", "@material-ui/lab": "^4.0.0-alpha.56", + "@react-hook/window-size": "^3.0.7", "bowser": "^2.7.0", "classnames": "^2.2.6", "create-torrent": "^4.4.1", @@ -29,7 +30,7 @@ "is-electron": "^2.2.0", "marked": "^0.8.0", "material-ui-popup-state": "^1.7.0", - "mediasoup-client": "^3.6.5", + "mediasoup-client": "^3.6.21", "notistack": "^0.9.5", "prop-types": "^15.7.2", "random-string": "^0.2.0", @@ -37,6 +38,8 @@ "react-cookie-consent": "^2.5.0", "react-dom": "^16.10.2", "react-flip-toolkit": "^7.0.9", + "react-image-file-resizer": "^0.3.8", + "react-images-upload": "^1.2.0", "react-intl": "^3.4.0", "react-intl-redux": "^2.2.0", "react-redux": "^7.1.1", diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index 1742aab2..16836056 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -4,6 +4,9 @@ var config = loginEnabled : false, developmentPort : 3443, productionPort : 443, + // If the server component runs on a different host than the app + // you can uncomment the following line and specify the host name. + //serverHostname : 'external-server.com', /** * Supported browsers version @@ -74,6 +77,11 @@ var config = { scaleResolutionDownBy: 2 }, { scaleResolutionDownBy: 1 } ], + // The adaptive spatial layer selection scaling factor (in the range [0.5, 1.0]) + // example: + // with level width=640px, the minimum width required to trigger the + // level change will be: 640 * 0.75 = 480px + adaptiveScalingFactor: 0.75, /** * Alternative simulcast setting: @@ -163,7 +171,7 @@ var config = lockLastN : false, // Show logo if "logo" is not null, else show title // Set logo file name using logo.* pattern like "logo.png" to not track it by git - logo : 'images/logo.example.png', + logo : 'images/logo.edumeet.svg', title : 'edumeet', // Service & Support URL // if not set then not displayed on the about modals diff --git a/app/public/images/logo.edumeet.svg b/app/public/images/logo.edumeet.svg new file mode 100644 index 00000000..717c04e8 --- /dev/null +++ b/app/public/images/logo.edumeet.svg @@ -0,0 +1,88 @@ + + + +image/svg+xml diff --git a/app/public/images/logo.example.png b/app/public/images/logo.example.png deleted file mode 100644 index 514f602d..00000000 Binary files a/app/public/images/logo.example.png and /dev/null differ diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 4cb33c14..02ad2896 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -20,6 +20,7 @@ import Spotlights from './Spotlights'; import { permissions } from './permissions'; import * as locales from './translations/locales'; import { createIntl } from 'react-intl'; +import { parseScalabilityMode } from 'mediasoup-client'; let createTorrent; @@ -102,6 +103,67 @@ const VIDEO_SVC_ENCODINGS = { scalabilityMode: 'S3T3', dtx: true } ]; +/** + * Validates the simulcast `encodings` array extracting the resolution scalings + * array. + * ref. https://www.w3.org/TR/webrtc/#rtp-media-api + * + * @param {*} encodings + * @returns the resolution scalings array + */ +function getResolutionScalings(encodings) +{ + const resolutionScalings = []; + + // SVC encodings + if (encodings.length === 1) + { + const { spatialLayers, temporalLayers } = + mediasoupClient.parseScalabilityMode(encodings[0].scalabilityMode); + + for (let i=0; i < spatialLayers; i++) + { + resolutionScalings.push(2 ** (spatialLayers - i - 1)); + } + + return resolutionScalings; + } + + // Simulcast encodings + let scaleResolutionDownByDefined = false; + + encodings.forEach((encoding) => + { + if (encoding.scaleResolutionDownBy !== undefined) + { + // at least one scaleResolutionDownBy is defined + scaleResolutionDownByDefined = true; + // scaleResolutionDownBy must be >= 1.0 + resolutionScalings.push(Math.max(1.0, encoding.scaleResolutionDownBy)); + } + else + { + // If encodings contains any encoding whose scaleResolutionDownBy + // attribute is defined, set any undefined scaleResolutionDownBy + // of the other encodings to 1.0. + resolutionScalings.push(1.0); + } + }); + + // If the scaleResolutionDownBy attribues of sendEncodings are + // still undefined, initialize each encoding's scaleResolutionDownBy + // to 2^(length of sendEncodings - encoding index - 1). + if (!scaleResolutionDownByDefined) + { + encodings.forEach((encoding, index) => + { + resolutionScalings[index] = 2 ** (encodings.length - index - 1); + }); + } + + return resolutionScalings; +} + let store; let intl; @@ -259,6 +321,10 @@ export default class RoomClient this.setLocale(store.getState().intl.locale); + if (store.getState().settings.localPicture) + { + store.dispatch(meActions.setPicture(store.getState().settings.localPicture)); + } } close() @@ -542,6 +608,13 @@ export default class RoomClient } + setPicture(picture) + { + store.dispatch(settingsActions.setLocalPicture(picture)); + store.dispatch(meActions.setPicture(picture)); + this.changePicture(picture); + } + receiveLoginChildWindow(data) { logger.debug('receiveFromChildWindow() | [data:"%o"]', data); @@ -549,7 +622,11 @@ export default class RoomClient const { displayName, picture } = data; store.dispatch(settingsActions.setDisplayName(displayName)); - store.dispatch(meActions.setPicture(picture)); + + if (!store.getState().settings.localPicture) + { + store.dispatch(meActions.setPicture(picture)); + } store.dispatch(meActions.loggedIn(true)); @@ -566,7 +643,10 @@ export default class RoomClient { logger.debug('receiveLogoutChildWindow()'); - store.dispatch(meActions.setPicture(null)); + if (!store.getState().settings.localPicture) + { + store.dispatch(meActions.setPicture(null)); + } store.dispatch(meActions.loggedIn(false)); @@ -1479,7 +1559,7 @@ export default class RoomClient ([ track ] = stream.getVideoTracks()); - const { deviceId: trackDeviceId } = track.getSettings(); + const { deviceId: trackDeviceId, width, height } = track.getSettings(); store.dispatch(settingsActions.setSelectedWebcamDevice(trackDeviceId)); @@ -1500,6 +1580,8 @@ export default class RoomClient else encodings = VIDEO_SIMULCAST_ENCODINGS; + const resolutionScalings = getResolutionScalings(encodings); + this._webcamProducer = await this._sendTransport.produce( { track, @@ -1510,7 +1592,10 @@ export default class RoomClient }, appData : { - source : 'webcam' + source : 'webcam', + width, + height, + resolutionScalings } }); } @@ -1520,7 +1605,9 @@ export default class RoomClient track, appData : { - source : 'webcam' + source : 'webcam', + width, + height } }); } @@ -2084,6 +2171,130 @@ export default class RoomClient } } + async restartIce() + { + logger.debug('restartIce()'); + + try + { + if (this._sendTransport) + { + const iceParameters = await this.sendRequest( + 'restartIce', + { transportId: this._sendTransport.id }); + + await this._sendTransport.restartIce({ iceParameters }); + } + + if (this._recvTransport) + { + const iceParameters = await this.sendRequest( + 'restartIce', + { transportId: this._recvTransport.id }); + + await this._recvTransport.restartIce({ iceParameters }); + } + + logger.debug('ICE restarted'); + } + catch (error) + { + logger.error('restartIce() | failed:%o', error); + } + } + + setConsumerPreferredLayersMax(consumer) + { + if (consumer.type === 'simple') + { + return; + } + + logger.debug( + 'setConsumerPreferredLayersMax() [consumerId:"%s"]', consumer.id); + + if (consumer.preferredSpatialLayer !== consumer.spatialLayers -1 || + consumer.preferredTemporalLayer !== consumer.temporalLayers -1) + { + return this.setConsumerPreferredLayers(consumer.id, + consumer.spatialLayers - 1, consumer.temporalLayers - 1); + } + } + + adaptConsumerPreferredLayers(consumer, viewportWidth, viewportHeight) + { + if (consumer.type === 'simple') + { + return; + } + + if (!viewportWidth || !viewportHeight) + { + return; + } + + const { + id, + preferredSpatialLayer, + preferredTemporalLayer, + width, + height, + resolutionScalings + } = consumer; + const adaptiveScalingFactor = Math.min(Math.max( + window.config.adaptiveScalingFactor || 0.75, 0.5), 1.0); + + logger.debug( + 'adaptConsumerPreferredLayers() [consumerId:"%s", width:"%d", height:"%d" resolutionScalings:[%s] viewportWidth:"%d", viewportHeight:"%d"]', + consumer.id, width, height, resolutionScalings.join(', '), + viewportWidth, viewportHeight); + + let newPreferredSpatialLayer = 0; + + for (let i = 0; i < resolutionScalings.length; i++) + { + const levelWidth = adaptiveScalingFactor * width / resolutionScalings[i]; + const levelHeight = adaptiveScalingFactor * height / resolutionScalings[i]; + + if (viewportWidth >= levelWidth || viewportHeight >= levelHeight) + { + newPreferredSpatialLayer = i; + } + else + { + break; + } + } + + let newPreferredTemporalLayer = consumer.temporalLayers - 1; + + if (newPreferredSpatialLayer === 0 && newPreferredTemporalLayer > 0) + { + const lowestLevelWidth = width / resolutionScalings[0]; + const lowestLevelHeight = height / resolutionScalings[0]; + + if (viewportWidth < lowestLevelWidth * 0.5 + && viewportHeight < lowestLevelHeight * 0.5) + { + newPreferredTemporalLayer -= 1; + } + if (newPreferredTemporalLayer > 0 + && viewportWidth < lowestLevelWidth * 0.25 + && viewportHeight < lowestLevelHeight * 0.25) + { + newPreferredTemporalLayer -= 1; + } + } + + if (preferredSpatialLayer !== newPreferredSpatialLayer || + preferredTemporalLayer !== newPreferredTemporalLayer) + { + return this.setConsumerPreferredLayers(id, + newPreferredSpatialLayer, newPreferredTemporalLayer); + } + + } + async setConsumerPriority(consumerId, priority) { logger.debug( @@ -2344,6 +2555,9 @@ export default class RoomClient remotelyPaused : producerPaused, rtpParameters : consumer.rtpParameters, source : consumer.appData.source, + width : consumer.appData.width, + height : consumer.appData.height, + resolutionScalings : consumer.appData.resolutionScalings, spatialLayers : spatialLayers, temporalLayers : temporalLayers, preferredSpatialLayer : spatialLayers - 1, @@ -3186,6 +3400,21 @@ export default class RoomClient .catch(errback); }); + this._sendTransport.on( + 'connectionstatechange', (connectState) => + { + switch (connectState) + { + case 'disconnected': + case 'failed': + this.restartIce(); + break; + + default: + break; + } + }); + this._sendTransport.on( 'produce', async ({ kind, rtpParameters, appData }, callback, errback) => { @@ -3249,6 +3478,21 @@ export default class RoomClient .catch(errback); }); + this._recvTransport.on( + 'connectionstatechange', (connectState) => + { + switch (connectState) + { + case 'disconnected': + case 'failed': + this.restartIce(); + break; + + default: + break; + } + }); + // Set our media capabilities. store.dispatch(meActions.setMediaCapabilities( { @@ -3377,8 +3621,10 @@ export default class RoomClient { autoMuteThreshold = window.config.autoMuteThreshold; } - if (autoMuteThreshold && peers.length >= autoMuteThreshold) + if (autoMuteThreshold >= 0 && peers.length >= autoMuteThreshold) + { this.muteMic(); + } } } diff --git a/app/src/actions/settingsActions.js b/app/src/actions/settingsActions.js index 28a8c71a..50a72b40 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -149,4 +149,9 @@ export const setVideoMuted = (videoMuted) => ({ type : 'SET_VIDEO_MUTED', payload : { videoMuted } - }); \ No newline at end of file + }); +export const setLocalPicture = (localPicture) => + ({ + type : 'SET_LOCAL_PICTURE', + payload : { localPicture } + }); diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 93d7cb7c..3437a1b6 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -316,6 +316,12 @@ const Me = (props) => 'margin' : spacing }; + if (me.picture) + { + spacingStyle.backgroundImage = `url(${me.picture})`; + spacingStyle.backgroundSize = 'auto 100%'; + } + let audioScore = null; if (micProducer && micProducer.score) diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index c81627bc..7ecf3591 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import { makePeerConsumerSelector } from '../Selectors'; import PropTypes from 'prop-types'; @@ -110,7 +110,16 @@ const styles = (theme) => alignItems : 'center', padding : theme.spacing(1), zIndex : 20, - '& p' : + '&.hide' : + { + transition : 'opacity 0.1s ease-in-out', + opacity : 0 + }, + '&.hover' : + { + opacity : 1 + }, + '& p' : { padding : '6px 12px', borderRadius : 6, @@ -146,8 +155,12 @@ const Peer = (props) => style, smallContainer, windowConsumer, + fullScreenConsumer, classes, - theme + theme, + enableLayersSwitch, + width, + height } = props; const micEnabled = ( @@ -176,6 +189,40 @@ const Peer = (props) => ...style }; + if (peer.picture) + { + rootStyle.backgroundImage = `url(${peer.picture})`; + rootStyle.backgroundSize = 'auto 100%'; + } + + useEffect(() => + { + const handler = setTimeout(() => + { + if (!webcamConsumer) + return; + + if (windowConsumer === webcamConsumer.id) + { + // if playing in external window, set the maximum quality levels + roomClient.setConsumerPreferredLayersMax(webcamConsumer); + } + else if (enableLayersSwitch && webcamConsumer?.type !== 'simple' + && fullScreenConsumer !== webcamConsumer.id) + { + roomClient.adaptConsumerPreferredLayers(webcamConsumer, width, height); + } + }, 1000); + + return () => { clearTimeout(handler); }; + }, [ + enableLayersSwitch, + webcamConsumer, + windowConsumer, + fullScreenConsumer, + roomClient, width, height + ]); + return (
>
{ !videoVisible && -
+

videoCodec={webcamConsumer && webcamConsumer.codec} audioScore={micConsumer ? micConsumer.score : null} videoScore={webcamConsumer ? webcamConsumer.score : null} + width={width} + height={height} > @@ -614,6 +668,8 @@ const Peer = (props) => videoVisible={videoVisible} videoCodec={consumer && consumer.codec} videoScore={consumer ? consumer.score : null} + width={width} + height={height} />

@@ -758,6 +814,8 @@ const Peer = (props) => videoVisible={screenVisible} videoCodec={screenConsumer && screenConsumer.codec} videoScore={screenConsumer ? screenConsumer.score : null} + width={width} + height={height} />
@@ -776,6 +834,7 @@ Peer.propTypes = screenConsumer : appPropTypes.Consumer, extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer), windowConsumer : PropTypes.string, + fullScreenConsumer : PropTypes.string, activeSpeaker : PropTypes.bool, browser : PropTypes.object.isRequired, spacing : PropTypes.number, @@ -784,7 +843,10 @@ Peer.propTypes = toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired, classes : PropTypes.object.isRequired, - theme : PropTypes.object.isRequired + theme : PropTypes.object.isRequired, + enableLayersSwitch : PropTypes.bool, + width : PropTypes.number, + height : PropTypes.number }; const makeMapStateToProps = (initialState, { id }) => @@ -794,11 +856,12 @@ const makeMapStateToProps = (initialState, { id }) => const mapStateToProps = (state) => { return { - peer : state.peers[id], + peer : state.peers[id], ...getPeerConsumers(state, id), - windowConsumer : state.room.windowConsumer, - activeSpeaker : id === state.room.activeSpeakerId, - browser : state.me.browser + windowConsumer : state.room.windowConsumer, + fullScreenConsumer : state.room.fullScreenConsumer, + activeSpeaker : id === state.room.activeSpeakerId, + browser : state.me.browser }; }; @@ -833,7 +896,11 @@ export default withRoomContext(connect( prev.consumers === next.consumers && prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.windowConsumer === next.room.windowConsumer && - prev.me.browser === next.me.browser + prev.room.fullScreenConsumer === next.room.fullScreenConsumer && + prev.me.browser === next.me.browser && + prev.enableLayersSwitch === next.enableLayersSwitch && + prev.width === next.width && + prev.height === next.height ); } } diff --git a/app/src/components/Containers/SpeakerPeer.js b/app/src/components/Containers/SpeakerPeer.js index 3edb7c00..b4389a40 100644 --- a/app/src/components/Containers/SpeakerPeer.js +++ b/app/src/components/Containers/SpeakerPeer.js @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { makePeerConsumerSelector } from '../Selectors'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; +import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { FormattedMessage } from 'react-intl'; import VideoView from '../VideoContainers/VideoView'; @@ -69,14 +70,19 @@ const styles = (theme) => const SpeakerPeer = (props) => { const { + roomClient, advancedMode, peer, micConsumer, webcamConsumer, screenConsumer, + windowConsumer, + fullScreenConsumer, spacing, style, - classes + classes, + width, + height } = props; const videoVisible = ( @@ -96,6 +102,33 @@ const SpeakerPeer = (props) => 'margin' : spacing }; + useEffect(() => + { + const handler = setTimeout(() => + { + if (!webcamConsumer) + return; + + if (windowConsumer === webcamConsumer.id) + { + // if playing in external window, set the maximum quality levels + roomClient.setConsumerPreferredLayersMax(webcamConsumer); + } + else if (webcamConsumer?.type !== 'simple' + && fullScreenConsumer !== webcamConsumer.id) + { + roomClient.adaptConsumerPreferredLayers(webcamConsumer, width, height); + } + }, 1000); + + return () => { clearTimeout(handler); }; + }, [ + webcamConsumer, + windowConsumer, + fullScreenConsumer, + roomClient, width, height + ]); + return (
videoCodec={webcamConsumer && webcamConsumer.codec} audioScore={micConsumer ? micConsumer.score : null} videoScore={webcamConsumer ? webcamConsumer.score : null} + width={width} + height={height} > @@ -205,14 +240,19 @@ const SpeakerPeer = (props) => SpeakerPeer.propTypes = { - advancedMode : PropTypes.bool, - peer : appPropTypes.Peer, - micConsumer : appPropTypes.Consumer, - webcamConsumer : appPropTypes.Consumer, - screenConsumer : appPropTypes.Consumer, - spacing : PropTypes.number, - style : PropTypes.object, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + advancedMode : PropTypes.bool, + peer : appPropTypes.Peer, + micConsumer : appPropTypes.Consumer, + webcamConsumer : appPropTypes.Consumer, + screenConsumer : appPropTypes.Consumer, + windowConsumer : PropTypes.string, + fullScreenConsumer : PropTypes.string, + spacing : PropTypes.number, + style : PropTypes.object, + classes : PropTypes.object.isRequired, + width : PropTypes.number, + height : PropTypes.number }; const mapStateToProps = (state, { id }) => @@ -220,12 +260,14 @@ const mapStateToProps = (state, { id }) => const getPeerConsumers = makePeerConsumerSelector(); return { - peer : state.peers[id], - ...getPeerConsumers(state, id) + peer : state.peers[id], + ...getPeerConsumers(state, id), + windowConsumer : state.room.windowConsumer, + fullScreenConsumer : state.room.fullScreenConsumer }; }; -export default connect( +export default withRoomContext(connect( mapStateToProps, null, null, @@ -235,8 +277,10 @@ export default connect( return ( prev.peers === next.peers && prev.consumers === next.consumers && - prev.room.activeSpeakerId === next.room.activeSpeakerId + prev.room.activeSpeakerId === next.room.activeSpeakerId && + prev.width === next.width && + prev.height === next.height ); } } -)(withStyles(styles, { withTheme: true })(SpeakerPeer)); +)(withStyles(styles, { withTheme: true })(SpeakerPeer))); diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 12e94acb..64336cc9 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -23,7 +23,6 @@ import Popover from '@material-ui/core/Popover'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; -import Avatar from '@material-ui/core/Avatar'; import Badge from '@material-ui/core/Badge'; import Paper from '@material-ui/core/Paper'; import AccountCircle from '@material-ui/icons/AccountCircle'; @@ -217,7 +216,6 @@ const TopBar = (props) => drawerOverlayed, toolAreaOpen, isMobile, - myPicture, loggedIn, loginEnabled, fullscreenEnabled, @@ -468,11 +466,7 @@ const TopBar = (props) => loggedIn ? roomClient.logout() : roomClient.login(); }} > - { myPicture ? - - : - - } + } @@ -692,11 +686,7 @@ const TopBar = (props) => loggedIn ? roomClient.logout() : roomClient.login(); }} > - { myPicture ? - - : - - } + { loggedIn ?

toolAreaOpen : state.toolarea.toolAreaOpen, loggedIn : state.me.loggedIn, loginEnabled : state.me.loginEnabled, - myPicture : state.me.picture, unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles + raisedHandsSelector(state), canProduceExtraVideo : hasExtraVideoPermission(state), diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index c1c1e6a7..3d284e48 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -11,7 +11,6 @@ import { useIntl, FormattedMessage } from 'react-intl'; import Dialog from '@material-ui/core/Dialog'; import DialogContentText from '@material-ui/core/DialogContentText'; import AccountCircle from '@material-ui/icons/AccountCircle'; -import Avatar from '@material-ui/core/Avatar'; import Typography from '@material-ui/core/Typography'; import FormControl from '@material-ui/core/FormControl'; import FormLabel from '@material-ui/core/FormLabel'; @@ -169,7 +168,6 @@ const JoinDialog = ({ displayName, displayNameInProgress, loggedIn, - myPicture, changeDisplayName, setMediaPerms, classes, @@ -413,21 +411,14 @@ const JoinDialog = ({ () => roomClient.login(roomId) } > - { myPicture ? - - : - - } + @@ -782,7 +773,6 @@ JoinDialog.propTypes = displayNameInProgress : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired, loggedIn : PropTypes.bool.isRequired, - myPicture : PropTypes.string, changeDisplayName : PropTypes.func.isRequired, setMediaPerms : PropTypes.func.isRequired, classes : PropTypes.object.isRequired, @@ -803,7 +793,6 @@ const mapStateToProps = (state) => displayNameInProgress : state.me.displayNameInProgress, loginEnabled : state.me.loginEnabled, loggedIn : state.me.loggedIn, - myPicture : state.me.picture, locale : state.intl.locale, localesList : state.intl.list diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index 27319e7a..eb9c68e8 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -192,6 +192,9 @@ class Democratic extends React.PureComponent id={peer} spacing={6} style={style} + enableLayersSwitch + width={this.state.peerWidth} + height={this.state.peerHeight} /> ); })} diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index 5e533e10..124e2a91 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -276,6 +276,8 @@ class Filmstrip extends React.PureComponent advancedMode={advancedMode} id={activePeerId} style={speakerStyle} + width={speakerStyle.width} + height={speakerStyle.height} /> }

@@ -317,6 +319,9 @@ class Filmstrip extends React.PureComponent id={peerId} style={peerStyle} smallContainer + enableLayersSwitch={activePeerId !== peerId} + width={peerStyle.width} + height={peerStyle.height} /> diff --git a/app/src/components/Settings/MediaSettings.js b/app/src/components/Settings/MediaSettings.js index b6eb8404..e5ecdf35 100644 --- a/app/src/components/Settings/MediaSettings.js +++ b/app/src/components/Settings/MediaSettings.js @@ -21,6 +21,8 @@ import ListItemText from '@material-ui/core/ListItemText'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import Switch from '@material-ui/core/Switch'; +import ImageUploader from 'react-images-upload'; +import Resizer from 'react-image-file-resizer'; const NoiseSlider = withStyles( { @@ -98,6 +100,30 @@ const MediaSettings = ({ const [ audioSettingsOpen, setAudioSettingsOpen ] = React.useState(false); const [ videoSettingsOpen, setVideoSettingsOpen ] = React.useState(false); + const onDrop = (picture) => + { + if (picture.length > 0) + { + Resizer.imageFileResizer(picture[0], 1280, 720, 'JPEG', 99, 0, + (uri) => + { + const reader = new FileReader(); + + reader.addEventListener('load', () => + { + roomClient.setPicture(reader.result); + }); + + reader.readAsDataURL(uri); + }, + 'blob'); + } + else + { + roomClient.setPicture(null); + } + }; + const resolutions = [ { value : 'low', label : intl.formatMessage({ @@ -159,6 +185,32 @@ const MediaSettings = ({
+ + { + if (event.target.value) + roomClient.updateWebcam({ newResolution: event.target.value }); + }} + name='Video resolution' + autoWidth + className={classes.selectEmpty} + > + {resolutions.map((resolution, index) => + { + return ( + + {resolution.label} + + ); + })} + + + + + setVideoSettingsOpen(!videoSettingsOpen)}> const FullScreenView = (props) => { const { + roomClient, advancedMode, consumer, + fullScreenConsumer, toggleConsumerFullscreen, toolbarsVisible, permanentTopBar, classes } = props; + const elementRef = useRef(null); + const size = useWindowSize({ + wait : 400 + }); + + useEffect(() => + { + if (!elementRef.current) + return; + + if (consumer && consumer.type !== 'simple') + { + roomClient.adaptConsumerPreferredLayers(consumer, size[0], size[1]); + } + }, [ size, fullScreenConsumer ]); + if (!consumer) return null; @@ -97,7 +117,7 @@ const FullScreenView = (props) => ); return ( -
+
videoVisible={consumerVisible} videoCodec={consumer && consumer.codec} videoScore={consumer ? consumer.score : null} + width={size[0]} + height={size[1]} />
); @@ -142,8 +164,10 @@ const FullScreenView = (props) => FullScreenView.propTypes = { + roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, consumer : appPropTypes.Consumer, + fullScreenConsumer : PropTypes.string, toggleConsumerFullscreen : PropTypes.func.isRequired, toolbarsVisible : PropTypes.bool, permanentTopBar : PropTypes.bool, @@ -152,9 +176,10 @@ FullScreenView.propTypes = const mapStateToProps = (state) => ({ - consumer : state.consumers[state.room.fullScreenConsumer], - toolbarsVisible : state.room.toolbarsVisible, - permanentTopBar : state.settings.permanentTopBar + consumer : state.consumers[state.room.fullScreenConsumer], + toolbarsVisible : state.room.toolbarsVisible, + permanentTopBar : state.settings.permanentTopBar, + fullScreenConsumer : state.room.fullScreenConsumer }); const mapDispatchToProps = (dispatch) => @@ -166,7 +191,7 @@ const mapDispatchToProps = (dispatch) => } }); -export default connect( +export default withRoomContext(connect( mapStateToProps, mapDispatchToProps, null, @@ -181,4 +206,4 @@ export default connect( ); } } -)(withStyles(styles)(FullScreenView)); +)(withStyles(styles)(FullScreenView))); diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index a9e0821b..57325bee 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -92,6 +92,7 @@ const styles = (theme) => "AcodL Acod Acod Acod Acod" \ "VcodL Vcod Vcod Vcod Vcod" \ "ResL Res Res Res Res" \ + "VPortL VPort VPort VPort VPort" \ "RecvL RecvBps RecvBps RecvSum RecvSum" \ "SendL SendBps SendBps SendSum SendSum" \ "IPlocL IPloc IPloc IPloc IPloc" \ @@ -105,6 +106,8 @@ const styles = (theme) => '& .Vcod' : { gridArea: 'Vcod' }, '& .ResL' : { gridArea: 'ResL' }, '& .Res' : { gridArea: 'Res' }, + '& .VPortL' : { gridArea: 'VPortL' }, + '& .VPort' : { gridArea: 'VPort' }, '& .RecvL' : { gridArea: 'RecvL' }, '& .RecvBps' : { gridArea: 'RecvBps', justifySelf: 'flex-end' }, '& .RecvSum' : { gridArea: 'RecvSum', justifySelf: 'flex-end' }, @@ -143,16 +146,19 @@ const styles = (theme) => color : 'rgba(255, 255, 255, 0.85)', border : 'none', borderBottom : '1px solid #aeff00', - backgroundColor : 'transparent' + backgroundColor : 'rgba(0, 0, 0, 0.25)', + padding : theme.spacing(0.6) }, displayNameStatic : { - userSelect : 'none', - cursor : 'text', - fontSize : 14, - fontWeight : 400, - color : 'rgba(255, 255, 255, 0.85)', - '&:hover' : + userSelect : 'none', + cursor : 'text', + fontSize : 14, + fontWeight : 400, + color : 'rgba(255, 255, 255, 0.85)', + backgroundColor : 'rgba(0, 0, 0, 0.25)', + padding : theme.spacing(0.6), + '&:hover' : { backgroundColor : 'rgb(174, 255, 0, 0.25)' } @@ -204,7 +210,9 @@ class VideoView extends React.PureComponent onChangeDisplayName, children, classes, - netInfo + netInfo, + width, + height } = this.props; const { @@ -305,6 +313,15 @@ class VideoView extends React.PureComponent } + { (videoVisible && width && height) && + + VPort: + + {Math.round(width)}x{Math.round(height)} + + + } + { isMe && !isScreen && !isExtraVideo && (netInfo.recv && netInfo.send && netInfo.send.iceSelectedTuple) && @@ -525,7 +542,9 @@ VideoView.propTypes = onChangeDisplayName : PropTypes.func, children : PropTypes.object, classes : PropTypes.object.isRequired, - netInfo : PropTypes.object + netInfo : PropTypes.object, + width : PropTypes.number, + height : PropTypes.number }; export default withStyles(styles)(VideoView); diff --git a/app/src/components/appPropTypes.js b/app/src/components/appPropTypes.js index 0ae4fc20..1492ece7 100644 --- a/app/src/components/appPropTypes.js +++ b/app/src/components/appPropTypes.js @@ -40,6 +40,8 @@ export const Consumer = PropTypes.shape( source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired, locallyPaused : PropTypes.bool.isRequired, remotelyPaused : PropTypes.bool.isRequired, + width : PropTypes.number, + height : PropTypes.number, profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]), track : PropTypes.any, codec : PropTypes.string diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index f529208e..fe1223df 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -26,6 +26,7 @@ const initialState = drawerOverlayed : window.config.drawerOverlayed || true, aspectRatio : window.config.viewAspectRatio || 1.777, // 16 : 9 mediaPerms : { audio: true, video: true }, + localPicture : null, ...window.config.defaultAudio }; @@ -244,6 +245,13 @@ const settings = (state = initialState, action) => return { ...state, videoMuted }; } + case 'SET_LOCAL_PICTURE': + { + const { localPicture } = action.payload; + + return { ...state, localPicture }; + } + default: return state; } diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index 5f7ea044..4102adc0 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "无法保存文件", "filesharing.startingFileShare": "正在尝试共享文件", diff --git a/app/src/translations/cs.json b/app/src/translations/cs.json index 349cc469..5825d026 100644 --- a/app/src/translations/cs.json +++ b/app/src/translations/cs.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Mezní hodnota hluku", "settings.mirrorOwnVideo": "Zrcadlové zobrazení vlastního videa", "settings.language": "Vyberte jazyk", + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Není možné uložit soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index bfc4ead0..aaa8d713 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Geräuschepegel", "settings.mirrorOwnVideo": "Ansicht des eigenen Videos spiegeln", "settings.language": "Sprache auswählen", + "settings.myPhotoButton": "Mein Foto festlegen", + "settings.myPhotoLabel": "Maximale Dateigröße: 5MB, akzeptiert: jpg, jpeg, png", + "settings.myPhotoSizeError": " Datei ist zu groß", + "settings.myPhotoTypeError": " ist keine unterstützte Dateierweiterung", "filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei", diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index 19f23710..f06a97fe 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.startingFileShare": "Forsøger at dele filen", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index 5968466b..c60edea2 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 01489b7c..c8261f2a 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Noise threshold", "settings.mirrorOwnVideo": "Mirror view of own video", "settings.language": "Select language", + "settings.myPhotoButton": "Set my photo", + "settings.myPhotoLabel": "Max. file size: 5MB, accepted: jpg, jpeg, png", + "settings.myPhotoSizeError": " file is too large", + "settings.myPhotoTypeError": " is not a supported file extension", "filesharing.saveFileError": "Unable to save file", "filesharing.startingFileShare": "Attempting to share file", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index de3b2802..35f4100b 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero", diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index 0265b044..4ba7dbc0 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.startingFileShare": "Début du transfert de fichier", diff --git a/app/src/translations/hi.json b/app/src/translations/hi.json index a3158812..32148123 100644 --- a/app/src/translations/hi.json +++ b/app/src/translations/hi.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "शोर सीमा", "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "फ़ाइल सहेजने में असमर्थ", "filesharing.startingFileShare": "फ़ाइल के आदान-प्रदान का प्रयास है", diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index c8e4d644..c13371d8 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index 72e30b33..00f0a486 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Zajszint", "settings.mirrorOwnVideo": "Saját videókép tükrözése", "settings.language": "Nyelv", + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.startingFileShare": "Fájl megosztása", diff --git a/app/src/translations/it.json b/app/src/translations/it.json index 7fdf48ce..b093d0bf 100644 --- a/app/src/translations/it.json +++ b/app/src/translations/it.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Soglia di rumore", "settings.mirrorOwnVideo": "Specchiare il proprio video", "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Impossibile salvare file", "filesharing.startingFileShare": "Tentativo di condivisione file", diff --git a/app/src/translations/kk.json b/app/src/translations/kk.json index afc2bcd1..5fd4cb9f 100644 --- a/app/src/translations/kk.json +++ b/app/src/translations/kk.json @@ -169,6 +169,10 @@ "settings.noiseThreshold": "Шу деңгейі", "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Файлды сақтау мүмкін емес", "filesharing.startingFileShare": "Файлды бөлісу әрекеті", diff --git a/app/src/translations/lv.json b/app/src/translations/lv.json index db230606..005d71a2 100644 --- a/app/src/translations/lv.json +++ b/app/src/translations/lv.json @@ -165,6 +165,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Nav iespējams saglabāt failu", "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index 4d02d198..f2d91dbb 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Støyterskel", "settings.mirrorOwnVideo": "Vis eget bilde speilet for deg", "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.startingFileShare": "Starter fildeling", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 9580b52d..50ae6f19 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Próg szumu", "settings.mirrorOwnVideo": "Lustrzany widok własnego wideo", "settings.language": "Wybór języka", + "settings.myPhotoButton": "Ustaw moje zdjęcie", + "settings.myPhotoLabel": "Maksymalny rozmiar pliku: 5MB, dozwolone: jpg, jpeg, png", + "settings.myPhotoSizeError": " plik jest zbyt duży", + "settings.myPhotoTypeError": " nie jest obsługiwanym rozszerzeniem pliku", "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index f3e9d624..eca1cf0b 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index 67e3154e..668e2e62 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.startingFileShare": "Partajarea fișierului", diff --git a/app/src/translations/ru.json b/app/src/translations/ru.json index 9c8753ac..b66e1355 100644 --- a/app/src/translations/ru.json +++ b/app/src/translations/ru.json @@ -169,6 +169,10 @@ "settings.noiseThreshold": "Порог шума", "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Невозможно сохранить файл", "filesharing.startingFileShare": "Попытка поделиться файлом", diff --git a/app/src/translations/tr.json b/app/src/translations/tr.json index 1f21907a..0913f522 100644 --- a/app/src/translations/tr.json +++ b/app/src/translations/tr.json @@ -171,6 +171,10 @@ "settings.noiseThreshold": "Gürültü eşiği", "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", diff --git a/app/src/translations/tw.json b/app/src/translations/tw.json index 64b338c3..5bbdacc5 100644 --- a/app/src/translations/tw.json +++ b/app/src/translations/tw.json @@ -170,6 +170,10 @@ "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "無法保存文件", "filesharing.startingFileShare": "開始分享文件", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json index dad79833..6a61c599 100644 --- a/app/src/translations/uk.json +++ b/app/src/translations/uk.json @@ -170,6 +170,10 @@ "settings.noiseThreshold": "Поріг шуму", "settings.mirrorOwnVideo": null, "settings.language": null, + "settings.myPhotoButton": null, + "settings.myPhotoLabel": null, + "settings.myPhotoSizeError": null, + "settings.myPhotoTypeError": null, "filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.startingFileShare": "Спроба поділитися файлом", diff --git a/app/src/urlFactory.js b/app/src/urlFactory.js index ae97dbd7..bdfd9dcc 100644 --- a/app/src/urlFactory.js +++ b/app/src/urlFactory.js @@ -1,12 +1,13 @@ export function getSignalingUrl(peerId, roomId) { + const hostname = window.config.serverHostname || window.location.hostname; const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.config.productionPort; - const url = `wss://${window.location.hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`; + const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`; return url; } diff --git a/server/lib/Room.js b/server/lib/Room.js index 53be1869..372b09db 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -614,6 +614,13 @@ class Room extends EventEmitter this._roomId); this.close(); } + else if(this.checkEmpty() && !this._lobby.checkEmpty() && this.isLocked()){ + logger.info( + 'Room deserted for some time, closing the room [roomId:"%s"] and kick peers from the lobby', + this._roomId); + + this.close(); + } else logger.debug('selfDestructCountdown() aborted; room is not empty!'); }, 10000); @@ -847,6 +854,12 @@ class Room extends EventEmitter // lobby is empty, close the room after a while. if (this.checkEmpty() && this._lobby.checkEmpty()) this.selfDestructCountdown(); + // If this is the last Peer in the room, + // lobby is not empty and room is locked, + // close the room after a while. + else if(this.checkEmpty() && !this._lobby.checkEmpty() && this.isLocked()){ + this.selfDestructCountdown(); + } } async _handleSocketRequest(peer, request, cb) @@ -1372,7 +1385,7 @@ class Room extends EventEmitter break; } - /* case 'changePicture': + case 'changePicture': { // Ensure the Peer is joined. if (!peer.joined) @@ -1392,7 +1405,7 @@ class Room extends EventEmitter cb(); break; - } */ + } case 'chatMessage': { @@ -1884,6 +1897,8 @@ class Room extends EventEmitter { // Remove from its map. consumerPeer.removeConsumer(consumer.id); + + this._notification(consumerPeer.socket, 'consumerClosed', { consumerId: consumer.id }); }); consumer.on('producerclose', () => diff --git a/server/lib/interactiveServer.js b/server/lib/interactiveServer.js index 78eaf92c..1f34ac89 100644 --- a/server/lib/interactiveServer.js +++ b/server/lib/interactiveServer.js @@ -684,6 +684,11 @@ module.exports = async function(rooms, peers) const interactive = new Interactive(socket); interactive.openCommandConsole(); + + socket.on('error', (error) => + { + console.error(`interactiveServer socket error:`, error.message); + }); }); await new Promise((resolve) => diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index 82bb7a13..3f921abf 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -267,7 +267,7 @@ module.exports = async function(rooms, peers, config) await collect(registry); res.set('Content-Type', registry.contentType); - const data = registry.metrics(); + const data = await registry.metrics(); res.end(data); }); diff --git a/server/package.json b/server/package.json index 2dd31057..ba27cad0 100644 --- a/server/package.json +++ b/server/package.json @@ -35,7 +35,7 @@ "helmet": "^3.21.2", "ims-lti": "^3.0.2", "jsonwebtoken": "^8.5.1", - "mediasoup": "^3.5.14", + "mediasoup": "^3.6.27", "openid-client": "^3.7.3", "passport": "^0.4.0", "passport-local": "^1.0.0", diff --git a/server/server.js b/server/server.js index 77323018..02dfaee5 100755 --- a/server/server.js +++ b/server/server.js @@ -620,20 +620,7 @@ async function runHttpsServer() res.redirect(ltiURL); } else - { - const specialChars = "<>@!^*()[]{}:;|'\"\\,~`"; - - for (let i = 0; i < specialChars.length; i++) - { - if (req.url.substring(1).indexOf(specialChars[i]) > -1) - { - req.url = `/${encodeURIComponent(encodeURI(req.url.substring(1)))}`; - res.redirect(`${req.url}`); - } - } - return next(); - } } else res.redirect(`https://${req.hostname}${req.url}`); @@ -807,14 +794,18 @@ async function runMediasoupWorkers() logger.info('running %d mediasoup Workers...', numWorkers); - for (let i = 0; i < numWorkers; ++i) + const { logLevel, logTags, rtcMinPort, rtcMaxPort } = config.mediasoup.worker; + const portInterval = Math.floor((rtcMaxPort - rtcMinPort) / numWorkers); + + for (let i = 0; i < numWorkers; i++) { const worker = await mediasoup.createWorker( { - logLevel : config.mediasoup.worker.logLevel, - logTags : config.mediasoup.worker.logTags, - rtcMinPort : config.mediasoup.worker.rtcMinPort, - rtcMaxPort : config.mediasoup.worker.rtcMaxPort + logLevel, + logTags, + rtcMinPort : rtcMinPort + i * portInterval, + rtcMaxPort : i === numWorkers - 1 ? rtcMaxPort + : rtcMinPort + (i + 1) * portInterval - 1 }); worker.on('died', () =>