Merge branch 'develop'

master
Mészáros Mihály 2020-03-22 20:16:34 +01:00
commit cae50e3f33
66 changed files with 4520 additions and 873 deletions

View File

@ -1,6 +1,26 @@
# Changelog
### 3.1
## 3.2
* Add munin plugin
* Add muted=true search param to disble audio by deffault
* Modify webtorrent tracker
* Add key shortcut `space` for audio mute
* Add key shortcut `v` for video mute
* Add user configurable LastN
* Add option to sticky top bar (sticky by default)
* update mediasoup server
* Add simulcast options to app config (disabled by default)
* Add stats option to get counts of rooms and peers
* Add httpOnly option for loadbalancer backend setups
* LTI integration for LMS systems like moodle
* Add muted=false search parameter
* Add translations (12+1 languages)
* Add support IPv6
* Many other fixes and refactorings
## 3.1
* Browser session storage
* Virtual lobby for rooms
* Allow minimum TLSv1.2 and recommended ciphers
@ -9,7 +29,8 @@
* Internationalization support
* Can require sign in for access
### 3.0
## 3.0
* Updated to mediasoup v3
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
- OpenID Connect discovery
@ -18,25 +39,30 @@
- Notice it does not supports node 11.x
* Updated to Material UI v4
### 2.0
## 2.0
* Material UI
* Separate settings for lastN for desktop and mobile
### 1.2
## 1.2
* Add Lock Room feature
* Fix suspended Web Audio context / fixed delayed getUsermedia
* Added support for the new getdisplaymedia API in Chrome 72
### 1.1
## 1.1
* Moved Filesharing code out from React code to RoomClient
* Major cleanup of CSS. Variables for most colors and sizes exposed in :root
* Started using React Context instead of middleware
* Small fixes to buttons and layout
### 1.0
## 1.0
* Fixed toolarea button based on feedback from users
* Added possibility to move video to separate window
* Added SIP gateway
### RC1 1.0
## RC1 1.0
* First stable release?

View File

