Merge branch 'develop' of https://github.com/havfo/multiparty-meeting into develop

This commit is contained in:
Stefan Otto 2021-03-03 14:34:47 +01:00
commit cffcec4273
48 changed files with 797 additions and 110 deletions

5
.gitignore vendored
View file

@ -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
/app/public/config/*.pem
yarn.lock

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
xml:space="preserve"
width="139"
height="30"
viewBox="0 0 799.66669 172.29333"
sodipodi:docname="edumeet_logo.eps"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata8"><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="defs6" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1916"
inkscape:window-height="1014"
id="namedview4"
showgrid="false"
inkscape:zoom="0.53772404"
inkscape:cx="399.83334"
inkscape:cy="86.146667"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="g10" /><g
id="g10"
inkscape:groupmode="layer"
inkscape:label="ink_ext_XXXXXX"
transform="matrix(1.3333333,0,0,-1.3333333,0,172.29333)"><g
id="g12"
transform="scale(0.1)"><path
d="m 1667.64,373.02 c 0,0 -193.65,11.343 -394.11,119.109 72.23,293.047 -69.56,605.841 -353.264,738.851 C 597.211,1382.37 212.637,1243.22 61.2031,920.25 -90.1719,597.238 48.9063,212.57 371.965,61.1602 659.195,-73.4609 995.113,21.8086 1172.46,271.719 c 248.85,5.543 495.18,101.301 495.18,101.301"
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path14"
inkscape:connector-curvature="0" /><path
d="m 2241.9,497.863 c 1.02,45.887 -19.37,121.34 -102.98,121.34 -77.5,0 -110.14,-70.355 -115.23,-121.34 z m -218.21,-87.687 c 3.06,-89.742 73.42,-128.485 152.96,-128.485 58.12,0 99.91,8.161 137.64,22.434 l 18.37,-86.676 c -42.83,-17.34 -101.98,-30.59 -173.35,-30.59 -161.11,0 -255.95,98.922 -255.95,250.844 0,137.652 83.62,267.16 242.68,267.16 161.12,0 214.15,-132.562 214.15,-241.672 0,-23.453 -2.05,-41.8 -4.09,-53.015 h -332.41"
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path16"
inkscape:connector-curvature="0" /><path
d="m 2793.57,486.648 c 0,10.196 -1.01,22.434 -3.06,32.629 -11.2,48.946 -50.97,88.711 -108.08,88.711 -80.55,0 -125.42,-71.375 -125.42,-164.168 0,-90.75 44.87,-157.031 124.41,-157.031 50.98,0 95.85,34.672 108.08,88.715 3.06,11.215 4.07,23.449 4.07,36.703 z m 125.42,434.379 V 336.754 c 0,-50.973 2.05,-106.043 4.09,-139.703 h -112.17 l -5.1,78.527 h -2.05 c -29.55,-55.066 -89.71,-89.738 -162.11,-89.738 -118.29,0 -212.1,100.949 -212.1,253.898 -1.01,166.211 102.98,265.125 222.29,265.125 68.32,0 117.27,-28.554 139.69,-65.261 h 2.04 v 281.425 h 125.42"
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path18"
inkscape:connector-curvature="0" /><path
d="m 3498.2,345.93 c 0,-59.141 2.04,-108.078 4.09,-148.879 h -110.13 l -6.12,75.461 h -2.05 c -21.4,-35.684 -70.36,-86.672 -159.06,-86.672 -90.75,0 -173.35,54.051 -173.35,216.172 V 693.641 H 3177 V 423.422 c 0,-82.586 26.52,-135.613 92.79,-135.613 49.98,0 82.6,35.691 95.85,67.3 4.09,11.211 7.14,24.469 7.14,38.746 V 693.641 H 3498.2 V 345.93"
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path20"
inkscape:connector-curvature="0" /><path
d="m 4219.13,477.465 c -4.09,88.715 -9.19,195.781 -8.15,288.57 h -3.06 c -22.44,-83.609 -51,-176.398 -81.58,-263.07 l -99.93,-297.754 h -96.87 l -91.78,293.672 c -26.5,87.687 -50.97,181.5 -69.34,267.152 h -2.02 c -3.08,-89.726 -7.14,-198.836 -12.24,-292.648 L 3738.87,197.051 H 3621.6 l 45.88,687.273 h 165.19 l 89.73,-276.336 c 24.48,-80.558 45.88,-162.129 63.23,-238.609 h 3.04 c 18.37,74.441 41.82,159.07 68.32,239.629 l 94.84,275.316 h 163.14 l 39.78,-687.273 h -122.38 l -13.24,280.414"
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path22"
inkscape:connector-curvature="0" /><path
d="M 4866.67,501.941 H 4607.68 V 300.043 h 289.58 V 197.051 h -415 v 687.273 h 399.7 V 781.336 H 4607.68 V 603.91 h 258.99 V 501.941"
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24"
inkscape:connector-curvature="0" /><path
d="m 5391.83,501.941 h -259 V 300.043 h 289.59 V 197.051 h -415.01 v 687.273 h 399.71 V 781.336 H 5132.83 V 603.91 h 259 V 501.941"
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path26"
inkscape:connector-curvature="0" /><path
d="m 5674.31,779.297 h -195.78 v 105.027 h 519.02 V 779.297 H 5799.73 V 197.051 h -125.42 v 582.246"
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28"
inkscape:connector-curvature="0" /><path
d="m 804.438,984.711 c -187.032,87.649 -409.68,7.086 -497.36,-179.902 -55.531,-118.496 -43.469,-251.282 20.344,-355 -108.164,-67.911 -203.207,-92.024 -203.207,-92.024 0,0 143.82,-26.316 302.43,-14.781 18.66,-13.457 38.761,-25.453 60.351,-35.578 186.984,-87.625 409.707,-7.102 497.359,179.906 87.675,187.047 7.114,409.691 -179.917,497.379"
style="fill:#f9a72b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path30"
inkscape:connector-curvature="0" /><path
d="M 831.895,558.988 C 783.809,456.395 661.621,412.215 559.043,460.293 456.438,508.387 412.262,630.559 460.344,733.152 c 48.094,102.582 170.242,146.778 272.847,98.696 102.61,-48.106 146.797,-170.246 98.704,-272.86"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path32"
inkscape:connector-curvature="0" /></g></g></svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View file

@ -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();
}
}
}

View file

@ -149,4 +149,9 @@ export const setVideoMuted = (videoMuted) =>
({
type : 'SET_VIDEO_MUTED',
payload : { videoMuted }
});
});
export const setLocalPicture = (localPicture) =>
({
type : 'SET_LOCAL_PICTURE',
payload : { localPicture }
});

View file

@ -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)

View file

@ -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 (
<React.Fragment>
<div
@ -210,7 +257,12 @@ const Peer = (props) =>
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<div className={classnames(
classes.videoInfo,
'hide',
hover ? 'hover' : null
)}
>
<p>
<FormattedMessage
id='room.videoPaused'
@ -421,6 +473,8 @@ const Peer = (props) =>
videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null}
videoScore={webcamConsumer ? webcamConsumer.score : null}
width={width}
height={height}
>
<Volume id={peer.id} />
</VideoView>
@ -614,6 +668,8 @@ const Peer = (props) =>
videoVisible={videoVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
width={width}
height={height}
/>
</div>
</div>
@ -758,6 +814,8 @@ const Peer = (props) =>
videoVisible={screenVisible}
videoCodec={screenConsumer && screenConsumer.codec}
videoScore={screenConsumer ? screenConsumer.score : null}
width={width}
height={height}
/>
</div>
</div>
@ -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
);
}
}

View file

@ -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 (
<React.Fragment>
<div
@ -145,6 +178,8 @@ const SpeakerPeer = (props) =>
videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null}
videoScore={webcamConsumer ? webcamConsumer.score : null}
width={width}
height={height}
>
<Volume id={peer.id} />
</VideoView>
@ -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)));

View file

@ -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 ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
<AccountCircle className={loggedIn ? classes.green : null} />
</IconButton>
</Tooltip>
}
@ -692,11 +686,7 @@ const TopBar = (props) =>
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
<AccountCircle className={loggedIn ? classes.green : null} />
{ loggedIn ?
<p className={classes.moreAction}>
<FormattedMessage
@ -934,7 +924,6 @@ TopBar.propTypes =
permanentTopBar : PropTypes.bool.isRequired,
drawerOverlayed : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool,
@ -982,7 +971,6 @@ const makeMapStateToProps = () =>
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),

View file

@ -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 ?
<Avatar
src={myPicture}
className={classes.accountButtonAvatar}
/>
:
<AccountCircle
className={
classnames(
classes.accountButtonAvatar,
loggedIn ? classes.green : null
)
}
/>
}
<AccountCircle
className={
classnames(
classes.accountButtonAvatar,
loggedIn ? classes.green : null
)
}
/>
</IconButton>
</Grid>
<Grid item>
@ -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

View file

@ -192,6 +192,9 @@ class Democratic extends React.PureComponent
id={peer}
spacing={6}
style={style}
enableLayersSwitch
width={this.state.peerWidth}
height={this.state.peerHeight}
/>
);
})}

View file

@ -276,6 +276,8 @@ class Filmstrip extends React.PureComponent
advancedMode={advancedMode}
id={activePeerId}
style={speakerStyle}
width={speakerStyle.width}
height={speakerStyle.height}
/>
}
</div>
@ -317,6 +319,9 @@ class Filmstrip extends React.PureComponent
id={peerId}
style={peerStyle}
smallContainer
enableLayersSwitch={activePeerId !== peerId}
width={peerStyle.width}
height={peerStyle.height}
/>
</div>
</Grid>

View file

@ -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 = ({
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<ImageUploader
withIcon
onChange={onDrop}
imgExtension={[ '.jpg', '.jpeg', '.png' ]}
maxFileSize={5242880}
singleImage
withPreview
defaultImages={settings.localPicture?[ settings.localPicture ]:[]}
buttonType='button'
buttonText={intl.formatMessage({
id : 'settings.myPhotoButton',
defaultMessage : 'Set my photo'
})}
label={intl.formatMessage({
id : 'settings.myPhotoLabel',
defaultMessage : 'Max. file size: 5MB, accepted: jpg, jpeg, png'
})}
fileSizeError={intl.formatMessage({
id : 'settings.myPhotoSizeError',
defaultMessage : ' file is too large'
})}
fileTypeError={intl.formatMessage({
id : 'settings.myPhotoTypeError',
defaultMessage : ' is not a supported file extension'
})}
/>
<Select
value={settings.selectedWebcam || ''}
onChange={(event) =>
@ -201,6 +253,34 @@ const MediaSettings = ({
}
</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.updateWebcam({ newResolution: 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>
<List className={classes.root} component='nav'>
<ListItem button onClick={() => setVideoSettingsOpen(!videoSettingsOpen)}>
<ListItemText primary={intl.formatMessage({

View file

@ -1,10 +1,12 @@
import React from 'react';
import React, { useRef, useEffect } from 'react';
import { useWindowSize } from '@react-hook/window-size';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import * as roomActions from '../../actions/roomActions';
import { withRoomContext } from '../../RoomContext';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import VideoView from './VideoView';
@ -79,14 +81,32 @@ const styles = (theme) =>
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 (
<div className={classes.root}>
<div className={classes.root} ref={elementRef}>
<div className={classes.controls}>
<div
className={classnames(classes.button, {
@ -135,6 +155,8 @@ const FullScreenView = (props) =>
videoVisible={consumerVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
width={size[0]}
height={size[1]}
/>
</div>
);
@ -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)));

View file

@ -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
</React.Fragment>
}
{ (videoVisible && width && height) &&
<React.Fragment>
<span className={'VPortL'}>VPort: </span>
<span className={'VPort'}>
{Math.round(width)}x{Math.round(height)}
</span>
</React.Fragment>
}
{ isMe && !isScreen && !isExtraVideo &&
(netInfo.recv && netInfo.send && netInfo.send.iceSelectedTuple) &&
<React.Fragment>
@ -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);

View file

@ -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

View file

@ -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;
}

View file

@ -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": "正在尝试共享文件",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Προσπάθεια διαμοιρασμού αρχείου",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "फ़ाइल के आदान-प्रदान का प्रयास है",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Файлды бөлісу әрекеті",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Попытка поделиться файлом",

View file

@ -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",

View file

@ -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": "開始分享文件",

View file

@ -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": "Спроба поділитися файлом",

View file

@ -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;
}

View file

@ -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', () =>

View file

@ -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) =>

View file

@ -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);
});

View file

@ -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",

View file

@ -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', () =>