@ -2,6 +2,8 @@
A WebRTC meeting service using [mediasoup](https://mediasoup.org).
![](demo.gif)
Try it online at https://letsmeet.no. You can add /roomname to the URL for specifying a room.
## Features
@ -15,7 +17,19 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
## Docker
If you want the automatic approach, you can find a docker image [here](https://hub.docker.com/r/misi/mm/).
## Ansible
If you want the ansible approach, you can find ansible role [here](https://github.com/misi/mm-ansible/).
[![asciicast](https://asciinema.org/a/311365.svg)](https://asciinema.org/a/311365)
## Manual installation
* Prerequisites:
Currently multiparty-meeting will only run on nodejs v10.*
To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall).
```bash
$ sudo apt install npm build-essentials redis
```
* Clone the project:
@ -50,7 +64,6 @@ This will build the client application and copy everythink to `server/public` fr
* Set up the server:
```bash
$ sudo apt install redis
$ cd ..
$ cd server
$ npm install
@ -64,7 +77,8 @@ $ npm install
$ cd server
$ npm start
```
* test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
* Note: Do not run the server as root. If you need to use port 80/443 make a iptables-mapping for that or use systemd configuration for that (see futher down this doc).
* Test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
## Deploy it in a server
@ -74,14 +88,14 @@ $ cp multiparty-meeting.service /etc/systemd/system/
$ edit /etc/systemd/system/multiparty-meeting.service
```
* reload systemd configuration and start service:
* Reload systemd configuration and start service:
```bash
$ systemctl daemon-reload
$ systemctl start multiparty-meeting
```
* if you want to start multiparty-meeting at boot time:
* If you want to start multiparty-meeting at boot time:
```bash
$ systemctl enable multiparty-meeting
```

2
app/Procfile 100644
View File

@ -0,0 +1,2 @@
react: npm start
electron: node src/electron-wait-react

View File

@ -5,16 +5,20 @@
"description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"homepage": "./",
"main": "src/electron-starter.js",
"dependencies": {
"@material-ui/core": "^4.5.1",
"@material-ui/icons": "^4.5.1",
"bowser": "^2.7.0",
"dompurify": "^2.0.7",
"domready": "^1.0.8",
"end-of-stream": "1.4.0",
"file-saver": "^2.0.2",
"hark": "^1.2.3",
"marked": "^0.7.0",
"mediasoup-client": "^3.2.7",
"is-electron": "^2.2.0",
"marked": "^0.8.0",
"mediasoup-client": "^3.5.4",
"notistack": "^0.9.5",
"prop-types": "^15.7.2",
"random-string": "^0.2.0",
@ -23,7 +27,8 @@
"react-dom": "^16.10.2",
"react-intl": "^3.4.0",
"react-redux": "^7.1.1",
"react-scripts": "3.2.0",
"react-router-dom": "^5.1.2",
"react-scripts": "^3.3.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
@ -35,12 +40,13 @@
"webtorrent": "^0.107.16"
},
"scripts": {
"analyze-main": "source-map-explorer build/static/js/main.*",
"analyze-chunk": "source-map-explorer build/static/js/2.*",
"analyze": "source-map-explorer build/static/js/*",
"start": "HTTPS=true PORT=4443 react-scripts start",
"build": "react-scripts build && mkdir -p ../server/public && rm -rf ../server/public/* && cp -r build/* ../server/public/",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"electron": "electron --no-sandbox .",
"dev": "nf start -p 3000"
},
"browserslist": [
">0.2%",
@ -49,8 +55,12 @@
"not op_mini all"
],
"devDependencies": {
"electron": "^7.1.1",
"eslint": "^6.5.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-react": "^7.16.0"
"eslint-plugin-react": "^7.16.0",
"foreman": "^3.0.1",
"jest": "^24.9.0",
"redux-mock-store": "^1.5.3"
}
}

View File

@ -1,86 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Multiparty Meeting</title>
</head>
<style>
body
{
margin:auto;
padding:0.5vmin;
text-align:center;
position: fixed;
left: 50%;
top: 40%;
width: 90%;
transform: translate(-50%, 0%);
background-image: url('/images/background.jpg');
background-attachment: fixed;
background-position: center;
background-size: cover;
background-repeat: repeat;
}
input:hover { opacity:0.9; }
input[type=text]
{
font-size: 1.5em;
padding: 1.5vmin;
background-color: rgba(0,0,0,0.3);
border: 0;
color: #fff;
margin: 0.8vmin;
width: 50%;
}
button:hover { background-color: #f5f5f5; }
button
{
font-size: 1.5em;
padding: 1.5vmin;
margin: 0.8vmin;
background-color: #fafafa;
border-radius: 1.8vmin;
color: #000;
border: 0;
}
img
{
height: 15vmin;
}
</style>
<body>
<a>
<img src='/images/logo.svg'></img><br />
</a>
<input id='room' type='text' onkeypress='checkEnter(event)' value='' placeholder='your room name' />
<button onclick = 'start(location.href)'>Go to room</button>
</body>
<script>
let room = document.getElementById('room');
let stateObj = { foo: 'bar' };
room.addEventListener('input', (e) =>
{
console.log(e.charCode);
history.replaceState(stateObj, 'Multiparty Meeting', '/'+room.value);
}, true);
room.focus();
function start(target)
{
location.href;history.replaceState(stateObj, 'Multiparty Meeting', '/');
window.location = target;
}
function checkEnter(event)
{
let x = event.charCode || event.keyCode;
if (x == 13 )
{
start(location.href);
}
}
</script>
</html>

View File

@ -1,9 +1,11 @@
// eslint-disable-next-line
var config =
{
loginEnabled : false,
developmentPort : 3443,
turnServers : [
loginEnabled : false,
developmentPort : 3443,
productionPort : 443,
multipartyServer : 'letsmeet.no',
turnServers : [
{
urls : [
'turn:turn.example.com:443?transport=tcp'
@ -12,8 +14,29 @@ var config =
credential : 'example'
}
],
requestTimeout : 10000,
transportOptions :
/**
* If defaultResolution is set, it will override user settings when joining:
* low ~ 320x240
* medium ~ 640x480
* high ~ 1280x720
* veryhigh ~ 1920x1080
* ultra ~ 3840x2560
**/
defaultResolution : 'medium',
// Enable or disable simulcast for webcam video
simulcast : true,
// Enable or disable simulcast for screen sharing video
simulcastSharing : false,
// Simulcast encoding layers and levels
simulcastEncodings :
[
{ scaleResolutionDownBy: 4 },
{ scaleResolutionDownBy: 2 },
{ scaleResolutionDownBy: 1 }
],
// Socket.io request timeout
requestTimeout : 10000,
transportOptions :
{
tcp : true
},
@ -51,6 +74,17 @@ var config =
backgroundColor : '#518029'
}
}
},
MuiBadge :
{
colorPrimary :
{
backgroundColor : '#5F9B2D',
'&:hover' :
{
backgroundColor : '#518029'
}
}
}
},
typography :

View File

@ -26,13 +26,24 @@ let ScreenShare;
let Spotlights;
const {
turnServers,
let turnServers,
requestTimeout,
transportOptions,
lastN,
mobileLastN
} = window.config;
mobileLastN,
defaultResolution;
if (process.env.NODE_ENV !== 'test')
{
({
turnServers,
requestTimeout,
transportOptions,
lastN,
mobileLastN,
defaultResolution
} = window.config);
}
const logger = new Logger('RoomClient');
@ -72,11 +83,28 @@ const VIDEO_CONSTRAINS =
}
};
const VIDEO_ENCODINGS =
const PC_PROPRIETARY_CONSTRAINTS =
{
optional : [ { googDscp: true } ]
};
const VIDEO_SIMULCAST_ENCODINGS =
[
{ maxBitrate: 180000, scaleResolutionDownBy: 4 },
{ maxBitrate: 360000, scaleResolutionDownBy: 2 },
{ maxBitrate: 1500000, scaleResolutionDownBy: 1 }
{ scaleResolutionDownBy: 4 },
{ scaleResolutionDownBy: 2 },
{ scaleResolutionDownBy: 1 }
];
// Used for VP9 webcam video.
const VIDEO_KSVC_ENCODINGS =
[
{ scalabilityMode: 'S3T3_KEY' }
];
// Used for VP9 desktop sharing.
const VIDEO_SVC_ENCODINGS =
[
{ scalabilityMode: 'S3T3', dtx: true }
];
let store;
@ -97,13 +125,18 @@ export default class RoomClient
}
constructor(
{ roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp })
{ peerId, accessCode, device, useSimulcast, useSharingSimulcast, produce, forceTcp, displayName, muted } = {})
{
logger.debug(
'constructor() [roomId: "%s", peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s"]',
roomId, peerId, device.flag, useSimulcast, produce, forceTcp);
if (!peerId)
throw new Error('Missing peerId');
else if (!device)
throw new Error('Missing device');
this._signalingUrl = getSignalingUrl(peerId, roomId);
logger.debug(
'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s", displayName ""]',
peerId, device.flag, useSimulcast, produce, forceTcp, displayName);
this._signalingUrl = null;
// Closed flag.
this._closed = false;
@ -114,12 +147,27 @@ export default class RoomClient
// Wheter we force TCP
this._forceTcp = forceTcp;
// Use displayName
if (displayName)
store.dispatch(settingsActions.setDisplayName(displayName));
// Torrent support
this._torrentSupport = null;
// Whether simulcast should be used.
this._useSimulcast = useSimulcast;
if ('simulcast' in window.config)
this._useSimulcast = window.config.simulcast;
// Whether simulcast should be used for sharing
this._useSharingSimulcast = useSharingSimulcast;
if ('simulcastSharing' in window.config)
this._useSharingSimulcast = window.config.simulcastSharing;
this._muted = muted;
// This device
this._device = device;
@ -136,8 +184,7 @@ export default class RoomClient
this._signalingSocket = null;
// The room ID
this._roomId = roomId;
store.dispatch(roomActions.setRoomName(roomId));
this._roomId = null;
// mediasoup-client Device instance.
// @type {mediasoupClient.Device}
@ -146,11 +193,17 @@ export default class RoomClient
// Our WebTorrent client
this._webTorrent = null;
if (defaultResolution)
store.dispatch(settingsActions.setVideoResolution(defaultResolution));
// Max spotlights
if (device.bowser.ios || device.bowser.mobile || device.bowser.android)
this._maxSpotlights = mobileLastN;
else
if (device.bowser.getPlatformType() === 'desktop')
this._maxSpotlights = lastN;
else
this._maxSpotlights = mobileLastN;
store.dispatch(
settingsActions.setLastN(this._maxSpotlights));
// Manager of spotlight
this._spotlights = null;
@ -268,6 +321,7 @@ export default class RoomClient
break;
}
case ' ':
case 'm': // Toggle microphone
{
if (this._micProducer)
@ -313,6 +367,16 @@ export default class RoomClient
break;
}
case 'v': // Toggle video
{
if (this._webcamProducer)
this.disableWebcam();
else
this.enableWebcam();
break;
}
default:
{
break;
@ -640,30 +704,26 @@ export default class RoomClient
})
}));
this._webTorrent.seed(files, (torrent) =>
{
const existingTorrent = this._webTorrent.get(torrent);
if (existingTorrent)
this._webTorrent.seed(
files,
{ announceList: [['wss://tracker.lab.vvc.niif.hu:443']] },
(torrent) =>
{
return this._sendFile(existingTorrent.magnetURI);
}
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'filesharing.successfulFileShare',
defaultMessage : 'File successfully shared'
})
}));
store.dispatch(requestActions.notify(
{
text : intl.formatMessage({
id : 'filesharing.successfulFileShare',
defaultMessage : 'File successfully shared'
})
}));
store.dispatch(fileActions.addFile(
this._peerId,
torrent.magnetURI
));
store.dispatch(fileActions.addFile(
this._peerId,
torrent.magnetURI
));
this._sendFile(torrent.magnetURI);
});
this._sendFile(torrent.magnetURI);
});
}
// { file, name, picture }
@ -811,6 +871,14 @@ export default class RoomClient
}
}
changeMaxSpotlights(maxSpotlights)
{
this._spotlights.maxSpotlights = maxSpotlights;
store.dispatch(
settingsActions.setLastN(maxSpotlights));
}
// Updated consumers based on spotlights
async updateSpotlights(spotlights)
{
@ -873,7 +941,8 @@ export default class RoomClient
'changeAudioDevice() | new selected webcam [device:%o]',
device);
this._micProducer.track.stop();
if (this._micProducer && this._micProducer.track)
this._micProducer.track.stop();
logger.debug('changeAudioDevice() | calling getUserMedia()');
@ -887,9 +956,11 @@ export default class RoomClient
const track = stream.getAudioTracks()[0];
await this._micProducer.replaceTrack({ track });
if (this._micProducer)
await this._micProducer.replaceTrack({ track });
this._micProducer.volume = 0;
if (this._micProducer)
this._micProducer.volume = 0;
const harkStream = new MediaStream();
@ -925,9 +996,9 @@ export default class RoomClient
store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume));
}
});
store.dispatch(
producerActions.setProducerTrack(this._micProducer.id, track));
if (this._micProducer && this._micProducer.id)
store.dispatch(
producerActions.setProducerTrack(this._micProducer.id, track));
store.dispatch(settingsActions.setSelectedAudioDevice(deviceId));
@ -1195,6 +1266,75 @@ export default class RoomClient
meActions.setMyRaiseHandStateInProgress(false));
}
async setMaxSendingSpatialLayer(spatialLayer)
{
logger.debug('setMaxSendingSpatialLayer() [spatialLayer:%s]', spatialLayer);
try
{
if (this._webcamProducer)
await this._webcamProducer.setMaxSpatialLayer(spatialLayer);
if (this._screenSharingProducer)
await this._screenSharingProducer.setMaxSpatialLayer(spatialLayer);
}
catch (error)
{
logger.error('setMaxSendingSpatialLayer() | failed:"%o"', error);
}
}
async setConsumerPreferredLayers(consumerId, spatialLayer, temporalLayer)
{
logger.debug(
'setConsumerPreferredLayers() [consumerId:%s, spatialLayer:%s, temporalLayer:%s]',
consumerId, spatialLayer, temporalLayer);
try
{
await this.sendRequest(
'setConsumerPreferedLayers', { consumerId, spatialLayer, temporalLayer });
store.dispatch(consumerActions.setConsumerPreferredLayers(
consumerId, spatialLayer, temporalLayer));
}
catch (error)
{
logger.error('setConsumerPreferredLayers() | failed:"%o"', error);
}
}
async setConsumerPriority(consumerId, priority)
{
logger.debug(
'setConsumerPriority() [consumerId:%s, priority:%d]',
consumerId, priority);
try
{
await this.sendRequest('setConsumerPriority', { consumerId, priority });
store.dispatch(consumerActions.setConsumerPriority(consumerId, priority));
}
catch (error)
{
logger.error('setConsumerPriority() | failed:%o', error);
}
}
async requestConsumerKeyFrame(consumerId)
{
logger.debug('requestConsumerKeyFrame() [consumerId:%s]', consumerId);
try
{
await this.sendRequest('requestConsumerKeyFrame', { consumerId });
}
catch (error)
{
logger.error('requestConsumerKeyFrame() | failed:%o', error);
}
}
async _loadDynamicImports()
{
({ default: WebTorrent } = await import(
@ -1240,10 +1380,16 @@ export default class RoomClient
));
}
async join({ joinVideo })
async join({ roomId, joinVideo })
{
await this._loadDynamicImports();
this._roomId = roomId;
store.dispatch(roomActions.setRoomName(roomId));
this._signalingUrl = getSignalingUrl(this._peerId, roomId);
this._torrentSupport = WebTorrent.WEBRTC_SUPPORT;
this._webTorrent = this._torrentSupport && new WebTorrent({
@ -1406,6 +1552,7 @@ export default class RoomClient
temporalLayers : temporalLayers,
preferredSpatialLayer : spatialLayers - 1,
preferredTemporalLayer : temporalLayers - 1,
priority : 1,
codec : consumer.rtpParameters.codecs[0].mimeType.split('/')[1],
track : consumer.track
},
@ -1774,10 +1921,10 @@ export default class RoomClient
case 'newPeer':
{
const { id, displayName, picture, device } = notification.data;
const { id, displayName, picture } = notification.data;
store.dispatch(
peerActions.addPeer({ id, displayName, picture, device, consumers: [] }));
peerActions.addPeer({ id, displayName, picture, consumers: [] }));
store.dispatch(requestActions.notify(
{
@ -1941,7 +2088,9 @@ export default class RoomClient
id,
iceParameters,
iceCandidates,
dtlsParameters
dtlsParameters,
iceServers : ROOM_OPTIONS.turnServers,
proprietaryConstraints : PC_PROPRIETARY_CONSTRAINTS
});
this._sendTransport.on(
@ -1958,18 +2107,26 @@ export default class RoomClient
});
this._sendTransport.on(
'produce', ({ kind, rtpParameters, appData }, callback, errback) =>
'produce', async ({ kind, rtpParameters, appData }, callback, errback) =>
{
this.sendRequest(
'produce',
{
transportId : this._sendTransport.id,
kind,
rtpParameters,
appData
})
.then(callback)
.catch(errback);
try
{
// eslint-disable-next-line no-shadow
const { id } = await this.sendRequest(
'produce',
{
transportId : this._sendTransport.id,
kind,
rtpParameters,
appData
});
callback({ id });
}
catch (error)
{
errback(error);
}
});
}
@ -1993,7 +2150,8 @@ export default class RoomClient
id,
iceParameters,
iceCandidates,
dtlsParameters
dtlsParameters,
iceServers : ROOM_OPTIONS.turnServers
});
this._recvTransport.on(
@ -2047,7 +2205,8 @@ export default class RoomClient
if (this._produce)
{
if (this._mediasoupDevice.canProduce('audio'))
this.enableMic();
if (!this._muted)
this.enableMic();
if (joinVideo && this._mediasoupDevice.canProduce('video'))
this.enableWebcam();
@ -2214,7 +2373,7 @@ export default class RoomClient
if (this._micProducer)
return;
if (!this._mediasoupDevice.canProduce('audio'))
if (this._mediasoupDevice && !this._mediasoupDevice.canProduce('audio'))
{
logger.error('enableMic() | cannot produce audio');
@ -2411,19 +2570,45 @@ export default class RoomClient
logger.debug('enableScreenSharing() | calling getUserMedia()');
const stream = await this._screenSharing.start({
width : 1280,
height : 720,
frameRate : 3
width : 1920,
height : 1080,
frameRate : 5
});
track = stream.getVideoTracks()[0];
if (this._useSimulcast)
if (this._useSharingSimulcast)
{
// If VP9 is the only available video codec then use SVC.
const firstVideoCodec = this._mediasoupDevice
.rtpCapabilities
.codecs
.find((c) => c.kind === 'video');
let encodings;
if (firstVideoCodec.mimeType.toLowerCase() === 'video/vp9')
{
encodings = VIDEO_SVC_ENCODINGS;
}
else
{
if ('simulcastEncodings' in window.config)
{
encodings = window.config.simulcastEncodings
.map((encoding) => ({ ...encoding, dtx: true }));
}
else
{
encodings = VIDEO_SIMULCAST_ENCODINGS
.map((encoding) => ({ ...encoding, dtx: true }));
}
}
this._screenSharingProducer = await this._sendTransport.produce(
{
track,
encodings : VIDEO_ENCODINGS,
encodings,
codecOptions :
{
videoGoogleStartBitrate : 1000
@ -2574,10 +2759,28 @@ export default class RoomClient
if (this._useSimulcast)
{
// If VP9 is the only available video codec then use SVC.
const firstVideoCodec = this._mediasoupDevice
.rtpCapabilities
.codecs
.find((c) => c.kind === 'video');
let encodings;
if (firstVideoCodec.mimeType.toLowerCase() === 'video/vp9')
encodings = VIDEO_KSVC_ENCODINGS;
else
{
if ('simulcastEncodings' in window.config)
encodings = window.config.simulcastEncodings;
else
encodings = VIDEO_SIMULCAST_ENCODINGS;
}
this._webcamProducer = await this._sendTransport.produce(
{
track,
encodings : VIDEO_ENCODINGS,
encodings,
codecOptions :
{
videoGoogleStartBitrate : 1000

View File

@ -1,3 +1,70 @@
import isElectron from 'is-electron';
let electron = null;
if (isElectron())
electron = window.require('electron');
class ElectronScreenShare
{
constructor()
{
this._stream = null;
}
start()
{
return Promise.resolve()
.then(() =>
{
return electron.desktopCapturer.getSources({ types: [ 'window', 'screen' ] });
})
.then((sources) =>
{
for (const source of sources)
{
// Currently only getting whole screen
if (source.name === 'Entire Screen')
{
return navigator.mediaDevices.getUserMedia({
audio : false,
video :
{
mandatory :
{
chromeMediaSource : 'desktop',
chromeMediaSourceId : source.id
}
}
});
}
}
})
.then((stream) =>
{
this._stream = stream;
return stream;
});
}
stop()
{
if (this._stream instanceof MediaStream === false)
{
return;
}
this._stream.getTracks().forEach((track) => track.stop());
this._stream = null;
}
isScreenShareAvailable()
{
return true;
}
}
class DisplayMediaScreenShare
{
constructor()
@ -34,12 +101,25 @@ class DisplayMediaScreenShare
return true;
}
_toConstraints()
_toConstraints(options)
{
const constraints = {
video : true
video : {}
};
if (isFinite(options.width))
{
constraints.video.width = options.width;
}
if (isFinite(options.height))
{
constraints.video.height = options.height;
}
if (isFinite(options.frameRate))
{
constraints.video.frameRate = options.frameRate;
}
return constraints;
}
}
@ -131,26 +211,31 @@ export default class ScreenShare
{
static create(device)
{
switch (device.flag)
if (isElectron())
return new ElectronScreenShare();
else
{
case 'firefox':
switch (device.flag)
{
if (device.version < 66.0)
return new FirefoxScreenShare();
else
case 'firefox':
{
if (device.version < 66.0)
return new FirefoxScreenShare();
else
return new DisplayMediaScreenShare();
}
case 'chrome':
{
return new DisplayMediaScreenShare();
}
case 'chrome':
{
return new DisplayMediaScreenShare();
}
case 'msedge':
{
return new DisplayMediaScreenShare();
}
default:
{
return new DefaultScreenShare();
}
case 'msedge':
{
return new DisplayMediaScreenShare();
}
default:
{
return new DefaultScreenShare();
}
}
}
}

View File

@ -198,4 +198,19 @@ export default class Spotlights extends EventEmitter
return true;
}
get maxSpotlights()
{
return this._maxSpotlights;
}
set maxSpotlights(maxSpotlights)
{
const oldMaxSpotlights = this._maxSpotlights;
this._maxSpotlights = maxSpotlights;
if (oldMaxSpotlights !== this._maxSpotlights)
this._spotlightsUpdated();
}
}

View File

@ -0,0 +1,99 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Route, MemoryRouter } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import App from '../components/App';
import ChooseRoom from '../components/ChooseRoom';
import RoomContext from '../RoomContext';
import configureStore from 'redux-mock-store';
const mockStore = configureStore([]);
let container;
let store;
let intl;
const roomClient = {};
beforeEach(() =>
{
container = document.createElement('div');
store = mockStore({
me : {
displayNameInProgress : false,
id : 'jesttester',
loggedIn : false,
loginEnabled : true
},
room : {
},
settings : {
displayName : 'Jest Tester'
}
});
const cache = createIntlCache();
const locale = 'en';
intl = createIntl({
locale,
messages : {}
}, cache);
document.body.appendChild(container);
});
afterEach(() =>
{
document.body.removeChild(container);
container = null;
});
describe('<ChooseRoom />', () =>
{
test('renders chooseroom', () =>
{
act(() =>
{
ReactDOM.render(
<Provider store={store}>
<RawIntlProvider value={intl}>
<RoomContext.Provider value={roomClient}>
<MemoryRouter initialEntries={[ '/' ]}>
<Route path='/' component={ChooseRoom} />
</MemoryRouter>
</RoomContext.Provider>
</RawIntlProvider>
</Provider>,
container);
});
});
});
describe('<App />', () =>
{
test('renders joindialog', () =>
{
act(() =>
{
ReactDOM.render(
<Provider store={store}>
<RawIntlProvider value={intl}>
<RoomContext.Provider value={roomClient}>
<MemoryRouter initialEntries={[ '/test' ]}>
<Route path='/:id' component={App} />
</MemoryRouter>
</RoomContext.Provider>
</RawIntlProvider>
</Provider>,
container);
});
});
});

View File

@ -0,0 +1,126 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import Room from '../components/Room';
import { SnackbarProvider } from 'notistack';
import RoomContext from '../RoomContext';
import configureStore from 'redux-mock-store';
const mockStore = configureStore([]);
let container;
let store;
let intl;
const roomClient = {};
beforeEach(() =>
{
container = document.createElement('div');
store = mockStore({
chat : [],
consumers : {},
files : {},
lobbyPeers : {},
me : {
audioDevices : null,
audioInProgress : false,
canSendMic : false,
canSendWebcam : false,
canShareFiles : false,
canShareScreen : false,
displayNameInProgress : false,
id : 'jesttester',
loggedIn : false,
loginEnabled : true,
picture : null,
raiseHand : false,
raiseHandInProgress : false,
screenShareInProgress : false,
webcamDevices : null,
webcamInProgress : false
},
notifications : [],
peerVolumes : {},
peers : {},
producers : {},
room : {
accessCode : '',
activeSpeakerId : null,
fullScreenConsumer : null,
inLobby : true,
joinByAccessCode : true,
joined : false,
lockDialogOpen : false,
locked : false,
mode : 'democratic',
name : 'test',
selectedPeerId : null,
settingsOpen : false,
showSettings : false,
signInRequired : false,
spotlights : [],
state : 'connecting',
toolbarsVisible : true,
torrentSupport : false,
windowConsumer : null
},
settings : {
advancedMode : true,
displayName : 'Jest Tester',
resolution : 'ultra',
selectedAudioDevice : 'default',
selectedWebcam : 'soifjsiajosjfoi'
},
toolarea : {
currentToolTab : 'chat',
toolAreaOpen : false,
unreadFiles : 0,
unreadMessages : 0
}
});
const cache = createIntlCache();
const locale = 'en';
intl = createIntl({
locale,
messages : {}
}, cache);
document.body.appendChild(container);
});
afterEach(() =>
{
document.body.removeChild(container);
container = null;
});
describe('<Room />', () =>
{
test('renders correctly', () =>
{
act(() =>
{
ReactDOM.render(
<Provider store={store}>
<RawIntlProvider value={intl}>
<SnackbarProvider>
<RoomContext.Provider value={roomClient}>
<Room />
</RoomContext.Provider>
</SnackbarProvider>
</RawIntlProvider>
</Provider>,
container);
});
});
});

View File

@ -0,0 +1,9 @@
import RoomClient from '../RoomClient';
describe('new RoomClient() without paramaters throws Error', () =>
{
test('Matches the snapshot', () =>
{
expect(() => new RoomClient()).toThrow(Error);
});
});

View File

@ -34,6 +34,14 @@ export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLay
payload : { consumerId, spatialLayer, temporalLayer }
});
export const setConsumerPriority = (consumerId, priority) =>
{
return {
type : 'SET_CONSUMER_PRIORITY',
payload : { consumerId, priority }
};
};
export const setConsumerTrack = (consumerId, track) =>
({
type : 'SET_CONSUMER_TRACK',

View File

@ -1,9 +1,3 @@
export const setRoomUrl = (url) =>
({
type : 'SET_ROOM_URL',
payload : { url }
});
export const setRoomName = (name) =>
({
type : 'SET_ROOM_NAME',

View File

@ -26,3 +26,14 @@ export const toggleAdvancedMode = () =>
({
type : 'TOGGLE_ADVANCED_MODE'
});
export const togglePermanentTopBar = () =>
({
type : 'TOGGLE_PERMANENT_TOPBAR'
});
export const setLastN = (lastN) =>
({
type : 'SET_LAST_N',
payload : { lastN }
});

View File

@ -1,4 +1,5 @@
import React, { useEffect, Suspense } from 'react';
import { useParams } from 'react-router';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import JoinDialog from './JoinDialog';
@ -13,6 +14,8 @@ const App = (props) =>
room
} = props;
const { id } = useParams();
useEffect(() =>
{
Room.preload();
@ -23,7 +26,7 @@ const App = (props) =>
if (!room.joined)
{
return (
<JoinDialog />
<JoinDialog roomId={id} />
);
}
else

View File

@ -0,0 +1,291 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import isElectron from 'is-electron';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import randomString from 'random-string';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import IconButton from '@material-ui/core/IconButton';
import AccountCircle from '@material-ui/icons/AccountCircle';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import CookieConsent from 'react-cookie-consent';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
import MuiDialogActions from '@material-ui/core/DialogActions';
const styles = (theme) =>
({
root :
{
display : 'flex',
width : '100%',
height : '100%',
backgroundColor : 'var(--background-color)',
backgroundImage : `url(${window.config ? window.config.background : null})`,
backgroundAttachment : 'fixed',
backgroundPosition : 'center',
backgroundSize : 'cover',
backgroundRepeat : 'no-repeat'
},
dialogTitle :
{
},
dialogPaper :
{
width : '30vw',
padding : theme.spacing(2),
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
logo :
{
display : 'block',
paddingBottom : '1vh'
},
loginButton :
{
position : 'absolute',
right : theme.spacing(2),
top : theme.spacing(2),
padding : 0
},
largeIcon :
{
fontSize : '2em'
},
largeAvatar :
{
width : 50,
height : 50
},
green :
{
color : 'rgba(0, 153, 0, 1)'
}
});
const DialogTitle = withStyles(styles)((props) =>
{
const [ open, setOpen ] = useState(false);
const intl = useIntl();
useEffect(() =>
{
const openTimer = setTimeout(() => setOpen(true), 1000);
const closeTimer = setTimeout(() => setOpen(false), 4000);
return () =>
{
clearTimeout(openTimer);
clearTimeout(closeTimer);
};
}, []);
const { children, classes, myPicture, onLogin, ...other } = props;
const handleTooltipClose = () =>
{
setOpen(false);
};
const handleTooltipOpen = () =>
{
setOpen(true);
};
return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography>
{ window.config && window.config.loginEnabled &&
<Tooltip
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
open={open}
title={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left'
>
<IconButton
aria-label='Account'
className={classes.loginButton}
color='inherit'
onClick={onLogin}
>
{ myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} />
:
<AccountCircle className={classes.largeIcon} />
}
</IconButton>
</Tooltip>
}
</MuiDialogTitle>
);
});
const DialogContent = withStyles((theme) => ({
root :
{
padding : theme.spacing(2)
}
}))(MuiDialogContent);
const DialogActions = withStyles((theme) => ({
root :
{
margin : 0,
padding : theme.spacing(1)
}
}))(MuiDialogActions);
const ChooseRoom = ({
roomClient,
loggedIn,
myPicture,
classes
}) =>
{
const [ roomId, setRoomId ] =
useState(randomString({ length: 8 }).toLowerCase());
const intl = useIntl();
return (
<div className={classes.root}>
<Dialog
open
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle
myPicture={myPicture}
onLogin={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
<hr />
</DialogTitle>
<DialogContent>
<DialogContentText gutterBottom>
<FormattedMessage
id='room.chooseRoom'
defaultMessage='Choose the name of the room you would like to join'
/>
</DialogContentText>
<TextField
id='roomId'
label={intl.formatMessage({
id : 'label.roomName',
defaultMessage : 'Room name'
})}
value={roomId}
variant='outlined'
margin='normal'
onChange={(event) =>
{
const { value } = event.target;
setRoomId(value.toLowerCase());
}}
onBlur={() =>
{
if (roomId === '')
setRoomId(randomString({ length: 8 }).toLowerCase());
}}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button
component={Link}
to={roomId}
variant='contained'
color='secondary'
>
<FormattedMessage
id='label.chooseRoomButton'
defaultMessage='Continue'
/>
</Button>
</DialogActions>
{ !isElectron() &&
<CookieConsent buttonText={intl.formatMessage({
id : 'room.consentUnderstand',
defaultMessage : 'I understand'
})}>
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
}
</Dialog>
</div>
);
};
ChooseRoom.propTypes =
{
roomClient : PropTypes.any.isRequired,
loginEnabled : PropTypes.bool.isRequired,
loggedIn : PropTypes.bool.isRequired,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
loginEnabled : state.me.loginEnabled,
loggedIn : state.me.loggedIn,
myPicture : state.me.picture
};
};
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.picture === next.me.picture
);
}
}
)(withStyles(styles)(ChooseRoom)));

View File

@ -1,139 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import * as toolareaActions from '../../actions/toolareaActions';
import BuddyImage from '../../images/buddy.svg';
const styles = () =>
({
root :
{
width : '12vmin',
height : '9vmin',
position : 'absolute',
bottom : '3%',
right : '3%',
color : 'rgba(170, 170, 170, 1)',
cursor : 'pointer',
backgroundImage : `url(${BuddyImage})`,
backgroundColor : 'rgba(42, 75, 88, 1)',
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
border : 'var(--peer-border)',
boxShadow : 'var(--peer-shadow)',
textAlign : 'center',
verticalAlign : 'middle',
lineHeight : '1.8vmin',
fontSize : '1.7vmin',
fontWeight : 'bolder',
animation : 'none',
'&.pulse' :
{
animation : 'pulse 0.5s'
}
},
'@keyframes pulse' :
{
'0%' :
{
transform : 'scale3d(1, 1, 1)'
},
'50%' :
{
transform : 'scale3d(1.2, 1.2, 1.2)'
},
'100%' :
{
transform : 'scale3d(1, 1, 1)'
}
}
});
class HiddenPeers extends React.PureComponent
{
constructor(props)
{
super(props);
this.state = { className: '' };
}
componentDidUpdate(prevProps)
{
const { hiddenPeersCount } = this.props;
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ className: 'pulse' }, () =>
{
if (this.timeout)
{
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() =>
{
this.setState({ className: '' });
}, 500);
});
}
}
render()
{
const {
hiddenPeersCount,
openUsersTab,
classes
} = this.props;
return (
<div
className={classnames(classes.root, this.state.className)}
onClick={() => openUsersTab()}
>
<p>
+{hiddenPeersCount} <br />
<FormattedMessage
id='room.hiddenPeers'
defaultMessage={
`{hiddenPeersCount, plural,
one {participant}
other {participants}}`
}
values={{
hiddenPeersCount
}}
/>
</p>
</div>
);
}
}
HiddenPeers.propTypes =
{
hiddenPeersCount : PropTypes.number,
openUsersTab : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapDispatchToProps = (dispatch) =>
{
return {
openUsersTab : () =>
{
dispatch(toolareaActions.openToolArea());
dispatch(toolareaActions.setToolTab('users'));
}
};
};
export default connect(
null,
mapDispatchToProps
)(withStyles(styles)(HiddenPeers));

View File

@ -31,21 +31,28 @@ const styles = (theme) =>
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
'&.webcam' :
'&.hover' :
{
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
},
'&.active-speaker' :
{
// transition : 'filter .2s',
// filter : 'grayscale(0)',
borderColor : 'var(--active-speaker-border-color)'
},
'&:not(.active-speaker):not(.screen)' :
{
// transition : 'filter 10s',
// filter : 'grayscale(0.75)'
},
'&.webcam' :
{
order : 1
},
'&.screen' :
{
order : 2
},
'&.hover' :
{
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
},
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
}
},
fab :

View File

@ -31,21 +31,28 @@ const styles = (theme) =>
backgroundPosition : 'bottom',
backgroundSize : 'auto 85%',
backgroundRepeat : 'no-repeat',
'&.webcam' :
'&.hover' :
{
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
},
'&.active-speaker' :
{
// transition : 'filter .2s',
// filter : 'grayscale(0)',
borderColor : 'var(--active-speaker-border-color)'
},
'&:not(.active-speaker):not(.screen)' :
{
// transition : 'filter 10s',
// filter : 'grayscale(0.75)'
},
'&.webcam' :
{
order : 4
},
'&.screen' :
{
order : 3
},
'&.hover' :
{
boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)'
},
'&.active-speaker' :
{
borderColor : 'var(--active-speaker-border-color)'
}
},
fab :
@ -145,16 +152,6 @@ const Peer = (props) =>
!screenConsumer.remotelyPaused
);
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const rootStyle =
@ -325,11 +322,27 @@ const Peer = (props) =>
peer={peer}
displayName={peer.displayName}
showPeerInfo
consumerSpatialLayers={webcamConsumer ? webcamConsumer.spatialLayers : null}
consumerTemporalLayers={webcamConsumer ? webcamConsumer.temporalLayers : null}
consumerCurrentSpatialLayer={
webcamConsumer ? webcamConsumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
webcamConsumer ? webcamConsumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
webcamConsumer ? webcamConsumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
webcamConsumer ? webcamConsumer.preferredTemporalLayer : null
}
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
videoTrack={webcamConsumer && webcamConsumer.track}
videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer && micConsumer.codec}
videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null}
videoScore={webcamConsumer ? webcamConsumer.score : null}
>
<Volume id={peer.id} />
</VideoView>
@ -360,109 +373,124 @@ const Peer = (props) =>
}}
style={rootStyle}
>
{ !screenVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div className={classnames(classes.viewContainer)}>
{ !screenVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
{ screenVisible &&
<div className={classnames(classes.viewContainer)}>
<div
className={classnames(classes.controls, hover && 'hover')}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ !smallScreen &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
}
setHover(false);
}, 2000);
}}
>
{ !smallScreen &&
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={!screenVisible}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
toggleConsumerWindow(screenConsumer);
}}
>
<FullScreenIcon />
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
videoContain
videoTrack={screenConsumer && screenConsumer.track}
videoVisible={screenVisible}
videoProfile={screenProfile}
videoCodec={screenConsumer && screenConsumer.codec}
/>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
}
<VideoView
advancedMode={advancedMode}
videoContain
consumerSpatialLayers={
screenConsumer ? screenConsumer.spatialLayers : null
}
consumerTemporalLayers={
screenConsumer ? screenConsumer.temporalLayers : null
}
consumerCurrentSpatialLayer={
screenConsumer ? screenConsumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
screenConsumer ? screenConsumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
screenConsumer ? screenConsumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
screenConsumer ? screenConsumer.preferredTemporalLayer : null
}
videoMultiLayer={screenConsumer && screenConsumer.type !== 'simple'}
videoTrack={screenConsumer && screenConsumer.track}
videoVisible={screenVisible}
videoCodec={screenConsumer && screenConsumer.codec}
/>
</div>
</div>
}
</React.Fragment>
@ -488,17 +516,17 @@ Peer.propTypes =
theme : PropTypes.object.isRequired
};
const makeMapStateToProps = (initialState, props) =>
const makeMapStateToProps = (initialState, { id }) =>
{
const getPeerConsumers = makePeerConsumerSelector();
const mapStateToProps = (state) =>
{
return {
peer : state.peers[props.id],
...getPeerConsumers(state, props),
peer : state.peers[id],
...getPeerConsumers(state, id),
windowConsumer : state.room.windowConsumer,
activeSpeaker : props.id === state.room.activeSpeakerId
activeSpeaker : id === state.room.activeSpeakerId
};
};

View File

@ -190,13 +190,13 @@ SpeakerPeer.propTypes =
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state, props) =>
const mapStateToProps = (state, { id }) =>
{
const getPeerConsumers = makePeerConsumerSelector();
return {
peer : state.peers[props.id],
...getPeerConsumers(state, props)
peer : state.peers[id],
...getPeerConsumers(state, id)
};
};

View File

@ -2,7 +2,8 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
lobbyPeersKeySelector
lobbyPeersKeySelector,
peersLengthSelector
} from '../Selectors';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
@ -22,6 +23,7 @@ import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
import SettingsIcon from '@material-ui/icons/Settings';
import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import Button from '@material-ui/core/Button';
@ -43,6 +45,10 @@ const styles = (theme) =>
display : 'block'
}
},
divider :
{
marginLeft : theme.spacing(3),
},
show :
{
opacity : 1,
@ -115,7 +121,9 @@ const TopBar = (props) =>
const {
roomClient,
room,
peersLength,
lobbyPeers,
permanentTopBar,
myPicture,
loggedIn,
loginEnabled,
@ -125,6 +133,7 @@ const TopBar = (props) =>
setSettingsOpen,
setLockDialogOpen,
toggleToolArea,
openUsersTab,
unread,
classes
} = props;
@ -165,12 +174,13 @@ const TopBar = (props) =>
return (
<AppBar
position='fixed'
className={room.toolbarsVisible ? classes.show : classes.hide}
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
>
<Toolbar>
<PulsingBadge
color='secondary'
badgeContent={unread}
onClick={() => toggleToolArea()}
>
<IconButton
color='inherit'
@ -178,23 +188,81 @@ const TopBar = (props) =>
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography
className={classes.title}
variant='h6'
color='inherit'
noWrap
>
{ window.config.title }
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
<Tooltip title={lockTooltip}>
<IconButton
aria-label={intl.formatMessage({
@ -246,43 +314,6 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
}
{ fullscreenEnabled &&
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
</IconButton>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
@ -305,6 +336,7 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
}
<div className={classes.divider} />
<Button
aria-label={intl.formatMessage({
id : 'label.leave',
@ -330,7 +362,9 @@ TopBar.propTypes =
{
roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired,
peersLength : PropTypes.number,
lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool,
myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,
@ -341,6 +375,7 @@ TopBar.propTypes =
setSettingsOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
@ -349,8 +384,9 @@ TopBar.propTypes =
const mapStateToProps = (state) =>
({
room : state.room,
peersLength : peersLengthSelector(state),
lobbyPeers : lobbyPeersKeySelector(state),
advancedMode : state.settings.advancedMode,
permanentTopBar : state.settings.permanentTopBar,
loggedIn : state.me.loggedIn,
loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture,
@ -375,6 +411,11 @@ const mapDispatchToProps = (dispatch) =>
toggleToolArea : () =>
{
dispatch(toolareaActions.toggleToolArea());
},
openUsersTab : () =>
{
dispatch(toolareaActions.openToolArea());
dispatch(toolareaActions.setToolTab('users'));
}
});
@ -387,7 +428,9 @@ export default withRoomContext(connect(
{
return (
prev.room === next.room &&
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.me.loggedIn === next.me.loggedIn &&
prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture &&

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import isElectron from 'is-electron';
import * as settingsActions from '../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
@ -27,7 +28,7 @@ const styles = (theme) =>
width : '100%',
height : '100%',
backgroundColor : 'var(--background-color)',
backgroundImage : `url(${window.config.background})`,
backgroundImage : `url(${window.config ? window.config.background : null})`,
backgroundAttachment : 'fixed',
backgroundPosition : 'center',
backgroundSize : 'cover',
@ -116,9 +117,9 @@ const DialogTitle = withStyles(styles)((props) =>
return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography>
{ window.config.loginEnabled &&
{ window.config && window.config.loginEnabled &&
<Tooltip
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
@ -165,6 +166,7 @@ const DialogActions = withStyles((theme) => ({
const JoinDialog = ({
roomClient,
room,
roomId,
displayName,
displayNameInProgress,
loggedIn,
@ -210,7 +212,7 @@ const JoinDialog = ({
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ window.config.title }
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
<hr />
</DialogTitle>
<DialogContent>
@ -226,7 +228,7 @@ const JoinDialog = ({
id='room.roomId'
defaultMessage='Room ID: {roomName}'
values={{
roomName : room.name
roomName : roomId
}}
/>
</DialogContentText>
@ -275,7 +277,7 @@ const JoinDialog = ({
<Button
onClick={() =>
{
roomClient.join({ joinVideo: false });
roomClient.join({ roomId, joinVideo: false });
}}
variant='contained'
color='secondary'
@ -288,7 +290,7 @@ const JoinDialog = ({
<Button
onClick={() =>
{
roomClient.join({ joinVideo: true });
roomClient.join({ roomId, joinVideo: true });
}}
variant='contained'
color='secondary'
@ -333,12 +335,17 @@ const JoinDialog = ({
</DialogContent>
}
<CookieConsent>
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
{ !isElectron() &&
<CookieConsent buttonText={intl.formatMessage({
id : 'room.consentUnderstand',
defaultMessage : 'I understand'
})}>
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
}
</Dialog>
</div>
);
@ -348,6 +355,7 @@ JoinDialog.propTypes =
{
roomClient : PropTypes.any.isRequired,
room : PropTypes.object.isRequired,
roomId : PropTypes.string.isRequired,
displayName : PropTypes.string.isRequired,
displayNameInProgress : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired,

View File

@ -93,7 +93,7 @@ const ChatInput = (props) =>
{
if (message && message !== '')
{
const sendMessage = this.createNewMessage(message, 'response', displayName, picture);
const sendMessage = createNewMessage(message, 'response', displayName, picture);
roomClient.sendChatMessage(sendMessage);

View File

@ -14,7 +14,7 @@ linkRenderer.link = (href, title, text) =>
title = title ? title : href;
text = text ? text : href;
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
return `<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`;
};
const styles = (theme) =>
@ -81,7 +81,11 @@ const Message = (props) =>
marked.parse(
text,
{ renderer: linkRenderer }
)
),
{
ALLOWED_TAGS : [ 'a' ],
ALLOWED_ATTR : [ 'href', 'target', 'title' ]
}
) }}
/>
<Typography variant='caption'>{self ? 'Me' : name} - {time}</Typography>

View File

@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import IconButton from '@material-ui/core/IconButton';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import ScreenIcon from '@material-ui/icons/ScreenShare';
@ -128,6 +130,8 @@ const styles = (theme) =>
const ListPeer = (props) =>
{
const intl = useIntl();
const {
roomClient,
peer,
@ -174,47 +178,49 @@ const ListPeer = (props) =>
{children}
<div className={classes.controls}>
{ screenConsumer &&
<div
className={classnames(classes.button, 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteScreenSharing',
defaultMessage : 'Mute participant share'
})}
color={ screenVisible ? 'primary' : 'secondary'}
disabled={ peer.peerScreenInProgress }
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}}
{
e.stopPropagation();
screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
}}
>
{ screenVisible ?
<ScreenIcon />
:
<ScreenOffIcon />
}
</div>
</IconButton>
}
<div
className={classnames(classes.button, 'mic', {
on : micEnabled,
off : !micEnabled,
disabled : peer.peerAudioInProgress
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute participant'
})}
color={ micEnabled ? 'primary' : 'secondary'}
disabled={ peer.peerAudioInProgress }
onClick={(e) =>
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
{
e.stopPropagation();
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</div>
</IconButton>
</div>
</div>
);
@ -232,15 +238,15 @@ ListPeer.propTypes =
classes : PropTypes.object.isRequired
};
const makeMapStateToProps = (initialState, props) =>
const makeMapStateToProps = (initialState, { id }) =>
{
const getPeerConsumers = makePeerConsumerSelector();
const mapStateToProps = (state) =>
{
return {
peer : state.peers[props.id],
...getPeerConsumers(state, props)
peer : state.peers[id],
...getPeerConsumers(state, id)
};
};

View File

@ -100,16 +100,16 @@ class ParticipantList extends React.PureComponent
defaultMessage='Participants in Spotlight'
/>
</li>
{ spotlightPeers.map((peer) => (
{ spotlightPeers.map((peerId) => (
<li
key={peer.id}
key={peerId}
className={classNames(classes.listItem, {
selected : peer.id === selectedPeerId
selected : peerId === selectedPeerId
})}
onClick={() => roomClient.setSelectedPeer(peer.id)}
onClick={() => roomClient.setSelectedPeer(peerId)}
>
<ListPeer id={peer.id} advancedMode={advancedMode}>
<Volume small id={peer.id} />
<ListPeer id={peerId} advancedMode={advancedMode}>
<Volume small id={peerId} />
</ListPeer>
</li>
))}

View File

@ -2,16 +2,13 @@ import React from 'react';
import { connect } from 'react-redux';
import {
spotlightPeersSelector,
peersLengthSelector,
videoBoxesSelector,
spotlightsLengthSelector
videoBoxesSelector
} from '../Selectors';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Peer from '../Containers/Peer';
import Me from '../Containers/Me';
import HiddenPeers from '../Containers/HiddenPeers';
const RATIO = 1.334;
const PADDING_V = 50;
@ -71,7 +68,7 @@ class Democratic extends React.PureComponent
const width = this.peersRef.current.clientWidth - PADDING_H;
const height = this.peersRef.current.clientHeight -
(this.props.toolbarsVisible ? PADDING_V : PADDING_H);
(this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H);
let x, y, space;
@ -91,11 +88,11 @@ class Democratic extends React.PureComponent
break;
}
}
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.94 * x))
{
this.setState({
peerWidth : 0.95 * x,
peerHeight : 0.95 * y
peerWidth : 0.94 * x,
peerHeight : 0.94 * y
});
}
};
@ -130,10 +127,9 @@ class Democratic extends React.PureComponent
{
const {
advancedMode,
peersLength,
spotlightsPeers,
spotlightsLength,
toolbarsVisible,
permanentTopBar,
classes
} = this.props;
@ -147,7 +143,7 @@ class Democratic extends React.PureComponent
<div
className={classnames(
classes.root,
toolbarsVisible ? classes.showingToolBar : classes.hiddenToolBar
toolbarsVisible || permanentTopBar ? classes.showingToolBar : classes.hiddenToolBar
)}
ref={this.peersRef}
>
@ -160,19 +156,14 @@ class Democratic extends React.PureComponent
{
return (
<Peer
key={peer.id}
key={peer}
advancedMode={advancedMode}
id={peer.id}
id={peer}
spacing={6}
style={style}
/>
);
})}
{ spotlightsLength < peersLength &&
<HiddenPeers
hiddenPeersCount={peersLength - spotlightsLength}
/>
}
</div>
);
}
@ -181,22 +172,20 @@ class Democratic extends React.PureComponent
Democratic.propTypes =
{
advancedMode : PropTypes.bool,
peersLength : PropTypes.number,
boxes : PropTypes.number,
spotlightsLength : PropTypes.number,
spotlightsPeers : PropTypes.array.isRequired,
toolbarsVisible : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
peersLength : peersLengthSelector(state),
boxes : videoBoxesSelector(state),
spotlightsPeers : spotlightPeersSelector(state),
spotlightsLength : spotlightsLengthSelector(state),
toolbarsVisible : state.room.toolbarsVisible
boxes : videoBoxesSelector(state),
spotlightsPeers : spotlightPeersSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
permanentTopBar : state.settings.permanentTopBar
};
};
@ -212,7 +201,8 @@ export default connect(
prev.producers === next.producers &&
prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights &&
prev.room.toolbarsVisible === next.room.toolbarsVisible
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.settings.permanentTopBar === next.settings.permanentTopBar
);
}
}

View File

@ -4,14 +4,12 @@ import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import {
spotlightsLengthSelector,
videoBoxesSelector
} from '../Selectors';
import { withRoomContext } from '../../RoomContext';
import Me from '../Containers/Me';
import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer';
import HiddenPeers from '../Containers/HiddenPeers';
import Grid from '@material-ui/core/Grid';
const styles = () =>
@ -207,7 +205,6 @@ class Filmstrip extends React.PureComponent
myId,
advancedMode,
spotlights,
spotlightsLength,
classes
} = this.props;
@ -284,12 +281,6 @@ class Filmstrip extends React.PureComponent
})}
</Grid>
</div>
{ spotlightsLength<Object.keys(peers).length &&
<HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>
}
</div>
);
}
@ -303,7 +294,6 @@ Filmstrip.propTypes = {
consumers : PropTypes.object.isRequired,
myId : PropTypes.string.isRequired,
selectedPeerId : PropTypes.string,
spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number,
classes : PropTypes.object.isRequired
@ -318,8 +308,7 @@ const mapStateToProps = (state) =>
consumers : state.consumers,
myId : state.me.id,
spotlights : state.room.spotlights,
spotlightsLength : spotlightsLengthSelector(state),
boxes : videoBoxesSelector(state),
boxes : videoBoxesSelector(state)
};
};

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from './appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import isElectron from 'is-electron';
import * as roomActions from '../actions/roomActions';
import * as toolareaActions from '../actions/toolareaActions';
import { idle } from '../utils';
@ -33,7 +34,7 @@ const styles = (theme) =>
width : '100%',
height : '100%',
backgroundColor : 'var(--background-color)',
backgroundImage : `url(${window.config.background})`,
backgroundImage : `url(${window.config ? window.config.background : null})`,
backgroundAttachment : 'fixed',
backgroundPosition : 'center',
backgroundSize : 'cover',
@ -152,12 +153,21 @@ class Room extends React.PureComponent
return (
<div className={classes.root}>
<CookieConsent>
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
{ !isElectron() &&
<CookieConsent
buttonText={
<FormattedMessage
id = 'room.consentUnderstand'
defaultMessage = 'I understand'
/>
}
>
<FormattedMessage
id = 'room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
}
<FullScreenView advancedMode={advancedMode} />
@ -194,9 +204,13 @@ class Room extends React.PureComponent
<View advancedMode={advancedMode} />
<LockDialog />
{ room.lockDialogOpen &&
<LockDialog />
}
<Settings />
{ room.settingsOpen &&
<Settings />
}
</div>
);
}

View File

@ -5,8 +5,8 @@ const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights;
const peersSelector = (state) => state.peers;
const lobbyPeersSelector = (state) => state.lobbyPeers;
const getPeerConsumers = (state, props) =>
(state.peers[props.id] ? state.peers[props.id].consumers : null);
const getPeerConsumers = (state, id) =>
(state.peers[id] ? state.peers[id].consumers : null);
const getAllConsumers = (state) => state.consumers;
const peersKeySelector = createSelector(
peersSelector,
@ -70,15 +70,8 @@ export const spotlightsLengthSelector = createSelector(
export const spotlightPeersSelector = createSelector(
spotlightsSelector,
peersSelector,
(spotlights, peers) =>
spotlights.reduce((result, peerId) =>
{
if (peers[peerId])
result.push(peers[peerId]);
return result;
}, [])
peersKeySelector,
(spotlights, peers) => peers.filter((peerId) => spotlights.includes(peerId))
);
export const peersLengthSelector = createSelector(

View File

@ -59,6 +59,7 @@ const Settings = ({
me,
settings,
onToggleAdvancedMode,
onTogglePermanentTopBar,
handleCloseSettings,
handleChangeMode,
classes
@ -296,6 +297,48 @@ const Settings = ({
defaultMessage : 'Advanced mode'
})}
/>
{ settings.advancedMode &&
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.lastN || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeMaxSpotlights(event.target.value);
}}
name='Last N'
autoWidth
className={classes.selectEmpty}
>
{ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map((lastN) =>
{
return (
<MenuItem key={lastN} value={lastN}>
{lastN}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.lastn'
defaultMessage='Number of visible videos'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />}
label={intl.formatMessage({
id : 'settings.permanentTopBar',
defaultMessage : 'Permanent top bar'
})}
/>
</React.Fragment>
}
<DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
<FormattedMessage
@ -315,6 +358,7 @@ Settings.propTypes =
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
handleCloseSettings : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
@ -331,6 +375,7 @@ const mapStateToProps = (state) =>
const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
handleChangeMode : roomActions.setDisplayMode,
handleCloseSettings : roomActions.setSettingsOpen
};

View File

@ -83,6 +83,7 @@ const FullScreenView = (props) =>
consumer,
toggleConsumerFullscreen,
toolbarsVisible,
permanentTopBar,
classes
} = props;
@ -105,7 +106,7 @@ const FullScreenView = (props) =>
<div className={classes.controls}>
<div
className={classnames(classes.button, {
visible : toolbarsVisible
visible : toolbarsVisible || permanentTopBar
})}
onClick={(e) =>
{
@ -134,13 +135,15 @@ FullScreenView.propTypes =
consumer : appPropTypes.Consumer,
toggleConsumerFullscreen : PropTypes.func.isRequired,
toolbarsVisible : PropTypes.bool,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
consumer : state.consumers[state.room.fullScreenConsumer],
toolbarsVisible : state.room.toolbarsVisible
toolbarsVisible : state.room.toolbarsVisible,
permanentTopBar : state.settings.permanentTopBar
});
const mapDispatchToProps = (dispatch) =>
@ -162,7 +165,8 @@ export default connect(
return (
prev.consumers[prev.room.fullScreenConsumer] ===
next.consumers[next.room.fullScreenConsumer] &&
prev.room.toolbarsVisible === next.room.toolbarsVisible
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.settings.permanentTopBar === next.settings.permanentTopBar
);
}
}

View File

@ -137,7 +137,15 @@ class VideoView extends React.PureComponent
videoContain,
advancedMode,
videoVisible,
videoProfile,
videoMultiLayer,
// audioScore,
// videoScore,
// consumerSpatialLayers,
// consumerTemporalLayers,
consumerCurrentSpatialLayer,
consumerCurrentTemporalLayer,
consumerPreferredSpatialLayer,
consumerPreferredTemporalLayer,
audioCodec,
videoCodec,
onChangeDisplayName,
@ -161,7 +169,19 @@ class VideoView extends React.PureComponent
<div className={classes.box}>
{ audioCodec && <p>{audioCodec}</p> }
{ videoCodec && <p>{videoCodec} {videoProfile}</p> }
{ videoCodec &&
<p>
{videoCodec}
</p>
}
{ videoMultiLayer &&
<p>
{`current spatial-temporal layers: ${consumerCurrentSpatialLayer} ${consumerCurrentTemporalLayer}`}
<br />
{`preferred spatial-temporal layers: ${consumerPreferredSpatialLayer} ${consumerPreferredTemporalLayer}`}
</p>
}
{ (videoVisible && videoWidth !== null) &&
<p>{videoWidth}x{videoHeight}</p>
@ -202,12 +222,10 @@ class VideoView extends React.PureComponent
className={classnames(classes.video, {
hidden : !videoVisible,
'isMe' : isMe && !isScreen,
loading : videoProfile === 'none',
contain : videoContain
})}
autoPlay
playsInline
muted={isMe}
/>
{children}
@ -293,20 +311,28 @@ class VideoView extends React.PureComponent
VideoView.propTypes =
{
isMe : PropTypes.bool,
isScreen : PropTypes.bool,
displayName : PropTypes.string,
showPeerInfo : PropTypes.bool,
videoContain : PropTypes.bool,
advancedMode : PropTypes.bool,
videoTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired,
videoProfile : PropTypes.string,
audioCodec : PropTypes.string,
videoCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func,
children : PropTypes.object,
classes : PropTypes.object.isRequired
isMe : PropTypes.bool,
isScreen : PropTypes.bool,
displayName : PropTypes.string,
showPeerInfo : PropTypes.bool,
videoContain : PropTypes.bool,
advancedMode : PropTypes.bool,
videoTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired,
consumerSpatialLayers : PropTypes.number,
consumerTemporalLayers : PropTypes.number,
consumerCurrentSpatialLayer : PropTypes.number,
consumerCurrentTemporalLayer : PropTypes.number,
consumerPreferredSpatialLayer : PropTypes.number,
consumerPreferredTemporalLayer : PropTypes.number,
videoMultiLayer : PropTypes.bool,
audioScore : PropTypes.any,
videoScore : PropTypes.any,
audioCodec : PropTypes.string,
videoCodec : PropTypes.string,
onChangeDisplayName : PropTypes.func,
children : PropTypes.object,
classes : PropTypes.object.isRequired
};
export default withStyles(styles)(VideoView);

View File

@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
export const Room = PropTypes.shape(
{
url : PropTypes.string.isRequired,
state : PropTypes.oneOf(
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
activeSpeakerId : PropTypes.string

View File

@ -0,0 +1,53 @@
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const Menu = electron.Menu;
const path = require('path');
const url = require('url');
let mainWindow;
function createWindow()
{
mainWindow = new BrowserWindow({
width : 1280,
height : 720,
webPreferences : { nodeIntegration: true }
});
Menu.setApplicationMenu(null);
const startUrl = process.env.ELECTRON_START_URL || url.format({
pathname : path.join(__dirname, '/../build/index.html'),
protocol : 'file:',
slashes : true
});
mainWindow.loadURL(startUrl);
mainWindow.on('closed', () =>
{
mainWindow = null;
});
}
app.on('ready', createWindow);
app.on('window-all-closed', () =>
{
if (process.platform !== 'darwin')
{
app.quit();
}
});
app.on('activate', () =>
{
if (mainWindow === null)
{
createWindow();
}
});

View File

@ -0,0 +1,39 @@
const net = require('net');
const port = process.env.PORT ? (process.env.PORT - 100) : 3000;
process.env.ELECTRON_START_URL = `http://localhost:${port}`;
const client = new net.Socket();
let startedElectron = false;
const tryConnection = () =>
client.connect({ port: port }, () =>
{
client.end();
if (!startedElectron)
{
// eslint-disable-next-line no-console
console.log('starting electron');
startedElectron = true;
const exec = require('child_process').exec;
const electron = exec('npm run electron');
electron.stdout.on('data', (data) =>
{
// eslint-disable-next-line no-console
console.log(`stdout: ${data.toString()}`);
});
}
});
tryConnection();
client.on('error', () =>
{
setTimeout(tryConnection, 1000);
});

View File

@ -1,35 +1,61 @@
import domready from 'domready';
import React from 'react';
import React, { Suspense } from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import isElectron from 'is-electron';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import { Route, HashRouter, BrowserRouter } from 'react-router-dom';
import randomString from 'random-string';
import Logger from './Logger';
import debug from 'debug';
import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import deviceInfo from './deviceInfo';
import * as roomActions from './actions/roomActions';
import * as meActions from './actions/meActions';
import App from './components/App';
import ChooseRoom from './components/ChooseRoom';
import LoadingView from './components/LoadingView';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import { PersistGate } from 'redux-persist/lib/integration/react';
import { persistor, store } from './store';
import { SnackbarProvider } from 'notistack';
import * as serviceWorker from './serviceWorker';
import { ReactLazyPreload } from './components/ReactLazyPreload';
import messagesEnglish from './translations/en';
// import messagesEnglish from './translations/en';
import messagesNorwegian from './translations/nb';
import messagesGerman from './translations/de';
import messagesHungarian from './translations/hu';
import messagesPolish from './translations/pl';
import messagesDanish from './translations/dk';
import messagesFrench from './translations/fr';
import messagesGreek from './translations/el';
import messagesRomanian from './translations/ro';
import messagesPortuguese from './translations/pt';
import messagesChinese from './translations/cn';
import messagesSpanish from './translations/es';
import messagesCroatian from './translations/hr';
import './index.css';
const App = ReactLazyPreload(() => import(/* webpackChunkName: "app" */ './components/App'));
const cache = createIntlCache();
const messages =
{
'en' : messagesEnglish,
'nb' : messagesNorwegian
// 'en' : messagesEnglish,
'nb' : messagesNorwegian,
'de' : messagesGerman,
'hu' : messagesHungarian,
'pl' : messagesPolish,
'dk' : messagesDanish,
'fr' : messagesFrench,
'el' : messagesGreek,
'ro' : messagesRomanian,
'pt' : messagesPortuguese,
'zh' : messagesChinese,
'es' : messagesSpanish,
'hr' : messagesCroatian
};
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
@ -52,6 +78,13 @@ RoomClient.init({ store, intl });
const theme = createMuiTheme(window.config.theme);
let Router;
if (isElectron())
Router = HashRouter;
else
Router = BrowserRouter;
domready(() =>
{
logger.debug('DOM ready');
@ -67,34 +100,17 @@ function run()
const urlParser = new URL(window.location);
const parameters = urlParser.searchParams;
let roomId = (urlParser.pathname).substr(1);
if (!roomId)
roomId = parameters.get('roomId');
if (roomId)
roomId = roomId.toLowerCase();
else
{
roomId = randomString({ length: 8 }).toLowerCase();
parameters.set('roomId', roomId);
window.history.pushState('', '', urlParser.toString());
}
const accessCode = parameters.get('code');
const produce = parameters.get('produce') !== 'false';
const useSimulcast = parameters.get('simulcast') === 'true';
const useSharingSimulcast = parameters.get('sharingSimulcast') === 'true';
const forceTcp = parameters.get('forceTcp') === 'true';
const roomUrl = window.location.href.split('?')[0];
const displayName = parameters.get('displayName');
const muted = parameters.get('muted') === 'true';
// Get current device.
const device = deviceInfo();
store.dispatch(
roomActions.setRoomUrl(roomUrl));
store.dispatch(
meActions.setMe({
peerId,
@ -103,7 +119,17 @@ function run()
);
roomClient = new RoomClient(
{ roomId, peerId, accessCode, device, useSimulcast, produce, forceTcp });
{
peerId,
accessCode,
device,
useSimulcast,
useSharingSimulcast,
produce,
forceTcp,
displayName,
muted
});
global.CLIENT = roomClient;
@ -114,7 +140,14 @@ function run()
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<App />
<Router>
<Suspense fallback={<LoadingView />}>
<React.Fragment>
<Route exact path='/' component={ChooseRoom} />
<Route path='/:id' component={App} />
</React.Fragment>
</Suspense>
</Router>
</SnackbarProvider>
</RoomContext.Provider>
</PersistGate>

View File

@ -79,6 +79,15 @@ const consumers = (state = initialState, action) =>
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PRIORITY':
{
const { consumerId, priority } = action.payload;
const consumer = state[consumerId];
const newConsumer = { ...consumer, priority };
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_TRACK':
{
const { consumerId, track } = action.payload;

View File

@ -1,6 +1,5 @@
const initialState =
{
url : null,
name : '',
state : 'new', // new/connecting/connected/disconnected/closed,
locked : false,
@ -26,13 +25,6 @@ const room = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_URL':
{
const { url } = action.payload;
return { ...state, url };
}
case 'SET_ROOM_NAME':
{
const { name } = action.payload;

View File

@ -4,7 +4,8 @@ const initialState =
selectedWebcam : null,
selectedAudioDevice : null,
advancedMode : false,
resolution : 'high' // low, medium, high, veryhigh, ultra
resolution : 'medium', // low, medium, high, veryhigh, ultra
lastN : 4
};
const settings = (state = initialState, action) =>
@ -35,6 +36,20 @@ const settings = (state = initialState, action) =>
return { ...state, advancedMode };
}
case 'SET_LAST_N':
{
const { lastN } = action.payload;
return { ...state, lastN };
}
case 'TOGGLE_PERMANENT_TOPBAR':
{
const permanentTopBar = !state.permanentTopBar;
return { ...state, permanentTopBar };
}
case 'SET_VIDEO_RESOLUTION':
{
const { resolution } = action.payload;

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "您已断开连接",
"socket.reconnecting": "尝试重新连接",
"socket.reconnected": "您已重新连接",
"socket.requestError": "服务器请求错误",
"room.chooseRoom": "选择您要加入的房间的名称",
"room.cookieConsent": "这个网站使用cookies来增强用户体验",
"room.consentUnderstand": "I understand",
"room.joined": "您已加入房间",
"room.cantJoin": "无法加入房间",
"room.youLocked": "您已锁定房间",
"room.cantLock": "无法锁定房间",
"room.youUnLocked": "您解锁了房间",
"room.cantUnLock": "无法解锁房间",
"room.locked": "房间已锁定",
"room.unlocked": "房间现已解锁",
"room.newLobbyPeer": "新参与者进入大厅",
"room.lobbyPeerLeft": "新参与者离开大厅",
"room.lobbyPeerChangedDisplayName": "大厅的参与者将名称更改为{displayName}",
"room.lobbyPeerChangedPicture": "大厅的参与者已更改图片",
"room.setAccessCode": "设置房间的访问密码",
"room.accessCodeOn": "房间的访问密码现已激活",
"room.accessCodeOff": "房间的访问密码已停用",
"room.peerChangedDisplayName": "{oldDisplayName}现在为{displayName}",
"room.newPeer": "{displayName}加入了会议室",
"room.newFile": "新文件可用",
"room.toggleAdvancedMode": "切换高级模式",
"room.setDemocraticView": "将布局更改为民主视图",
"room.setFilmStripView": "将布局更改为幻灯片视图",
"room.loggedIn": "您已登录",
"room.loggedOut": "您已登出",
"room.changedDisplayName": "您的显示名称更改为{displayName}",
"room.changeDisplayNameError": "更改显示名称时发生错误",
"room.chatError": "无法发送聊天消息",
"room.aboutToJoin": "您即将参加会议",
"room.roomId": "房间ID: {roomName}",
"room.setYourName": "设置您的参与名,并选择您想加入的方式:",
"room.audioOnly": "仅音频",
"room.audioVideo": "音频和视频",
"room.youAreReady": "好,您准备好了",
"room.emptyRequireLogin": "房间是空的! 您可以登录以开始会议或等待主持人加入",
"room.locketWait": "房间已锁定-挂起,直到有人允许您进入...",
"room.lobbyAdministration": "大厅管理",
"room.peersInLobby": "大厅的参与者",
"room.lobbyEmpty": "大厅目前没有人",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "我",
"room.spotlights": "Spotlight中的参与者",
"room.passive": "被动参与者",
"room.videoPaused": "该视频已暂停",
"tooltip.login": "登录",
"tooltip.logout": "注销",
"tooltip.admitFromLobby": "从大厅允许",
"tooltip.lockRoom": "锁房",
"tooltip.unLockRoom": "解锁房间",
"tooltip.enterFullscreen": "进入全屏",
"tooltip.leaveFullscreen": "退出全屏",
"tooltip.lobby": "显示大厅",
"tooltip.settings": "显示设置",
"tooltip.participants": "显示参加者",
"label.roomName": "房间名称",
"label.chooseRoomButton": "继续",
"label.yourName": "你的名字",
"label.newWindow": "新窗口",
"label.fullscreen": "全屏",
"label.openDrawer": "打开抽屉",
"label.leave": "离开",
"label.chatInput": "输入聊天消息",
"label.chat": "聊天",
"label.filesharing": "文件共享",
"label.participants": "参与者",
"label.shareFile": "共享文件",
"label.fileSharingUnsupported": "不支持文件共享",
"label.unknown": "未知",
"label.democratic": "民主视图",
"label.filmstrip": "幻灯片视图",
"label.low": "低",
"label.medium": "中等",
"label.high": "高 (HD)",
"label.veryHigh": "非常高 (FHD)",
"label.ultra": "超高 (UHD)",
"label.close": "关闭",
"settings.settings": "设置",
"settings.camera": "视频设备",
"settings.selectCamera": "选择视频设备",
"settings.cantSelectCamera": "无法选择视频设备",
"settings.audio": "音频设备",
"settings.selectAudio": "选择音频设备",
"settings.cantSelectAudio": "无法选择音频设备",
"settings.resolution": "选择视频分辨率",
"settings.layout": "房间布局",
"settings.selectRoomLayout": "选择房间布局",
"settings.advancedMode": "高级模式",
"settings.permanentTopBar": "永久顶吧",
"settings.lastn": "可见视频数量",
"filesharing.saveFileError": "无法保存文件",
"filesharing.startingFileShare": "正在尝试共享文件",
"filesharing.successfulFileShare": "文件已成功共享",
"filesharing.unableToShare": "无法共享文件",
"filesharing.error": "文件共享发生错误",
"filesharing.finished": "文件共享完成",
"filesharing.save": "保存共享文件",
"filesharing.sharedFile": "{displayName}共享了一个文件",
"filesharing.download": "下载共享文件",
"filesharing.missingSeeds": "如果此过程需要很长时间,则可能没有人播下该种子。请尝试让某人重新上传您想要的文件。",
"devices.devicesChanged": "您的设备已更改,请在设置对话框中配置设备",
"device.audioUnsupported": "音频不受支持",
"device.activateAudio": "激活音频",
"device.muteAudio": "静音",
"device.unMuteAudio": "取消静音",
"device.videoUnsupported": "视频不受支持",
"device.startVideo": "开始视频",
"device.stopVideo": "停止视频",
"device.screenSharingUnsupported": "不支持屏幕共享",
"device.startScreenSharing": "开始屏幕共享",
"device.stopScreenSharing": "停止屏幕共享",
"devices.microphoneDisconnected": "麦克风已断开",
"devices.microphoneError": "麦克风发生错误",
"devices.microPhoneMute": "麦克风静音",
"devices.micophoneUnMute": "取消麦克风静音",
"devices.microphoneEnable": "启用了麦克风",
"devices.microphoneMuteError": "无法使麦克风静音",
"devices.microphoneUnMuteError": "无法取消麦克风静音",
"devices.screenSharingDisconnected" : "屏幕共享已断开",
"devices.screenSharingError": "访问屏幕时发生错误",
"devices.cameraDisconnected": "相机已断开连接",
"devices.cameraError": "访问相机时发生错误"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Verbindung unterbrochen",
"socket.reconnecting": "Verbindung unterbrochen, versuche neu zu verbinden",
"socket.reconnected": "Verbindung wieder herges|tellt",
"socket.requestError": "Fehler bei Serveranfrage",
"room.chooseRoom": "Choose the name of the room you would like to join",
"room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen",
"room.consentUnderstand": "I understand",
"room.joined": "Konferenzraum betreten",
"room.cantJoin": "Betreten des Raumes nicht möglich",
"room.youLocked": "Raum wurde abgeschlossen",
"room.cantLock": "Abschließen des Raumes nicht möglich",
"room.youUnLocked": "Raum geöffnet",
"room.cantUnLock": "Öffnen des Raumes nicht möglich",
"room.locked": "Raum wurde abgeschlossen",
"room.unlocked": "Raum wurde geöffnet",
"room.newLobbyPeer": "Neuer Teilnehmer im Empfangsraum",
"room.lobbyPeerLeft": "Teilnehmer hat Empfangsraum verlassen",
"room.lobbyPeerChangedDisplayName": "Teilnehmer im Empfangsraum hat seinen Namen geändert: {displayName}",
"room.lobbyPeerChangedPicture": "Teilnehmer in Empfangsraum hat sein Avatar geändert",
"room.setAccessCode": "Zugangskode für den Raum geändert",
"room.accessCodeOn": "Zugangskode aktiviert",
"room.accessCodeOff": "Zugangskode deaktiviert",
"room.peerChangedDisplayName": "{oldDisplayName} heißt jetzt {displayName}",
"room.newPeer": "{displayName} hat den Raum betreten",
"room.newFile": "Neue Datei verfügbar",
"room.toggleAdvancedMode": "Erweiterter Modus aktiv",
"room.setDemocraticView": "Raumlayout demokratisch",
"room.setFilmStripView": "Raumlayout Filmstreifen",
"room.loggedIn": "Angemeldet",
"room.loggedOut": "Abgemeldet",
"room.changedDisplayName": "Dein Name ist jetzt {displayName}",
"room.changeDisplayNameError": "Konnte Name nicht ändern",
"room.chatError": "Konnte Meldung nicht senden",
"room.aboutToJoin": "Du bist dabei den Raum zu betreten",
"room.roomId": "Raum ID: {roomName}",
"room.setYourName": "Gib deinen Namen an und wähle wie den Raum betreten willst",
"room.audioOnly": "Nur Audio",
"room.audioVideo": "Audio und Video",
"room.youAreReady": "Ok, Du bist bereit",
"room.emptyRequireLogin": "Der Raum ist leer. Melde dich an um den Raum zu aktivieren, oder warte bis der Raum aktiviert wird",
"room.locketWait": "Der Raum ist abgeschlossen, warte bis Dir jemand öffnet",
"room.lobbyAdministration": "Warteraum",
"room.peersInLobby": "Teilnehmer im Warteraum",
"room.lobbyEmpty": "Der Warteraum ist leer",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {Teilnehmer} other {Teilnehmer}}",
"room.me": "Ich",
"room.spotlights": "Aktive Teinehmer",
"room.passive": "Passive Teilnehmer",
"room.videoPaused": "Video gestoppt",
"tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden",
"tooltip.admitFromLobby": "Teilnehmer aktivieren",
"tooltip.lockRoom": "Raum abschließen",
"tooltip.unLockRoom": "Raum öffnen",
"tooltip.enterFullscreen": "Vollbild",
"tooltip.leaveFullscreen": "Vollbild verlassen",
"tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer",
"label.roomName": "Room name",
"label.chooseRoomButton": "Continue",
"label.yourName": "Dein Name",
"label.newWindow": "In separatem Fenster öffnen",
"label.fullscreen": "Vollbild",
"label.openDrawer": "Menü",
"label.leave": "Ausgang",
"label.chatInput": "Schreibe Chat...",
"label.chat": "Chat",
"label.filesharing": "Dateien",
"label.participants": "Teilnehmer",
"label.shareFile": "Teile Datai",
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt",
"label.democratic": "Demokratisch",
"label.filmstrip": "Filmstreifen",
"label.low": "Niedrig",
"label.medium": "Medium",
"label.high": "Hoch (HD)",
"label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Schließen",
"settings.settings": "Einstellungen",
"settings.camera": "Kamera",
"settings.selectCamera": "Wähle Videogerät",
"settings.cantSelectCamera": "Kann Videogerät nicht aktivieren",
"settings.audio": "Audiogerät",
"settings.selectAudio": "Wähle Audiogerät",
"settings.cantSelectAudio": "Kann Audiogerät nicht aktivieren",
"settings.resolution": "Wähle Auflösung",
"settings.layout": "Raumlayout",
"settings.selectRoomLayout": "Wähle Raumlayout",
"settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos",
"filesharing.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei",
"filesharing.successfulFileShare": "Datei wurde geteilt",
"filesharing.unableToShare": "Kann Datei nicht teilen",
"filesharing.error": "Fehler beim Teilen der Datei",
"filesharing.finished": "Datei heruntergeladen",
"filesharing.save": "Speichern",
"filesharing.sharedFile": "{displayName} hat eine Datei geteilt",
"filesharing.download": "Herunterladen",
"filesharing.missingSeeds": "Wenn das Herunterladen nicht pausiert ist wahrscheinlich niemeand mehr im Raum der die Datei teilen kann. Datei muss erneut geteilt werden.",
"devices.devicesChanged": "Mediengeräte wurden aktualisiert und sind in Einstellungen verfügbar",
"device.audioUnsupported": "Audio nicht unterstützt",
"device.activateAudio": "Aktiviere Audio",
"device.muteAudio": "stummschalten",
"device.unMuteAudio": "Aktiviere Audio",
"device.videoUnsupported": "Video nicht unterstützt",
"device.startVideo": "Starte Video",
"device.stopVideo": "Stoppe Video",
"device.screenSharingUnsupported": "Bildschirmteilen nicht unterstützt",
"device.startScreenSharing": "Bildschirmteilen",
"device.stopScreenSharing": "Beende Bildschirmteilen",
"devices.microphoneDisconnected": "Mikrophon nicht verbunden",
"devices.microphoneError": "Fehler mit Mikrophon",
"devices.microPhoneMute": "Mikrophon stumm geschaltet",
"devices.micophoneUnMute": "Mikrophon aktiviert",
"devices.microphoneEnable": "Mikrofonen aktiviert",
"devices.microphoneMuteError": "Kann Mikrophon nicht stummschalten",
"devices.microphoneUnMuteError": "Kann Mikrophon nicht aktivieren",
"devices.screenSharingDisconnected" : "Bildschirmteilen unterbrochen",
"devices.screenSharingError": "Fehler beim Bildschirmteilen",
"devices.cameraDisconnected": "Video unterbrochen",
"devices.cameraError": "Fehler mit Videogerät"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Du er frakoblet",
"socket.reconnecting": "Du er frakoblet, forsøger at oprette forbindelse igen",
"socket.reconnected": "Du er tilsluttet igen",
"socket.requestError": "Fejl ved serveranmodning",
"room.chooseRoom": "Vælg navnet på det rum, du vil være med på",
"room.cookieConsent": "Dette websted bruger cookies til at forbedre brugeroplevelsen",
"room.consentUnderstand": "I understand",
"room.joined": "Du er tilsluttet mødet",
"room.cantJoin": "Kan ikke deltage i mødet",
"room.youLocked": "Du låste mødet",
"room.cantLock": "Kan ikke låse mødet",
"room.youUnLocked": "Du låste lokalet op",
"room.cantUnLock": "Kan ikke låse mødet op",
"room.locked": "Mødet er nu låst",
"room.unlocked": "Mødet er nu ulåst",
"room.newLobbyPeer": "Ny deltager kom ind i lobbyen",
"room.lobbyPeerLeft": "Deltager i lobbyen tilbage",
"room.lobbyPeerChangedDisplayName": "Deltager i lobbyen ændrede navn til {displayName}",
"room.lobbyPeerChangedPicture": "Deltager i lobbyen ændrede billede",
"room.setAccessCode": "Adgangskode til værelse opdateret",
"room.accessCodeOn": "Adgangskode til værelse er nu aktiveret",
"room.accessCodeOff": "Adgangskode til værelse er nu deaktiveret",
"room.peerChangedDisplayName": "{oldDisplayName} er nu {displayName}",
"room.newPeer": "{displayName} kom med i mødet",
"room.newFile": "Ny fil tilgængelig",
"room.toggleAdvancedMode": "Skiftet avanceret tilstand",
"room.setDemocracyView": "Ændret layout til galleri visning",
"room.setFilmStripView": "Ændret layout til filmstrimmel visning",
"room.loggedIn": "Du er logget ind",
"room.loggedOut": "Du er logget ud",
"room.changDisplayName": "Dit visningsnavn blev ændret til {displayName}",
"room.changeDisplayNameError": "Der opstod en fejl under ændring af dit visningsnavn",
"room.chatError": "Kan ikke sende chatbesked",
"room.aboutToJoin": "Du er ved at deltage i et møde",
"room.roomId": "Værelse-ID: {roomName}",
"room.setYourName": "Indstil dit navn til deltagelse, og vælg, hvordan du vil være med:",
"room.audioOnly": "Kun lyd",
"room.audioVideo": "Audio og video",
"room.youAreReady": "Ok, du er klar",
"room.emptyRequireLogin": "Værelset er tomt! Du kan logge ind for at starte mødet eller vente til værten starter mødet",
"room.locketWait": "Værelset er låst - vent venligst, indtil nogen slipper dig ind ...",
"room.lobbyAdministration": "Lobbyadministration",
"room.peersInLobby": "Deltagere i lobbyen",
"room.lobbyEmpty": "Der er i øjeblikket ingen i lobbyen",
"room.hiddenPeers": "{HiddenPeersCount, plural, en {deltager} andre {deltagere}}",
"room.me": "Mig",
"room.spotlights": "Deltagere i fokus",
"room.passive": "Passive deltagere",
"room.videoPaused": "Denne video er sat på pause",
"tooltip.login": "Log ind",
"tooltip.logout": "Log ud",
"tooltip.admitFromLobby": "Giv adgang fra lobbyen",
"tooltip.lockRoom": "Låseværelse",
"tooltip.unLockRoom": "Lås op plads",
"tooltip.enterFullscreen": "Indtast fuldskærm",
"tooltip.leaveFullscreen": "Efterlad fuldskærm",
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere",
"label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt",
"label.yourName": "Dit navn",
"label.newWindow": "Nyt vindue",
"label.fullscreen": "Fuldskærm",
"label.openDrawer": "Åben skuffe",
"label.leave": "Forlad",
"label.chatInput": "Indtast chatbesked ...",
"label.chat": "Chat",
"label.filesharing": "Fildeling",
"label.participants": "Deltagere",
"label.shareFile": "Del fil",
"label.fileSharingUnsupported": "Fildeling er ikke understøttet",
"label.unknown": "Ukendt",
"label.democracy": "Galleri visning",
"label.filmstrip": "Filmstrimmel visning",
"label.low": "Lav",
"label.medium": "Medium",
"label.high": "Høj (HD)",
"label.veryHigh": "Meget høj (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Luk",
"settings.settings": "Indstillinger",
"settings.camera": "Kamera",
"settings.selectCamera": "Vælg kamera",
"settings.cantSelectCamera": "Kan ikke vælge kamera",
"settings.audio": "Lydenhed",
"settings.selectAudio": "Vælg lydenhed",
"settings.cantSelectAudio": "Kan ikke vælge lydenhed",
"settings.resolution": "Vælg din videoopløsning",
"settings.layout": "Møde visning",
"settings.selectRoomLayout": "Vælg møde visning",
"settings.advancedMode": "Avanceret tilstand",
"settings.permanentTopBar": "Permanent øverste linje",
"settings.lastn": "Antal synlige videoer",
"filesharing.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen",
"filesharing.successfulFileShare": "Filen blev delet med succes",
"filesharing.unableToShare": "Kan ikke dele fil",
"filesharing.error": "Der var en fildelings fejl",
"filesharing.finished": "Filen er færdig med at downloade",
"filesharing.save": "Gem",
"filesharing.sharedFile": "{displayName} delte en fil",
"filesharing.download": "Download",
"filesharing.missingSeeds": "Hvis denne proces tager lang tid, er der muligvis ikke nogen, der seedede denne torrent. Prøv at bede nogen om at uploade den fil, du ønsker at hente.",
"device.devicesChanged": "Detekteret ndringer i dine enheder, konfigurer dine enheder i indstillingsdialogen",
"device.audioUnsupported": "Lyd ikke understøttet",
"device.activateAudio": "Aktivér lyd",
"device.muteAudio": "Slå lyd til",
"device.unMuteAudio": "Slå lyd til",
"device.videoUnsupported": "Video ikke understøttet",
"device.startVideo": "Start video",
"device.stopVideo": "Stop video",
"device.screenSharingUnsupported": "Skærmdeling understøttes ikke",
"device.startScreenSharing": "Start skærmdeling",
"device.stopScreenSharing": "Stop skærmdeling",
"device.microphoneDisconnected": "Mikrofon frakoblet",
"device.microphoneError": "Der opstod en fejl under adgang til din mikrofon",
"device.microPhoneMute": "Dæmp din mikrofon",
"device.micophoneUnMute": "Slå ikke lyden fra din mikrofon",
"device.microphoneEnable": "Aktiveret din mikrofon",
"device.microphoneMuteError": "Kan ikke slå din mikrofon fra",
"device.microphoneUnMuteError": "Kan ikke slå lyden til på din mikrofon",
"device.screenSharingDisconnected": "Skærmdelingen er frakoblet",
"devices.screenSharingError": "Der opstod en fejl ved adgang til skærmdeling",
"device.cameraDisconnected": "Kamera frakoblet",
"device.cameraError": "Der opstod en fejl ved tilkobling af dit kamera"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Είστε αποσυνδεδεμένος",
"socket.reconnecting": "Είστε αποσυνδεδεμένος, προσπάθεια επανασύνδεσης",
"socket.reconnected": "Επανασυνδεθήκατε",
"socket.requestError": "Σφάλμα στο αίτημα του διακομιστή",
"room.chooseRoom": "Επιλέξτε το όνομα του δωματίου που θέλετε να συμμετάσχετε",
"room.cookieConsent": "Αυτός ο ιστότοπος χρησιμοποιεί cookies για να βελτιώσει την εμπειρία χρήσης",
"room.consentUnderstand": "I understand",
"room.joined": "Έχετε εισέλθει στο δωμάτιο",
"room.cantJoin": "Αδυναμία εισόδου στο δωμάτιο",
"room.youLocked": "Κλειδώσατε το δωμάτιο",
"room.cantLock": "Δεν είναι δυνατό να κλειδώσετε το δωμάτιο",
"room.youUnLocked": "Ξεκλειδώσατε το δωμάτιο",
"room.cantUnLock": "Δεν είναι δυνατό το ξεκλείδωμα του δωματίου",
"room.locked": "Το δωμάτιο είναι πλέον κλειδωμένο",
"room.unlocked": "Το δωμάτιο είναι τώρα ξεκλειδωμένο",
"room.newLobbyPeer": "Ένας νέος συμμετέχων μπήκε στο λόμπι",
"room.lobbyPeerLeft": "Ο συμμετέχων στο λόμπι έφυγε",
"room.lobbyPeerChangedDisplayName": "Ο συμμετέχων στο λόμπι άλλαξε το όνομά του σε {displayName}",
"room.lobbyPeerChangedPicture": "Ο συμμετέχων στο λόμπι άλλαξε εικόνα",
"room.setAccessCode": "Ο κωδικός πρόσβασης για το δωμάτιο ενημερώθηκε",
"room.accessCodeOn": "Ο κωδικός πρόσβασης για το δωμάτιο είναι τώρα ενεργοποιημένος",
"room.accessCodeOff": "Ο κωδικός πρόσβασης για το δωμάτιο είναι τώρα απενεργοποιημένος",
"room.peerChangedDisplayName": "{oldDisplayName} είναι τώρα {displayName}",
"room.newPeer": "{displayName} μπήκε στο δωμάτιο",
"room.newFile": "Νέο διαθέσιμο αρχείο",
"room.toggleAdvancedMode": "Επεξεργασία προηγμένης λειτουργίας",
"room.setDemocraticView": "Αλλαγή εμφάνισης σε democratic view",
"room.setFilmStripView": "Αλλαγή εμφάνισης σε filmstrip view",
"room.loggedIn": "Είστε συνδεδεμένοι",
"room.loggedOut": "Έχετε αποσυνδεθεί",
"room.changedDisplayName": "Το εμφανιζόμενο όνομα σας άλλαξε σε {displayName}",
"room.changeDisplayNameError": "Παρουσιάστηκε σφάλμα κατά την αλλαγή του ονόματος εμφάνισης",
"room.chatError": "Δεν είναι δυνατή η αποστολή μηνυμάτων συνομιλίας",
"room.aboutToJoin": "Είστε έτοιμοι να συμμετάσχετε σε μια συνάντηση",
"room.roomId": "Αναγνωριστικό δωματίου: {roomName}",
"room.setYourName": "Ορίστε το όνομά σας για συμμετοχή και επιλέξτε τον τρόπο συμμετοχής:",
"room.audioOnly": "Μόνο ήχος",
"room.audioVideo": "Ήχος και video",
"room.youAreReady": "Είστε έτοιμος",
"room.emptyRequireLogin": "Το δωμάτιο είναι άδειο! Μπορείτε να συνδεθείτε για να ξεκινήσετε τη σύσκεψη ή να περιμένετε έως ότου ο οικοδεσπότης συνδεθεί",
"room.locketWait": "Το δωμάτιο είναι κλειδωμένο - περιμένετε μέχρι να σας αφήσει κάποιος ...",
"room.lobbyAdministration": "Διαχείριση Δωματίου",
"room.peersInLobby": "Συμμετέχοντες στο δωμάτιο",
"room.lobbyEmpty": "Δεν υπάρχει κανένας συμμετέχοντας",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Εγώ",
"room.spotlights": "Συμμετέχοντες στο Spotlight",
"room.passive": "Παθητικοί συμμετέχοντες",
"room.videoPaused": "Το βίντεο έχει σταματήσει",
"tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση",
"tooltip.admitFromLobby": "Admit from lobby",
"tooltip.lockRoom": "Κλείδωμα δωματίου",
"tooltip.unLockRoom": "Ξεκλείδωμα δωματίου",
"tooltip.enterFullscreen": "Πλήρης οθόνη",
"tooltip.leaveFullscreen": "Έξοδος από την πλήρη οθόνη",
"tooltip.lobby": "Εμφάνιση λόμπι",
"tooltip.settings": "Εμφάνιση ρυθμίσεων",
"tooltip.participants": "Εμφάνιση συμμετεχόντων",
"label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια",
"label.yourName": "Το όνομά σας",
"label.newWindow": "Νέο παράθυρο",
"label.fullscreen": "Πλήρης οθόνη",
"label.openDrawer": "Άνοιγμα drawer",
"label.leave": "Αποχώρηση",
"label.chatInput": "Γράψτε το μήνυμά σας...",
"label.chat": "Συνομολία",
"label.filesharing": "Διαμοιρασμοός αρχείου",
"label.participants": "Συμμετέχοντες",
"label.shareFile": "Διαμοιραστείτε ένα αρχείο",
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
"label.unknown": "Άγνωστο",
"label.democratic": "Democratic view",
"label.filmstrip": "Filmstrip view",
"label.low": "Χαμηλή",
"label.medium": "Μέτρια",
"label.high": "Υψηλή (HD)",
"label.veryHigh": "Πολύ υψηλή (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Κλείσιμο",
"settings.settings": "Ρυθμίσεις",
"settings.camera": "Κάμερα",
"settings.selectCamera": "Επιλέξτε συσκευή video",
"settings.cantSelectCamera": "Αδυναμία επιλογής συσκευής video",
"settings.audio": "Συσκευή ήχου",
"settings.selectAudio": "Επιλογή συσκευής ήχου",
"settings.cantSelectAudio": "Αδυναμία επιλογής συσκευής ήχου",
"settings.resolution": "Επιλέξτε την ανάλυση του video",
"settings.layout": "Περιβάλλον δωματίου",
"settings.selectRoomLayout": "Επιλογή περιβάλλοντος δωματίου",
"settings.advancedMode": "Προηγμένη λειτουργία",
"settings.permanentTopBar": "Μόνιμη μπάρα κορυφής",
"settings.lastn": "Αριθμός ορατών βίντεο",
"filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",
"filesharing.successfulFileShare": "Το αρχείο έχει διαμοιραστεί επιτυχώς",
"filesharing.unableToShare": "Το αρχείο δεν μπορεί να διαμοιραστεί",
"filesharing.error": "Υπήρξε σφάλμα κατά τον διαμοιρασμό του αρχείου",
"filesharing.finished": "Το αρχείο έχει κατέβει",
"filesharing.save": "Αποθήκευση",
"filesharing.sharedFile": "{displayName} μοιράστηκε ένα αρχείο",
"filesharing.download": "Κατέβασμα",
"filesharing.missingSeeds": "Αν αυτή η διαδικασία διαρκεί πολύ, ίσως να μην υπάρχει κάποιος να διαμοιραστεί το αρχείο. Δοκιμάστε να ζητήσετε από κάποιον να φορτώσει εκ νέου το αρχείο που θέλετε.",
"devices.devicesChanged": "Οι συσκευές σας άλλαξαν, ρυθμίστε τις συσκευές σας στο παράθυρο ρυθμίσεων",
"device.audioUnsupported": "Δεν υποστηρίζεται ήχος",
"device.activateAudio": "Ενεργοποίηση ήχου",
"device.muteAudio": "Σίγαση ήχου",
"device.unMuteAudio": "Άνοιγμα ήχου",
"device.videoUnsupported": "Το βίντεο δεν υποστηρίζεται",
"device.startVideo": "Έναρξη βίντεο",
"device.stopVideo": "Διακοπή βίντεο",
"device.screenSharingUnsupported": "Ο διαμοιρασμός της οθόνης δεν υποστηρίζεται",
"device.startScreenSharing": "Έναρξη της κοινής χρήσης της οθόνης",
"device.stopScreenSharing": "Διακοπή της κοινής χρήσης της οθόνης",
"devices.microphoneDisconnected": "Το μικρόφωνο αποσυνδέθηκε",
"devices.microphoneError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στο μικρόφωνό σας",
"devices.microPhoneMute": "Το μικρόφωνό σας είναι σε σίγαση",
"devices.micophoneUnMute": "Ανοίξτε το μικρόφωνό σας",
"devices.microphoneEnable": "Ενεργοποίησε το μικρόφωνό σας",
"devices.microphoneMuteError": "Δεν είναι δυνατή η σίγαση του μικροφώνου σας",
"devices.microphoneUnMuteError": "Δεν είναι δυνατό το άνοιγμα του μικροφώνου σας",
"devices.screenSharingDisconnected" : "Ο διαμοιρασμός οθόνης αποσυνδέθηκε",
"devices.screenSharingError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην οθόνη σας",
"devices.cameraDisconnected": "Η κάμερα αποσυνδέθηκε",
"devices.cameraError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην κάμερά σας"
}

View File

@ -4,7 +4,9 @@
"socket.reconnected": "You are reconnected",
"socket.requestError": "Error on server request",
"room.chooseRoom": "Choose the name of the room you would like to join",
"room.cookieConsent": "This website uses cookies to enhance the user experience",
"room.consentUnderstand": "I understand",
"room.joined": "You have joined the room",
"room.cantJoin": "Unable to join the room",
"room.youLocked": "You locked the room",
@ -57,7 +59,10 @@
"tooltip.leaveFullscreen": "Leave fullscreen",
"tooltip.lobby": "Show lobby",
"tooltip.settings": "Show settings",
"tooltip.participants": "Show participants",
"label.roomName": "Room name",
"label.chooseRoomButton": "Continue",
"label.yourName": "Your name",
"label.newWindow": "New window",
"label.fullscreen": "Fullscreen",
@ -90,6 +95,8 @@
"settings.layout": "Room layout",
"settings.selectRoomLayout": "Select room layout",
"settings.advancedMode": "Advanced mode",
"settings.permanentTopBar": "Permanent top bar",
"settings.lastn": "Number of visible videos",
"filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file",

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Desconectado",
"socket.reconnecting": "Desconectado, intentando reconectar",
"socket.reconnected": "Reconectado",
"socket.requestError": "Error en la petición al servidor",
"room.chooseRoom": "Indique el nombre de la sala a la que le gustaría unirse",
"room.cookieConsent": "Esta web utiliza cookies para mejorar la experiencia de usuario",
"room.consentUnderstand": "I understand",
"room.joined": "Se ha unido a la sala",
"room.cantJoin": "No ha sido posible unirse a la sala",
"room.youLocked": "Ha cerrado la sala",
"room.cantLock": "No ha sido posible cerrar la sala",
"room.youUnLocked": "Ha abierto la sala",
"room.cantUnLock": "No ha sido posible abrir la sala",
"room.locked": "La sala ahora es privada",
"room.unlocked": "La sala ahora es pública",
"room.newLobbyPeer": "Nuevo participante en la sala de espera",
"room.lobbyPeerLeft": "Un participante en espera ha salido",
"room.lobbyPeerChangedDisplayName": "Participante en espera cambió su nombre a {displayName}",
"room.lobbyPeerChangedPicture": "Participante en espera cambió su foto",
"room.setAccessCode": "Código de acceso de la sala actualizado",
"room.accessCodeOn": "Código de acceso de la sala activado",
"room.accessCodeOff": "Código de acceso de la sala desactivado",
"room.peerChangedDisplayName": "{oldDisplayName} es ahora {displayName}",
"room.newPeer": "{displayName} se unió a la sala",
"room.newFile": "Nuevo fichero disponible",
"room.toggleAdvancedMode": "Cambiado a modo avanzado",
"room.setDemocraticView": "Cambiado a modo democrático",
"room.setFilmStripView": "Cambiado a modo viñeta",
"room.loggedIn": "Ha iniciado sesión",
"room.loggedOut": "Ha cerrado su sesión",
"room.changedDisplayName": "Ha cambiado su nombre a {displayName}",
"room.changeDisplayNameError": "Hubo un error al intentar cambiar su nombre",
"room.chatError": "No ha sido posible enviar su mensaje",
"room.aboutToJoin": "Está a punto de unirse a una reunión",
"room.roomId": "ID de la sala: {roomName}",
"room.setYourName": "Indique el nombre con el que quiere participar y cómo quiere unirse:",
"room.audioOnly": "Solo sonido",
"room.audioVideo": "Sonido y vídeo",
"room.youAreReady": "Ok, está preparado",
"room.emptyRequireLogin": "¡La sala está vacía! Puede iniciar sesión para comenzar la reunión o esperar hasta que el anfitrión se una",
"room.locketWait": "La sala es privada - espere hasta que alguien le invite ...",
"room.lobbyAdministration": "Administración de la sala de espera",
"room.peersInLobby": "Participantes en la sala de espera",
"room.lobbyEmpty": "La sala de espera está vacía",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}",
"room.me": "Yo",
"room.spotlights": "Participantes destacados",
"room.passive": "Participantes pasivos",
"room.videoPaused": "El vídeo está pausado",
"tooltip.login": "Entrar",
"tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera",
"tooltip.lockRoom": "Configurar sala como privada",
"tooltip.unLockRoom": "Configurar sala como pública",
"tooltip.enterFullscreen": "Presentar en pantalla completa",
"tooltip.leaveFullscreen": "Salir de la pantalla completa",
"tooltip.lobby": "Mostrar sala de espera",
"tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes",
"label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar",
"label.yourName": "Su nombre",
"label.newWindow": "Nueva ventana",
"label.fullscreen": "Pantalla completa",
"label.openDrawer": "Abrir panel",
"label.leave": "Salir",
"label.chatInput": "Escriba su mensaje...",
"label.chat": "Chat",
"label.filesharing": "Compartir ficheros",
"label.participants": "Participantes",
"label.shareFile": "Compartir fichero",
"label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido",
"label.democratic": "Vista democrática",
"label.filmstrip": "Vista en viñeta",
"label.low": "Baja",
"label.medium": "Media",
"label.high": "Alta (HD)",
"label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Cerrar",
"settings.settings": "Ajustes",
"settings.camera": "Cámara",
"settings.selectCamera": "Seleccionar dispositivo de vídeo",
"settings.cantSelectCamera": "No ha sido posible seleccionar el dispositivo de vídeo",
"settings.audio": "Dispositivo de sonido",
"settings.selectAudio": "Seleccione dispositivo de sonido",
"settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido",
"settings.resolution": "Seleccione su resolución de imagen",
"settings.layout": "Disposición de la sala",
"settings.selectRoomLayout": "Seleccione la disposición de la sala",
"settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles",
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero",
"filesharing.successfulFileShare": "El fichero se compartió con éxito",
"filesharing.unableToShare": "No ha sido posible compartir el fichero",
"filesharing.error": "Hubo un error al compartir el fichero",
"filesharing.finished": "Descarga del fichero finalizada",
"filesharing.save": "Guardar",
"filesharing.sharedFile": "{displayName} compartió un fichero",
"filesharing.download": "Descargar",
"filesharing.missingSeeds": "Si este proceso demora en exceso, puede ocurrir que no haya nadie compartiendo el fichero. Pruebe a pedirle a alguien que vuelva a subir el fichero que busca.",
"devices.devicesChanged": "Sus dispositivos han cambiado, vuelva a configurarlos en la ventana de ajustes",
"device.audioUnsupported": "Sonido no disponible",
"device.activateAudio": "Activar sonido",
"device.muteAudio": "Silenciar sonido",
"device.unMuteAudio": "Reactivar sonido",
"device.videoUnsupported": "Vídeo no disponible",
"device.startVideo": "Iniciar vídeo",
"device.stopVideo": "Detener vídeo",
"device.screenSharingUnsupported": "Compartir pantalla no disponible",
"device.startScreenSharing": "Iniciar compartir pantalla",
"device.stopScreenSharing": "Detener compartir pantalla",
"devices.microphoneDisconnected": "Micrófono desconectado",
"devices.microphoneError": "Hubo un error al acceder a su micrófono",
"devices.microPhoneMute": "Desactivar micrófono",
"devices.micophoneUnMute": "Activar micrófono",
"devices.microphoneEnable": "Micrófono activado",
"devices.microphoneMuteError": "No ha sido posible desactivar su micrófono",
"devices.microphoneUnMuteError": "No ha sido posible activar su micrófono",
"devices.screenSharingDisconnected": "Pantalla compartida desconectada",
"devices.screenSharingError": "Hubo un error al acceder a su pantalla",
"devices.cameraDisconnected": "Cámara desconectada",
"devices.cameraError": "Hubo un error al acceder a su cámara"
}

View File

@ -0,0 +1,139 @@
{
"socket.disconnected" : " Vous avez été déconnecté",
"socket.reconnecting" : " Vous avez été déconnecté, reconnexion en cours",
"socket.reconnected" : " Vous êtes reconnecté",
"socket.requestError" : " Erreur sur une requête serveur",
"room.chooseRoom" : " Choisissez le nom de la réunion que vous souhaitez rejoindre",
"room.cookieConsent" : " Ce site utilise les cookies pour améliorer votre expérience utilisateur",
"room.consentUnderstand": "I understand",
"room.joined" : " Vous avez rejoint la salle",
"room.cantJoin" : " Impossible de rejoindre la salle",
"room.youLocked" : " Vous avez privatisé la salle",
"room.cantLock" : " Impossible de privatiser la salle",
"room.youUnLocked" : " Vous avez dé-privatiser la salle",
"room.cantUnLock" : " Impossible de dé-privatiser la réunion",
"room.locked" : " La réunion est privée",
"room.unlocked" : " La réunion est publique",
"room.newLobbyPeer" : " Un nouveau participant est dans la salle dattente",
"room.lobbyPeerLeft" : " Un participant de la salle dattente vient de partir",
"room.lobbyPeerChangedDisplayName" : " Un participant dans la salle dattente a changé de nom pour {displayName}",
"room.lobbyPeerChangedPicture" : " Un participant dans le hall à changer de photo",
"room.setAccessCode" : " Code daccès à la réunion mis à jour",
"room.accessCodeOn" : " Code daccès à la réunion activée",
"room.accessCodeOff" : " Code daccès à la réunion désactivée",
"room.peerChangedDisplayName" : " {oldDisplayName} est maintenant {displayName}",
"room.newPeer" : " {displayName} a rejoint la salle",
"room.newFile" : " Nouveau fichier disponible",
"room.toggleAdvancedMode" : " Basculer en mode avancé",
"room.setDemocraticView" : " Passer en vue démocratique",
"room.setFilmStripView" : " Passer en vue vignette",
"room.loggedIn" : " Vous êtes connecté",
"room.loggedOut" : " Vous êtes déconnecté",
"room.changedDisplayName" : " Votre nom à changer pour {displayname}",
"room.changeDisplayNameError" : " Une erreur sest produite pour votre changement de nom",
"room.chatError" : " Impossible denvoyer un message",
"room.aboutToJoin" : " Vous allez rejoindre une réunion",
"room.roomId" : " Salle ID: {roomName}",
"room.setYourName" : " Choisissez votre nom de participant puis comment vous connecter :",
"room.audioOnly" : " Audio uniquement",
"room.audioVideo" : " Audio et Vidéo",
"room.youAreReady" : " Ok, vous êtes prêt",
"room.emptyRequireLogin" : " La réunion est vide ! Vous pouvez vous connecter pour commencer la réunion ou attendre qu'un hôte se connecte",
"room.locketWait" : " La réunion est privatisée - attendez que quelquun vous laisse entrer",
"room.lobbyAdministration" : " Administration de la salle dattente",
"room.peersInLobby" : " Participants dans la salle dattente",
"room.lobbyEmpty" : " Il n'y a actuellement aucun participant dans la salle d'attente",
"room.hiddenPeers" : " {hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me" : " Moi",
"room.spotlights" : " Participants actifs",
"room.passive" : " Participants passifs",
"room.videoPaused" : " La vidéo est en pause",
"tooltip.login" : " Connexion",
"tooltip.logout" : " Déconnexion",
"tooltip.admitFromLobby" : " Autorisé depuis la salle d'attente",
"tooltip.lockRoom" : " Privatisation de la salle",
"tooltip.unLockRoom" : " Dé-privatisation de la salle",
"tooltip.enterFullscreen" : " Afficher en plein écran",
"tooltip.leaveFullscreen" : " Quitter le plein écran",
"tooltip.lobby" : " Afficher la salle d'attente",
"tooltip.settings" : " Afficher les paramètres",
"tooltip.participants": "Afficher les participants",
"label.roomName" : " Nom de la salle",
"label.chooseRoomButton" : " Continuer",
"label.yourName" : " Votre nom",
"label.newWindow" : " Nouvelle fenêtre",
"label.fullscreen" : " Plein écran",
"label.openDrawer" : " Ouvrir Drawer",
"label.leave" : " Quiter",
"label.chatInput" : " Entrer un message",
"label.chat" : " Chat",
"label.filesharing" : " Partage de fichier",
"label.participants" : " Participants",
"label.shareFile" : " Partager un fichier",
"label.fileSharingUnsupported" : " Partage de fichier non supporté",
"label.unknown" : " Inconnu",
"label.democratic" : " Vue démocratique",
"label.filmstrip" : " Vue avec miniature",
"label.low" : " Basse définition",
"label.medium" : " Définition normale",
"label.high" : " Haute Définition (HD)",
"label.veryHigh" : " Très Haute Définition (FHD)",
"label.ultra" : " Ultra Haute Définition",
"label.close" : " Fermer",
"settings.settings" : " Paramètres",
"settings.camera" : " Caméra",
"settings.selectCamera" : " Sélectionner votre caméra",
"settings.cantSelectCamera" : " Impossible de sélectionner votre caméra",
"settings.audio" : " Microphone",
"settings.selectAudio" : " Sélectionner votre microphone",
"settings.cantSelectAudio" : " Impossible de sélectionner votre la caméra",
"settings.resolution" : " Sélection votre résolution",
"settings.layout" : " Mode d'affichage de la salle",
"settings.selectRoomLayout" : " Sélectionner l'affiche de la salle",
"settings.advancedMode" : " Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles",
"filesharing.saveFileError" : " Impossible d'enregistrer le fichier",
"filesharing.startingFileShare" : " Début du transfert de fichier",
"filesharing.successfulFileShare" : " Fichier transféré",
"filesharing.unableToShare" : " Impossible de transférer le fichier",
"filesharing.error" : " Erreur lors du transfert de fichier",
"filesharing.finished" : " Fin du transfert de fichier",
"filesharing.save" : " Sauver",
"filesharing.sharedFile" : " {displayName} a partagé un fichier",
"filesharing.download" : " Télécharger",
"filesharing.missingSeeds" : " Si le téléchargement prend trop de temps cest quil ny a peut-être plus personne qui partage ce torrent. Demander à quelquun de repartager le document.",
"devices.devicesChanged" : " Vos périphériques ont changé, reconfigurer vos périphériques avec le menu paramètre",
"device.audioUnsupported" : " Microphone non supporté",
"device.activateAudio" : " Activer l'audio",
"device.muteAudio" : " Désactiver l'audio",
"device.unMuteAudio" : " Réactiver l'audio",
"device.videoUnsupported" : " Vidéo non supporté",
"device.startVideo" : " Démarrer la vidéo",
"device.stopVideo" : " Arrêter la vidéo",
"device.screenSharingUnsupported" : " Partage d'écran non supporté",
"device.startScreenSharing" : " Démarrer le partage d 'écran'",
"device.stopScreenSharing" : " Arrêter le partage d'écran",
"devices.microphoneDisconnected" : " Microphone déconnecté",
"devices.microphoneError" : " Une erreur est apparue lors de l'accès à votre microphone",
"devices.microPhoneMute" : " Désactiver le microphone",
"devices.micophoneUnMute" : " Réactiver le microphone",
"devices.microphoneEnable" : " Activer le microphone",
"devices.microphoneMuteError" : " Impossible de désactiver le microphone",
"devices.microphoneUnMuteError" : " Impossible de réactiver le microphone",
"devices.screenSharingDisconnected" : " Partage d'écran déconnecté",
"devices.screenSharingError" : " Une erreur est apparue lors de l'accès à votre partage d'écran",
"devices.cameraDisconnected" : " Caméra déconnectée",
"devices.cameraError" : " Une erreur est apparue lors de l'accès à votre caméra"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Odspojeni ste",
"socket.reconnecting": "Odspojeni ste, pokušavamo vas ponovno spojiti",
"socket.reconnected": "Ponovno ste spojeni",
"socket.requestError": "Greška na poslužitelju",
"room.chooseRoom": "Izaberite ime sobe u koju se želite prijaviti",
"room.cookieConsent": "Ova stranica koristi kolačiće radi poboljšanja korisničkog iskustva",
"room.consentUnderstand": "I understand",
"room.joined": "Prijavljeni ste u sobu",
"room.cantJoin": "Prijava u sobu nije moguća",
"room.youLocked": "Zaključali ste sobu",
"room.cantLock": "Zaključavanje sobe nije moguće",
"room.youUnLocked": "Otključali ste sobu",
"room.cantUnLock": "Otključavanje sobe nije moguće",
"room.locked": "Soba je sada zaključana",
"room.unlocked": "Soba je sada otključana",
"room.newLobbyPeer": "U predvorju je novi učesnik",
"room.lobbyPeerLeft": "Učesnik je napustio predvorje",
"room.lobbyPeerChangedDisplayName": "Učesnik u predvorju je promijenio ime u {displayName}",
"room.lobbyPeerChangedPicture": "Učesnik u predvorju je promijenio sliku",
"room.setAccessCode": "Obnovljena pristupna šifra za sobu",
"room.accessCodeOn": "Pristupna šifra sobe je aktivna",
"room.accessCodeOff":"Pristupna šifra sobe je neaktivna",
"room.peerChangedDisplayName": "{oldDisplayName} je sada {displayName}",
"room.newPeer": "{displayName} je ušao u sobu",
"room.newFile": "Dostupna nova datoteka",
"room.toggleAdvancedMode": "Uključen napredni način",
"room.setDemocraticView": "Prikaz promijenjen u način: demokratski",
"room.setFilmStripView": "Prikaz promijenjen u način: filmska traka",
"room.loggedIn": "Prijavljeni ste",
"room.loggedOut": "Odjavljeni ste",
"room.changedDisplayName": "Ime promijenjeno u {displayName}",
"room.changeDisplayNameError": "Dogodila se greška prilikom promjene imena",
"room.chatError": "Poruku nije moguće poslati",
"room.aboutToJoin": "Upravo ćete se priključiti sastanku",
"room.roomId": "Oznaka sobe: {roomName}",
"room.setYourName": "Postavite ime za sudjelovanje, i odaberite način prijave",
"room.audioOnly": "Samo zvuk",
"room.audioVideo": "Zvuk i slika",
"room.youAreReady": "Spremni ste",
"room.emptyRequireLogin": "Soba je trenutno prazna! Prijavite se za pokretanje sastanka, ili sačekajte organizatora" ,
"room.locketWait": "Soba je zaključana - pričekajte odobrenje ...",
"room.lobbyAdministration":"Upravljanje predvorjem",
"room.peersInLobby":"Učesnici u predvorju",
"room.lobbyEmpty": "Trenutno nema nikoga u predvorju",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Ja",
"room.spotlights": "Učesnici u fokusu",
"room.passive": "Pasivni učesnici",
"room.videoPaused": "Video pauziran",
"tooltip.login": "Prijava",
"tooltip.logout": "Odjava",
"tooltip.admitFromLobby": "Pusti iz predvorja",
"tooltip.lockRoom": "Zaključaj sobu",
"tooltip.unLockRoom": "Otključaj sobu",
"tooltip.enterFullscreen": "Postavi puni ekran",
"tooltip.leaveFullscreen": "Izađi iz punog ekrana",
"tooltip.lobby": "Prikaži predvorje",
"tooltip.settings": "Prikaži postavke",
"tooltip.participants": "Pokažite sudionike",
"label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi",
"label.yourName": "Vaše ime",
"label.newWindow": "Novi ekran",
"label.fullscreen": "Puni ekran",
"label.openDrawer": "Otvori ladicu",
"label.leave": "Napusti",
"label.chatInput":"Uđi u razgovor porukama",
"label.chat": "Razgovor",
"label.filesharing": "Dijeljenje datoteka",
"label.participants": "Učesnici",
"label.shareFile": "Dijeli datoteku",
"label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano",
"label.unknown": "Nepoznato",
"label.democratic":"Demokratski prikaz",
"label.filmstrip": "Prikaz filmska traka",
"label.low": "Niska",
"label.medium": "Srednja",
"label.high": "Visoka (HD)",
"label.veryHigh": "Vrlo visoka (FHD)",
"label.ultra": "Ultra visoka (UHD)",
"label.close": "Zatvori",
"settings.settings": "Postavke",
"settings.camera": "Kamera",
"settings.selectCamera": "Odaberi video uređaj",
"settings.cantSelectCamera": "Nije moguće odabrati video uređaj",
"settings.audio": "Uređaj za zvuk",
"settings.selectAudio": "Odaberi uređaj za zvuk",
"settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk",
"settings.resolution": "Odaberi video rezoluciju",
"settings.layout": "Način prikaza",
"settings.selectRoomLayout": "Odaberi način prikaza",
"settings.advancedMode": "Napredne mogućnosti",
"settings.permanentTopBar": "Stalna gornja šipka",
"settings.lastn": "Broj vidljivih videozapisa",
"filesharing.saveFileError": "Nije moguće spremiti datoteku",
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",
"filesharing.successfulFileShare": "Datoteka uspješno podijeljena",
"filesharing.unableToShare": "Nije moguće podijeliti datoteku",
"filesharing.error": "Greška prilikom dijeljenja datoteke",
"filesharing.finished": "Završeno učitavanje datoteke",
"filesharing.save": "Spremi",
"filesharing.sharedFile": "{displayName} je podijelio datoteku",
"filesharing.download": "Učitaj",
"filesharing.missingSeeds": "Ako ovaj proces traje dugo, možda više nitko ne dijeli datoteku. Pokušajte zamoliti nekoga da ponovo učita željenu datoteku." ,
"devices.devicesChanged": "Uređaji su se promijenili, podesite ponovno uređaje u postavkama",
"device.audioUnsupported": "Zvuk nije podržan",
"device.activateAudio": "Omogući zvuk",
"device.muteAudio": "Utišaj zvuk",
"device.unMuteAudio": "Vrati zvuk",
"device.videoUnsupported": "Slika nije podržana",
"device.startVideo": "Pokreni sliku",
"device.stopVideo": "Zaustavi sliku",
"device.screenSharingUnsupported": "Dijeljenje ekrana nije podržano",
"device.startScreenSharing": "Pokreni dijeljenje ekrana",
"device.stopScreenSharing": "Zaustavi dijeljenje ekrana",
"devices.microphoneDisconnected": "Mikrofon odspojen",
"devices.microphoneError": "Greška prilikom pristupa mikrofonu",
"devices.microPhoneMute": "Mikrofon utišan",
"devices.micophoneUnMute": "Mikrofon pojačan",
"devices.microphoneEnable": "Mikrofon omogućen",
"devices.microphoneMuteError": "Nije moguće utišati mikrofon",
"devices.microphoneUnMuteError": "Nije moguće pojačati mikrofon",
"devices.screenSharingDisconnected" : "Prekinuto dijeljenje ekrana",
"devices.screenSharingError": "Greška prilikom pristupa ekranu",
"devices.cameraDisconnected": "Kamera odspojena",
"devices.cameraError": "Greška prilikom pristupa kameri"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "A kapcsolat lebomlott",
"socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás",
"socket.reconnected": "Sikeres újarkapcsolódás",
"socket.requestError": "Sikertelen szerver lekérés",
"room.chooseRoom": "Choose the name of the room you would like to join",
"room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ",
"room.consentUnderstand": "I understand",
"room.joined": "Csatlakozátál a konferenciához",
"room.cantJoin": "Sikertelen csatlakozás a konferenciához",
"room.youLocked": "A konferenciába való belépés letiltva",
"room.cantLock": "Sikertelen a konferenciaba való belépés letiltása",
"room.youUnLocked": "A konferenciába való belépés engedélyezve",
"room.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése",
"room.locked": "A konferenciába való belépés letiltva",
"room.unlocked": "A konferenciába való belépés engedélyezve",
"room.newLobbyPeer": "Új részvevő lépett be a konferencia előszobájába",
"room.lobbyPeerLeft": "A konferencia előszobájából a részvevő távozott",
"room.lobbyPeerChangedDisplayName": "Az előszobai résztvevő meváltoztatta a nevét: {displayName}",
"room.lobbyPeerChangedPicture": "Az előszobai résztvevő meváltoztatta a képét",
"room.setAccessCode": "A konferencia hozzáférési kódja megváltozott",
"room.accessCodeOn": "A konferencia hozzáférési kódja aktiválva",
"room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva",
"room.peerChangedDisplayName": "{oldDisplayName} megváltozott erre {displayName}",
"room.newPeer": "{displayName} kapcsolódott a konferenciához",
"room.newFile": "Új fájl érhető el",
"room.toggleAdvancedMode": "Részletes információk megjelenítése",
"room.setDemocraticView": "Egyenlő képméretű képkiosztás",
"room.setFilmStripView": "Egy nagy + sok kis filmkocka képkiosztás",
"room.loggedIn": "Sikeres belépés",
"room.loggedOut": "Sikeres kijelentkezés",
"room.changedDisplayName": "A neved sikeresen megváltozott: {displayName}",
"room.changeDisplayNameError": "Hiba történt a név megváltoztatásakor",
"room.chatError": "Hiba lépett fel a chat üzenetet elküldése során",
"room.aboutToJoin": "Hamarosan csatlakozol a konferenciához",
"room.roomId": "Konferenciaazonosító: {roomName}",
"room.setYourName": "Állítsd be a neved, és válaszd ki hogyan szeretnél csatlakozni:",
"room.audioOnly": "csak Hang",
"room.audioVideo": "Hang és Videó",
"room.youAreReady": "Ok, kész vagy",
"room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferecnia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.",
"room.locketWait": "A konferencia szobába a a belépés tilos - Várj amíg valaki be nem enged ...",
"room.lobbyAdministration": "Előszoba adminisztráció",
"room.peersInLobby": "Résztvevők az előszobában",
"room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Én",
"room.spotlights": "Látható résztvevők",
"room.passive": "Passzív résztvevők",
"room.videoPaused": "Ez a videóstream szünetel",
"tooltip.login": "Belépés",
"tooltip.logout": "Kilépés",
"tooltip.admitFromLobby": "Beenegdem az előszobából",
"tooltip.lockRoom": "A konferenciába való belépés letiltása",
"tooltip.unLockRoom": "konferenciába való belépés engedélyezése",
"tooltip.enterFullscreen": "Teljes képernyős mód",
"tooltip.leaveFullscreen": "Kilépés teljes képernyős módból",
"tooltip.lobby": "Az előszobában várakozók listája",
"tooltip.settings": "Beállítások",
"tooltip.participants": "Résztvevők",
"label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább",
"label.yourName": "A neved",
"label.newWindow": "Új ablak",
"label.fullscreen": "Teljes képernyős mód",
"label.openDrawer": "Oldalsáv megnyitása",
"label.leave": "Kilépés",
"label.chatInput": "Chat üzenet ...",
"label.chat": "Chat",
"label.filesharing": "Fájl megosztás",
"label.participants": "Résztvevők",
"label.shareFile": "Fájl megosztása",
"label.fileSharingUnsupported": "Fájl megosztás nem támogatott",
"label.unknown": "Ismeretlen",
"label.democratic": "Egyforma képméretű képkiosztás",
"label.filmstrip": "Egy nagy és kis filmkockák képkiosztás",
"label.low": "Alacsony felbontás",
"label.medium": "Közepes felbontás",
"label.high": "Magas (HD) felbontás",
"label.veryHigh": "Nagyon magas (FHD)",
"label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár",
"settings.settings": "Beállítások",
"settings.camera": "Kamera",
"settings.selectCamera": "Válasz videóeszközt",
"settings.cantSelectCamera": "Nem lehet a videó eszközt kiválasztani",
"settings.audio": "Hang eszköz",
"settings.selectAudio": "Válasz hangeszközt",
"settings.cantSelectAudio": "Nem lehet a hang eszközt kiválasztani",
"settings.resolution": "Válaszd ki a videóeszközöd felbontását",
"settings.layout": "A konferencia képkiosztása",
"settings.selectRoomLayout": "Válaszd ki a konferencia képkiosztását",
"settings.advancedMode": "Részletes információk",
"settings.permanentTopBar": "Állandó felső sáv",
"settings.lastn": "A látható videók száma",
"filesharing.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása",
"filesharing.successfulFileShare": "A fájl sikeresen megosztva",
"filesharing.unableToShare": "Sikereteln fájl megosztás",
"filesharing.error": "Hiba a fájlmegosztás során",
"filesharing.finished": "A fájl letöltés befejeződött",
"filesharing.save": "Mentés",
"filesharing.sharedFile": "{displayName} megosztott egy fájlt",
"filesharing.download": "Letöltés",
"filesharing.missingSeeds": "Ha a folyamat túl sok ideig tart, akkor lehet hogy senki sem seed-eli ez a torrent-et. Próbálj meg megkérni valakit hogy töltse fel újra ezt a fájlt.",
"devices.devicesChanged": "Az eszközei megváltoztak, konfiguráld őket be a beállítások menüben",
"device.audioUnsupported": "A hnag nem támogatott",
"device.activateAudio": "Hang aktiválása",
"device.muteAudio": "Hang némítása",
"device.unMuteAudio": "Hang némítás kikapcsolása",
"device.videoUnsupported": "A videó nem támogatott",
"device.startVideo": "Videó indítása",
"device.stopVideo": "Videó leállítása",
"device.screenSharingUnsupported": "A képernyő megosztás nem támogatott",
"device.startScreenSharing": "Képernyőmegosztás indítása",
"device.stopScreenSharing": "Képernyőmegosztás leáłłítása",
"devices.microphoneDisconnected": "Microphone kapcsolat bontva",
"devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben",
"devices.microPhoneMute": "A mikrofon némítva lett",
"devices.micophoneUnMute": "A mikrofon némítása ki lett kapocsolva",
"devices.microphoneEnable": "A mikrofon engedéylezve",
"devices.microphoneMuteError": "Nem sikerült a mikrofonod némítása",
"devices.microphoneUnMuteError": "Nem sikerült a mikrofonod némításának kikapcsolása",
"devices.screenSharingDisconnected" : "Képernyőmegosztás kapcsolat bontva",
"devices.screenSharingError": "Hiba történt a képernyőd megosztása során",
"devices.cameraDisconnected": "A kamera kapcsolata lebomlott",
"devices.cameraError": "Hiba történt a kamera elérése során"
}

View File

@ -4,7 +4,9 @@
"socket.reconnected": "Du er koblet til igjen",
"socket.requestError": "Feil på server melding",
"room.chooseRoom": "Velg navn på møtet du vil bli med i eller starte",
"room.cookieConsent": "Denne siden bruker cookies for å forbedre brukeropplevelsen",
"room.consentUnderstand": "Jeg forstår",
"room.joined": "Du ble med i møtet",
"room.cantJoin": "Kunne ikke bli med i møtet",
"room.youLocked": "Du låste møtet",
@ -57,7 +59,10 @@
"tooltip.leaveFullscreen": "Forlat fullskjerm",
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere",
"label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett",
"label.yourName": "Ditt navn",
"label.newWindow": "Flytt til separat vindu",
"label.fullscreen": "Fullskjerm",
@ -90,6 +95,8 @@
"settings.layout": "Møtelayout",
"settings.selectRoomLayout": "Velg møtelayout",
"settings.advancedMode": "Avansert modus",
"settings.permanentTopBar": "Permanent topplinje",
"settings.lastn": "Antall videoer synlig",
"filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling",

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Rozłączono",
"socket.reconnecting": "Próba ponownego połączenia",
"socket.reconnected": "Połączono",
"socket.requestError": "Błąd żądania serwera",
"room.chooseRoom": "Wybór konferencji",
"room.cookieConsent": "Ta strona internetowa wykorzystuje pliki cookie w celu zwiększenia wygody użytkowania.",
"room.consentUnderstand": "I understand",
"room.joined": "Podłączono do konferencji",
"room.cantJoin": "Brak możliwości dołączenia do pokoju",
"room.youLocked": "Zakluczono pokój",
"room.cantLock": "Nie można zakluczyć pokoju",
"room.youUnLocked": "Odkluczono pokój",
"room.cantUnLock": "Nie można odkluczyć pokoju",
"room.locked": "Pokój jest zakluczony",
"room.unlocked": "Pokój jest odkluczony",
"room.newLobbyPeer": "Nowy uczestnik w poczekalni",
"room.lobbyPeerLeft": "Uczestnik opuścił poczekalnię",
"room.lobbyPeerChangedDisplayName": "Uczestnik w poczekalni zmienił nazwę na {displayName}",
"room.lobbyPeerChangedPicture": "Uczestnik w poczekalni zmienił swój obraz profilowy",
"room.setAccessCode": "Kod dostępu do konferencji został zaktualizowany",
"room.accessCodeOn": "Kod dostępu do konferencji został włączony",
"room.accessCodeOff": "Kod dostępu do konferencji został wyłączony",
"room.peerChangedDisplayName": "Zmieniono nazwę użytkownika {oldDisplayName} na {displayName}",
"room.newPeer": "Nowy uczestnik {displayName} dołączył do konferencji",
"room.newFile": "Dostępny jest nowy plik",
"room.toggleAdvancedMode": "Przełączono tryb zaawansowany",
"room.setDemocraticView": "Widok zmieniony na: układ demokratyczny",
"room.setFilmStripView": "Widok zmieniony na: układ filmowy",
"room.loggedIn": "Zalogowano",
"room.loggedOut": "Wylogowano",
"room.changedDisplayName": "Nazwa użytkownika zmieniona na {displayName}",
"room.changeDisplayNameError": "Wystąpił błąd podczas zmiany nazwy użytkownika",
"room.chatError": "Nie można wysłać wiadomości",
"room.aboutToJoin": "Wkrótce nastąpi podłączenie do konferencji",
"room.roomId": "Identyfikator pokoju: {roomName}",
"room.setYourName": "Proszę podać swoją nazwę użytkownika i wybrać sposób połączenia:",
"room.audioOnly": "Tylko audio",
"room.audioVideo": "Audio i wideo",
"room.youAreReady": "Wszystko gotowe",
"room.emptyRequireLogin": "Pokój jest pusty. Proszę się zalogować lub poczekać na gospodarza pokoju.",
"room.locketWait": "Pokój jest zakluczony - proszę poczekać na otwarcie...",
"room.lobbyAdministration": "Administracja poczekalnią",
"room.peersInLobby": "Uczestnicy w poczekalni",
"room.lobbyEmpty": "Aktualnie nie ma nikogo w poczekalni",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {uczestnik} other {uczestników}}",
"room.me": "Ja",
"room.spotlights": "Aktywni uczestnicy",
"room.passive": "Pasywni uczestnicy",
"room.videoPaused": "To wideo jest wstrzymane.",
"tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj",
"tooltip.admitFromLobby": "Przejście z poczekalni",
"tooltip.lockRoom": "Zaklucz pokój",
"tooltip.unLockRoom": "Odklucz pokój",
"tooltip.enterFullscreen": "Włącz tryb pełnoekranowy",
"tooltip.leaveFullscreen": "Wyłącz tryb pełnoekranowy",
"tooltip.lobby": "Pokaż poczekalnię",
"tooltip.settings": "Pokaż ustawienia",
"tooltip.participants": "Pokaż uczestników",
"label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj",
"label.yourName": "Moja nazwa użytkownika",
"label.newWindow": "Nowe okno",
"label.fullscreen": "Tryb pełnoekranowy",
"label.openDrawer": "Menu",
"label.leave": "Zakończ",
"label.chatInput": "Treść wiadomości...",
"label.chat": "Czat",
"label.filesharing": "Udostępnianie plików",
"label.participants": "Uczestnicy",
"label.shareFile": "Udostępnij plik",
"label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane",
"label.unknown": "Nieznane",
"label.democratic": "Układ demokratyczny",
"label.filmstrip": "Układ filmowy",
"label.low": "Niska",
"label.medium": "Średnia",
"label.high": "Wysoka (HD)",
"label.veryHigh": "Bardzo wysoka (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Zamknij",
"settings.settings": "Ustawienia",
"settings.camera": "Kamera",
"settings.selectCamera": "Wybór urządzenia wideo",
"settings.cantSelectCamera": "Nie można wybrać urządzenia wideo",
"settings.audio": "Urządzenie audio",
"settings.selectAudio": "Wybór urządzenia audio",
"settings.cantSelectAudio": "Nie można wybrać urządzenia audio",
"settings.resolution": "Wybór rozdzielczości wideo",
"settings.layout": "Układ konferencji",
"settings.selectRoomLayout": "Ustawienia układu konferencji",
"settings.advancedMode": "Tryb zaawansowany",
"settings.permanentTopBar": "Stały górny pasek",
"settings.lastn": "Liczba widocznych filmów",
"filesharing.saveFileError": "Nie można zapisać pliku",
"filesharing.startingFileShare": "Próba udostępnienia pliku",
"filesharing.successfulFileShare": "Plik został udostępniony",
"filesharing.unableToShare": "Nie można udostępnić pliku",
"filesharing.error": "Błąd udostępniania pliku",
"filesharing.finished": "Ukończono pobieranie pliku",
"filesharing.save": "Zapisz",
"filesharing.sharedFile": "{displayName} współdzieli plik",
"filesharing.download": "Pobierz",
"filesharing.missingSeeds": "Udostępnianie plików przez Torrent - jeśli ten proces trwa długo sugerujemy poprosić o ponowne załadowanie żądanego pliku",
"devices.devicesChanged": "Urządzenia wejściowe się zmieniły, konfiguracja dostępna w oknie dialogowym ustawień",
"device.audioUnsupported": "Dźwięk nieobsługiwany",
"device.activateAudio": "Aktywuj mikrofon",
"device.muteAudio": "Wyłącz mikrofon",
"device.unMuteAudio": "Włącz mikrofon",
"device.videoUnsupported": "Wideo nie jest obsługiwane",
"device.startVideo": "Włącz kamerę",
"device.stopVideo": "Wyłącz kamerę",
"device.screenSharingUnsupported": "Udostępnianie ekranu nie jest obsługiwane",
"device.startScreenSharing": "Rozpocznij udostępnianie ekranu",
"device.stopScreenSharing": "Zatrzymaj udostępnianie ekranu",
"devices.microphoneDisconnected": "Odłączono mikrofon",
"devices.microphoneError": "Błąd dostępu do mikrofonu",
"devices.microPhoneMute": "Wyciszenie mikrofonu włączone",
"devices.micophoneUnMute": "Wyciszenie mikrofonu wyłączone",
"devices.microphoneEnable": "Włączono mikrofon",
"devices.microphoneMuteError": "Nie można wyciszyć mikrofonu",
"devices.microphoneUnMuteError": "Nie można wyłączyć wyciszenia mikrofonu.",
"devices.screenSharingDisconnected" : "Udostępnianie ekranu zakończone",
"devices.screenSharingError": "Wystąpił błąd podczas uzyskiwania dostępu do ekranu",
"devices.cameraDisconnected": "Kamera odłączona",
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery"
}

View File

@ -0,0 +1,140 @@
{
"socket.disconnected": "Está desligado",
"socket.reconnecting": "Está desligado, tentando ligar novamente.",
"socket.reconnected": "Está novamente em sessão",
"socket.requestError": "Ocorreu um erro no pedido ao servidor",
"room.chooseRoom": "Indique o nome da sala em que pretende entrar",
"room.cookieConsent": "Este sitio web utiliza cookie para melhorar a sua experiência de utilização",
"room.consentUnderstand": "I understand",
"room.joined": "Entrou na sala",
"room.cantJoin": "Não foi possivel entrar na sala",
"room.youLocked": "Bloqueou o acesso à sala",
"room.cantLock": "Impossível bloquear acesso à sala",
"room.youUnLocked": "Desbloqueou o acesso à sala",
"room.cantUnLock": "Impossível desbloquear o acesso à sala",
"room.locked": "A sala está bloqueada",
"room.unlocked": "A sala está desbloqueada",
"room.newLobbyPeer": "Novo participante no em espera",
"room.lobbyPeerLeft": "Participantes em espera",
"room.lobbyPeerChangedDisplayName": "Participante em espera alterou o nome para {displayName}",
"room.lobbyPeerChangedPicture": "Participante em espera alterou a fotografia",
"room.setAccessCode": "Código de acesso para a sala atualizado",
"room.accessCodeOn": "Código de acesso à sala está ativado",
"room.accessCodeOff": "Códio de acesso à sala está desativado",
"room.peerChangedDisplayName": "{oldDisplayName} é agora {displayName}",
"room.newPeer": "{displayName} entrou na sala",
"room.newFile": "Novo ficheiro disponível",
"room.toggleAdvancedMode": "Alterado para modo avançado",
"room.setDemocraticView": "Alterar para vista democrática",
"room.setFilmStripView": "Alterar para vista de fita de cinema",
"room.loggedIn": "Realizou o acesso",
"room.loggedOut": "Terminou o acesso",
"room.changedDisplayName": "O seu nome foi alterado para {displayName}",
"room.changeDisplayNameError": "Um erro ocorreu enquanto estava a alterar o seu nome",
"room.chatError": "Foi impossível enviar a sua mensagem",
"room.aboutToJoin": "Está prestes a entrar numa reunião",
"room.roomId": "ID da Sala: {roomName}",
"room.setYourName": "Indique o seu nome de participante e escolha como pretende juntar-se à reunião:",
"room.audioOnly": "Apenas áudio",
"room.audioVideo": "Áudio e Vídeo",
"room.youAreReady": "Ok, está pronto",
"room.emptyRequireLogin": "A sala está vazia! Pode entrar e começar a reunião ou esperar até que o organizador da reunião entre",
"room.locketWait": "A sala está bloqueada - aguarde até que alguem o deixe entrar...",
"room.lobbyAdministration": "Administração da sala de espera",
"room.peersInLobby": "Participantes na sala de espera",
"room.lobbyEmpty": "É neste momento o único participante em espera",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participante} other {participantes}}",
"room.me": "Eu",
"room.spotlights": "Participantes em foco",
"room.passive": "Participantes passivos",
"room.videoPaused": "Este vídeo está em pausa",
"tooltip.login": "Entrar",
"tooltip.logout": "Sair",
"tooltip.admitFromLobby": "Admitir da sala de espera",
"tooltip.lockRoom": "Bloquear sala",
"tooltip.unLockRoom": "Desbloquear sala",
"tooltip.enterFullscreen": "Apresentar em ecrã completo",
"tooltip.leaveFullscreen": "Sair de ecrã completo",
"tooltip.lobby": "Apresentar sala de espera",
"tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes",
"label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar",
"label.yourName": "O seu nome",
"label.newWindow": "Nova janela",
"label.fullscreen": "Ecrã completo",
"label.openDrawer": "Abrir painel",
"label.leave": "Sair",
"label.chatInput": "Indique mensagem...",
"label.chat": "Chat",
"label.filesharing": "Partilha de ficheiro",
"label.participants": "Participantes",
"label.shareFile": "Partilhar ficheiro",
"label.fileSharingUnsupported": "Partilha de ficheiro não disponível",
"label.unknown": "Desconhecido",
"label.democratic": "Vista democrática",
"label.filmstrip": "Vista filme de cinema",
"label.low": "Biaixo",
"label.medium": "Medio",
"label.high": "Alta (HD)",
"label.veryHigh": "Muito alta (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Fechar",
"settings.settings": "Definições",
"settings.camera": "Camera",
"settings.selectCamera": "Selecione o seu dispositivo de fonte de vídeo",
"settings.cantSelectCamera": "Impossível selecionar dispositivo de vídeo",
"settings.audio": "Dispositivo Áudio",
"settings.selectAudio": "Selecione o seu dispositivo de áudio",
"settings.cantSelectAudio": "Impossível selecionar o seu dispositivo de áudio",
"settings.resolution": "Selecione a sua resolução de vídeo",
"settings.layout": "Disposição da sala",
"settings.selectRoomLayout": "Seleccione a disposição da sala",
"settings.advancedMode": "Modo avançado",
"settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Número de vídeos visíveis",
"filesharing.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro",
"filesharing.successfulFileShare": "Ficheiro partilhado com sucesso",
"filesharing.unableToShare": "Foi impossível partilhar o ficheiro",
"filesharing.error": "Aconteceu um erro na partilha do ficheiro",
"filesharing.finished": "Ficheiro acabou de ser descarregado",
"filesharing.save": "Gravar",
"filesharing.sharedFile": "{displayName} partilhou um ficheiro",
"filesharing.download": "Descarregar",
"filesharing.missingSeeds": "Se este processo demorar muito tempo, pode ser quem ninguém esteja a partilhar este ficheiro. Solicite a alguém para partilhar o ficheiro que pretende.",
"devices.devicesChanged": "Os seus dispositivos foram alterados, configure os dispositivos na opção de definições",
"device.audioUnsupported": "Áudio não disponível",
"device.activateAudio": "Ativar áudio",
"device.muteAudio": "Desativar o microfone",
"device.unMuteAudio": "Ativar o microfone",
"device.videoUnsupported": "Video não disponível",
"device.startVideo": "Iniciar vídeo",
"device.stopVideo": "Parar vídeo",
"device.screenSharingUnsupported": "Partilha de ecrã não disponível",
"device.startScreenSharing": "Iniciar partilha de ecrã",
"device.stopScreenSharing": "Parar partilha de ecrã",
"devices.microphoneDisconnected": "Microfone desiligado",
"devices.microphoneError": "Ocorreu um erro no acesso ao microfone",
"devices.microPhoneMute": "Som microfone desativado",
"devices.micophoneUnMute": "Som mmicrofone ativado",
"devices.microphoneEnable": "Microfone ativado",
"devices.microphoneMuteError": "Não foi possível cortar o som do microfone",
"devices.microphoneUnMuteError": "Não foi possível ativar o som do microfone",
"devices.screenSharingDisconnected" : "Partilha de ecrã desligada",
"devices.screenSharingError": "Ocorreu um erro no acesso ao seu ecrã",
"devices.cameraDisconnected": "Câmara desconectada",
"devices.cameraError": "Ocorreu um erro no acesso à sua câmara"
}

View File

@ -0,0 +1,139 @@
{
"socket.disconnected": "Ești disconectat",
"socket.reconnecting": "Ești disconectat, reconectare",
"socket.reconnected": "Ești reconectat",
"socket.requestError": "Eroare la solicitarea serverului",
"room.chooseRoom": "Alege numele camerei ai cărui vrei să te alături",
"room.cookieConsent": "Acest website utilizează cookie-uri pentru a îmbunătăți experiența utilizatorului",
"room.consentUnderstand": "I understand",
"room.joined": "Te-ai alăturat camerei",
"room.cantJoin": "Nu se poate alătura camerei",
"room.youLocked": "Ai blocat camera",
"room.cantLock": "Încercarea de a bloca camera a eșuat",
"room.youUnLocked": "Ai deblocat camera",
"room.cantUnLock": "Încercarea de a debloca camera a eșuat",
"room.locked": "Camera e blocată",
"room.unlocked": "Camera e deblocată",
"room.newLobbyPeer": "Un nou participant a intrat în hol",
"room.lobbyPeerLeft": "Participantul din hol a plecat",
"room.lobbyPeerChangedDisplayName": "Participantul din hol și-a schimbat numele în {displayName}",
"room.lobbyPeerChangedPicture": "Participantul din hol și-a schimbat imaginea",
"room.setAccessCode": "Codul de accesare pentru cameră e actualizat",
"room.accessCodeOn": "Codul de accesare pentru cameră e activat",
"room.accessCodeOff": "Codul de accesare pentru cameră e deactivat",
"room.peerChangedDisplayName": "{oldDisplayName} e acum {displayName}",
"room.newPeer": "{displayName} s-a alăturat camerei",
"room.newFile": "Un fișier nou e disponibil",
"room.toggleAdvancedMode": "Modul avansat e activat",
"room.setDemocraticView": "Aspectul e schimat la distribuție egală a dimensiunii imaginii",
"room.setFilmStripView": "Aspectul e schimat la aspect filmstrip",
"room.loggedIn": "Ești autentificat",
"room.loggedOut": "Ești deconectat",
"room.changedDisplayName": "Numele afișat e schimbat în {displayName}",
"room.changeDisplayNameError": "A apărut o eroare la schimbarea numelui afișat",
"room.chatError": "Încercarea de a trimite mesajul de chat a eșuat",
"room.aboutToJoin": "Vei alătura conferinței în curând",
"room.roomId": "ID cameră: {roomName}",
"room.setYourName": "Stabilește numele pentru participare și alege cum dorești să te alături:",
"room.audioOnly": "Doar audio",
"room.audioVideo": "Audio și video",
"room.youAreReady": "Ok, ești gata",
"room.emptyRequireLogin": "Camera e goală! Poți să te conectezi pentru a începe întâlnirea sau să aștepți până gazda se alătură",
"room.locketWait": "Camera este închisă - așteaptă până când cineva te lasă să intri ...",
"room.lobbyAdministration": "Administrarea holului",
"room.peersInLobby": "Participanți în hol",
"room.lobbyEmpty": "În prezent nu e nimeni în hol",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Eu",
"room.spotlights": "Participanți în Spotlight",
"room.passive": "Participanți pasivi",
"room.videoPaused": "Acest video este pus pe pauză",
"tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare",
"tooltip.admitFromLobby": "Admite din hol",
"tooltip.lockRoom": "Blocarea camerei",
"tooltip.unLockRoom": "Deblocarea camerei",
"tooltip.enterFullscreen": "Modul ecran complet",
"tooltip.leaveFullscreen": "Ieșire din modul ecran complet",
"tooltip.lobby": "Arată holul",
"tooltip.settings": "Arată participanții",
"label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare",
"label.yourName": "Numele tău",
"label.newWindow": "Fereastră nouă",
"label.fullscreen": "Mod ecran complet",
"label.openDrawer": "Deschiderea panoului lateral",
"label.leave": "Deconectare",
"label.chatInput": "Introduce mesajul de chat...",
"label.chat": "Chat",
"label.filesharing": "Partajarea fișierelor",
"label.participants": "Participanți",
"label.shareFile": "Partajează fișierul",
"label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată",
"label.unknown": "Necunoscut",
"label.democratic": "Distribuție egală a dimensiunii imaginii",
"label.filmstrip": "Aspect filmstrip",
"label.low": "Rezoluție scăzută",
"label.medium": "Rezoluție mediu",
"label.high": "Rezoluție înaltă (HD)",
"label.veryHigh": "Rezoluție foarte înaltă (FHD)",
"label.ultra": "Rezoluție ultra înaltă (UHD)",
"label.close": "Închide",
"settings.settings": "Setări",
"settings.camera": "Cameră video",
"settings.selectCamera": "Selectarea dispozitivul video",
"settings.cantSelectCamera": "Încercarea de a selecta dispozitivul video a eșuat",
"settings.audio": "Dispozitivul audio",
"settings.selectAudio": "Selectarea dispozitivul audio",
"settings.cantSelectAudio": "Încercarea de a selecta dispozitivul audio a eșuat",
"settings.resolution": "Selectează rezoluția video",
"settings.layout": "Aspectul camerei video",
"settings.selectRoomLayout": "Selectează spectul camerei video",
"settings.advancedMode": "Mod avansat",
"settings.permanentTopBar": "Bara de sus permanentă",
"settings.lastn": "Numărul de videoclipuri vizibile",
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului",
"filesharing.successfulFileShare": "Fișierul a fost partajat cu succes",
"filesharing.unableToShare": "Încercarea de a partaja fișierul a eșuat",
"filesharing.error": "Eroare la partajarea fișierului",
"filesharing.finished": "Fișier descărcat",
"filesharing.save": "Salvare",
"filesharing.sharedFile": "{displayName} a partajat un fișier",
"filesharing.download": "Descărcare",
"filesharing.missingSeeds": "Dacă acest proces durează mult timp, s-ar putea să nu fie nimeni care seed-ează acest torrent. Încearcă să ceri cuiva să reîncarce fișierul.",
"devices.devicesChanged": "Dispozitivele tale s-au schimbat, configurează-ți dispozitivele în dialogul de setări",
"device.audioUnsupported": "Audioul nu e suportat",
"device.activateAudio": "Activează audioul",
"device.muteAudio": "Dezactivează audioul",
"device.unMuteAudio": "Retragerea dezactivării audioului",
"device.videoUnsupported": "Videoul nu e suportat",
"device.startVideo": "Pornirea videoului",
"device.stopVideo": "Oprirea videoului",
"device.screenSharingUnsupported": "Partajarea ecranulului nu e suportat",
"device.startScreenSharing": "Pornește partajarea ecranului",
"device.stopScreenSharing": "Oprește partajarea ecranului",
"devices.microphoneDisconnected": "Microfonul e deconectat",
"devices.microphoneError": "A apărut o eroare la accesarea microfonului",
"devices.microPhoneMute": "Microfonul e dezactivat",
"devices.micophoneUnMute": "Retragerea dezactivării microfonului",
"devices.microphoneEnable": "Microfonul e activat",
"devices.microphoneMuteError": "Încercarea de a dezactiva microfonului a eșuat",
"devices.microphoneUnMuteError": "Încercarea de a retrage dezactivarea microfonului a eșuat",
"devices.screenSharingDisconnected": "Partajarea ecranului este deconectată",
"devices.screenSharingError": "A apărut o eroare la accesarea ecranului",
"devices.cameraDisconnected": "Camera video e disconectată",
"devices.cameraError": "A apărut o eroare la accesarea camerei video"
}

View File

@ -1,8 +1,12 @@
export function getSignalingUrl(peerId, roomId)
{
const hostname = window.location.hostname;
const hostname = window.config.multipartyServer;
const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port;
const port =
process.env.NODE_ENV !== 'production' ?
window.config.developmentPort
:
window.config.productionPort;
const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`;

BIN
demo.gif 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

61
munin/mm-plugin 100755
View File

@ -0,0 +1,61 @@
#!/bin/sh
# -*- sh -*-
: << =cut
=head1 NAME
turn - Plugin to monitor the turn server test probe.
=head1 CONFIGURATION
No configuration
=head1 AUTHOR
Unknown author
=head1 LICENSE
GPLv2
=head1 MAGIC MARKERS
#%# family=auto
#%# capabilities=autoconf
=cut
. "$MUNIN_LIBDIR/plugins/plugin.sh"
if [ "$1" = "autoconf" ]; then
if [ -r /proc/sys/kernel/random/entropy_avail ]; then
echo yes
exit 0
else
echo no
exit 0
fi
fi
if [ "$1" = "config" ]; then
echo 'graph_title MM stats'
#echo 'graph_args --base 1000 -l 0'
echo 'graph_vlabel Actual Seesion Count'
echo 'graph_category other'
echo 'graph_info This graph shows the mm stats.'
echo 'rooms.label rooms'
echo 'rooms.info The count of rooms.'
echo 'peers.label peers'
echo 'peers.info The count of peers.'
exit 0
fi
ROOMS=`docker exec -t mm_mm_1 /opt/multiparty-meeting/server/connect.js --stats | grep 'rooms' | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" | sed -E 's/rooms:([0-9]+)/\1/g'`
PEERS=`docker exec -t mm_mm_1 /opt/multiparty-meeting/server/connect.js --stats | grep 'peers' | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" | sed -E 's/peers:([0-9]+)/\1/g'`
echo "rooms.value ${ROOMS}"
echo "peers.value ${PEERS}"
:

View File

@ -0,0 +1,2 @@
[mm]
user root

45
munin/munin.md 100644
View File

@ -0,0 +1,45 @@
# Install a munin plugin, as a very basic monitoring
## munin-node
* install on your docker host munin-node on mm.example.com
```bash
apt install munin-node
```
* Copy mm-plugin from this directory to plugins dir as mm
cp mm-plugin /usr/share/munin/plugins/mm
```bash
sudo ln -s /usr/share/munin/plugins/mm /etc/munin/plugins/mm
```
* Copy mm-plugin-conf from this directory to munin plugins conf dir as mm
```bash
cp mm-plugin-conf /etc/munin/plugin-conf.d/mm
```
* Restart munin
```bash
systemctl restart munin-node
```
## munin master
* Install a munin master on different host if you don't have munin already.
```bash
apt install munin
```
* On your munin master configure the new node
edit and add to /etc/munin.conf
```bash
[mm]
mm.example.com
```

View File

@ -2,25 +2,37 @@ const os = require('os');
module.exports =
{
// oAuth2 conf
/* auth :
// Auth conf
/*
auth :
{
lti :
{
consumerKey : 'key',
consumerSecret : 'secret'
},
oidc:
{
// The issuer URL for OpenID Connect discovery
// The OpenID Provider Configuration Document
// could be discovered on:
// issuerURL + '/.well-known/openid-configuration'
// The issuer URL for OpenID Connect discovery
// The OpenID Provider Configuration Document
// could be discovered on:
// issuerURL + '/.well-known/openid-configuration'
issuerURL : 'https://example.com',
clientOptions :
{
client_id : '',
client_secret : '',
scope : 'openid email profile',
// where client.example.com is your multiparty meeting server
redirect_uri : 'https://client.example.com/auth/callback'
}
// issuerURL : 'https://example.com',
// clientOptions :
// {
client_id : '',
client_secret : '',
scope : 'openid email profile',
// where client.example.com is your multiparty meeting server
redirect_uri : 'https://client.example.com/auth/callback'
}
},*/
},
*/
redisOptions : {},
// session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e',
cookieName : 'multiparty-meeting.sid',
@ -34,6 +46,10 @@ module.exports =
// Any http request is redirected to https.
// Listening port for http server.
listeningRedirectPort : 80,
// Listens only on http, only on listeningPort
// listeningRedirectPort disabled
// use case: loadbalancer backend
httpOnly : false,
// If this is set to true, only signed-in users will be able
// to join a room directly. Non-signed-in users (guests) will
// always be put in the lobby regardless of room lock status.
@ -76,6 +92,37 @@ module.exports =
clockRate : 48000,
channels : 2
},
{
kind : 'video',
mimeType : 'video/VP8',
clockRate : 90000,
parameters :
{
'x-google-start-bitrate' : 1000
}
},
{
kind : 'video',
mimeType : 'video/VP9',
clockRate : 90000,
parameters :
{
'profile-id' : 2,
'x-google-start-bitrate' : 1000
}
},
{
kind : 'video',
mimeType : 'video/h264',
clockRate : 90000,
parameters :
{
'packetization-mode' : 1,
'profile-level-id' : '4d0032',
'level-asymmetry-allowed' : 1,
'x-google-start-bitrate' : 1000
}
},
{
kind : 'video',
mimeType : 'video/h264',
@ -96,10 +143,15 @@ module.exports =
listenIps :
[
// change ip to your servers IP address!
{ ip: '1.2.3.4', announcedIp: null }
{ ip: '0.0.0.0', announcedIp: null }
// Can have multiple listening interfaces
// { ip: '::/0', announcedIp: null }
],
maxIncomingBitrate : 1500000,
initialAvailableOutgoingBitrate : 1000000
initialAvailableOutgoingBitrate : 1000000,
minimumAvailableOutgoingBitrate : 600000,
// Additional options that are not part of WebRtcTransportOptions.
maxIncomingBitrate : 1500000
}
}
};

View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
const interactiveClient = require('./lib/interactiveClient');
interactiveClient();

View File

@ -68,7 +68,7 @@ class Room extends EventEmitter
this._lastN = [];
this._peers = new Map();
this._peers = {};
// mediasoup Router instance.
this._mediasoupRouter = mediasoupRouter;
@ -96,13 +96,17 @@ class Room extends EventEmitter
this._lobby.close();
this._peers.forEach((peer) =>
// Close the peers.
for (const peer in this._peers)
{
if (!peer.closed)
peer.close();
});
if (Object.prototype.hasOwnProperty.call(this._peers, peer))
{
if (!peer.closed)
peer.close();
}
}
this._peers.clear();
this._peers = null;
// Close the mediasoup Router.
this._mediasoupRouter.close();
@ -116,7 +120,7 @@ class Room extends EventEmitter
logger.info('handlePeer() [peer:"%s"]', peer.id);
// This will allow reconnects to join despite lock
if (this._peers.has(peer.id))
if (this._peers[peer.id])
{
logger.warn(
'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
@ -168,10 +172,10 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer);
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
});
}
});
this._lobby.on('peerAuthenticated', (peer) =>
@ -183,20 +187,20 @@ class Room extends EventEmitter
{
const { id, displayName } = changedPeer;
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
});
}
});
this._lobby.on('changePicture', (changedPeer) =>
{
const { id, picture } = changedPeer;
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
});
}
});
this._lobby.on('peerClosed', (closedPeer) =>
@ -205,10 +209,10 @@ class Room extends EventEmitter
const { id } = closedPeer;
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
});
}
});
// If nobody left in lobby we should check if room is empty too and initiating
@ -230,22 +234,29 @@ class Room extends EventEmitter
const { producer, volume } = volumes[0];
// Notify all Peers.
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'activeSpeaker', {
peerId : producer.appData.peerId,
volume : volume
});
});
this._notification(
peer.socket,
'activeSpeaker',
{
peerId : producer.appData.peerId,
volume : volume
});
}
});
this._audioLevelObserver.on('silence', () =>
{
// Notify all Peers.
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'activeSpeaker', { peerId: null });
});
this._notification(
peer.socket,
'activeSpeaker',
{ peerId: null }
);
}
});
}
@ -254,10 +265,18 @@ class Room extends EventEmitter
logger.info(
'logStatus() [room id:"%s", peers:"%s"]',
this._roomId,
this._peers.size
Object.keys(this._peers).length
);
}
async dump()
{
return {
roomId : this._roomId,
peers : Object.keys(this._peers).length
};
}
get id()
{
return this._roomId;
@ -287,17 +306,17 @@ class Room extends EventEmitter
// checks both room and lobby
checkEmpty()
{
return this._peers.size === 0;
return Object.keys(this._peers).length === 0;
}
_parkPeer(parkPeer)
{
this._lobby.parkPeer(parkPeer);
this._peers.forEach((peer) =>
for (const peer of this._getJoinedPeers())
{
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
});
}
}
_peerJoining(peer)
@ -311,7 +330,7 @@ class Room extends EventEmitter
this._lastN.push(peer.id);
}
this._peers.set(peer.id, peer);
this._peers[peer.id] = peer;
this._handlePeer(peer);
this._notification(peer.socket, 'roomReady');
@ -354,7 +373,7 @@ class Room extends EventEmitter
this._lastN.splice(index, 1);
}
this._peers.delete(peer.id);
delete this._peers[peer.id];
// If this is the last Peer in the room and
// lobby is empty, close the room after a while.
@ -405,6 +424,27 @@ class Room extends EventEmitter
case 'join':
{
try
{
if (peer.socket.handshake.session.passport.user.displayName)
{
this._notification(
peer.socket,
'changeDisplayname',
{
peerId : peer.id,
displayName : peer.socket.handshake.session.passport.user.displayName,
oldDisplayName : ''
},
true
);
}
}
catch (error)
{
logger.error(error);
}
// Ensure the Peer is not already joined.
if (peer.joined)
throw new Error('Peer already joined');
@ -423,41 +463,47 @@ class Room extends EventEmitter
// Tell the new Peer about already joined Peers.
// And also create Consumers for existing Producers.
const peerInfos = [];
const joinedPeers =
[
...this._getJoinedPeers()
];
this._peers.forEach((joinedPeer) =>
{
if (joinedPeer.joined)
{
peerInfos.push(joinedPeer.peerInfo);
joinedPeer.producers.forEach((producer) =>
{
this._createConsumer(
{
consumerPeer : peer,
producerPeer : joinedPeer,
producer
});
});
}
});
const peerInfos = joinedPeers
.filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo));
cb(null, { peers: peerInfos });
// Mark the new Peer as joined.
peer.joined = true;
this._notification(
peer.socket,
'newPeer',
for (const joinedPeer of joinedPeers)
{
// Create Consumers for existing Producers.
for (const producer of joinedPeer.producers.values())
{
id : peer.id,
displayName : displayName,
picture : picture
},
true
);
this._createConsumer(
{
consumerPeer : peer,
producerPeer : joinedPeer,
producer
});
}
}
// Notify the new Peer to all other Peers.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
this._notification(
otherPeer.socket,
'newPeer',
{
id : peer.id,
displayName : displayName,
picture : picture
}
);
}
logger.debug(
'peer joined [peer: "%s", displayName: "%s", picture: "%s"]',
@ -472,20 +518,28 @@ class Room extends EventEmitter
// initiate mediasoup Transports and be ready when he later joins.
const { forceTcp, producing, consuming } = request.data;
const {
maxIncomingBitrate,
initialAvailableOutgoingBitrate
} = config.mediasoup.webRtcTransport;
const webRtcTransportOptions =
{
...config.mediasoup.webRtcTransport,
appData : { producing, consuming }
};
if (forceTcp)
{
webRtcTransportOptions.enableUdp = false;
webRtcTransportOptions.enableTcp = true;
}
const transport = await this._mediasoupRouter.createWebRtcTransport(
{
listenIps : config.mediasoup.webRtcTransport.listenIps,
enableUdp : !forceTcp,
enableTcp : true,
preferUdp : true,
initialAvailableOutgoingBitrate,
appData : { producing, consuming }
});
webRtcTransportOptions
);
transport.on('dtlsstatechange', (dtlsState) =>
{
if (dtlsState === 'failed' || dtlsState === 'closed')
logger.warn('WebRtcTransport "dtlsstatechange" event [dtlsState:%s]', dtlsState);
});
// Store the WebRtcTransport into the Peer data Object.
peer.addTransport(transport.id, transport);
@ -499,6 +553,8 @@ class Room extends EventEmitter
dtlsParameters : transport.dtlsParameters
});
const { maxIncomingBitrate } = config.mediasoup.webRtcTransport;
// If set, apply max incoming bitrate limit.
if (maxIncomingBitrate)
{
@ -577,18 +633,16 @@ class Room extends EventEmitter
cb(null, { id: producer.id });
this._peers.forEach((otherPeer) =>
// Optimization: Create a server-side Consumer for each Peer.
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
if (otherPeer.joined && otherPeer !== peer)
{
this._createConsumer(
{
consumerPeer : otherPeer,
producerPeer : peer,
producer
});
}
});
this._createConsumer(
{
consumerPeer : otherPeer,
producerPeer : peer,
producer
});
}
// Add into the audioLevelObserver.
if (kind === 'audio')
@ -717,6 +771,25 @@ class Room extends EventEmitter
break;
}
case 'setConsumerPriority':
{
// Ensure the Peer is joined.
if (!peer.joined)
throw new Error('Peer not yet joined');
const { consumerId, priority } = request.data;
const consumer = peer.getConsumer(consumerId);
if (!consumer)
throw new Error(`consumer with id "${consumerId}" not found`);
await consumer.setPriority(priority);
cb();
break;
}
case 'requestConsumerKeyFrame':
{
// Ensure the Peer is joined.
@ -1120,7 +1193,7 @@ class Room extends EventEmitter
'newConsumer',
{
peerId : producerPeer.id,
kind : producer.kind,
kind : consumer.kind,
producerId : producer.id,
id : consumer.id,
rtpParameters : consumer.rtpParameters,
@ -1132,8 +1205,7 @@ class Room extends EventEmitter
// Now that we got the positive response from the remote Peer and, if
// video, resume the Consumer to ask for an efficient key frame.
if (producer.kind === 'video')
await consumer.resume();
await consumer.resume();
this._notification(
consumerPeer.socket,
@ -1150,6 +1222,15 @@ class Room extends EventEmitter
}
}
/**
* Helper to get the list of joined peers.
*/
_getJoinedPeers({ excludePeer = undefined } = {})
{
return Object.values(this._peers)
.filter((peer) => peer.joined && peer !== excludePeer);
}
_timeoutCallback(callback)
{
let called = false;

View File

@ -0,0 +1,27 @@
const net = require('net');
const os = require('os');
const path = require('path');
const SOCKET_PATH_UNIX = '/tmp/multiparty-meeting-server.sock';
const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'multiparty-meeting-server');
const SOCKET_PATH = os.platform() === 'win32'? SOCKET_PATH_WIN : SOCKET_PATH_UNIX;
module.exports = async function()
{
const socket = net.connect(SOCKET_PATH);
process.stdin.pipe(socket);
socket.pipe(process.stdout);
socket.on('connect', () => process.stdin.setRawMode(true));
socket.on('close', () => process.exit(0));
socket.on('exit', () => socket.end());
if (process.argv && process.argv[2] === '--stats')
{
await socket.write('stats\n');
socket.end();
}
};

View File

@ -0,0 +1,699 @@
const os = require('os');
const path = require('path');
const repl = require('repl');
const readline = require('readline');
const net = require('net');
const fs = require('fs');
const mediasoup = require('mediasoup');
const colors = require('colors/safe');
const pidusage = require('pidusage');
const SOCKET_PATH_UNIX = '/tmp/multiparty-meeting-server.sock';
const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'multiparty-meeting-server');
const SOCKET_PATH = os.platform() === 'win32' ? SOCKET_PATH_WIN : SOCKET_PATH_UNIX;
// Maps to store all mediasoup objects.
const workers = new Map();
const routers = new Map();
const transports = new Map();
const producers = new Map();
const consumers = new Map();
const dataProducers = new Map();
const dataConsumers = new Map();
class Interactive
{
constructor(socket)
{
this._socket = socket;
this._isTerminalOpen = false;
}
openCommandConsole()
{
const cmd = readline.createInterface(
{
input : this._socket,
output : this._socket,
terminal : true
});
cmd.on('close', () =>
{
if (this._isTerminalOpen)
return;
this.log('\nexiting...');
this._socket.end();
});
const readStdin = () =>
{
cmd.question('cmd> ', async (input) =>
{
const params = input.split(/[\s\t]+/);
const command = params.shift();
switch (command)
{
case '':
{
readStdin();
break;
}
case 'h':
case 'help':
{
this.log('');
this.log('available commands:');
this.log('- h, help : show this message');
this.log('- usage : show CPU and memory usage of the Node.js and mediasoup-worker processes');
this.log('- logLevel level : changes logLevel in all mediasoup Workers');
this.log('- logTags [tag] [tag] : changes logTags in all mediasoup Workers (values separated by space)');
this.log('- dumpRooms : dump all rooms');
this.log('- dumpPeers : dump all peers');
this.log('- dw, dumpWorkers : dump mediasoup Workers');
this.log('- dr, dumpRouter [id] : dump mediasoup Router with given id (or the latest created one)');
this.log('- dt, dumpTransport [id] : dump mediasoup Transport with given id (or the latest created one)');
this.log('- dp, dumpProducer [id] : dump mediasoup Producer with given id (or the latest created one)');
this.log('- dc, dumpConsumer [id] : dump mediasoup Consumer with given id (or the latest created one)');
this.log('- ddp, dumpDataProducer [id] : dump mediasoup DataProducer with given id (or the latest created one)');
this.log('- ddc, dumpDataConsumer [id] : dump mediasoup DataConsumer with given id (or the latest created one)');
this.log('- st, statsTransport [id] : get stats for mediasoup Transport with given id (or the latest created one)');
this.log('- sp, statsProducer [id] : get stats for mediasoup Producer with given id (or the latest created one)');
this.log('- sc, statsConsumer [id] : get stats for mediasoup Consumer with given id (or the latest created one)');
this.log('- sdp, statsDataProducer [id] : get stats for mediasoup DataProducer with given id (or the latest created one)');
this.log('- sdc, statsDataConsumer [id] : get stats for mediasoup DataConsumer with given id (or the latest created one)');
this.log('- t, terminal : open Node REPL Terminal');
this.log('');
readStdin();
break;
}
case 'u':
case 'usage':
{
let usage = await pidusage(process.pid);
this.log(`Node.js process [pid:${process.pid}]:\n${JSON.stringify(usage, null, ' ')}`);
for (const worker of workers.values())
{
usage = await pidusage(worker.pid);
this.log(`mediasoup-worker process [pid:${worker.pid}]:\n${JSON.stringify(usage, null, ' ')}`);
}
break;
}
case 'logLevel':
{
const level = params[0];
const promises = [];
for (const worker of workers.values())
{
promises.push(worker.updateSettings({ logLevel: level }));
}
try
{
await Promise.all(promises);
this.log('done');
}
catch (error)
{
this.error(String(error));
}
break;
}
case 'logTags':
{
const tags = params;
const promises = [];
for (const worker of workers.values())
{
promises.push(worker.updateSettings({ logTags: tags }));
}
try
{
await Promise.all(promises);
this.log('done');
}
catch (error)
{
this.error(String(error));
}
break;
}
case 'stats':
{
this.log(`rooms:${global.rooms.size}\npeers:${global.peers.size}`);
break;
}
case 'dumpRooms':
{
for (const room of global.rooms.values())
{
try
{
const dump = await room.dump();
this.log(`room.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`room.dump() failed: ${error}`);
}
}
break;
}
case 'dumpPeers':
{
for (const peer of global.peers.values())
{
try
{
const dump = await peer.peerInfo;
this.log(`peer.peerInfo():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`peer.peerInfo() failed: ${error}`);
}
}
break;
}
case 'dw':
case 'dumpWorkers':
{
for (const worker of workers.values())
{
try
{
const dump = await worker.dump();
this.log(`worker.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`worker.dump() failed: ${error}`);
}
}
break;
}
case 'dr':
case 'dumpRouter':
{
const id = params[0] || Array.from(routers.keys()).pop();
const router = routers.get(id);
if (!router)
{
this.error('Router not found');
break;
}
try
{
const dump = await router.dump();
this.log(`router.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`router.dump() failed: ${error}`);
}
break;
}
case 'dt':
case 'dumpTransport':
{
const id = params[0] || Array.from(transports.keys()).pop();
const transport = transports.get(id);
if (!transport)
{
this.error('Transport not found');
break;
}
try
{
const dump = await transport.dump();
this.log(`transport.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`transport.dump() failed: ${error}`);
}
break;
}
case 'dp':
case 'dumpProducer':
{
const id = params[0] || Array.from(producers.keys()).pop();
const producer = producers.get(id);
if (!producer)
{
this.error('Producer not found');
break;
}
try
{
const dump = await producer.dump();
this.log(`producer.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`producer.dump() failed: ${error}`);
}
break;
}
case 'dc':
case 'dumpConsumer':
{
const id = params[0] || Array.from(consumers.keys()).pop();
const consumer = consumers.get(id);
if (!consumer)
{
this.error('Consumer not found');
break;
}
try
{
const dump = await consumer.dump();
this.log(`consumer.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`consumer.dump() failed: ${error}`);
}
break;
}
case 'ddp':
case 'dumpDataProducer':
{
const id = params[0] || Array.from(dataProducers.keys()).pop();
const dataProducer = dataProducers.get(id);
if (!dataProducer)
{
this.error('DataProducer not found');
break;
}
try
{
const dump = await dataProducer.dump();
this.log(`dataProducer.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`dataProducer.dump() failed: ${error}`);
}
break;
}
case 'ddc':
case 'dumpDataConsumer':
{
const id = params[0] || Array.from(dataConsumers.keys()).pop();
const dataConsumer = dataConsumers.get(id);
if (!dataConsumer)
{
this.error('DataConsumer not found');
break;
}
try
{
const dump = await dataConsumer.dump();
this.log(`dataConsumer.dump():\n${JSON.stringify(dump, null, ' ')}`);
}
catch (error)
{
this.error(`dataConsumer.dump() failed: ${error}`);
}
break;
}
case 'st':
case 'statsTransport':
{
const id = params[0] || Array.from(transports.keys()).pop();
const transport = transports.get(id);
if (!transport)
{
this.error('Transport not found');
break;
}
try
{
const stats = await transport.getStats();
this.log(`transport.getStats():\n${JSON.stringify(stats, null, ' ')}`);
}
catch (error)
{
this.error(`transport.getStats() failed: ${error}`);
}
break;
}
case 'sp':
case 'statsProducer':
{
const id = params[0] || Array.from(producers.keys()).pop();
const producer = producers.get(id);
if (!producer)
{
this.error('Producer not found');
break;
}
try
{
const stats = await producer.getStats();
this.log(`producer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
}
catch (error)
{
this.error(`producer.getStats() failed: ${error}`);
}
break;
}
case 'sc':
case 'statsConsumer':
{
const id = params[0] || Array.from(consumers.keys()).pop();
const consumer = consumers.get(id);
if (!consumer)
{
this.error('Consumer not found');
break;
}
try
{
const stats = await consumer.getStats();
this.log(`consumer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
}
catch (error)
{
this.error(`consumer.getStats() failed: ${error}`);
}
break;
}
case 'sdp':
case 'statsDataProducer':
{
const id = params[0] || Array.from(dataProducers.keys()).pop();
const dataProducer = dataProducers.get(id);
if (!dataProducer)
{
this.error('DataProducer not found');
break;
}
try
{
const stats = await dataProducer.getStats();
this.log(`dataProducer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
}
catch (error)
{
this.error(`dataProducer.getStats() failed: ${error}`);
}
break;
}
case 'sdc':
case 'statsDataConsumer':
{
const id = params[0] || Array.from(dataConsumers.keys()).pop();
const dataConsumer = dataConsumers.get(id);
if (!dataConsumer)
{
this.error('DataConsumer not found');
break;
}
try
{
const stats = await dataConsumer.getStats();
this.log(`dataConsumer.getStats():\n${JSON.stringify(stats, null, ' ')}`);
}
catch (error)
{
this.error(`dataConsumer.getStats() failed: ${error}`);
}
break;
}
case 't':
case 'terminal':
{
this._isTerminalOpen = true;
cmd.close();
this.openTerminal();
return;
}
default:
{
this.error(`unknown command '${command}'`);
this.log('press \'h\' or \'help\' to get the list of available commands');
}
}
readStdin();
});
};
readStdin();
}
openTerminal()
{
this.log('\n[opening Node REPL Terminal...]');
this.log('here you have access to workers, routers, transports, producers, consumers, dataProducers and dataConsumers ES6 maps');
const terminal = repl.start(
{
input : this._socket,
output : this._socket,
terminal : true,
prompt : 'terminal> ',
useColors : true,
useGlobal : true,
ignoreUndefined : false
});
this._isTerminalOpen = true;
terminal.on('exit', () =>
{
this.log('\n[exiting Node REPL Terminal...]');
this._isTerminalOpen = false;
this.openCommandConsole();
});
}
log(msg)
{
try
{
this._socket.write(`${colors.green(msg)}\n`);
}
catch (error)
{}
}
error(msg)
{
try
{
this._socket.write(`${colors.red.bold('ERROR: ')}${colors.red(msg)}\n`);
}
catch (error)
{}
}
}
function runMediasoupObserver()
{
mediasoup.observer.on('newworker', (worker) =>
{
// Store the latest worker in a global variable.
global.worker = worker;
workers.set(worker.pid, worker);
worker.observer.on('close', () => workers.delete(worker.pid));
worker.observer.on('newrouter', (router) =>
{
// Store the latest router in a global variable.
global.router = router;
routers.set(router.id, router);
router.observer.on('close', () => routers.delete(router.id));
router.observer.on('newtransport', (transport) =>
{
// Store the latest transport in a global variable.
global.transport = transport;
transports.set(transport.id, transport);
transport.observer.on('close', () => transports.delete(transport.id));
transport.observer.on('newproducer', (producer) =>
{
// Store the latest producer in a global variable.
global.producer = producer;
producers.set(producer.id, producer);
producer.observer.on('close', () => producers.delete(producer.id));
});
transport.observer.on('newconsumer', (consumer) =>
{
// Store the latest consumer in a global variable.
global.consumer = consumer;
consumers.set(consumer.id, consumer);
consumer.observer.on('close', () => consumers.delete(consumer.id));
});
transport.observer.on('newdataproducer', (dataProducer) =>
{
// Store the latest dataProducer in a global variable.
global.dataProducer = dataProducer;
dataProducers.set(dataProducer.id, dataProducer);
dataProducer.observer.on('close', () => dataProducers.delete(dataProducer.id));
});
transport.observer.on('newdataconsumer', (dataConsumer) =>
{
// Store the latest dataConsumer in a global variable.
global.dataConsumer = dataConsumer;
dataConsumers.set(dataConsumer.id, dataConsumer);
dataConsumer.observer.on('close', () => dataConsumers.delete(dataConsumer.id));
});
});
});
});
}
module.exports = async function(rooms, peers)
{
try
{
// Run the mediasoup observer API.
runMediasoupObserver();
// Make maps global so they can be used during the REPL terminal.
global.rooms = rooms;
global.peers = peers;
global.workers = workers;
global.routers = routers;
global.transports = transports;
global.producers = producers;
global.consumers = consumers;
global.dataProducers = dataProducers;
global.dataConsumers = dataConsumers;
const server = net.createServer((socket) =>
{
const interactive = new Interactive(socket);
interactive.openCommandConsole();
});
await new Promise((resolve) =>
{
try { fs.unlinkSync(SOCKET_PATH); }
catch (error) {}
server.listen(SOCKET_PATH, resolve);
});
}
catch (error)
{}
};

View File

@ -6,6 +6,10 @@
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
"license": "MIT",
"main": "lib/index.js",
"scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js",
"connect": "node connect.js"
},
"dependencies": {
"awaitqueue": "^1.0.0",
"base-64": "^0.1.0",
@ -19,9 +23,12 @@
"express-session": "^1.17.0",
"express-socket.io-session": "^1.3.5",
"helmet": "^3.21.2",
"mediasoup": "^3.0.12",
"ims-lti": "^3.0.2",
"mediasoup": "^3.5.5",
"openid-client": "^3.7.3",
"passport": "^0.4.0",
"passport-lti": "0.0.7",
"pidusage": "^2.0.17",
"redis": "^2.8.0",
"socket.io": "^2.3.0",
"spdy": "^4.0.1"

View File

@ -17,18 +17,22 @@ const Room = require('./lib/Room');
const Peer = require('./lib/Peer');
const base64 = require('base-64');
const helmet = require('helmet');
const {
loginHelper,
logoutHelper
} = require('./httpHelper');
// auth
const passport = require('passport');
const LTIStrategy = require('passport-lti');
const imsLti = require('ims-lti');
const redis = require('redis');
const client = redis.createClient();
const redisClient = redis.createClient(config.redisOptions);
const { Issuer, Strategy } = require('openid-client');
const expressSession = require('express-session');
const RedisStore = require('connect-redis')(expressSession);
const sharedSession = require('express-socket.io-session');
const interactiveServer = require('./lib/interactiveServer');
/* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG);
@ -87,7 +91,7 @@ const session = expressSession({
name : config.cookieName,
resave : true,
saveUninitialized : true,
store : new RedisStore({ client }),
store : new RedisStore({ client: redisClient }),
cookie : {
secure : true,
httpOnly : true,
@ -107,54 +111,34 @@ passport.deserializeUser((user, done) =>
done(null, user);
});
let httpsServer;
let mainListener;
let io;
let oidcClient;
let oidcStrategy;
const auth = config.auth;
async function run()
{
if (
typeof(auth) !== 'undefined' &&
typeof(auth.issuerURL) !== 'undefined' &&
typeof(auth.clientOptions) !== 'undefined'
)
// Open the interactive server.
await interactiveServer(rooms, peers);
if (typeof(config.auth) === 'undefined')
{
Issuer.discover(auth.issuerURL).then(async (oidcIssuer) =>
{
// Setup authentication
await setupAuth(oidcIssuer);
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
})
.catch((err) =>
{
logger.error(err);
});
logger.warn('Auth is not configured properly!');
}
else
{
logger.error('Auth is not configure properly!');
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
await setupAuth();
}
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
// Log rooms status every 30 seconds.
setInterval(() =>
{
@ -174,16 +158,67 @@ async function run()
}, 10000);
}
async function setupAuth(oidcIssuer)
function setupLTI(ltiConfig)
{
oidcClient = new oidcIssuer.Client(auth.clientOptions);
// Add redis nonce store
ltiConfig.nonceStore = new imsLti.Stores.RedisStore(ltiConfig.consumerKey, redisClient);
ltiConfig.passReqToCallback= true;
const ltiStrategy = new LTIStrategy(
ltiConfig,
function(req, lti, done)
{
// LTI launch parameters
if (lti)
{
const user = {};
if (lti.user_id && lti.custom_room)
{
user.id = lti.user_id;
user._lti = lti;
}
if (lti.custom_room)
{
user.room = lti.custom_room;
}
else
{
user.room = '';
}
if (lti.lis_person_name_full)
{
user.displayName=lti.lis_person_name_full;
}
// Perform local authentication if necessary
return done(null, user);
}
else
{
return done('LTI error');
}
}
);
passport.use('lti', ltiStrategy);
}
function setupOIDC(oidcIssuer)
{
oidcClient = new oidcIssuer.Client(config.auth.oidc.clientOptions);
// ... any authorization request parameters go here
// client_id defaults to client.client_id
// redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid'
const params = auth.clientOptions;
const params = config.auth.oidc.clientOptions;
// optional, defaults to false, when true req is passed as a first
// argument to verify fn
@ -235,16 +270,19 @@ async function setupAuth(oidcIssuer)
if (userinfo.given_name != null)
{
user.name={};
user.name.givenName = userinfo.given_name;
}
if (userinfo.family_name != null)
{
if (user.name == null) user.name={};
user.name.familyName = userinfo.family_name;
}
if (userinfo.middle_name != null)
{
if (user.name == null) user.name={};
user.name.middleName = userinfo.middle_name;
}
@ -253,6 +291,30 @@ async function setupAuth(oidcIssuer)
);
passport.use('oidc', oidcStrategy);
}
async function setupAuth()
{
// LTI
if (
typeof(config.auth.lti) !== 'undefined' &&
typeof(config.auth.lti.consumerKey) !== 'undefined' &&
typeof(config.auth.lti.consumerSecret) !== 'undefined'
) setupLTI(config.auth.lti);
// OIDC
if (
typeof(config.auth.oidc) !== 'undefined' &&
typeof(config.auth.oidc.issuerURL) !== 'undefined' &&
typeof(config.auth.oidc.clientOptions) !== 'undefined'
)
{
const oidcIssuer = await Issuer.discover(config.auth.oidc.issuerURL);
// Setup authentication
setupOIDC(oidcIssuer);
}
app.use(passport.initialize());
app.use(passport.session());
@ -267,6 +329,15 @@ async function setupAuth(oidcIssuer)
})(req, res, next);
});
// lti launch
app.post('/auth/lti',
passport.authenticate('lti', { failureRedirect: '/' }),
function(req, res)
{
res.redirect(`/${req.user.room}`);
}
);
// logout
app.get('/auth/logout', (req, res) =>
{
@ -318,19 +389,31 @@ async function runHttpsServer()
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge'));
app.all('*', (req, res, next) =>
app.all('*', async (req, res, next) =>
{
if (req.secure)
{
return next();
const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`);
if (
req.isAuthenticated &&
req.user &&
req.user.displayName &&
!ltiURL.searchParams.get('displayName') &&
!isPathAlreadyTaken(req.url)
)
{
ltiURL.searchParams.append('displayName', req.user.displayName);
res.redirect(ltiURL);
}
else
return next();
}
else
res.redirect(`https://${req.hostname}${req.url}`);
res.redirect(`https://${req.hostname}${req.url}`);
});
app.get('/', (req, res) =>
{
res.sendFile(`${__dirname}/public/chooseRoom.html`);
});
// Serve all files in the public folder as static files.
@ -338,19 +421,45 @@ async function runHttpsServer()
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
httpsServer = spdy.createServer(tls, app);
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
if (config.httpOnly === true)
{
logger.info('Server running on port: ', config.listeningPort);
// http
mainListener = http.createServer(app);
}
else
{
// https
mainListener = spdy.createServer(tls, app);
// http
const redirectListener = http.createServer(app);
redirectListener.listen(config.listeningRedirectPort);
}
// https or http
mainListener.listen(config.listeningPort);
}
function isPathAlreadyTaken(url)
{
const alreadyTakenPath =
[
'/config/',
'/static/',
'/images/',
'/sounds/',
'/favicon.',
'/auth/'
];
alreadyTakenPath.forEach((path) =>
{
if (url.toString().startsWith(path))
return true;
});
const httpServer = http.createServer(app);
httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () =>
{
logger.info('Server redirecting port: ', config.listeningRedirectPort);
});
return false;
}
/**
@ -358,7 +467,7 @@ async function runHttpsServer()
*/
async function runWebSocketServer()
{
io = require('socket.io')(httpsServer);
io = require('socket.io')(mainListener);
io.use(
sharedSession(session, {
@ -466,6 +575,7 @@ async function getOrCreateRoom({ roomId })
room = await Room.create({ mediasoupWorker, roomId });
rooms.set(roomId, room);
room.on('close', () => rooms.delete(roomId));
}