merge from develop

auto_join_3.3
Stefan Otto 2020-05-06 02:33:37 +02:00
parent da6c9d3ecf
commit aca3499afb
99 changed files with 6709 additions and 2038 deletions

View File

@ -1,20 +1,32 @@
# Changelog # Changelog
## 3.2.1
* Fix: permananent top bar by default
* Fix: `httpOnly` mode https redirect
* Add some extra checks for video stream and track
* Add Italian translation
* Add Czech translation
* Add new server option `trustProxy` for load balancing http only use case
* Add HAproxy load balance example
* Add LTI LMS integration documentation
* Fix spacing of leave button
* Fix for sharing same file multiple times
## 3.2 ## 3.2
* Add munin plugin * Add munin plugin
* Add muted=true search param to disble audio by deffault * Add `muted=true` search param to disable audio by default
* Modify webtorrent tracker * Modify webtorrent tracker
* Add key shortcut `space` for audio mute * Add key shortcut `space` for audio mute
* Add key shortcut `v` for video mute * Add key shortcut `v` for video mute
* Add user configurable LastN * Add user configurable LastN
* Add option to sticky top bar (sticky by default) * Add option to permananent top bar (permanent by default)
* update mediasoup server * Update mediasoup server
* Add simulcast options to app config (disabled by default) * Add `simulcast` options to app config (disabled by default)
* Add stats option to get counts of rooms and peers * Add `stats` option to get counts of rooms and peers
* Add httpOnly option for loadbalancer backend setups * Add `httpOnly` option for loadbalancer backend setups
* LTI integration for LMS systems like moodle * LTI integration for LMS systems like moodle
* Add muted=false search parameter
* Add translations (12+1 languages) * Add translations (12+1 languages)
* Add support IPv6 * Add support IPv6
* Many other fixes and refactorings * Many other fixes and refactorings
@ -33,10 +45,10 @@
* Updated to mediasoup v3 * Updated to mediasoup v3
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client) * Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
- OpenID Connect discovery * OpenID Connect discovery
- Auth code flow * Auth code flow
* Add spdy http2 support. * Add spdy http2 support.
- Notice it does not supports node 11.x * Notice it does not supports node 11.x
* Updated to Material UI v4 * Updated to Material UI v4
## 2.0 ## 2.0

1
CONTRIBUTING.md 100644
View File

@ -0,0 +1 @@
Source code contributions should pass static code analysis as performed by `npm run lint` in `server` and `app` respectively.

101
HAproxy.md 100644
View File

@ -0,0 +1,101 @@
# Howto deploy a (room based) load balanced cluster
This example will show how to setup an HA proxy to provide load balancing between several
multiparty-meeting servers.
## IP and DNS
In this basic example we use the following names and ips:
### Backend
* `mm1.example.com` <=> `192.0.2.1`
* `mm2.example.com` <=> `192.0.2.2`
* `mm3.example.com` <=> `192.0.2.3`
### Redis
* `redis.example.com` <=> `192.0.2.4`
### Load balancer HAproxy
* `meet.example.com` <=> `192.0.2.5`
## Deploy multiple multiparty-meeting servers
This is most easily done using Ansible (see below), but can be done
in any way you choose (manual, Docker, Ansible).
Read more here: [mm-ansible](https://github.com/misi/mm-ansible)
[![asciicast](https://asciinema.org/a/311365.svg)](https://asciinema.org/a/311365)
## Setup Redis for central HTTP session store
### Use one Redis for all multiparty-meeting servers
* Deploy a Redis cluster for all instances.
* We will use in our actual example `192.0.2.4` as redis HA cluster ip. It is out of scope howto deploy it.
OR
* For testing you can use Redis from one the multiparty-meeting servers. e.g. If you plan only for testing on your first multiparty-meeting server.
* Configure Redis `redis.conf` to not only bind to your loopback but also to your global ip address too:
``` plaintext
bind 192.0.2.1
```
This example sets this to `192.0.2.1`, change this according to your local installation.
* Change your firewall config to allow incoming Redis. Example (depends on the type of firewall):
``` plaintext
chain INPUT {
policy DROP;
saddr mm2.example.com proto tcp dport 6379 ACCEPT;
saddr mm3.example.com proto tcp dport 6379 ACCEPT;
}
```
* **Set a password, or if you don't (like in this basic example) take care to set strict firewall rules**
## Configure multiparty-meeting servers
### Server config
mm/configs/server/config.js
``` js
redisOptions : { host: '192.0.2.4'},
listeningPort: 80,
httpOnly: true,
trustProxy : ['192.0.2.5'],
```
## Deploy HA proxy
* Configure certificate / letsencrypt for `meet.example.com`
* In this example we put a complete chain and private key in /root/certificate.pem.
* Install and setup haproxy
`apt install haproxy`
* Add to /etc/haproxy/haproxy.cfg config
``` plaintext
backend multipartymeeting
balance url_param roomId
hash-type consistent
server mm1 192.0.2.1:80 check maxconn 20 verify none
server mm2 192.0.2.2:80 check maxconn 20 verify none
server mm3 192.0.2.3:80 check maxconn 20 verify none
frontend meet.example.com
bind 192.0.2.5:80
bind 192.0.2.5:443 ssl crt /root/certificate.pem
http-request redirect scheme https unless { ssl_fc }
reqadd X-Forwarded-Proto:\ https
default_backend multipartymeeting
```

21
LICENSE.md 100644
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 GÉANT Association
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

61
LTI/LTI.md 100644
View File

@ -0,0 +1,61 @@
# Learning Tools Interoperability (LTI)
## LTI
Read more about IMS Global defined interface for tools like our VideoConference system integration with Learning Management Systems(LMS) (e.g. moodle).
See: [IMS Global Learning Tool Interoperability](https://www.imsglobal.org/activity/learning-tools-interoperability)
We implemented LTI interface version 1.0/1.1
### Server config auth section LTI settings
Set in server configuration a random key and secret
``` json
auth :
{
lti :
{
consumerKey : 'key',
consumerSecret : 'secret'
},
}
```
### Configure your LMS system with secret and key settings above
#### Auth tool URL
Set tool URL to your server with path /auth/lti
``` url
https://mm.example.com/auth/lti
```
#### In moodle find external tool plugin setting and external tool action
See: [moodle external tool settings](https://docs.moodle.org/38/en/External_tool_settings)
#### Add and activity
![Add external tool](lti1.png)
#### Setup Activity
##### Activity setup basic form
Open fully the settings **Click on show more!!**
![Add external tool config](lti2.png)
##### Empty full form
![Opened external tool config](lti3.png)
##### Filled out form
![Filled out external tool config](lti4.png)
## moodle plugin
Alternatively you can use multipartymeeting moodle plugin:
[https://github.com/misi/moodle-mod_multipartymeeting](https://github.com/misi/moodle-mod_multipartymeeting)

BIN
LTI/lti1.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
LTI/lti2.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
LTI/lti3.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
LTI/lti4.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@ -21,10 +21,9 @@ If you want the automatic approach, you can find a docker image [here](https://h
If you want the ansible approach, you can find ansible role [here](https://github.com/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) [![asciicast](https://asciinema.org/a/311365.svg)](https://asciinema.org/a/311365)
## Manual installation ## Manual installation
* Prerequisites: * Prerequisites:
Currently multiparty-meeting will only run on nodejs v10.* Currently multiparty-meeting will only run on nodejs v13.x
To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall). To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall).
```bash ```bash
@ -77,7 +76,7 @@ $ npm install
$ cd server $ cd server
$ npm start $ npm start
``` ```
* 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). * 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 further down this doc).
* Test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname` * Test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
## Deploy it in a server ## Deploy it in a server
@ -103,12 +102,24 @@ $ systemctl enable multiparty-meeting
## Ports and firewall ## Ports and firewall
* 3443/tcp (default https webserver and signaling - adjustable in `server/config.js`) * 3443/tcp (default https webserver and signaling - adjustable in `server/config.js`)
* 4443/tcp (default `npm start` port for developing with live browser reload, not needed in production enviroments - adjustable in app/package.json) * 4443/tcp (default `npm start` port for developing with live browser reload, not needed in production environments - adjustable in app/package.json)
* 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) * 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`)
## Load balanced installation
To deploy this as a load balanced cluster, have a look at [HAproxy](HAproxy.md).
## Learning management integration
To integrate with an LMS (e.g. Moodle), have a look at [LTI](LTI/LTI.md).
## TURN configuration ## TURN configuration
* You need an addtional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! Add your server and credentials to `app/config.js` * You need an additional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! Add your server and credentials to `app/config.js`
## Community-driven support
* Open mailing list: community@lists.edumeet.org
* Subscribe: lists.edumeet.org/sympa/subscribe/community/
* Open archive: lists.edumeet.org/sympa/arc/community/
## Authors ## Authors
@ -123,7 +134,7 @@ This started as a fork of the [work](https://github.com/versatica/mediasoup-demo
## License ## License
MIT MIT License (see `LICENSE.md`)
Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project. Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project.

View File

@ -1,6 +1,6 @@
{ {
"name": "multiparty-meeting", "name": "multiparty-meeting",
"version": "3.1.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting service", "description": "multiparty meeting service",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
@ -12,14 +12,15 @@
"@material-ui/icons": "^4.5.1", "@material-ui/icons": "^4.5.1",
"bowser": "^2.7.0", "bowser": "^2.7.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"create-torrent": "^4.4.1",
"dompurify": "^2.0.7", "dompurify": "^2.0.7",
"domready": "^1.0.8", "domready": "^1.0.8",
"end-of-stream": "1.4.0", "end-of-stream": "1.4.1",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"hark": "^1.2.3", "hark": "^1.2.3",
"is-electron": "^2.2.0", "is-electron": "^2.2.0",
"marked": "^0.8.0", "marked": "^0.8.0",
"mediasoup-client": "^3.5.4", "mediasoup-client": "^3.6.4",
"notistack": "^0.9.5", "notistack": "^0.9.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"random-string": "^0.2.0", "random-string": "^0.2.0",
@ -29,7 +30,8 @@
"react-intl": "^3.4.0", "react-intl": "^3.4.0",
"react-redux": "^7.1.1", "react-redux": "^7.1.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "^3.3.0", "react-scripts": "3.4.1",
"react-wakelock-react16": "0.0.7",
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
@ -38,7 +40,7 @@
"riek": "^1.1.0", "riek": "^1.1.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"source-map-explorer": "^2.1.0", "source-map-explorer": "^2.1.0",
"webtorrent": "^0.107.16" "webtorrent": "^0.107.17"
}, },
"scripts": { "scripts": {
"analyze": "source-map-explorer build/static/js/*", "analyze": "source-map-explorer build/static/js/*",
@ -58,11 +60,8 @@
], ],
"devDependencies": { "devDependencies": {
"electron": "^7.1.1", "electron": "^7.1.1",
"eslint": "^6.5.1", "eslint-plugin-react": "^7.19.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-react": "^7.16.0",
"foreman": "^3.0.1", "foreman": "^3.0.1",
"jest": "^24.9.0",
"redux-mock-store": "^1.5.3" "redux-mock-store": "^1.5.3"
} }
} }

View File

@ -1,9 +1,9 @@
// eslint-disable-next-line // eslint-disable-next-line
var config = var config =
{ {
loginEnabled : false, loginEnabled : false,
developmentPort : 3443, developmentPort : 3443,
productionPort : 443, productionPort : 443,
/** /**
* If defaultResolution is set, it will override user settings when joining: * If defaultResolution is set, it will override user settings when joining:
@ -25,29 +25,45 @@ var config =
{ scaleResolutionDownBy: 2 }, { scaleResolutionDownBy: 2 },
{ scaleResolutionDownBy: 1 } { scaleResolutionDownBy: 1 }
], ],
/**
* White listing browsers that support audio output device selection.
* It is not yet fully implemented in Firefox.
* See: https://bugzilla.mozilla.org/show_bug.cgi?id=1498512
*/
audioOutputSupportedBrowsers :
[
'chrome',
'opera'
],
// Socket.io request timeout // Socket.io request timeout
requestTimeout : 10000, requestTimeout : 10000,
transportOptions : transportOptions :
{ {
tcp : true tcp : true
}, },
lastN : 4,
mobileLastN : 1,
defaultAudio : defaultAudio :
{ {
sampleRate : 48000, sampleRate : 48000,
channelCount : 1, channelCount : 1,
volume : 1.0, volume : 1.0,
autoGainControl : true, autoGainControl : false,
echoCancellation : true, echoCancellation : true,
noiseSuppression : true, noiseSuppression : true,
sampleSize : 16 sampleSize : 16
}, },
background : 'images/background.jpg', background : 'images/background.jpg',
defaultLayout : 'democratic', // democratic, filmstrip
lastN : 4,
mobileLastN : 1,
// Highest number of speakers user can select
maxLastN : 5,
// If truthy, users can NOT change number of speakers visible
lockLastN : false,
// Add file and uncomment for adding logo to appbar // Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg', // logo : 'images/logo.svg',
title : 'Multiparty meeting', title : 'Multiparty meeting',
theme : theme :
{ {
palette : palette :
{ {

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pleaceholder for Privacy Statetment/Policy, AUP</title>
</head>
<body>
<h1>Privacy Statement</h1>
<h1>Privacy Policy</h1>
<h1>Acceptable use policy (AUP)</h1>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -225,10 +225,7 @@ export default class ScreenShare
return new DisplayMediaScreenShare(); return new DisplayMediaScreenShare();
} }
case 'chrome': case 'chrome':
{ case 'edge':
return new DisplayMediaScreenShare();
}
case 'msedge':
{ {
return new DisplayMediaScreenShare(); return new DisplayMediaScreenShare();
} }

View File

@ -31,6 +31,8 @@ beforeEach(() =>
me : { me : {
audioDevices : null, audioDevices : null,
audioInProgress : false, audioInProgress : false,
audioOutputDevices : null,
audioOutputInProgress : false,
canSendMic : false, canSendMic : false,
canSendWebcam : false, canSendWebcam : false,
canShareFiles : false, canShareFiles : false,
@ -40,8 +42,8 @@ beforeEach(() =>
loggedIn : false, loggedIn : false,
loginEnabled : true, loginEnabled : true,
picture : null, picture : null,
raiseHand : false, raisedHand : false,
raiseHandInProgress : false, raisedHandInProgress : false,
screenShareInProgress : false, screenShareInProgress : false,
webcamDevices : null, webcamDevices : null,
webcamInProgress : false webcamInProgress : false
@ -72,11 +74,12 @@ beforeEach(() =>
windowConsumer : null windowConsumer : null
}, },
settings : { settings : {
advancedMode : true, advancedMode : true,
displayName : 'Jest Tester', displayName : 'Jest Tester',
resolution : 'ultra', resolution : 'ultra',
selectedAudioDevice : 'default', selectedAudioDevice : 'default',
selectedWebcam : 'soifjsiajosjfoi' selectedAudioOutputDevice : 'default',
selectedWebcam : 'soifjsiajosjfoi'
}, },
toolarea : { toolarea : {
currentToolTab : 'chat', currentToolTab : 'chat',

View File

@ -1,6 +1,6 @@
import RoomClient from '../RoomClient'; import RoomClient from '../RoomClient';
describe('new RoomClient() without paramaters throws Error', () => describe('new RoomClient() without parameters throws Error', () =>
{ {
test('Matches the snapshot', () => test('Matches the snapshot', () =>
{ {

View File

@ -15,3 +15,8 @@ export const addChatHistory = (chatHistory) =>
type : 'ADD_CHAT_HISTORY', type : 'ADD_CHAT_HISTORY',
payload : { chatHistory } payload : { chatHistory }
}); });
export const clearChat = () =>
({
type : 'CLEAR_CHAT'
});

View File

@ -33,3 +33,8 @@ export const setFileDone = (magnetUri, sharedFiles) =>
type : 'SET_FILE_DONE', type : 'SET_FILE_DONE',
payload : { magnetUri, sharedFiles } payload : { magnetUri, sharedFiles }
}); });
export const clearFiles = () =>
({
type : 'CLEAR_FILES'
});

View File

@ -4,9 +4,10 @@ export const setMe = ({ peerId, loginEnabled }) =>
payload : { peerId, loginEnabled } payload : { peerId, loginEnabled }
}); });
export const setIsMobile = () => export const setBrowser = (browser) =>
({ ({
type : 'SET_IS_MOBILE' type : 'SET_BROWSER',
payload : { browser }
}); });
export const loggedIn = (flag) => export const loggedIn = (flag) =>
@ -50,15 +51,21 @@ export const setAudioDevices = (devices) =>
payload : { devices } payload : { devices }
}); });
export const setAudioOutputDevices = (devices) =>
({
type : 'SET_AUDIO_OUTPUT_DEVICES',
payload : { devices }
});
export const setWebcamDevices = (devices) => export const setWebcamDevices = (devices) =>
({ ({
type : 'SET_WEBCAM_DEVICES', type : 'SET_WEBCAM_DEVICES',
payload : { devices } payload : { devices }
}); });
export const setMyRaiseHandState = (flag) => export const setRaisedHand = (flag) =>
({ ({
type : 'SET_MY_RAISE_HAND_STATE', type : 'SET_RAISED_HAND',
payload : { flag } payload : { flag }
}); });
@ -68,6 +75,12 @@ export const setAudioInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setAudioOutputInProgress = (flag) =>
({
type : 'SET_AUDIO_OUTPUT_IN_PROGRESS',
payload : { flag }
});
export const setWebcamInProgress = (flag) => export const setWebcamInProgress = (flag) =>
({ ({
type : 'SET_WEBCAM_IN_PROGRESS', type : 'SET_WEBCAM_IN_PROGRESS',
@ -80,9 +93,9 @@ export const setScreenShareInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setMyRaiseHandStateInProgress = (flag) => export const setRaisedHandInProgress = (flag) =>
({ ({
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS', type : 'SET_RAISED_HAND_IN_PROGRESS',
payload : { flag } payload : { flag }
}); });

View File

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

View File

@ -40,6 +40,12 @@ export const setSignInRequired = (signInRequired) =>
payload : { signInRequired } payload : { signInRequired }
}); });
export const setOverRoomLimit = (overRoomLimit) =>
({
type : 'SET_OVER_ROOM_LIMIT',
payload : { overRoomLimit }
});
export const setAccessCode = (accessCode) => export const setAccessCode = (accessCode) =>
({ ({
type : 'SET_ACCESS_CODE', type : 'SET_ACCESS_CODE',
@ -52,13 +58,25 @@ export const setJoinByAccessCode = (joinByAccessCode) =>
payload : { joinByAccessCode } payload : { joinByAccessCode }
}); });
export const setSettingsOpen = ({ settingsOpen }) => export const setSettingsOpen = (settingsOpen) =>
({ ({
type : 'SET_SETTINGS_OPEN', type : 'SET_SETTINGS_OPEN',
payload : { settingsOpen } payload : { settingsOpen }
}); });
export const setLockDialogOpen = ({ lockDialogOpen }) => export const setExtraVideoOpen = (extraVideoOpen) =>
({
type : 'SET_EXTRA_VIDEO_OPEN',
payload : { extraVideoOpen }
});
export const setSettingsTab = (tab) =>
({
type : 'SET_SETTINGS_TAB',
payload : { tab }
});
export const setLockDialogOpen = (lockDialogOpen) =>
({ ({
type : 'SET_LOCK_DIALOG_OPEN', type : 'SET_LOCK_DIALOG_OPEN',
payload : { lockDialogOpen } payload : { lockDialogOpen }
@ -111,6 +129,12 @@ export const toggleConsumerFullscreen = (consumerId) =>
payload : { consumerId } payload : { consumerId }
}); });
export const setLobbyPeersPromotionInProgress = (flag) =>
({
type : 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS',
payload : { flag }
});
export const setMuteAllInProgress = (flag) => export const setMuteAllInProgress = (flag) =>
({ ({
type : 'MUTE_ALL_IN_PROGRESS', type : 'MUTE_ALL_IN_PROGRESS',
@ -128,3 +152,27 @@ export const setCloseMeetingInProgress = (flag) =>
type : 'CLOSE_MEETING_IN_PROGRESS', type : 'CLOSE_MEETING_IN_PROGRESS',
payload : { flag } payload : { flag }
}); });
export const setClearChatInProgress = (flag) =>
({
type : 'CLEAR_CHAT_IN_PROGRESS',
payload : { flag }
});
export const setClearFileSharingInProgress = (flag) =>
({
type : 'CLEAR_FILE_SHARING_IN_PROGRESS',
payload : { flag }
});
export const setUserRoles = (userRoles) =>
({
type : 'SET_USER_ROLES',
payload : { userRoles }
});
export const setPermissionsFromRoles = (permissionsFromRoles) =>
({
type : 'SET_PERMISSIONS_FROM_ROLES',
payload : { permissionsFromRoles }
});

View File

@ -4,6 +4,12 @@ export const setSelectedAudioDevice = (deviceId) =>
payload : { deviceId } payload : { deviceId }
}); });
export const setSelectedAudioOutputDevice = (deviceId) =>
({
type : 'CHANGE_AUDIO_OUTPUT_DEVICE',
payload : { deviceId }
});
export const setSelectedWebcamDevice = (deviceId) => export const setSelectedWebcamDevice = (deviceId) =>
({ ({
type : 'CHANGE_WEBCAM', type : 'CHANGE_WEBCAM',
@ -71,6 +77,16 @@ export const toggleNoiseSuppression = () =>
type : 'TOGGLE_NOISE_SUPPRESSION' type : 'TOGGLE_NOISE_SUPPRESSION'
}); });
export const toggleHiddenControls = () =>
({
type : 'TOGGLE_HIDDEN_CONTROLS'
});
export const toggleNotificationSounds = () =>
({
type : 'TOGGLE_NOTIFICATION_SOUNDS'
});
export const setLastN = (lastN) => export const setLastN = (lastN) =>
({ ({
type : 'SET_LAST_N', type : 'SET_LAST_N',

View File

@ -7,72 +7,16 @@ import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import IconButton from '@material-ui/core/IconButton';
import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import PromoteIcon from '@material-ui/icons/OpenInBrowser'; import PromoteIcon from '@material-ui/icons/OpenInBrowser';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
const styles = (theme) => const styles = () =>
({ ({
root : root :
{
padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
avatar :
{
borderRadius : '50%',
height : '2rem'
},
peerInfo :
{
fontSize : '1rem',
border : 'none',
display : 'flex',
paddingLeft : theme.spacing(1),
flexGrow : 1,
alignItems : 'center'
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
},
button :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.5)',
cursor : 'pointer',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.disabled' :
{
pointerEvents : 'none',
backgroundColor : 'var(--media-control-botton-disabled)'
},
'&.promote' :
{
backgroundColor : 'var(--media-control-botton-on)'
}
},
ListItem :
{ {
alignItems : 'center' alignItems : 'center'
} }
@ -83,6 +27,8 @@ const ListLobbyPeer = (props) =>
const { const {
roomClient, roomClient,
peer, peer,
promotionInProgress,
canPromote,
classes classes
} = props; } = props;
@ -92,7 +38,7 @@ const ListLobbyPeer = (props) =>
return ( return (
<ListItem <ListItem
className={classnames(classes.ListItem)} className={classnames(classes.root)}
key={peer.peerId} key={peer.peerId}
button button
alignItems='flex-start' alignItems='flex-start'
@ -109,10 +55,13 @@ const ListLobbyPeer = (props) =>
defaultMessage : 'Click to let them in' defaultMessage : 'Click to let them in'
})} })}
> >
<ListItemIcon <IconButton
className={classnames(classes.button, 'promote', { disabled={
disabled : peer.promotionInProgress !canPromote ||
})} peer.promotionInProgress ||
promotionInProgress
}
color='primary'
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -120,7 +69,7 @@ const ListLobbyPeer = (props) =>
}} }}
> >
<PromoteIcon /> <PromoteIcon />
</ListItemIcon> </IconButton>
</Tooltip> </Tooltip>
</ListItem> </ListItem>
); );
@ -128,16 +77,22 @@ const ListLobbyPeer = (props) =>
ListLobbyPeer.propTypes = ListLobbyPeer.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peer : PropTypes.object.isRequired, peer : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired promotionInProgress : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state, { id }) => const mapStateToProps = (state, { id }) =>
{ {
return { return {
peer : state.lobbyPeers[id] peer : state.lobbyPeers[id],
promotionInProgress : state.room.lobbyPeersPromotionInProgress,
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}; };
}; };
@ -149,6 +104,10 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.room.lobbyPeersPromotionInProgress ===
next.room.lobbyPeersPromotionInProgress &&
prev.me.roles === next.me.roles &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );
} }

View File

@ -15,14 +15,6 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
// import FormLabel from '@material-ui/core/FormLabel';
// import FormControl from '@material-ui/core/FormControl';
// import FormGroup from '@material-ui/core/FormGroup';
// import FormControlLabel from '@material-ui/core/FormControlLabel';
// import Checkbox from '@material-ui/core/Checkbox';
// import InputLabel from '@material-ui/core/InputLabel';
// import OutlinedInput from '@material-ui/core/OutlinedInput';
// import Switch from '@material-ui/core/Switch';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader'; import ListSubheader from '@material-ui/core/ListSubheader';
import ListLobbyPeer from './ListLobbyPeer'; import ListLobbyPeer from './ListLobbyPeer';
@ -59,11 +51,11 @@ const styles = (theme) =>
}); });
const LockDialog = ({ const LockDialog = ({
// roomClient, roomClient,
room, room,
handleCloseLockDialog, handleCloseLockDialog,
// handleAccessCode,
lobbyPeers, lobbyPeers,
canPromote,
classes classes
}) => }) =>
{ {
@ -71,7 +63,7 @@ const LockDialog = ({
<Dialog <Dialog
className={classes.root} className={classes.root}
open={room.lockDialogOpen} open={room.lockDialogOpen}
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })} onClose={() => handleCloseLockDialog(false)}
classes={{ classes={{
paper : classes.dialogPaper paper : classes.dialogPaper
}} }}
@ -82,54 +74,6 @@ const LockDialog = ({
defaultMessage='Lobby administration' defaultMessage='Lobby administration'
/> />
</DialogTitle> </DialogTitle>
{/*
<FormControl component='fieldset' className={classes.formControl}>
<FormLabel component='legend'>Room lock</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Switch checked={room.locked} onChange={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
/>}
label='Lock'
/>
TODO: access code
<FormControlLabel disabled={ room.locked ? false : true }
control={
<Checkbox checked={room.joinByAccessCode}
onChange={
(event) => roomClient.setJoinByAccessCode(event.target.checked)
}
/>}
label='Join by Access code'
/>
<InputLabel htmlFor='access-code-input' />
<OutlinedInput
disabled={ room.locked ? false : true }
id='acces-code-input'
label='Access code'
labelWidth={0}
variant='outlined'
value={room.accessCode}
onChange={(event) => handleAccessCode(event.target.value)}
>
</OutlinedInput>
<Button onClick={() => roomClient.setAccessCode(room.accessCode)} color='primary'>
save
</Button>
</FormGroup>
</FormControl>
*/}
{ lobbyPeers.length > 0 ? { lobbyPeers.length > 0 ?
<List <List
dense dense
@ -160,7 +104,21 @@ const LockDialog = ({
</DialogContent> </DialogContent>
} }
<DialogActions> <DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'> <Button
disabled={
lobbyPeers.length === 0 ||
!canPromote ||
room.lobbyPeersPromotionInProgress
}
onClick={() => roomClient.promoteAllLobbyPeers()}
color='primary'
>
<FormattedMessage
id='label.promoteAllPeers'
defaultMessage='Promote all'
/>
</Button>
<Button onClick={() => handleCloseLockDialog(false)} color='primary'>
<FormattedMessage <FormattedMessage
id='label.close' id='label.close'
defaultMessage='Close' defaultMessage='Close'
@ -173,11 +131,12 @@ const LockDialog = ({
LockDialog.propTypes = LockDialog.propTypes =
{ {
// roomClient : PropTypes.any.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
handleCloseLockDialog : PropTypes.func.isRequired, handleCloseLockDialog : PropTypes.func.isRequired,
handleAccessCode : PropTypes.func.isRequired, handleAccessCode : PropTypes.func.isRequired,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
canPromote : PropTypes.bool,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
@ -185,7 +144,10 @@ const mapStateToProps = (state) =>
{ {
return { return {
room : state.room, room : state.room,
lobbyPeers : lobbyPeersKeySelector(state) lobbyPeers : lobbyPeersKeySelector(state),
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}; };
}; };
@ -202,12 +164,8 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.locked === next.room.locked && prev.room === next.room &&
prev.room.joinByAccessCode === next.room.joinByAccessCode && prev.me.roles === next.me.roles &&
prev.room.accessCode === next.room.accessCode &&
prev.room.code === next.room.code &&
prev.room.lockDialogOpen === next.room.lockDialogOpen &&
prev.room.codeHidden === next.room.codeHidden &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );
} }

View File

@ -14,7 +14,7 @@ const App = (props) =>
room room
} = props; } = props;
const { id } = useParams(); const id = useParams().id.toLowerCase();
useEffect(() => useEffect(() =>
{ {

View File

@ -86,7 +86,7 @@ const DialogTitle = withStyles(styles)((props) =>
return ( return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}> <MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography> <Typography variant='h5'>{children}</Typography>
</MuiDialogTitle> </MuiDialogTitle>
); );
@ -125,7 +125,7 @@ const ChooseRoom = ({
}} }}
> >
<DialogTitle> <DialogTitle>
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config.title ? window.config.title : 'Multiparty meeting' }
<hr /> <hr />
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>

View File

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors'; import { meProducersSelector } from '../Selectors';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
@ -11,6 +10,7 @@ import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume'; import Volume from './Volume';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic'; import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff'; import MicOffIcon from '@material-ui/icons/MicOff';
@ -60,6 +60,19 @@ const styles = (theme) =>
margin : theme.spacing(1), margin : theme.spacing(1),
pointerEvents : 'auto' pointerEvents : 'auto'
}, },
smallContainer :
{
backgroundColor : 'rgba(255, 255, 255, 0.9)',
margin : '0.5vmin',
padding : '0.5vmin',
boxShadow : '0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)',
pointerEvents : 'auto',
transition : 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
'&:hover' :
{
backgroundColor : 'rgba(213, 213, 213, 1)'
}
},
viewContainer : viewContainer :
{ {
position : 'relative', position : 'relative',
@ -68,21 +81,27 @@ const styles = (theme) =>
}, },
controls : controls :
{ {
position : 'absolute', position : 'absolute',
width : '100%', width : '100%',
height : '100%', height : '100%',
backgroundColor : 'rgba(0, 0, 0, 0.3)', display : 'flex',
display : 'flex', flexDirection : 'column',
flexDirection : 'column', justifyContent : 'center',
justifyContent : 'center', alignItems : 'flex-end',
alignItems : 'flex-end', padding : theme.spacing(1),
padding : theme.spacing(1), zIndex : 21,
zIndex : 21, touchAction : 'none',
opacity : 1, pointerEvents : 'none',
transition : 'opacity 0.3s', '&.hide' :
touchAction : 'none', {
pointerEvents : 'none', transition : 'opacity 0.1s ease-in-out',
'& p' : opacity : 0
},
'&.hover' :
{
opacity : 1
},
'& p' :
{ {
position : 'absolute', position : 'absolute',
float : 'left', float : 'left',
@ -97,6 +116,10 @@ const styles = (theme) =>
'&.hover' : '&.hover' :
{ {
opacity : 1 opacity : 1
},
'&.smallContainer' :
{
fontSize : '3em'
} }
} }
}, },
@ -111,7 +134,8 @@ const styles = (theme) =>
fontSize : '2vs', fontSize : '2vs',
backgroundColor : 'rgba(255, 0, 0, 0.5)', backgroundColor : 'rgba(255, 0, 0, 0.5)',
margin : '4px', margin : '4px',
padding : '15px', padding : theme.spacing(2),
zIndex : 31,
borderRadius : '20px', borderRadius : '20px',
textAlign : 'center', textAlign : 'center',
opacity : 0, opacity : 0,
@ -139,13 +163,14 @@ const Me = (props) =>
activeSpeaker, activeSpeaker,
spacing, spacing,
style, style,
smallButtons, smallContainer,
advancedMode, advancedMode,
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer, screenProducer,
classes, extraVideoProducers,
theme canShareScreen,
classes
} = props; } = props;
const videoVisible = ( const videoVisible = (
@ -260,8 +285,6 @@ const Me = (props) =>
'margin' : spacing 'margin' : spacing
}; };
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return ( return (
<React.Fragment> <React.Fragment>
<div <div
@ -294,9 +317,25 @@ const Me = (props) =>
}} }}
style={spacingStyle} style={spacingStyle}
> >
<div className={classnames(classes.viewContainer)} style={style}> <div className={classes.viewContainer} style={style}>
{ !smallContainer &&
<div className={classnames(
classes.ptt,
(micState === 'muted' && me.isSpeaking) ? 'enabled' : null
)}
>
<FormattedMessage
id='me.mutedPTT'
defaultMessage='You are muted, hold down SPACE-BAR to talk'
/>
</div>
}
<div <div
className={classnames(classes.controls)} className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -317,24 +356,48 @@ const Me = (props) =>
}, 2000); }, 2000);
}} }}
> >
<p className={classnames(hover && 'hover')}> <p className={
classnames(
hover ? 'hover' : null,
smallContainer ? 'smallContainer' : null
)}
>
<FormattedMessage <FormattedMessage
id='room.me' id='room.me'
defaultMessage='ME' defaultMessage='ME'
/> />
</p> </p>
<div className={classnames(classes.ptt, (micState ==='muted' && me.isSpeaking) ? 'enabled' : null)} > <React.Fragment>
<FormattedMessage <Tooltip title={micTip} placement='left'>
id='me.mutedPTT' <div>
defaultMessage='You are muted, hold down SPACE-BAR to talk' { smallContainer ?
/> <IconButton
</div> aria-label={intl.formatMessage({
id : 'device.muteAudio',
{ !me.isMobile && defaultMessage : 'Mute audio'
<React.Fragment> })}
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'left'}> className={classes.smallContainer}
<div> disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</IconButton>
:
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'device.muteAudio', id : 'device.muteAudio',
@ -343,7 +406,7 @@ const Me = (props) =>
className={classes.fab} className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress} disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'} color={micState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
if (micState === 'off') if (micState === 'off')
@ -360,10 +423,35 @@ const Me = (props) =>
<MicOffIcon /> <MicOffIcon />
} }
</Fab> </Fab>
</div> }
</Tooltip> </div>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'left'}> </Tooltip>
<div> <Tooltip title={webcamTip} placement='left'>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'primary' : 'secondary'}
size='small'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</IconButton>
:
<Fab <Fab
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'device.startVideo', id : 'device.startVideo',
@ -372,7 +460,7 @@ const Me = (props) =>
className={classes.fab} className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress} disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'} color={webcamState === 'on' ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'} size='large'
onClick={() => onClick={() =>
{ {
webcamState === 'on' ? webcamState === 'on' ?
@ -386,51 +474,102 @@ const Me = (props) =>
<VideoOffIcon /> <VideoOffIcon />
} }
</Fab> </Fab>
</div> }
</Tooltip> </div>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'left'}> </Tooltip>
{ me.browser.platform !== 'mobile' &&
<Tooltip title={screenTip} placement='left'>
<div> <div>
<Fab { smallContainer ?
aria-label={intl.formatMessage({ <IconButton
id : 'device.startScreenSharing', aria-label={intl.formatMessage({
defaultMessage : 'Start screen sharing' id : 'device.startScreenSharing',
})} defaultMessage : 'Start screen sharing'
className={classes.fab} })}
disabled={!me.canShareScreen || me.screenShareInProgress} className={classes.smallContainer}
color={screenState === 'on' ? 'primary' : 'default'} disabled={
size={smallButtons ? 'small' : 'large'} !canShareScreen ||
onClick={() => !me.canShareScreen ||
{ me.screenShareInProgress
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
} }
}} color='primary'
> size='small'
{ (screenState === 'on' || screenState === 'unsupported') && onClick={() =>
<ScreenOffIcon/> {
} switch (screenState)
{ screenState === 'off' && {
<ScreenIcon/> case 'on':
} {
</Fab> roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.fab}
disabled={
!canShareScreen ||
!me.canShareScreen ||
me.screenShareInProgress
}
color={screenState === 'on' ? 'primary' : 'default'}
size='large'
onClick={() =>
{
switch (screenState)
{
case 'on':
{
roomClient.disableScreenSharing();
break;
}
case 'off':
{
roomClient.enableScreenSharing();
break;
}
default:
{
break;
}
}
}}
>
{ (screenState === 'on' || screenState === 'unsupported') &&
<ScreenOffIcon/>
}
{ screenState === 'off' &&
<ScreenIcon/>
}
</Fab>
}
</div> </div>
</Tooltip> </Tooltip>
</React.Fragment> }
} </React.Fragment>
</div> </div>
<VideoView <VideoView
@ -452,9 +591,135 @@ const Me = (props) =>
</VideoView> </VideoView>
</div> </div>
</div> </div>
{ extraVideoProducers.map((producer) =>
{
return (
<div key={producer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={spacingStyle}
>
<div className={classes.viewContainer} style={style}>
<div
className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
<p className={hover ? 'hover' : null}>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={webcamTip} placement='left'>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.smallContainer}
disabled={!me.canSendWebcam || me.webcamInProgress}
size='small'
color='primary'
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
size={smallContainer ? 'small' : 'large'}
onClick={() =>
{
roomClient.disableExtraVideo(producer.id);
}}
>
<VideoIcon />
</Fab>
}
</div>
</Tooltip>
</div>
<VideoView
isMe
advancedMode={advancedMode}
peer={me}
displayName={settings.displayName}
showPeerInfo
videoTrack={producer && producer.track}
videoVisible={videoVisible}
videoCodec={producer && producer.codec}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/>
</div>
</div>
);
})}
{ screenProducer && { screenProducer &&
<div <div
className={classnames(classes.root, 'screen', hover && 'hover')} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -476,9 +741,13 @@ const Me = (props) =>
}} }}
style={spacingStyle} style={spacingStyle}
> >
<div className={classnames(classes.viewContainer)} style={style}> <div className={classes.viewContainer} style={style}>
<div <div
className={classnames(classes.controls, hover && 'hover')} className={classnames(
classes.controls,
settings.hiddenControls ? 'hide' : null,
hover ? 'hover' : null
)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -500,7 +769,7 @@ const Me = (props) =>
}, 2000); }, 2000);
}} }}
> >
<p> <p className={hover ? 'hover' : null}>
<FormattedMessage <FormattedMessage
id='room.me' id='room.me'
defaultMessage='ME' defaultMessage='ME'
@ -526,28 +795,33 @@ const Me = (props) =>
Me.propTypes = Me.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
settings : PropTypes.object, settings : PropTypes.object,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
spacing : PropTypes.number, extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
style : PropTypes.object, spacing : PropTypes.number,
smallButtons : PropTypes.bool, style : PropTypes.object,
classes : PropTypes.object.isRequired, smallContainer : PropTypes.bool,
theme : PropTypes.object.isRequired canShareScreen : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
me : state.me, me : state.me,
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_SCREEN.includes(role))
}; };
}; };
@ -559,6 +833,7 @@ export default withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.me === next.me && prev.me === next.me &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings && prev.settings === next.settings &&

View File

@ -12,8 +12,9 @@ import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView'; import VideoView from '../VideoContainers/VideoView';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
import MicIcon from '@material-ui/icons/Mic'; import IconButton from '@material-ui/core/IconButton';
import MicOffIcon from '@material-ui/icons/MicOff'; import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import NewWindowIcon from '@material-ui/icons/OpenInNew'; import NewWindowIcon from '@material-ui/icons/OpenInNew';
import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen';
import Volume from './Volume'; import Volume from './Volume';
@ -59,6 +60,19 @@ const styles = (theme) =>
{ {
margin : theme.spacing(1) margin : theme.spacing(1)
}, },
smallContainer :
{
backgroundColor : 'rgba(255, 255, 255, 0.9)',
margin : '0.5vmin',
padding : '0.5vmin',
boxShadow : '0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)',
pointerEvents : 'auto',
transition : 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
'&:hover' :
{
backgroundColor : 'rgba(213, 213, 213, 1)'
}
},
viewContainer : viewContainer :
{ {
position : 'relative', position : 'relative',
@ -121,15 +135,16 @@ const Peer = (props) =>
advancedMode, advancedMode,
peer, peer,
activeSpeaker, activeSpeaker,
isMobile, browser,
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer, screenConsumer,
extraVideoConsumers,
toggleConsumerFullscreen, toggleConsumerFullscreen,
toggleConsumerWindow, toggleConsumerWindow,
spacing, spacing,
style, style,
smallButtons, smallContainer,
windowConsumer, windowConsumer,
classes, classes,
theme theme
@ -168,8 +183,8 @@ const Peer = (props) =>
classnames( classnames(
classes.root, classes.root,
'webcam', 'webcam',
hover && 'hover', hover ? 'hover' : null,
activeSpeaker && 'active-speaker' activeSpeaker ? 'active-speaker' : null
) )
} }
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
@ -206,7 +221,7 @@ const Peer = (props) =>
} }
<div <div
className={classnames(classes.controls, hover && 'hover')} className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -235,32 +250,57 @@ const Peer = (props) =>
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> <div>
<Fab { smallContainer ?
aria-label={intl.formatMessage({ <IconButton
id : 'device.muteAudio', aria-label={intl.formatMessage({
defaultMessage : 'Mute audio' id : 'device.muteAudio',
})} defaultMessage : 'Mute audio'
className={classes.fab} })}
disabled={!micConsumer} className={classes.smallContainer}
color={micEnabled ? 'default' : 'secondary'} disabled={!micConsumer}
size={smallButtons ? 'small' : 'large'} color='primary'
onClick={() => size='small'
{ onClick={() =>
micEnabled ? {
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
}} roomClient.modifyPeerConsumer(peer.id, 'mic', false);
> }}
{ micEnabled ? >
<MicIcon /> { micEnabled ?
: <VolumeUpIcon />
<MicOffIcon /> :
} <VolumeOffIcon />
</Fab> }
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<VolumeUpIcon />
:
<VolumeOffIcon />
}
</Fab>
}
</div> </div>
</Tooltip> </Tooltip>
{ !isMobile && { browser.platform !== 'mobile' &&
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
id : 'label.newWindow', id : 'label.newWindow',
@ -269,24 +309,46 @@ const Peer = (props) =>
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> <div>
<Fab { smallContainer ?
aria-label={intl.formatMessage({ <IconButton
id : 'label.newWindow', aria-label={intl.formatMessage({
defaultMessage : 'New window' id : 'label.newWindow',
})} defaultMessage : 'New window'
className={classes.fab} })}
disabled={ className={classes.smallContainer}
!videoVisible || disabled={
(windowConsumer === webcamConsumer.id) !videoVisible ||
} (windowConsumer === webcamConsumer.id)
size={smallButtons ? 'small' : 'large'} }
onClick={() => size='small'
{ color='primary'
toggleConsumerWindow(webcamConsumer); onClick={() =>
}} {
> toggleConsumerWindow(webcamConsumer);
<NewWindowIcon /> }}
</Fab> >
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</Fab>
}
</div> </div>
</Tooltip> </Tooltip>
} }
@ -299,21 +361,40 @@ const Peer = (props) =>
placement={smallScreen ? 'top' : 'left'} placement={smallScreen ? 'top' : 'left'}
> >
<div> <div>
<Fab { smallContainer ?
aria-label={intl.formatMessage({ <IconButton
id : 'label.fullscreen', aria-label={intl.formatMessage({
defaultMessage : 'Fullscreen' id : 'label.fullscreen',
})} defaultMessage : 'Fullscreen'
className={classes.fab} })}
disabled={!videoVisible} className={classes.smallContainer}
size={smallButtons ? 'small' : 'large'} disabled={!videoVisible}
onClick={() => size='small'
{ color='primary'
toggleConsumerFullscreen(webcamConsumer); onClick={() =>
}} {
> toggleConsumerFullscreen(webcamConsumer);
<FullScreenIcon /> }}
</Fab> >
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</Fab>
}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
@ -340,6 +421,7 @@ const Peer = (props) =>
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'} videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
videoTrack={webcamConsumer && webcamConsumer.track} videoTrack={webcamConsumer && webcamConsumer.track}
videoVisible={videoVisible} videoVisible={videoVisible}
audioTrack={micConsumer && micConsumer.track}
audioCodec={micConsumer && micConsumer.codec} audioCodec={micConsumer && micConsumer.codec}
videoCodec={webcamConsumer && webcamConsumer.codec} videoCodec={webcamConsumer && webcamConsumer.codec}
audioScore={micConsumer ? micConsumer.score : null} audioScore={micConsumer ? micConsumer.score : null}
@ -350,9 +432,205 @@ const Peer = (props) =>
</div> </div>
</div> </div>
{ extraVideoConsumers.map((consumer) =>
{
return (
<div key={consumer.id}
className={
classnames(
classes.root,
'webcam',
hover ? 'hover' : null,
activeSpeaker ? 'active-speaker' : null
)
}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
style={rootStyle}
>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
<div
className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onTouchStart={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
setHover(true);
}}
onTouchEnd={() =>
{
if (touchTimeout)
clearTimeout(touchTimeout);
touchTimeout = setTimeout(() =>
{
setHover(false);
}, 2000);
}}
>
{ browser.platform !== 'mobile' &&
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.smallContainer}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === consumer.id)
}
size='large'
onClick={() =>
{
toggleConsumerWindow(consumer);
}}
>
<NewWindowIcon />
</Fab>
}
</div>
</Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<div>
{ smallContainer ?
<IconButton
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.smallContainer}
disabled={!videoVisible}
size='small'
color='primary'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</IconButton>
:
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size='large'
onClick={() =>
{
toggleConsumerFullscreen(consumer);
}}
>
<FullScreenIcon />
</Fab>
}
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}
peer={peer}
displayName={peer.displayName}
showPeerInfo
consumerSpatialLayers={consumer ? consumer.spatialLayers : null}
consumerTemporalLayers={consumer ? consumer.temporalLayers : null}
consumerCurrentSpatialLayer={
consumer ? consumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
consumer ? consumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
consumer ? consumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
consumer ? consumer.preferredTemporalLayer : null
}
videoMultiLayer={consumer && consumer.type !== 'simple'}
videoTrack={consumer && consumer.track}
videoVisible={videoVisible}
videoCodec={consumer && consumer.codec}
videoScore={consumer ? consumer.score : null}
/>
</div>
</div>
);
})}
{ screenConsumer && { screenConsumer &&
<div <div
className={classnames(classes.root, 'screen', hover && 'hover')} className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -386,7 +664,7 @@ const Peer = (props) =>
</div> </div>
} }
<div <div
className={classnames(classes.controls, hover && 'hover')} className={classnames(classes.controls, hover ? 'hover' : null)}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)} onMouseOut={() => setHover(false)}
onTouchStart={() => onTouchStart={() =>
@ -408,7 +686,7 @@ const Peer = (props) =>
}, 2000); }, 2000);
}} }}
> >
{ !isMobile && { browser.platform !== 'mobile' &&
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
id : 'label.newWindow', id : 'label.newWindow',
@ -427,7 +705,7 @@ const Peer = (props) =>
!screenVisible || !screenVisible ||
(windowConsumer === screenConsumer.id) (windowConsumer === screenConsumer.id)
} }
size={smallButtons ? 'small' : 'large'} size={smallContainer ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerWindow(screenConsumer); toggleConsumerWindow(screenConsumer);
@ -454,7 +732,7 @@ const Peer = (props) =>
})} })}
className={classes.fab} className={classes.fab}
disabled={!screenVisible} disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'} size={smallContainer ? 'small' : 'large'}
onClick={() => onClick={() =>
{ {
toggleConsumerFullscreen(screenConsumer); toggleConsumerFullscreen(screenConsumer);
@ -490,6 +768,7 @@ const Peer = (props) =>
videoTrack={screenConsumer && screenConsumer.track} videoTrack={screenConsumer && screenConsumer.track}
videoVisible={screenVisible} videoVisible={screenVisible}
videoCodec={screenConsumer && screenConsumer.codec} videoCodec={screenConsumer && screenConsumer.codec}
videoScore={screenConsumer ? screenConsumer.score : null}
/> />
</div> </div>
</div> </div>
@ -506,12 +785,13 @@ Peer.propTypes =
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
windowConsumer : PropTypes.string, windowConsumer : PropTypes.string,
activeSpeaker : PropTypes.bool, activeSpeaker : PropTypes.bool,
isMobile : PropTypes.bool, browser : PropTypes.object.isRequired,
spacing : PropTypes.number, spacing : PropTypes.number,
style : PropTypes.object, style : PropTypes.object,
smallButtons : PropTypes.bool, smallContainer : PropTypes.bool,
toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
toggleConsumerWindow : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
@ -529,7 +809,7 @@ const makeMapStateToProps = (initialState, { id }) =>
...getPeerConsumers(state, id), ...getPeerConsumers(state, id),
windowConsumer : state.room.windowConsumer, windowConsumer : state.room.windowConsumer,
activeSpeaker : id === state.room.activeSpeakerId, activeSpeaker : id === state.room.activeSpeakerId,
isMobile : state.me.isMobile browser : state.me.browser
}; };
}; };
@ -564,7 +844,7 @@ export default withRoomContext(connect(
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.windowConsumer === next.room.windowConsumer && prev.room.windowConsumer === next.room.windowConsumer &&
prev.me.isMobile === next.me.isMobile prev.me.browser === next.me.browser
); );
} }
} }

View File

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

View File

@ -0,0 +1,167 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
const styles = (theme) =>
({
dialogPaper :
{
width : '30vw',
[theme.breakpoints.down('lg')] :
{
width : '40vw'
},
[theme.breakpoints.down('md')] :
{
width : '50vw'
},
[theme.breakpoints.down('sm')] :
{
width : '70vw'
},
[theme.breakpoints.down('xs')] :
{
width : '90vw'
}
},
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const ExtraVideo = ({
roomClient,
extraVideoOpen,
webcamDevices,
handleCloseExtraVideo,
classes
}) =>
{
const intl = useIntl();
const [ videoDevice, setVideoDevice ] = React.useState('');
const handleChange = (event) =>
{
setVideoDevice(event.target.value);
};
let videoDevices;
if (webcamDevices)
videoDevices = Object.values(webcamDevices);
else
videoDevices = [];
return (
<Dialog
open={extraVideoOpen}
onClose={() => handleCloseExtraVideo(false)}
classes={{
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.extraVideo'
defaultMessage='Extra video'
/>
</DialogTitle>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={videoDevice}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={videoDevices.length === 0}
onChange={handleChange}
>
{ videoDevices.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ videoDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
</form>
<DialogActions>
<Button onClick={() => roomClient.addExtraVideo(videoDevice)} color='primary'>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</Button>
</DialogActions>
</Dialog>
);
};
ExtraVideo.propTypes =
{
roomClient : PropTypes.object.isRequired,
extraVideoOpen : PropTypes.bool.isRequired,
webcamDevices : PropTypes.object,
handleCloseExtraVideo : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
webcamDevices : state.me.webcamDevices,
extraVideoOpen : state.room.extraVideoOpen
});
const mapDispatchToProps = {
handleCloseExtraVideo : roomActions.setExtraVideoOpen
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me.webcamDevices === next.me.webcamDevices &&
prev.room.extraVideoOpen === next.room.extraVideoOpen
);
}
}
)(withStyles(styles)(ExtraVideo)));

View File

@ -1,172 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors';
import { withStyles } from '@material-ui/core/styles';
import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext';
import Fab from '@material-ui/core/Fab';
import Tooltip from '@material-ui/core/Tooltip';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VideoIcon from '@material-ui/icons/Videocam';
import VideoOffIcon from '@material-ui/icons/VideocamOff';
const styles = (theme) =>
({
root :
{
position : 'fixed',
zIndex : 500,
display : 'flex',
flexDirection : 'row',
bottom : '0.5em',
left : '50%',
transform : 'translate(-50%, -0%)'
},
fab :
{
margin : theme.spacing(1)
}
});
const MobileControls = (props) =>
{
const {
roomClient,
me,
micProducer,
webcamProducer,
classes
} = props;
let micState;
let micTip;
if (!me.canSendMic)
{
micState = 'unsupported';
micTip = 'Audio unsupported';
}
else if (!micProducer)
{
micState = 'off';
micTip = 'Activate audio';
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = 'Mute audio';
}
else
{
micState = 'muted';
micTip = 'Unmute audio';
}
let webcamState;
let webcamTip;
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = 'Video unsupported';
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = 'Stop video';
}
else
{
webcamState = 'off';
webcamTip = 'Start video';
}
return (
<div className={classes.root}>
<Tooltip title={micTip} placement='top'>
<div>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
if (micState === 'off')
roomClient.enableMic();
else if (micState === 'on')
roomClient.muteMic();
else
roomClient.unmuteMic();
}}
>
{ micState === 'on' ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
</div>
</Tooltip>
<Tooltip title={webcamTip} placement='top'>
<div>
<Fab
aria-label='Mute video'
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
size='large'
onClick={() =>
{
webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}}
>
{ webcamState === 'on' ?
<VideoIcon />
:
<VideoOffIcon />
}
</Fab>
</div>
</Tooltip>
</div>
);
};
MobileControls.propTypes =
{
roomClient : PropTypes.any.isRequired,
me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
...meProducersSelector(state),
me : state.me
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.producers === next.producers &&
prev.me === next.me
);
}
}
)(withStyles(styles, { withTheme: true })(MobileControls)));

View File

@ -1,9 +1,10 @@
import React from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
lobbyPeersKeySelector, lobbyPeersKeySelector,
peersLengthSelector peersLengthSelector,
raisedHandsSelector
} from '../Selectors'; } from '../Selectors';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
@ -13,11 +14,16 @@ import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Popover from '@material-ui/core/Popover';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Badge from '@material-ui/core/Badge'; import Badge from '@material-ui/core/Badge';
import Paper from '@material-ui/core/Paper';
import ExtensionIcon from '@material-ui/icons/Extension';
import AccountCircle from '@material-ui/icons/AccountCircle'; import AccountCircle from '@material-ui/icons/AccountCircle';
import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen';
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
@ -26,8 +32,10 @@ import SecurityIcon from '@material-ui/icons/Security';
import PeopleIcon from '@material-ui/icons/People'; import PeopleIcon from '@material-ui/icons/People';
import LockIcon from '@material-ui/icons/Lock'; import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen'; import LockOpenIcon from '@material-ui/icons/LockOpen';
import VideoCallIcon from '@material-ui/icons/VideoCall';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import MoreIcon from '@material-ui/icons/MoreVert';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -72,14 +80,34 @@ const styles = (theme) =>
display : 'block' display : 'block'
} }
}, },
actionButtons : sectionDesktop : {
{ display : 'none',
display : 'flex' [theme.breakpoints.up('md')] : {
display : 'flex'
}
},
sectionMobile : {
display : 'flex',
[theme.breakpoints.up('md')] : {
display : 'none'
}
}, },
actionButton : actionButton :
{ {
margin : theme.spacing(1), margin : theme.spacing(1, 0),
padding : 0 padding : theme.spacing(0, 1)
},
disabledButton :
{
margin : theme.spacing(1, 0)
},
green :
{
color : 'rgba(0, 153, 0, 1)'
},
moreAction :
{
margin : theme.spacing(0.5, 0, 0.5, 1.5)
} }
}); });
@ -118,6 +146,38 @@ const TopBar = (props) =>
{ {
const intl = useIntl(); const intl = useIntl();
const [ mobileMoreAnchorEl, setMobileMoreAnchorEl ] = useState(null);
const [ anchorEl, setAnchorEl ] = useState(null);
const [ currentMenu, setCurrentMenu ] = useState(null);
const handleExited = () =>
{
setCurrentMenu(null);
};
const handleMobileMenuOpen = (event) =>
{
setMobileMoreAnchorEl(event.currentTarget);
};
const handleMobileMenuClose = () =>
{
setMobileMoreAnchorEl(null);
};
const handleMenuOpen = (event, menu) =>
{
setAnchorEl(event.currentTarget);
setCurrentMenu(menu);
};
const handleMenuClose = () =>
{
setAnchorEl(null);
handleMobileMenuClose();
};
const { const {
roomClient, roomClient,
room, room,
@ -131,13 +191,20 @@ const TopBar = (props) =>
fullscreen, fullscreen,
onFullscreen, onFullscreen,
setSettingsOpen, setSettingsOpen,
setExtraVideoOpen,
setLockDialogOpen, setLockDialogOpen,
toggleToolArea, toggleToolArea,
openUsersTab, openUsersTab,
unread, unread,
canProduceExtraVideo,
canLock,
canPromote,
classes classes
} = props; } = props;
const isMenuOpen = Boolean(anchorEl);
const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
const lockTooltip = room.locked ? const lockTooltip = room.locked ?
intl.formatMessage({ intl.formatMessage({
id : 'tooltip.unLockRoom', id : 'tooltip.unLockRoom',
@ -172,170 +239,200 @@ const TopBar = (props) =>
}); });
return ( return (
<AppBar <React.Fragment>
position='fixed' <AppBar
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide} position='fixed'
> className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
<Toolbar> >
<PulsingBadge <Toolbar>
color='secondary' <PulsingBadge
badgeContent={unread} color='secondary'
onClick={() => toggleToolArea()} badgeContent={unread}
> onClick={() => toggleToolArea()}
<IconButton
color='inherit'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ 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 && 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 <IconButton
color='inherit'
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
</PulsingBadge>
{ 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.title : 'Multiparty meeting' }
</Typography>
<div className={classes.grow} />
<div className={classes.sectionDesktop}>
<IconButton
aria-owns={
isMenuOpen &&
currentMenu === 'moreActions' ?
'material-appbar' : undefined
}
aria-haspopup='true'
onClick={(event) => handleMenuOpen(event, 'moreActions')}
color='inherit'
>
<ExtensionIcon />
</IconButton>
{ 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', id : 'tooltip.participants',
defaultMessage : 'Show participants' defaultMessage : 'Show participants'
})} })}
color='inherit'
onClick={() => openUsersTab()}
> >
<Badge <IconButton
color='primary' aria-label={intl.formatMessage({
badgeContent={peersLength + 1} id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
color='inherit'
onClick={() => openUsersTab()}
> >
<PeopleIcon /> <Badge
</Badge> color='primary'
</IconButton> badgeContent={peersLength + 1}
</Tooltip> >
<Tooltip <PeopleIcon />
title={intl.formatMessage({ </Badge>
id : 'tooltip.settings', </IconButton>
defaultMessage : 'Show settings' </Tooltip>
})} <Tooltip
> title={intl.formatMessage({
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.settings', id : 'tooltip.settings',
defaultMessage : 'Show settings' defaultMessage : 'Show settings'
})} })}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
>
<SettingsIcon />
</IconButton>
</Tooltip>
<Tooltip title={lockTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
> >
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.lobby', id : 'tooltip.settings',
defaultMessage : 'Show lobby' defaultMessage : 'Show settings'
})}
color='inherit'
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})} })}
className={classes.actionButton} className={classes.actionButton}
color='inherit' color='inherit'
onClick={() => onClick={() => setSettingsOpen(!room.settingsOpen)}
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
> >
{ myPicture ? <SettingsIcon />
<Avatar src={myPicture} />
:
<AccountCircle />
}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} <Tooltip title={lockTooltip}>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
disabled={!canLock}
onClick={() =>
{
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
</IconButton>
</span>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<span className={classes.disabledButton}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
className={classes.actionButton}
color='inherit'
disabled={!canPromote}
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
</IconButton>
</span>
</Tooltip>
}
{ loginEnabled &&
<Tooltip title={loginTooltip}>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
{
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
</IconButton>
</Tooltip>
}
</div>
<div className={classes.sectionMobile}>
<IconButton
aria-haspopup='true'
onClick={handleMobileMenuOpen}
color='inherit'
>
<MoreIcon />
</IconButton>
</div>
<div className={classes.divider} /> <div className={classes.divider} />
<Button <Button
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
@ -352,33 +449,260 @@ const TopBar = (props) =>
defaultMessage='Leave' defaultMessage='Leave'
/> />
</Button> </Button>
</div> </Toolbar>
</Toolbar> </AppBar>
</AppBar> <Popover
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
open={isMenuOpen}
onClose={handleMenuClose}
onExited={handleExited}
getContentAnchorEl={null}
>
{ currentMenu === 'moreActions' &&
<Paper>
<MenuItem
disabled={!canProduceExtraVideo}
onClick={() =>
{
handleMenuClose();
setExtraVideoOpen(!room.extraVideoOpen);
}}
>
<VideoCallIcon
aria-label={intl.formatMessage({
id : 'label.addVideo',
defaultMessage : 'Add video'
})}
/>
<p className={classes.moreAction}>
<FormattedMessage
id='label.addVideo'
defaultMessage='Add video'
/>
</p>
</MenuItem>
</Paper>
}
</Popover>
<Menu
anchorEl={mobileMoreAnchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'bottom', horizontal: 'right' }}
open={isMobileMenuOpen}
onClose={handleMenuClose}
getContentAnchorEl={null}
>
{ loginEnabled &&
<MenuItem
aria-label={loginTooltip}
onClick={() =>
{
handleMenuClose();
loggedIn ? roomClient.logout() : roomClient.login();
}}
>
{ myPicture ?
<Avatar src={myPicture} />
:
<AccountCircle className={loggedIn ? classes.green : null} />
}
{ loggedIn ?
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.logout'
defaultMessage='Log out'
/>
</p>
:
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.login'
defaultMessage='Log in'
/>
</p>
}
</MenuItem>
}
<MenuItem
aria-label={lockTooltip}
disabled={!canLock}
onClick={() =>
{
handleMenuClose();
if (room.locked)
{
roomClient.unlockRoom();
}
else
{
roomClient.lockRoom();
}
}}
>
{ room.locked ?
<LockIcon />
:
<LockOpenIcon />
}
{ room.locked ?
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.unLockRoom'
defaultMessage='Unlock room'
/>
</p>
:
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.lockRoom'
defaultMessage='Lock room'
/>
</p>
}
</MenuItem>
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
onClick={() =>
{
handleMenuClose();
setSettingsOpen(!room.settingsOpen);
}}
>
<SettingsIcon />
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.settings'
defaultMessage='Show settings'
/>
</p>
</MenuItem>
{ lobbyPeers.length > 0 &&
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
disabled={!canPromote}
onClick={() =>
{
handleMenuClose();
setLockDialogOpen(!room.lockDialogOpen);
}}
>
<PulsingBadge
color='secondary'
badgeContent={lobbyPeers.length}
>
<SecurityIcon />
</PulsingBadge>
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.lobby'
defaultMessage='Show lobby'
/>
</p>
</MenuItem>
}
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.participants',
defaultMessage : 'Show participants'
})}
onClick={() =>
{
handleMenuClose();
openUsersTab();
}}
>
<Badge
color='primary'
badgeContent={peersLength + 1}
>
<PeopleIcon />
</Badge>
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.participants'
defaultMessage='Show participants'
/>
</p>
</MenuItem>
{ fullscreenEnabled &&
<MenuItem
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
onClick={() =>
{
handleMenuClose();
onFullscreen();
}}
>
{ fullscreen ?
<FullScreenExitIcon />
:
<FullScreenIcon />
}
<p className={classes.moreAction}>
<FormattedMessage
id='tooltip.enterFullscreen'
defaultMessage='Enter fullscreen'
/>
</p>
</MenuItem>
}
<MenuItem
aria-label={intl.formatMessage({
id : 'label.moreActions',
defaultMessage : 'Add video'
})}
onClick={(event) => handleMenuOpen(event, 'moreActions')}
>
<ExtensionIcon />
<p className={classes.moreAction}>
<FormattedMessage
id='label.moreActions'
defaultMessage='More actions'
/>
</p>
</MenuItem>
</Menu>
</React.Fragment>
); );
}; };
TopBar.propTypes = TopBar.propTypes =
{ {
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
peersLength : PropTypes.number, peersLength : PropTypes.number,
lobbyPeers : PropTypes.array, lobbyPeers : PropTypes.array,
permanentTopBar : PropTypes.bool, permanentTopBar : PropTypes.bool,
myPicture : PropTypes.string, myPicture : PropTypes.string,
loggedIn : PropTypes.bool.isRequired, loggedIn : PropTypes.bool.isRequired,
loginEnabled : PropTypes.bool.isRequired, loginEnabled : PropTypes.bool.isRequired,
fullscreenEnabled : PropTypes.bool, fullscreenEnabled : PropTypes.bool,
fullscreen : PropTypes.bool, fullscreen : PropTypes.bool,
onFullscreen : PropTypes.func.isRequired, onFullscreen : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired, setToolbarsVisible : PropTypes.func.isRequired,
setSettingsOpen : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired,
setLockDialogOpen : PropTypes.func.isRequired, setExtraVideoOpen : PropTypes.func.isRequired,
toggleToolArea : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired,
openUsersTab : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired, openUsersTab : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, unread : PropTypes.number.isRequired,
theme : PropTypes.object.isRequired canProduceExtraVideo : PropTypes.bool.isRequired,
canLock : PropTypes.bool.isRequired,
canPromote : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -391,7 +715,16 @@ const mapStateToProps = (state) =>
loginEnabled : state.me.loginEnabled, loginEnabled : state.me.loginEnabled,
myPicture : state.me.picture, myPicture : state.me.picture,
unread : state.toolarea.unreadMessages + unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)),
canLock :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)),
canPromote :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}); });
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
@ -402,11 +735,15 @@ const mapDispatchToProps = (dispatch) =>
}, },
setSettingsOpen : (settingsOpen) => setSettingsOpen : (settingsOpen) =>
{ {
dispatch(roomActions.setSettingsOpen({ settingsOpen })); dispatch(roomActions.setSettingsOpen(settingsOpen));
},
setExtraVideoOpen : (extraVideoOpen) =>
{
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
}, },
setLockDialogOpen : (lockDialogOpen) => setLockDialogOpen : (lockDialogOpen) =>
{ {
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen })); dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
}, },
toggleToolArea : () => toggleToolArea : () =>
{ {
@ -434,6 +771,7 @@ export default withRoomContext(connect(
prev.me.loggedIn === next.me.loggedIn && prev.me.loggedIn === next.me.loggedIn &&
prev.me.loginEnabled === next.me.loginEnabled && prev.me.loginEnabled === next.me.loginEnabled &&
prev.me.picture === next.me.picture && prev.me.picture === next.me.picture &&
prev.me.roles === next.me.roles &&
prev.toolarea.unreadMessages === next.toolarea.unreadMessages && prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
prev.toolarea.unreadFiles === next.toolarea.unreadFiles prev.toolarea.unreadFiles === next.toolarea.unreadFiles
); );

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext'; import { withRoomContext } from '../RoomContext';
import classnames from 'classnames';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import * as settingsActions from '../actions/settingsActions'; import * as settingsActions from '../actions/settingsActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -82,6 +83,10 @@ const styles = (theme) =>
green : green :
{ {
color : 'rgba(0, 153, 0, 1)' color : 'rgba(0, 153, 0, 1)'
},
red :
{
color : 'rgba(153, 0, 0, 1)'
} }
}); });
@ -103,7 +108,7 @@ const DialogTitle = withStyles(styles)((props) =>
}; };
}, []); }, []);
const { children, classes, myPicture, onLogin, ...other } = props; const { children, classes, myPicture, onLogin, loggedIn, ...other } = props;
const handleTooltipClose = () => const handleTooltipClose = () =>
{ {
@ -115,19 +120,27 @@ const DialogTitle = withStyles(styles)((props) =>
setOpen(true); setOpen(true);
}; };
const loginTooltip = loggedIn ?
intl.formatMessage({
id : 'tooltip.logout',
defaultMessage : 'Log out'
})
:
intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
});
return ( return (
<MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}> <MuiDialogTitle disableTypography className={classes.dialogTitle} {...other}>
{ window.config && window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> } { window.config.logo && <img alt='Logo' className={classes.logo} src={window.config.logo} /> }
<Typography variant='h5'>{children}</Typography> <Typography variant='h5'>{children}</Typography>
{ window.config && window.config.loginEnabled && { window.config.loginEnabled &&
<Tooltip <Tooltip
onClose={handleTooltipClose} onClose={handleTooltipClose}
onOpen={handleTooltipOpen} onOpen={handleTooltipOpen}
open={open} open={open}
title={intl.formatMessage({ title={loginTooltip}
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left' placement='left'
> >
<IconButton <IconButton
@ -139,7 +152,9 @@ const DialogTitle = withStyles(styles)((props) =>
{ myPicture ? { myPicture ?
<Avatar src={myPicture} className={classes.largeAvatar} /> <Avatar src={myPicture} className={classes.largeAvatar} />
: :
<AccountCircle className={classes.largeIcon} /> <AccountCircle
className={classnames(classes.largeIcon, loggedIn ? classes.green : null)}
/>
} }
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -209,10 +224,11 @@ const JoinDialog = ({
myPicture={myPicture} myPicture={myPicture}
onLogin={() => onLogin={() =>
{ {
loggedIn ? roomClient.logout() : roomClient.login(); loggedIn ? roomClient.logout() : roomClient.login(roomId);
}} }}
loggedIn={loggedIn}
> >
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' } { window.config.title ? window.config.title : 'Multiparty meeting' }
<hr /> <hr />
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
@ -269,6 +285,16 @@ const JoinDialog = ({
}} }}
fullWidth fullWidth
/> />
{!room.inLobby && room.overRoomLimit &&
<DialogContentText className={classes.red} variant='h6' gutterBottom>
<FormattedMessage
id='room.overRoomLimit'
defaultMessage={
'The room is full, retry after some time.'
}
/>
</DialogContentText>
}
</DialogContent> </DialogContent>
@ -307,6 +333,7 @@ const JoinDialog = ({
className={classes.green} className={classes.green}
gutterBottom gutterBottom
variant='h6' variant='h6'
style={{ fontWeight: '600' }}
align='center' align='center'
> >
<FormattedMessage <FormattedMessage
@ -315,7 +342,11 @@ const JoinDialog = ({
/> />
</DialogContentText> </DialogContentText>
{ room.signInRequired ? { room.signInRequired ?
<DialogContentText gutterBottom> <DialogContentText
gutterBottom
variant='h5'
style={{ fontWeight: '600' }}
>
<FormattedMessage <FormattedMessage
id='room.emptyRequireLogin' id='room.emptyRequireLogin'
defaultMessage={ defaultMessage={
@ -325,7 +356,11 @@ const JoinDialog = ({
/> />
</DialogContentText> </DialogContentText>
: :
<DialogContentText gutterBottom> <DialogContentText
gutterBottom
variant='h5'
style={{ fontWeight: '600' }}
>
<FormattedMessage <FormattedMessage
id='room.locketWait' id='room.locketWait'
defaultMessage='The room is locked - hang on until somebody lets you in ...' defaultMessage='The room is locked - hang on until somebody lets you in ...'
@ -398,6 +433,7 @@ export default withRoomContext(connect(
return ( return (
prev.room.inLobby === next.room.inLobby && prev.room.inLobby === next.room.inLobby &&
prev.room.signInRequired === next.room.signInRequired && prev.room.signInRequired === next.room.signInRequired &&
prev.room.overRoomLimit === next.room.overRoomLimit &&
prev.settings.displayName === next.settings.displayName && prev.settings.displayName === next.settings.displayName &&
prev.me.displayNameInProgress === next.me.displayNameInProgress && prev.me.displayNameInProgress === next.me.displayNameInProgress &&
prev.me.loginEnabled === next.me.loginEnabled && prev.me.loginEnabled === next.me.loginEnabled &&

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import ChatModerator from './ChatModerator';
import MessageList from './MessageList'; import MessageList from './MessageList';
import ChatInput from './ChatInput'; import ChatInput from './ChatInput';
@ -25,6 +26,7 @@ const Chat = (props) =>
return ( return (
<Paper className={classes.root}> <Paper className={classes.root}>
<ChatModerator />
<MessageList /> <MessageList />
<ChatInput /> <ChatInput />
</Paper> </Paper>

View File

@ -54,6 +54,7 @@ const ChatInput = (props) =>
roomClient, roomClient,
displayName, displayName,
picture, picture,
canChat,
classes classes
} = props; } = props;
@ -66,6 +67,7 @@ const ChatInput = (props) =>
defaultMessage : 'Enter chat message...' defaultMessage : 'Enter chat message...'
})} })}
value={message || ''} value={message || ''}
disabled={!canChat}
onChange={handleChange} onChange={handleChange}
onKeyPress={(ev) => onKeyPress={(ev) =>
{ {
@ -89,6 +91,7 @@ const ChatInput = (props) =>
color='primary' color='primary'
className={classes.iconButton} className={classes.iconButton}
aria-label='Send' aria-label='Send'
disabled={!canChat}
onClick={() => onClick={() =>
{ {
if (message && message !== '') if (message && message !== '')
@ -112,13 +115,17 @@ ChatInput.propTypes =
roomClient : PropTypes.object.isRequired, roomClient : PropTypes.object.isRequired,
displayName : PropTypes.string, displayName : PropTypes.string,
picture : PropTypes.string, picture : PropTypes.string,
canChat : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
displayName : state.settings.displayName, displayName : state.settings.displayName,
picture : state.me.picture picture : state.me.picture,
canChat :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SEND_CHAT.includes(role))
}); });
export default withRoomContext( export default withRoomContext(
@ -130,6 +137,8 @@ export default withRoomContext(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.me.roles === next.me.roles &&
prev.settings.displayName === next.settings.displayName && prev.settings.displayName === next.settings.displayName &&
prev.me.picture === next.me.picture prev.me.picture === next.me.picture
); );

View File

@ -0,0 +1,100 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
display : 'flex',
padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)'
},
listheader :
{
padding : theme.spacing(1),
fontWeight : 'bolder'
},
actionButton :
{
marginLeft : 'auto'
}
});
const ChatModerator = (props) =>
{
const intl = useIntl();
const {
roomClient,
isChatModerator,
room,
classes
} = props;
if (!isChatModerator)
return null;
return (
<ul className={classes.root}>
<li className={classes.listheader}>
<FormattedMessage
id='room.moderatoractions'
defaultMessage='Moderator actions'
/>
</li>
<Button
aria-label={intl.formatMessage({
id : 'room.clearChat',
defaultMessage : 'Clear chat'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.clearChatInProgress}
onClick={() => roomClient.clearChat()}
>
<FormattedMessage
id='room.clearChat'
defaultMessage='Clear chat'
/>
</Button>
</ul>
);
};
ChatModerator.propTypes =
{
roomClient : PropTypes.any.isRequired,
isChatModerator : PropTypes.bool,
room : PropTypes.object,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isChatModerator :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)),
room : state.room
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room &&
prev.me === next.me
);
}
}
)(withStyles(styles)(ChatModerator)));

View File

@ -6,6 +6,7 @@ import DOMPurify from 'dompurify';
import marked from 'marked'; import marked from 'marked';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useIntl } from 'react-intl';
const linkRenderer = new marked.Renderer(); const linkRenderer = new marked.Renderer();
@ -55,6 +56,8 @@ const styles = (theme) =>
const Message = (props) => const Message = (props) =>
{ {
const intl = useIntl();
const { const {
self, self,
picture, picture,
@ -88,7 +91,16 @@ const Message = (props) =>
} }
) }} ) }}
/> />
<Typography variant='caption'>{self ? 'Me' : name} - {time}</Typography> <Typography variant='caption'>
{ self ?
intl.formatMessage({
id : 'room.me',
defaultMessage : 'Me'
})
:
name
} - {time}
</Typography>
</div> </div>
</Paper> </Paper>
); );

View File

@ -5,6 +5,7 @@ import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import FileList from './FileList'; import FileList from './FileList';
import FileSharingModerator from './FileSharingModerator';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
@ -24,6 +25,10 @@ const styles = (theme) =>
button : button :
{ {
margin : theme.spacing(1) margin : theme.spacing(1)
},
shareButtonsWrapper :
{
display : 'flex'
} }
}); });
@ -35,12 +40,14 @@ const FileSharing = (props) =>
{ {
if (event.target.files.length > 0) if (event.target.files.length > 0)
{ {
props.roomClient.shareFiles(event.target.files); await props.roomClient.shareFiles(event.target.files);
} }
}; };
const { const {
canShareFiles, canShareFiles,
browser,
canShare,
classes classes
} = props; } = props;
@ -55,25 +62,61 @@ const FileSharing = (props) =>
defaultMessage : 'File sharing not supported' defaultMessage : 'File sharing not supported'
}); });
const buttonGalleryDescription = canShareFiles ?
intl.formatMessage({
id : 'label.shareGalleryFile',
defaultMessage : 'Share image'
})
:
intl.formatMessage({
id : 'label.fileSharingUnsupported',
defaultMessage : 'File sharing not supported'
});
return ( return (
<Paper className={classes.root}> <Paper className={classes.root}>
<input <FileSharingModerator />
className={classes.input} <div className={classes.shareButtonsWrapper} >
type='file' <input
onChange={handleFileChange} className={classes.input}
id='share-files-button' type='file'
/> disabled={!canShare}
<label htmlFor='share-files-button'> onChange={handleFileChange}
<Button // Need to reset to be able to share same file twice
variant='contained' onClick={(e) => (e.target.value = null)}
component='span' id='share-files-button'
className={classes.button} />
disabled={!canShareFiles} <input
> className={classes.input}
{buttonDescription} type='file'
</Button> disabled={!canShare}
</label> onChange={handleFileChange}
accept='image/*'
id='share-files-gallery-button'
/>
<label htmlFor='share-files-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!canShareFiles || !canShare}
>
{buttonDescription}
</Button>
</label>
{
(browser.platform === 'mobile') && canShareFiles && canShare && <label htmlFor='share-files-gallery-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!canShareFiles || !canShare}
>
{buttonGalleryDescription}
</Button>
</label>
}
</div>
<FileList /> <FileList />
</Paper> </Paper>
); );
@ -81,8 +124,10 @@ const FileSharing = (props) =>
FileSharing.propTypes = { FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
browser : PropTypes.object.isRequired,
canShareFiles : PropTypes.bool.isRequired, canShareFiles : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired, tabOpen : PropTypes.bool.isRequired,
canShare : PropTypes.bool.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
@ -90,10 +135,28 @@ const mapStateToProps = (state) =>
{ {
return { return {
canShareFiles : state.me.canShareFiles, canShareFiles : state.me.canShareFiles,
tabOpen : state.toolarea.currentToolTab === 'files' browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files',
canShare :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_FILE.includes(role))
}; };
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.me.browser === next.me.browser &&
prev.me.roles === next.me.roles &&
prev.me.canShareFiles === next.me.canShareFiles &&
prev.toolarea.currentToolTab === next.toolarea.currentToolTab
);
}
}
)(withStyles(styles)(FileSharing))); )(withStyles(styles)(FileSharing)));

View File

@ -0,0 +1,100 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl';
import Button from '@material-ui/core/Button';
const styles = (theme) =>
({
root :
{
display : 'flex',
padding : theme.spacing(1),
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
backgroundColor : 'rgba(255, 255, 255, 1)'
},
listheader :
{
padding : theme.spacing(1),
fontWeight : 'bolder'
},
actionButton :
{
marginLeft : 'auto'
}
});
const FileSharingModerator = (props) =>
{
const intl = useIntl();
const {
roomClient,
isFileSharingModerator,
room,
classes
} = props;
if (!isFileSharingModerator)
return null;
return (
<ul className={classes.root}>
<li className={classes.listheader}>
<FormattedMessage
id='room.moderatoractions'
defaultMessage='Moderator actions'
/>
</li>
<Button
aria-label={intl.formatMessage({
id : 'room.clearFileSharing',
defaultMessage : 'Clear files'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
disabled={room.clearFileSharingInProgress}
onClick={() => roomClient.clearFileSharing()}
>
<FormattedMessage
id='room.clearFileSharing'
defaultMessage='Clear files'
/>
</Button>
</ul>
);
};
FileSharingModerator.propTypes =
{
roomClient : PropTypes.any.isRequired,
isFileSharingModerator : PropTypes.bool,
room : PropTypes.object,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
isFileSharingModerator :
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_FILES.includes(role)),
room : state.room
});
export default withRoomContext(connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room &&
prev.me === next.me
);
}
}
)(withStyles(styles)(FileSharingModerator)));

View File

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

View File

@ -1,79 +1,50 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames'; import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { useIntl } from 'react-intl';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import PanIcon from '@material-ui/icons/PanTool';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex'
}, },
listPeer :
{
display : 'flex'
},
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(1)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
alignItems : 'center' alignItems : 'center'
}, },
indicators : green :
{ {
left : 0, color : 'rgba(0, 153, 0, 1)'
top : 0,
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
},
icon :
{
flex : '0 0 auto',
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`,
opacity : 1
}
} }
}); });
const ListMe = (props) => const ListMe = (props) =>
{ {
const intl = useIntl();
const { const {
roomClient,
me, me,
settings, settings,
classes classes
@ -82,29 +53,47 @@ const ListMe = (props) =>
const picture = me.picture || EmptyAvatar; const picture = me.picture || EmptyAvatar;
return ( return (
<li className={classes.root}> <div className={classes.root}>
<div className={classes.listPeer}> <img alt='My avatar' className={classes.avatar} src={picture} />
<img alt='My avatar' className={classes.avatar} src={picture} />
<div className={classes.peerInfo}> <div className={classes.peerInfo}>
{settings.displayName} {settings.displayName}
</div>
<div className={classes.indicators}>
{ me.raisedHand &&
<div className={classnames(classes.icon, 'raise-hand')} />
}
</div>
</div> </div>
</li> <Tooltip
title={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.raisedHand',
defaultMessage : 'Raise hand'
})}
className={me.raisedHand ? classes.green : null}
disabled={me.raisedHandInProgress}
color='primary'
onClick={(e) =>
{
e.stopPropagation();
roomClient.setRaisedHand(!me.raisedHand);
}}
>
<PanIcon />
</IconButton>
</Tooltip>
</div>
); );
}; };
ListMe.propTypes = ListMe.propTypes =
{ {
me : appPropTypes.Me.isRequired, roomClient : PropTypes.object.isRequired,
settings : PropTypes.object.isRequired, me : appPropTypes.Me.isRequired,
classes : PropTypes.object.isRequired settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@ -112,7 +101,7 @@ const mapStateToProps = (state) => ({
settings : state.settings settings : state.settings
}); });
export default connect( export default withRoomContext(connect(
mapStateToProps, mapStateToProps,
null, null,
null, null,
@ -125,4 +114,4 @@ export default connect(
); );
} }
} }
)(withStyles(styles)(ListMe)); )(withStyles(styles)(ListMe)));

View File

@ -10,14 +10,7 @@ const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1), padding : theme.spacing(1),
width : '100%',
overflow : 'hidden',
cursor : 'auto',
display : 'flex'
},
actionButtons :
{
display : 'flex' display : 'flex'
}, },
divider : divider :
@ -43,7 +36,6 @@ const ListModerator = (props) =>
id : 'room.muteAll', id : 'room.muteAll',
defaultMessage : 'Mute all' defaultMessage : 'Mute all'
})} })}
className={classes.actionButton}
variant='contained' variant='contained'
color='secondary' color='secondary'
disabled={room.muteAllInProgress} disabled={room.muteAllInProgress}
@ -60,7 +52,6 @@ const ListModerator = (props) =>
id : 'room.stopAllVideo', id : 'room.stopAllVideo',
defaultMessage : 'Stop all video' defaultMessage : 'Stop all video'
})} })}
className={classes.actionButton}
variant='contained' variant='contained'
color='secondary' color='secondary'
disabled={room.stopAllVideoInProgress} disabled={room.stopAllVideoInProgress}
@ -77,7 +68,6 @@ const ListModerator = (props) =>
id : 'room.closeMeeting', id : 'room.closeMeeting',
defaultMessage : 'Close meeting' defaultMessage : 'Close meeting'
})} })}
className={classes.actionButton}
variant='contained' variant='contained'
color='secondary' color='secondary'
disabled={room.closeMeetingInProgress} disabled={room.closeMeetingInProgress}

View File

@ -3,42 +3,39 @@ import { connect } from 'react-redux';
import { makePeerConsumerSelector } from '../../Selectors'; import { makePeerConsumerSelector } from '../../Selectors';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MicIcon from '@material-ui/icons/Mic'; import Tooltip from '@material-ui/core/Tooltip';
import MicOffIcon from '@material-ui/icons/MicOff'; import VideocamIcon from '@material-ui/icons/Videocam';
import VideocamOffIcon from '@material-ui/icons/VideocamOff';
import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
import ScreenIcon from '@material-ui/icons/ScreenShare'; import ScreenIcon from '@material-ui/icons/ScreenShare';
import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
import ExitIcon from '@material-ui/icons/ExitToApp'; import ExitIcon from '@material-ui/icons/ExitToApp';
import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import EmptyAvatar from '../../../images/avatar-empty.jpeg';
import HandIcon from '../../../images/icon-hand-white.svg'; import PanIcon from '@material-ui/icons/PanTool';
const styles = (theme) => const styles = (theme) =>
({ ({
root : root :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'auto', cursor : 'auto',
display : 'flex' display : 'flex'
}, },
listPeer :
{
display : 'flex'
},
avatar : avatar :
{ {
borderRadius : '50%', borderRadius : '50%',
height : '2rem' height : '2rem',
marginTop : theme.spacing(1)
}, },
peerInfo : peerInfo :
{ {
fontSize : '1rem', fontSize : '1rem',
border : 'none',
display : 'flex', display : 'flex',
paddingLeft : theme.spacing(1), paddingLeft : theme.spacing(1),
flexGrow : 1, flexGrow : 1,
@ -46,52 +43,12 @@ const styles = (theme) =>
}, },
indicators : indicators :
{ {
left : 0, display : 'flex',
top : 0, padding : theme.spacing(1.5)
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center',
transition : 'opacity 0.3s'
}, },
icon : green :
{ {
flex : '0 0 auto', color : 'rgba(0, 153, 0, 1)'
margin : '0.3rem',
borderRadius : 2,
backgroundPosition : 'center',
backgroundSize : '75%',
backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 0.5)',
transitionProperty : 'opacity, background-color',
transitionDuration : '0.15s',
width : 'var(--media-control-button-size)',
height : 'var(--media-control-button-size)',
opacity : 0.85,
'&:hover' :
{
opacity : 1
},
'&.on' :
{
opacity : 1
},
'&.off' :
{
opacity : 0.2
},
'&.raise-hand' :
{
backgroundImage : `url(${HandIcon})`
}
},
controls :
{
float : 'right',
display : 'flex',
flexDirection : 'row',
justifyContent : 'flex-start',
alignItems : 'center'
} }
}); });
@ -104,11 +61,18 @@ const ListPeer = (props) =>
isModerator, isModerator,
peer, peer,
micConsumer, micConsumer,
webcamConsumer,
screenConsumer, screenConsumer,
children, children,
classes classes
} = props; } = props;
const webcamEnabled = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const micEnabled = ( const micEnabled = (
Boolean(micConsumer) && Boolean(micConsumer) &&
!micConsumer.locallyPaused && !micConsumer.locallyPaused &&
@ -131,21 +95,18 @@ const ListPeer = (props) =>
{peer.displayName} {peer.displayName}
</div> </div>
<div className={classes.indicators}> <div className={classes.indicators}>
{ peer.raiseHandState && { peer.raisedHand &&
<div className={ <PanIcon className={classes.green} />
classnames(
classes.icon, 'raise-hand', {
on : peer.raiseHandState,
off : !peer.raiseHandState
}
)
}
/>
} }
</div> </div>
{children} { screenConsumer &&
<div className={classes.controls}> <Tooltip
{ screenConsumer && title={intl.formatMessage({
id : 'tooltip.muteScreenSharing',
defaultMessage : 'Mute participant share'
})}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.muteScreenSharing', id : 'tooltip.muteScreenSharing',
@ -153,8 +114,10 @@ const ListPeer = (props) =>
})} })}
color={screenVisible ? 'primary' : 'secondary'} color={screenVisible ? 'primary' : 'secondary'}
disabled={peer.peerScreenInProgress} disabled={peer.peerScreenInProgress}
onClick={() => onClick={(e) =>
{ {
e.stopPropagation();
screenVisible ? screenVisible ?
roomClient.modifyPeerConsumer(peer.id, 'screen', true) : roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
roomClient.modifyPeerConsumer(peer.id, 'screen', false); roomClient.modifyPeerConsumer(peer.id, 'screen', false);
@ -166,7 +129,45 @@ const ListPeer = (props) =>
<ScreenOffIcon /> <ScreenOffIcon />
} }
</IconButton> </IconButton>
} </Tooltip>
}
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
placement='bottom'
>
<IconButton
aria-label={intl.formatMessage({
id : 'tooltip.muteParticipantVideo',
defaultMessage : 'Mute participant video'
})}
color={webcamEnabled ? 'primary' : 'secondary'}
disabled={peer.peerVideoInProgress}
onClick={(e) =>
{
e.stopPropagation();
webcamEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'webcam', true) :
roomClient.modifyPeerConsumer(peer.id, 'webcam', false);
}}
>
{ webcamEnabled ?
<VideocamIcon />
:
<VideocamOffIcon />
}
</IconButton>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.muteParticipant',
defaultMessage : 'Mute participant'
})}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.muteParticipant', id : 'tooltip.muteParticipant',
@ -174,35 +175,49 @@ const ListPeer = (props) =>
})} })}
color={micEnabled ? 'primary' : 'secondary'} color={micEnabled ? 'primary' : 'secondary'}
disabled={peer.peerAudioInProgress} disabled={peer.peerAudioInProgress}
onClick={() => onClick={(e) =>
{ {
e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) : roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false); roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}} }}
> >
{ micEnabled ? { micEnabled ?
<MicIcon /> <VolumeUpIcon />
: :
<MicOffIcon /> <VolumeOffIcon />
} }
</IconButton> </IconButton>
{ isModerator && </Tooltip>
{ isModerator &&
<Tooltip
title={intl.formatMessage({
id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant'
})}
placement='bottom'
>
<IconButton <IconButton
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
id : 'tooltip.kickParticipant', id : 'tooltip.kickParticipant',
defaultMessage : 'Kick out participant' defaultMessage : 'Kick out participant'
})} })}
disabled={peer.peerKickInProgress} disabled={peer.peerKickInProgress}
onClick={() => color='secondary'
onClick={(e) =>
{ {
e.stopPropagation();
roomClient.kickPeer(peer.id); roomClient.kickPeer(peer.id);
}} }}
> >
<ExitIcon /> <ExitIcon />
</IconButton> </IconButton>
} </Tooltip>
</div> }
{children}
</div> </div>
); );
}; };

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
passivePeersSelector, passivePeersSelector,
spotlightPeersSelector spotlightSortedPeersSelector
} from '../../Selectors'; } from '../../Selectors';
import classNames from 'classnames'; import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
@ -13,7 +13,6 @@ import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
import ListModerator from './ListModerator'; import ListModerator from './ListModerator';
import Volume from '../../Containers/Volume'; import Volume from '../../Containers/Volume';
import * as userRoles from '../../../reducers/userRoles';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -32,12 +31,10 @@ const styles = (theme) =>
}, },
listheader : listheader :
{ {
padding : theme.spacing(1),
fontWeight : 'bolder' fontWeight : 'bolder'
}, },
listItem : listItem :
{ {
padding : theme.spacing(1),
width : '100%', width : '100%',
overflow : 'hidden', overflow : 'hidden',
cursor : 'pointer', cursor : 'pointer',
@ -114,16 +111,20 @@ class ParticipantList extends React.PureComponent
defaultMessage='Participants in Spotlight' defaultMessage='Participants in Spotlight'
/> />
</li> </li>
{ spotlightPeers.map((peerId) => ( { spotlightPeers.map((peer) => (
<li <li
key={peerId} key={peer.id}
className={classNames(classes.listItem, { className={classNames(classes.listItem, {
selected : peerId === selectedPeerId selected : peer.id === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peerId)} onClick={() => roomClient.setSelectedPeer(peer.id)}
> >
<ListPeer id={peerId} advancedMode={advancedMode} isModerator={isModerator}> <ListPeer
<Volume small id={peerId} /> id={peer.id}
advancedMode={advancedMode}
isModerator={isModerator}
>
<Volume small id={peer.id} />
</ListPeer> </ListPeer>
</li> </li>
))} ))}
@ -135,16 +136,16 @@ class ParticipantList extends React.PureComponent
defaultMessage='Passive Participants' defaultMessage='Passive Participants'
/> />
</li> </li>
{ passivePeers.map((peerId) => ( { passivePeers.map((peer) => (
<li <li
key={peerId} key={peer.id}
className={classNames(classes.listItem, { className={classNames(classes.listItem, {
selected : peerId === selectedPeerId selected : peer.id === selectedPeerId
})} })}
onClick={() => roomClient.setSelectedPeer(peerId)} onClick={() => roomClient.setSelectedPeer(peer.id)}
> >
<ListPeer <ListPeer
id={peerId} id={peer.id}
advancedMode={advancedMode} advancedMode={advancedMode}
isModerator={isModerator} isModerator={isModerator}
/> />
@ -170,11 +171,12 @@ ParticipantList.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
isModerator : state.me.roles.includes(userRoles.MODERATOR) || isModerator :
state.me.roles.includes(userRoles.ADMIN), state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)),
passivePeers : passivePeersSelector(state), passivePeers : passivePeersSelector(state),
selectedPeerId : state.room.selectedPeerId, selectedPeerId : state.room.selectedPeerId,
spotlightPeers : spotlightPeersSelector(state) spotlightPeers : spotlightSortedPeersSelector(state)
}; };
}; };
@ -186,6 +188,7 @@ const ParticipantListContainer = withRoomContext(connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.room.spotlights === next.room.spotlights && prev.room.spotlights === next.room.spotlights &&

View File

@ -12,6 +12,12 @@ import Peer from '../Containers/Peer';
import SpeakerPeer from '../Containers/SpeakerPeer'; import SpeakerPeer from '../Containers/SpeakerPeer';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
const RATIO = 1.334;
const PADDING_V = 40;
const PADDING_H = 0;
const FILMSTRING_PADDING_V = 10;
const FILMSTRING_PADDING_H = 0;
const styles = () => const styles = () =>
({ ({
root : root :
@ -20,24 +26,22 @@ const styles = () =>
width : '100%', width : '100%',
display : 'grid', display : 'grid',
gridTemplateColumns : '1fr', gridTemplateColumns : '1fr',
gridTemplateRows : '1.6fr minmax(0, 0.4fr)' gridTemplateRows : '1fr 0.25fr'
}, },
speaker : speaker :
{ {
gridArea : '1 / 1 / 2 / 2', gridArea : '1 / 1 / 1 / 1',
display : 'flex', display : 'flex',
justifyContent : 'center', justifyContent : 'center',
alignItems : 'center', alignItems : 'center'
paddingTop : 40
}, },
filmStrip : filmStrip :
{ {
gridArea : '2 / 1 / 3 / 2' gridArea : '2 / 1 / 2 / 1'
}, },
filmItem : filmItem :
{ {
display : 'flex', display : 'flex',
marginLeft : '6px',
border : 'var(--peer-border)', border : 'var(--peer-border)',
'&.selected' : '&.selected' :
{ {
@ -45,8 +49,18 @@ const styles = () =>
}, },
'&.active' : '&.active' :
{ {
opacity : '0.6' borderColor : 'var(--selected-peer-border-color)'
} }
},
hiddenToolBar :
{
paddingTop : 0,
transition : 'padding .5s'
},
showingToolBar :
{
paddingTop : 60,
transition : 'padding .5s'
} }
}); });
@ -58,6 +72,8 @@ class Filmstrip extends React.PureComponent
this.resizeTimeout = null; this.resizeTimeout = null;
this.rootContainer = React.createRef();
this.activePeerContainer = React.createRef(); this.activePeerContainer = React.createRef();
this.filmStripContainer = React.createRef(); this.filmStripContainer = React.createRef();
@ -105,24 +121,38 @@ class Filmstrip extends React.PureComponent
{ {
const newState = {}; const newState = {};
const root = this.rootContainer.current;
if (!root)
return;
const availableWidth = root.clientWidth;
// Grid is:
// 4/5 speaker
// 1/5 filmstrip
const availableSpeakerHeight = (root.clientHeight * 0.8) -
(this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H);
const availableFilmstripHeight = root.clientHeight * 0.2;
const speaker = this.activePeerContainer.current; const speaker = this.activePeerContainer.current;
if (speaker) if (speaker)
{ {
let speakerWidth = (speaker.clientWidth - 100); let speakerWidth = (availableWidth - PADDING_H);
let speakerHeight = (speakerWidth / 4) * 3; let speakerHeight = speakerWidth / RATIO;
if (this.isSharingCamera(this.getActivePeerId())) if (this.isSharingCamera(this.getActivePeerId()))
{ {
speakerWidth /= 2; speakerWidth /= 2;
speakerHeight = (speakerWidth / 4) * 3; speakerHeight = speakerWidth / RATIO;
} }
if (speakerHeight > (speaker.clientHeight - 60)) if (speakerHeight > (availableSpeakerHeight - PADDING_V))
{ {
speakerHeight = (speaker.clientHeight - 60); speakerHeight = (availableSpeakerHeight - PADDING_V);
speakerWidth = (speakerHeight / 3) * 4; speakerWidth = speakerHeight * RATIO;
} }
newState.speakerWidth = speakerWidth; newState.speakerWidth = speakerWidth;
@ -133,14 +163,18 @@ class Filmstrip extends React.PureComponent
if (filmStrip) if (filmStrip)
{ {
let filmStripHeight = filmStrip.clientHeight - 10; let filmStripHeight = availableFilmstripHeight - FILMSTRING_PADDING_V;
let filmStripWidth = (filmStripHeight / 3) * 4; let filmStripWidth = filmStripHeight * RATIO;
if (filmStripWidth * this.props.boxes > (filmStrip.clientWidth - 50)) if (
(filmStripWidth * this.props.boxes) >
(availableWidth - FILMSTRING_PADDING_H)
)
{ {
filmStripWidth = (filmStrip.clientWidth - 50) / this.props.boxes; filmStripWidth = (availableWidth - FILMSTRING_PADDING_H) /
filmStripHeight = (filmStripWidth / 4) * 3; this.props.boxes;
filmStripHeight = filmStripWidth / RATIO;
} }
newState.filmStripWidth = filmStripWidth; newState.filmStripWidth = filmStripWidth;
@ -172,27 +206,21 @@ class Filmstrip extends React.PureComponent
window.removeEventListener('resize', this.updateDimensions); window.removeEventListener('resize', this.updateDimensions);
} }
componentWillUpdate(nextProps)
{
if (nextProps !== this.props)
{
if (
nextProps.activeSpeakerId != null &&
nextProps.activeSpeakerId !== this.props.myId
)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : nextProps.activeSpeakerId
});
}
}
}
componentDidUpdate(prevProps) componentDidUpdate(prevProps)
{ {
if (prevProps !== this.props) if (prevProps !== this.props)
{ {
if (
this.props.activeSpeakerId != null &&
this.props.activeSpeakerId !== this.props.myId
)
{
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lastSpeaker : this.props.activeSpeakerId
});
}
this.updateDimensions(); this.updateDimensions();
} }
} }
@ -205,6 +233,8 @@ class Filmstrip extends React.PureComponent
myId, myId,
advancedMode, advancedMode,
spotlights, spotlights,
toolbarsVisible,
permanentTopBar,
classes classes
} = this.props; } = this.props;
@ -223,7 +253,14 @@ class Filmstrip extends React.PureComponent
}; };
return ( return (
<div className={classes.root}> <div
className={classnames(
classes.root,
toolbarsVisible || permanentTopBar ?
classes.showingToolBar : classes.hiddenToolBar
)}
ref={this.rootContainer}
>
<div className={classes.speaker} ref={this.activePeerContainer}> <div className={classes.speaker} ref={this.activePeerContainer}>
{ peers[activePeerId] && { peers[activePeerId] &&
<SpeakerPeer <SpeakerPeer
@ -245,7 +282,7 @@ class Filmstrip extends React.PureComponent
<Me <Me
advancedMode={advancedMode} advancedMode={advancedMode}
style={peerStyle} style={peerStyle}
smallButtons smallContainer
/> />
</div> </div>
</Grid> </Grid>
@ -268,7 +305,7 @@ class Filmstrip extends React.PureComponent
advancedMode={advancedMode} advancedMode={advancedMode}
id={peerId} id={peerId}
style={peerStyle} style={peerStyle}
smallButtons smallContainer
/> />
</div> </div>
</Grid> </Grid>
@ -296,6 +333,8 @@ Filmstrip.propTypes = {
selectedPeerId : PropTypes.string, selectedPeerId : PropTypes.string,
spotlights : PropTypes.array.isRequired, spotlights : PropTypes.array.isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
toolbarsVisible : PropTypes.bool.isRequired,
permanentTopBar : PropTypes.bool,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
@ -308,7 +347,9 @@ const mapStateToProps = (state) =>
consumers : state.consumers, consumers : state.consumers,
myId : state.me.id, myId : state.me.id,
spotlights : state.room.spotlights, spotlights : state.room.spotlights,
boxes : videoBoxesSelector(state) boxes : videoBoxesSelector(state),
toolbarsVisible : state.room.toolbarsVisible,
permanentTopBar : state.settings.permanentTopBar
}; };
}; };
@ -322,6 +363,8 @@ export default withRoomContext(connect(
return ( return (
prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.activeSpeakerId === next.room.activeSpeakerId &&
prev.room.selectedPeerId === next.room.selectedPeerId && prev.room.selectedPeerId === next.room.selectedPeerId &&
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.consumers === next.consumers && prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights && prev.room.spotlights === next.room.spotlights &&

View File

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { micConsumerSelector } from '../Selectors'; import { passiveMicConsumerSelector } from '../Selectors';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import PeerAudio from './PeerAudio'; import PeerAudio from './PeerAudio';
const AudioPeers = (props) => const AudioPeers = (props) =>
{ {
const { const {
micConsumers micConsumers,
audioOutputDevice
} = props; } = props;
return ( return (
@ -19,6 +20,7 @@ const AudioPeers = (props) =>
<PeerAudio <PeerAudio
key={micConsumer.id} key={micConsumer.id}
audioTrack={micConsumer.track} audioTrack={micConsumer.track}
audioOutputDevice={audioOutputDevice}
/> />
); );
}) })
@ -29,12 +31,14 @@ const AudioPeers = (props) =>
AudioPeers.propTypes = AudioPeers.propTypes =
{ {
micConsumers : PropTypes.array micConsumers : PropTypes.array,
audioOutputDevice : PropTypes.string
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
micConsumers : micConsumerSelector(state) micConsumers : passiveMicConsumerSelector(state),
audioOutputDevice : state.settings.selectedAudioOutputDevice
}); });
const AudioPeersContainer = connect( const AudioPeersContainer = connect(
@ -45,7 +49,10 @@ const AudioPeersContainer = connect(
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.consumers === next.consumers prev.consumers === next.consumers &&
prev.room.spotlights === next.room.spotlights &&
prev.settings.selectedAudioOutputDevice ===
next.settings.selectedAudioOutputDevice
); );
} }
} }

View File

@ -10,6 +10,7 @@ export default class PeerAudio extends React.PureComponent
// Latest received audio track. // Latest received audio track.
// @type {MediaStreamTrack} // @type {MediaStreamTrack}
this._audioTrack = null; this._audioTrack = null;
this._audioOutputDevice = null;
} }
render() render()
@ -24,17 +25,21 @@ export default class PeerAudio extends React.PureComponent
componentDidMount() componentDidMount()
{ {
const { audioTrack } = this.props; const { audioTrack, audioOutputDevice } = this.props;
this._setTrack(audioTrack); this._setTrack(audioTrack);
this._setOutputDevice(audioOutputDevice);
} }
// eslint-disable-next-line camelcase componentDidUpdate(prevProps)
UNSAFE_componentWillReceiveProps(nextProps)
{ {
const { audioTrack } = nextProps; if (prevProps !== this.props)
{
const { audioTrack, audioOutputDevice } = this.props;
this._setTrack(audioTrack); this._setTrack(audioTrack);
this._setOutputDevice(audioOutputDevice);
}
} }
_setTrack(audioTrack) _setTrack(audioTrack)
@ -60,9 +65,23 @@ export default class PeerAudio extends React.PureComponent
audio.srcObject = null; audio.srcObject = null;
} }
} }
_setOutputDevice(audioOutputDevice)
{
if (this._audioOutputDevice === audioOutputDevice)
return;
this._audioOutputDevice = audioOutputDevice;
const { audio } = this.refs;
if (audioOutputDevice && typeof audio.setSinkId === 'function')
audio.setSinkId(audioOutputDevice);
}
} }
PeerAudio.propTypes = PeerAudio.propTypes =
{ {
audioTrack : PropTypes.any audioTrack : PropTypes.any,
audioOutputDevice : PropTypes.string
}; };

View File

@ -23,7 +23,8 @@ import VideoWindow from './VideoWindow/VideoWindow';
import LockDialog from './AccessControl/LockDialog/LockDialog'; import LockDialog from './AccessControl/LockDialog/LockDialog';
import Settings from './Settings/Settings'; import Settings from './Settings/Settings';
import TopBar from './Controls/TopBar'; import TopBar from './Controls/TopBar';
import MobileControls from './Controls/MobileControls'; import WakeLock from 'react-wakelock-react16';
import ExtraVideo from './Controls/ExtraVideo';
const TIMEOUT = 5 * 1000; const TIMEOUT = 5 * 1000;
@ -139,9 +140,9 @@ class Room extends React.PureComponent
{ {
const { const {
room, room,
browser,
advancedMode, advancedMode,
toolAreaOpen, toolAreaOpen,
isMobile,
toggleToolArea, toggleToolArea,
classes, classes,
theme theme
@ -204,12 +205,12 @@ class Room extends React.PureComponent
</Hidden> </Hidden>
</nav> </nav>
<View advancedMode={advancedMode} /> { browser.platform === 'mobile' && browser.os !== 'ios' &&
<WakeLock />
{ isMobile &&
<MobileControls />
} }
<View advancedMode={advancedMode} />
{ room.lockDialogOpen && { room.lockDialogOpen &&
<LockDialog /> <LockDialog />
} }
@ -217,6 +218,10 @@ class Room extends React.PureComponent
{ room.settingsOpen && { room.settingsOpen &&
<Settings /> <Settings />
} }
{ room.extraVideoOpen &&
<ExtraVideo />
}
</div> </div>
); );
} }
@ -225,10 +230,10 @@ class Room extends React.PureComponent
Room.propTypes = Room.propTypes =
{ {
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
browser : PropTypes.object.isRequired,
advancedMode : PropTypes.bool.isRequired, advancedMode : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired,
setToolbarsVisible : PropTypes.func.isRequired, setToolbarsVisible : PropTypes.func.isRequired,
isMobile : PropTypes.bool,
toggleToolArea : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
@ -237,9 +242,9 @@ Room.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
room : state.room, room : state.room,
browser : state.me.browser,
advancedMode : state.settings.advancedMode, advancedMode : state.settings.advancedMode,
toolAreaOpen : state.toolarea.toolAreaOpen, toolAreaOpen : state.toolarea.toolAreaOpen
isMobile : state.me.isMobile
}); });
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
@ -263,9 +268,9 @@ export default connect(
{ {
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me.browser === next.me.browser &&
prev.settings.advancedMode === next.settings.advancedMode && prev.settings.advancedMode === next.settings.advancedMode &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
prev.me.isMobile === next.me.isMobile
); );
} }
} }

View File

@ -12,6 +12,10 @@ const peersKeySelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.keys(peers) (peers) => Object.keys(peers)
); );
const peersValueSelector = createSelector(
peersSelector,
(peers) => Object.values(peers)
);
export const lobbyPeersKeySelector = createSelector( export const lobbyPeersKeySelector = createSelector(
lobbyPeersSelector, lobbyPeersSelector,
@ -33,6 +37,11 @@ export const screenProducersSelector = createSelector(
(producers) => Object.values(producers).filter((producer) => producer.source === 'screen') (producers) => Object.values(producers).filter((producer) => producer.source === 'screen')
); );
export const extraVideoProducersSelector = createSelector(
producersSelect,
(producers) => Object.values(producers).filter((producer) => producer.source === 'extravideo')
);
export const micProducerSelector = createSelector( export const micProducerSelector = createSelector(
producersSelect, producersSelect,
(producers) => Object.values(producers).find((producer) => producer.source === 'mic') (producers) => Object.values(producers).find((producer) => producer.source === 'mic')
@ -63,6 +72,33 @@ export const screenConsumerSelector = createSelector(
(consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen') (consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen')
); );
export const spotlightScreenConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'screen' && spotlights.includes(consumer.peerId)
)
);
export const spotlightExtraVideoConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'extravideo' && spotlights.includes(consumer.peerId)
)
);
export const passiveMicConsumerSelector = createSelector(
spotlightsSelector,
consumersSelect,
(spotlights, consumers) =>
Object.values(consumers).filter(
(consumer) => consumer.source === 'mic' && !spotlights.includes(consumer.peerId)
)
);
export const spotlightsLengthSelector = createSelector( export const spotlightsLengthSelector = createSelector(
spotlightsSelector, spotlightsSelector,
(spotlights) => spotlights.length (spotlights) => spotlights.length
@ -74,35 +110,60 @@ export const spotlightPeersSelector = createSelector(
(spotlights, peers) => peers.filter((peerId) => spotlights.includes(peerId)) (spotlights, peers) => peers.filter((peerId) => spotlights.includes(peerId))
); );
export const spotlightSortedPeersSelector = createSelector(
spotlightsSelector,
peersValueSelector,
(spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id))
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
export const peersLengthSelector = createSelector( export const peersLengthSelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.values(peers).length (peers) => Object.values(peers).length
); );
export const passivePeersSelector = createSelector( export const passivePeersSelector = createSelector(
peersKeySelector, peersValueSelector,
spotlightsSelector, spotlightsSelector,
(peers, spotlights) => peers.filter((peerId) => !spotlights.includes(peerId)) (peers, spotlights) => peers.filter((peer) => !spotlights.includes(peer.id))
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
);
export const raisedHandsSelector = createSelector(
peersValueSelector,
(peers) => peers.reduce((a, b) => (a + (b.raisedHand ? 1 : 0)), 0)
); );
export const videoBoxesSelector = createSelector( export const videoBoxesSelector = createSelector(
spotlightsLengthSelector, spotlightsLengthSelector,
screenProducersSelector, screenProducersSelector,
screenConsumerSelector, spotlightScreenConsumerSelector,
(spotlightsLength, screenProducers, screenConsumers) => extraVideoProducersSelector,
spotlightsLength + 1 + screenProducers.length + screenConsumers.length spotlightExtraVideoConsumerSelector,
(
spotlightsLength,
screenProducers,
screenConsumers,
extraVideoProducers,
extraVideoConsumers
) =>
spotlightsLength + 1 + screenProducers.length +
screenConsumers.length + extraVideoProducers.length +
extraVideoConsumers.length
); );
export const meProducersSelector = createSelector( export const meProducersSelector = createSelector(
micProducerSelector, micProducerSelector,
webcamProducerSelector, webcamProducerSelector,
screenProducerSelector, screenProducerSelector,
(micProducer, webcamProducer, screenProducer) => extraVideoProducersSelector,
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
{ {
return { return {
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer screenProducer,
extraVideoProducers
}; };
} }
); );
@ -125,8 +186,10 @@ export const makePeerConsumerSelector = () =>
consumersArray.find((consumer) => consumer.source === 'webcam'); consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer = const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen'); consumersArray.find((consumer) => consumer.source === 'screen');
const extraVideoConsumers =
consumersArray.filter((consumer) => consumer.source === 'extravideo');
return { micConsumer, webcamConsumer, screenConsumer }; return { micConsumer, webcamConsumer, screenConsumer, extraVideoConsumers };
} }
); );
}; };

View File

@ -0,0 +1,125 @@
import React from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
const styles = (theme) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const AdvancedSettings = ({
roomClient,
settings,
onToggleAdvancedMode,
onToggleNotificationSounds,
classes
}) =>
{
const intl = useIntl();
return (
<React.Fragment>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
label={intl.formatMessage({
id : 'settings.advancedMode',
defaultMessage : 'Advanced mode'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.notificationSounds} onChange={onToggleNotificationSounds} value='notificationSounds' />}
label={intl.formatMessage({
id : 'settings.notificationSounds',
defaultMessage : 'Notification sounds'
})}
/>
{ !window.config.lockLastN &&
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.lastN || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeMaxSpotlights(event.target.value);
}}
name='Last N'
autoWidth
className={classes.selectEmpty}
>
{ Array.from(
{ length: window.config.maxLastN || 10 },
(_, i) => i + 1
).map((lastN) =>
{
return (
<MenuItem key={lastN} value={lastN}>
{lastN}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.lastn'
defaultMessage='Number of visible videos'
/>
</FormHelperText>
</FormControl>
</form>
}
</React.Fragment>
);
};
AdvancedSettings.propTypes =
{
roomClient : PropTypes.any.isRequired,
settings : PropTypes.object.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
onToggleNotificationSounds : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
settings : state.settings
});
const mapDispatchToProps = {
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
onToggleNotificationSounds : settingsActions.toggleNotificationSounds
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.settings === next.settings
);
}
}
)(withStyles(styles)(AdvancedSettings)));

View File

@ -0,0 +1,143 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
const styles = (theme) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const AppearenceSettings = ({
room,
settings,
onTogglePermanentTopBar,
onToggleHiddenControls,
handleChangeMode,
classes
}) =>
{
const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
return (
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={room.mode || ''}
onChange={(event) =>
{
if (event.target.value)
handleChangeMode(event.target.value);
}}
name={intl.formatMessage({
id : 'settings.layout',
defaultMessage : 'Room layout'
})}
autoWidth
className={classes.selectEmpty}
>
{ modes.map((mode, index) =>
{
return (
<MenuItem key={index} value={mode.value}>
{mode.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.selectRoomLayout'
defaultMessage='Select room layout'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />}
label={intl.formatMessage({
id : 'settings.permanentTopBar',
defaultMessage : 'Permanent top bar'
})}
/>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.hiddenControls} onChange={onToggleHiddenControls} value='hiddenControls' />}
label={intl.formatMessage({
id : 'settings.hiddenControls',
defaultMessage : 'Hidden media controls'
})}
/>
</React.Fragment>
);
};
AppearenceSettings.propTypes =
{
room : appPropTypes.Room.isRequired,
settings : PropTypes.object.isRequired,
onTogglePermanentTopBar : PropTypes.func.isRequired,
onToggleHiddenControls : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
({
room : state.room,
settings : state.settings
});
const mapDispatchToProps = {
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
onToggleHiddenControls : settingsActions.toggleHiddenControls,
handleChangeMode : roomActions.setDisplayMode
};
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.room === next.room &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(AppearenceSettings));

View File

@ -0,0 +1,341 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
const styles = (theme) =>
({
setting :
{
padding : theme.spacing(2)
},
formControl :
{
display : 'flex'
}
});
const MediaSettings = ({
setEchoCancellation,
setAutoGainControl,
setNoiseSuppression,
roomClient,
me,
settings,
classes
}) =>
{
const intl = useIntl();
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
webcams = Object.values(me.webcamDevices);
else
webcams = [];
let audioDevices;
if (me.audioDevices)
audioDevices = Object.values(me.audioDevices);
else
audioDevices = [];
let audioOutputDevices;
if (me.audioOutputDevices)
audioOutputDevices = Object.values(me.audioOutputDevices);
else
audioOutputDevices = [];
return (
<React.Fragment>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedWebcam || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeWebcam(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={webcams.length === 0 || me.webcamInProgress}
>
{ webcams.map((webcam, index) =>
{
return (
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ webcams.length > 0 ?
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
</form>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audio',
defaultMessage : 'Audio device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioDevices.length === 0 || me.audioInProgress}
>
{ audioDevices.map((audio, index) =>
{
return (
<MenuItem key={index} value={audio.deviceId}>{audio.label}</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudio',
defaultMessage : 'Select audio device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device'
})
}
</FormHelperText>
</FormControl>
</form>
{ 'audioOutputSupportedBrowsers' in window.config &&
window.config.audioOutputSupportedBrowsers.includes(me.browser.name) &&
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.selectedAudioOutputDevice || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeAudioOutputDevice(event.target.value);
}}
displayEmpty
name={intl.formatMessage({
id : 'settings.audioOutput',
defaultMessage : 'Audio output device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioOutputDevices.length === 0 || me.audioOutputInProgress}
>
{ audioOutputDevices.map((audioOutput, index) =>
{
return (
<MenuItem
key={index}
value={audioOutput.deviceId}
>
{audioOutput.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
{ audioOutputDevices.length > 0 ?
intl.formatMessage({
id : 'settings.selectAudioOutput',
defaultMessage : 'Select audio output device'
})
:
intl.formatMessage({
id : 'settings.cantSelectAudioOutput',
defaultMessage : 'Unable to select audio output device'
})
}
</FormHelperText>
</FormControl>
</form>
}
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeVideoResolution(event.target.value);
}}
name='Video resolution'
autoWidth
className={classes.selectEmpty}
>
{ resolutions.map((resolution, index) =>
{
return (
<MenuItem key={index} value={resolution.value}>
{resolution.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.echoCancellation} onChange={
(event) => {
setEchoCancellation(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.echoCancellation',
defaultMessage : 'Echo Cancellation'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.autoGainControl} onChange={
(event) => {
setAutoGainControl(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.autoGainControl',
defaultMessage : 'Auto Gain Control'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.noiseSuppression} onChange={
(event) => {
setNoiseSuppression(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.noiseSuppression',
defaultMessage : 'Noise Suppression'
})}
/>
</form>
</React.Fragment>
);
};
MediaSettings.propTypes =
{
roomClient : PropTypes.any.isRequired,
setEchoCancellation : PropTypes.func.isRequired,
setAutoGainControl : PropTypes.func.isRequired,
setNoiseSuppression : PropTypes.func.isRequired,
me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
settings : state.settings
};
};
const mapDispatchToProps = {
setEchoCancellation : settingsActions.setEchoCancellation,
setAutoGainControl : settingsActions.toggleAutoGainControl,
setNoiseSuppression : settingsActions.toggleNoiseSuppression
};
export default withRoomContext(connect(
mapStateToProps,
mapDispatchToProps,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.me === next.me &&
prev.settings === next.settings
);
}
}
)(withStyles(styles)(MediaSettings)));

View File

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

View File

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

View File

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

View File

@ -3,6 +3,16 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import EditableInput from '../Controls/EditableInput'; import EditableInput from '../Controls/EditableInput';
import Logger from '../../Logger';
import { green, yellow, orange, red } from '@material-ui/core/colors';
import SignalCellularOffIcon from '@material-ui/icons/SignalCellularOff';
import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar';
import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar';
import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar';
import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar';
import SignalCellularAltIcon from '@material-ui/icons/SignalCellularAlt';
const logger = new Logger('VideoView');
const styles = (theme) => const styles = (theme) =>
({ ({
@ -60,24 +70,32 @@ const styles = (theme) =>
{ {
display : 'flex', display : 'flex',
transitionProperty : 'opacity', transitionProperty : 'opacity',
transitionDuration : '.15s', transitionDuration : '.15s'
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
}
}, },
box : box :
{ {
padding : theme.spacing(0.5), padding : theme.spacing(0.5),
borderRadius : 2, borderRadius : 2,
backgroundColor : 'rgba(0, 0, 0, 0.25)', '& p' :
'& p' :
{ {
userSelect : 'none', userSelect : 'none',
margin : 0, margin : 0,
color : 'rgba(255, 255, 255, 0.7)', color : 'rgba(255, 255, 255, 0.7)',
fontSize : '0.8em' fontSize : '0.8em'
},
'&.left' :
{
backgroundColor : 'rgba(0, 0, 0, 0.25)'
},
'&.right' :
{
marginLeft : 'auto',
width : 30
},
'&.hidden' :
{
opacity : 0,
transitionDuration : '0s'
} }
}, },
peer : peer :
@ -119,6 +137,10 @@ class VideoView extends React.PureComponent
videoHeight : null videoHeight : null
}; };
// Latest received audio track
// @type {MediaStreamTrack}
this._audioTrack = null;
// Latest received video track. // Latest received video track.
// @type {MediaStreamTrack} // @type {MediaStreamTrack}
this._videoTrack = null; this._videoTrack = null;
@ -138,8 +160,8 @@ class VideoView extends React.PureComponent
advancedMode, advancedMode,
videoVisible, videoVisible,
videoMultiLayer, videoMultiLayer,
// audioScore, audioScore,
// videoScore, videoScore,
// consumerSpatialLayers, // consumerSpatialLayers,
// consumerTemporalLayers, // consumerTemporalLayers,
consumerCurrentSpatialLayer, consumerCurrentSpatialLayer,
@ -158,15 +180,67 @@ class VideoView extends React.PureComponent
videoHeight videoHeight
} = this.state; } = this.state;
let quality = <SignalCellularOffIcon style={{ color: red[500] }}/>;
if (videoScore || audioScore)
{
const score = videoScore ? videoScore : audioScore;
switch (score.producerScore)
{
case 0:
case 1:
{
quality = <SignalCellular0BarIcon style={{ color: red[500] }}/>;
break;
}
case 2:
case 3:
{
quality = <SignalCellular1BarIcon style={{ color: red[500] }}/>;
break;
}
case 4:
case 5:
case 6:
{
quality = <SignalCellular2BarIcon style={{ color: orange[500] }}/>;
break;
}
case 7:
case 8:
case 9:
{
quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>;
break;
}
case 10:
{
quality = null; // <SignalCellularAltIcon style={{ color: green[500] }}/>;
break;
}
default:
{
break;
}
}
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
<div className={classes.info}> <div className={classes.info}>
<div className={classnames(classes.media, <div className={classes.media}>
{ <div className={classnames(classes.box, 'left', { hidden: !advancedMode })}>
hidden : !advancedMode
})}
>
<div className={classes.box}>
{ audioCodec && <p>{audioCodec}</p> } { audioCodec && <p>{audioCodec}</p> }
{ videoCodec && { videoCodec &&
@ -187,6 +261,13 @@ class VideoView extends React.PureComponent
<p>{videoWidth}x{videoHeight}</p> <p>{videoWidth}x{videoHeight}</p>
} }
</div> </div>
{ !isMe &&
<div className={classnames(classes.box, 'right')}>
{
quality
}
</div>
}
</div> </div>
{ showPeerInfo && { showPeerInfo &&
@ -218,7 +299,7 @@ class VideoView extends React.PureComponent
</div> </div>
<video <video
ref='video' ref='videoElement'
className={classnames(classes.video, { className={classnames(classes.video, {
hidden : !videoVisible, hidden : !videoVisible,
'isMe' : isMe && !isScreen, 'isMe' : isMe && !isScreen,
@ -226,6 +307,16 @@ class VideoView extends React.PureComponent
})} })}
autoPlay autoPlay
playsInline playsInline
muted
controls={false}
/>
<audio
ref='audioElement'
autoPlay
playsInline
muted={isMe}
controls={false}
/> />
{children} {children}
@ -235,52 +326,87 @@ class VideoView extends React.PureComponent
componentDidMount() componentDidMount()
{ {
const { videoTrack } = this.props; const { videoTrack, audioTrack } = this.props;
this._setTracks(videoTrack); this._setTracks(videoTrack, audioTrack);
} }
componentWillUnmount() componentWillUnmount()
{ {
clearInterval(this._videoResolutionTimer); clearInterval(this._videoResolutionTimer);
const { videoElement } = this.refs;
if (videoElement)
{
videoElement.oncanplay = null;
videoElement.onplay = null;
videoElement.onpause = null;
}
} }
// eslint-disable-next-line camelcase componentDidUpdate(prevProps)
UNSAFE_componentWillReceiveProps(nextProps)
{ {
const { videoTrack } = nextProps; if (prevProps !== this.props)
{
this._setTracks(videoTrack); const { videoTrack, audioTrack } = this.props;
this._setTracks(videoTrack, audioTrack);
}
} }
_setTracks(videoTrack) _setTracks(videoTrack, audioTrack)
{ {
if (this._videoTrack === videoTrack) if (this._videoTrack === videoTrack && this._audioTrack === audioTrack)
return; return;
this._videoTrack = videoTrack; this._videoTrack = videoTrack;
this._audioTrack = audioTrack;
clearInterval(this._videoResolutionTimer); clearInterval(this._videoResolutionTimer);
this._hideVideoResolution(); this._hideVideoResolution();
const { video } = this.refs; const { videoElement, audioElement } = this.refs;
if (videoTrack) if (videoTrack)
{ {
const stream = new MediaStream(); const stream = new MediaStream();
if (videoTrack) stream.addTrack(videoTrack);
stream.addTrack(videoTrack);
video.srcObject = stream; videoElement.srcObject = stream;
if (videoTrack) videoElement.oncanplay = () => this.setState({ videoCanPlay: true });
this._showVideoResolution();
videoElement.onplay = () =>
{
audioElement.play()
.catch((error) => logger.warn('audioElement.play() [error:"%o]', error));
};
videoElement.play()
.catch((error) => logger.warn('videoElement.play() [error:"%o]', error));
this._showVideoResolution();
} }
else else
{ {
video.srcObject = null; videoElement.srcObject = null;
}
if (audioTrack)
{
const stream = new MediaStream();
stream.addTrack(audioTrack);
audioElement.srcObject = stream;
audioElement.play()
.catch((error) => logger.warn('audioElement.play() [error:"%o]', error));
}
else
{
audioElement.srcObject = null;
} }
} }
@ -289,16 +415,19 @@ class VideoView extends React.PureComponent
this._videoResolutionTimer = setInterval(() => this._videoResolutionTimer = setInterval(() =>
{ {
const { videoWidth, videoHeight } = this.state; const { videoWidth, videoHeight } = this.state;
const { video } = this.refs; const { videoElement } = this.refs;
// Don't re-render if nothing changed. // Don't re-render if nothing changed.
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight) if (
videoElement.videoWidth === videoWidth &&
videoElement.videoHeight === videoHeight
)
return; return;
this.setState( this.setState(
{ {
videoWidth : video.videoWidth, videoWidth : videoElement.videoWidth,
videoHeight : video.videoHeight videoHeight : videoElement.videoHeight
}); });
}, 1000); }, 1000);
} }
@ -318,6 +447,7 @@ VideoView.propTypes =
videoContain : PropTypes.bool, videoContain : PropTypes.bool,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
videoTrack : PropTypes.any, videoTrack : PropTypes.any,
audioTrack : PropTypes.any,
videoVisible : PropTypes.bool.isRequired, videoVisible : PropTypes.bool.isRequired,
consumerSpatialLayers : PropTypes.number, consumerSpatialLayers : PropTypes.number,
consumerTemporalLayers : PropTypes.number, consumerTemporalLayers : PropTypes.number,

View File

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

View File

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

View File

@ -24,8 +24,10 @@ export default function()
return { return {
flag, flag,
name : browser.getBrowserName(), os : browser.getOSName(true), // ios, android, linux...
version : browser.getBrowserVersion(), platform : browser.getPlatformType(true), // mobile, desktop, tablet
bowser : browser name : browser.getBrowserName(true),
version : browser.getBrowserVersion(),
bowser : browser
}; };
} }

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -34,7 +34,11 @@ import messagesPortuguese from './translations/pt';
import messagesChinese from './translations/cn'; import messagesChinese from './translations/cn';
import messagesSpanish from './translations/es'; import messagesSpanish from './translations/es';
import messagesCroatian from './translations/hr'; import messagesCroatian from './translations/hr';
import messagesCzech from './translations/cz'; import messagesCzech from './translations/cs';
import messagesItalian from './translations/it';
import messagesUkrainian from './translations/uk';
import messagesTurkish from './translations/tr';
import messagesLatvian from './translations/lv';
import './index.css'; import './index.css';
@ -57,7 +61,11 @@ const messages =
'zh' : messagesChinese, 'zh' : messagesChinese,
'es' : messagesSpanish, 'es' : messagesSpanish,
'hr' : messagesCroatian, 'hr' : messagesCroatian,
'cz' : messagesCzech 'cs' : messagesCzech,
'it' : messagesItalian,
'uk' : messagesUkrainian,
'tr' : messagesTurkish,
'lv' : messagesLatvian
}; };
const locale = navigator.language.split(/[-_]/)[0]; // language without region code const locale = navigator.language.split(/[-_]/)[0]; // language without region code
@ -104,8 +112,6 @@ function run()
const accessCode = parameters.get('code'); const accessCode = parameters.get('code');
const produce = parameters.get('produce') !== 'false'; 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 forceTcp = parameters.get('forceTcp') === 'true';
const displayName = parameters.get('displayName'); const displayName = parameters.get('displayName');
const muted = parameters.get('muted') === 'true'; const muted = parameters.get('muted') === 'true';
@ -125,8 +131,6 @@ function run()
peerId, peerId,
accessCode, accessCode,
device, device,
useSimulcast,
useSharingSimulcast,
produce, produce,
forceTcp, forceTcp,
displayName, displayName,

View File

@ -30,6 +30,11 @@ const chat = (state = [], action) =>
return [ ...state, ...chatHistory ]; return [ ...state, ...chatHistory ];
} }
case 'CLEAR_CHAT':
{
return [];
}
default: default:
return state; return state;
} }

View File

@ -85,6 +85,9 @@ const files = (state = {}, action) =>
return { ...state, [magnetUri]: newFile }; return { ...state, [magnetUri]: newFile };
} }
case 'CLEAR_FILES':
return {};
default: default:
return state; return state;
} }

View File

@ -44,7 +44,7 @@ const lobbyPeers = (state = {}, action) =>
if (!oldLobbyPeer) if (!oldLobbyPeer)
{ {
// Tried to update non-existant lobbyPeer. Has probably been promoted, or left. // Tried to update non-existent lobbyPeer. Has probably been promoted, or left.
return state; return state;
} }

View File

@ -1,11 +1,9 @@
import * as userRoles from './userRoles';
const initialState = const initialState =
{ {
id : null, id : null,
picture : null, picture : null,
isMobile : false, browser : null,
roles : [ userRoles.ALL ], roles : [ 'normal' ], // Default role
canSendMic : false, canSendMic : false,
canSendWebcam : false, canSendWebcam : false,
canShareScreen : false, canShareScreen : false,
@ -17,8 +15,8 @@ const initialState =
screenShareInProgress : false, screenShareInProgress : false,
displayNameInProgress : false, displayNameInProgress : false,
loginEnabled : false, loginEnabled : false,
raiseHand : false, raisedHand : false,
raiseHandInProgress : false, raisedHandInProgress : false,
loggedIn : false, loggedIn : false,
isSpeaking : false isSpeaking : false
}; };
@ -41,9 +39,11 @@ const me = (state = initialState, action) =>
}; };
} }
case 'SET_IS_MOBILE': case 'SET_BROWSER':
{ {
return { ...state, isMobile: true }; const { browser } = action.payload;
return { ...state, browser };
} }
case 'LOGGED_IN': case 'LOGGED_IN':
@ -99,6 +99,13 @@ const me = (state = initialState, action) =>
return { ...state, audioDevices: devices }; return { ...state, audioDevices: devices };
} }
case 'SET_AUDIO_OUTPUT_DEVICES':
{
const { devices } = action.payload;
return { ...state, audioOutputDevices: devices };
}
case 'SET_WEBCAM_DEVICES': case 'SET_WEBCAM_DEVICES':
{ {
const { devices } = action.payload; const { devices } = action.payload;
@ -127,18 +134,18 @@ const me = (state = initialState, action) =>
return { ...state, screenShareInProgress: flag }; return { ...state, screenShareInProgress: flag };
} }
case 'SET_MY_RAISE_HAND_STATE': case 'SET_RAISED_HAND':
{ {
const { flag } = action.payload; const { flag } = action.payload;
return { ...state, raiseHand: flag }; return { ...state, raisedHand: flag };
} }
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS': case 'SET_RAISED_HAND_IN_PROGRESS':
{ {
const { flag } = action.payload; const { flag } = action.payload;
return { ...state, raiseHandInProgress: flag }; return { ...state, raisedHandInProgress: flag };
} }
case 'SET_DISPLAY_NAME_IN_PROGRESS': case 'SET_DISPLAY_NAME_IN_PROGRESS':

View File

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

View File

@ -1,27 +1,48 @@
const initialState = const initialState =
{ {
name : '', name : '',
state : 'new', // new/connecting/connected/disconnected/closed, // new/connecting/connected/disconnected/closed,
locked : false, state : 'new',
inLobby : false, locked : false,
signInRequired : false, inLobby : false,
accessCode : '', // access code to the room if locked and joinByAccessCode == true signInRequired : false,
joinByAccessCode : true, // if true: accessCode is a possibility to open the room overRoomLimit : false,
activeSpeakerId : null, // access code to the room if locked and joinByAccessCode == true
torrentSupport : false, accessCode : '',
showSettings : false, // if true: accessCode is a possibility to open the room
fullScreenConsumer : null, // ConsumerID joinByAccessCode : true,
windowConsumer : null, // ConsumerID activeSpeakerId : null,
toolbarsVisible : true, torrentSupport : false,
mode : 'democratic', showSettings : false,
selectedPeerId : null, fullScreenConsumer : null, // ConsumerID
spotlights : [], windowConsumer : null, // ConsumerID
settingsOpen : false, toolbarsVisible : true,
lockDialogOpen : false, mode : window.config.defaultLayout || 'democratic',
joined : false, selectedPeerId : null,
muteAllInProgress : false, spotlights : [],
stopAllVideoInProgress : false, settingsOpen : false,
closeMeetingInProgress : false extraVideoOpen : false,
currentSettingsTab : 'media', // media, appearence, advanced
lockDialogOpen : false,
joined : false,
muteAllInProgress : false,
lobbyPeersPromotionInProgress : false,
stopAllVideoInProgress : false,
closeMeetingInProgress : false,
clearChatInProgress : false,
clearFileSharingInProgress : false,
userRoles : { NORMAL: 'normal' }, // Default role
permissionsFromRoles : {
CHANGE_ROOM_LOCK : [],
PROMOTE_PEER : [],
SEND_CHAT : [],
MODERATE_CHAT : [],
SHARE_SCREEN : [],
EXTRA_VIDEO : [],
SHARE_FILE : [],
MODERATE_FILES : [],
MODERATE_ROOM : []
}
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -68,7 +89,12 @@ const room = (state = initialState, action) =>
return { ...state, signInRequired }; return { ...state, signInRequired };
} }
case 'SET_OVER_ROOM_LIMIT':
{
const { overRoomLimit } = action.payload;
return { ...state, overRoomLimit };
}
case 'SET_ACCESS_CODE': case 'SET_ACCESS_CODE':
{ {
const { accessCode } = action.payload; const { accessCode } = action.payload;
@ -97,6 +123,20 @@ const room = (state = initialState, action) =>
return { ...state, settingsOpen }; return { ...state, settingsOpen };
} }
case 'SET_EXTRA_VIDEO_OPEN':
{
const { extraVideoOpen } = action.payload;
return { ...state, extraVideoOpen };
}
case 'SET_SETTINGS_TAB':
{
const { tab } = action.payload;
return { ...state, currentSettingsTab: tab };
}
case 'SET_ROOM_ACTIVE_SPEAKER': case 'SET_ROOM_ACTIVE_SPEAKER':
{ {
const { peerId } = action.payload; const { peerId } = action.payload;
@ -166,6 +206,9 @@ const room = (state = initialState, action) =>
return { ...state, spotlights }; return { ...state, spotlights };
} }
case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS':
return { ...state, lobbyPeersPromotionInProgress: action.payload.flag };
case 'MUTE_ALL_IN_PROGRESS': case 'MUTE_ALL_IN_PROGRESS':
return { ...state, muteAllInProgress: action.payload.flag }; return { ...state, muteAllInProgress: action.payload.flag };
@ -175,6 +218,26 @@ const room = (state = initialState, action) =>
case 'CLOSE_MEETING_IN_PROGRESS': case 'CLOSE_MEETING_IN_PROGRESS':
return { ...state, closeMeetingInProgress: action.payload.flag }; return { ...state, closeMeetingInProgress: action.payload.flag };
case 'CLEAR_CHAT_IN_PROGRESS':
return { ...state, clearChatInProgress: action.payload.flag };
case 'CLEAR_FILE_SHARING_IN_PROGRESS':
return { ...state, clearFileSharingInProgress: action.payload.flag };
case 'SET_USER_ROLES':
{
const { userRoles } = action.payload;
return { ...state, userRoles };
}
case 'SET_PERMISSIONS_FROM_ROLES':
{
const { permissionsFromRoles } = action.payload;
return { ...state, permissionsFromRoles };
}
default: default:
return state; return state;
} }

View File

@ -11,9 +11,12 @@ const initialState =
echoCancellation : true, echoCancellation : true,
noiseSuppression : true, noiseSuppression : true,
sampleSize : 16, sampleSize : 16,
resolution : 'medium', // low, medium, high, veryhigh, ultra // low, medium, high, veryhigh, ultra
resolution : window.config.defaultResolution || 'medium',
lastN : 4, lastN : 4,
permanentTopBar : true permanentTopBar : true,
hiddenControls : false,
notificationSounds : true
}; };
const settings = (state = initialState, action) => const settings = (state = initialState, action) =>
@ -30,6 +33,11 @@ const settings = (state = initialState, action) =>
return { ...state, selectedAudioDevice: action.payload.deviceId }; return { ...state, selectedAudioDevice: action.payload.deviceId };
} }
case 'CHANGE_AUDIO_OUTPUT_DEVICE':
{
return { ...state, selectedAudioOutputDevice: action.payload.deviceId };
}
case 'SET_DISPLAY_NAME': case 'SET_DISPLAY_NAME':
{ {
const { displayName } = action.payload; const { displayName } = action.payload;
@ -135,6 +143,20 @@ const settings = (state = initialState, action) =>
return { ...state, permanentTopBar }; return { ...state, permanentTopBar };
} }
case 'TOGGLE_HIDDEN_CONTROLS':
{
const hiddenControls = !state.hiddenControls;
return { ...state, hiddenControls };
}
case 'TOGGLE_NOTIFICATION_SOUNDS':
{
const notificationSounds = !state.notificationSounds;
return { ...state, notificationSounds };
}
case 'SET_VIDEO_RESOLUTION': case 'SET_VIDEO_RESOLUTION':
{ {
const { resolution } = action.payload; const { resolution } = action.payload;

View File

@ -1,4 +0,0 @@
export const ADMIN = 'admin';
export const MODERATOR = 'moderator';
export const AUTHENTICATED = 'authenticated';
export const ALL = 'normal';

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "登录", "tooltip.login": "登录",
"tooltip.logout": "注销", "tooltip.logout": "注销",
"tooltip.admitFromLobby": "从大厅允许", "tooltip.admitFromLobby": "从大厅允许",
@ -67,6 +77,10 @@
"tooltip.settings": "显示设置", "tooltip.settings": "显示设置",
"tooltip.participants": "显示参加者", "tooltip.participants": "显示参加者",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "房间名称", "label.roomName": "房间名称",
"label.chooseRoomButton": "继续", "label.chooseRoomButton": "继续",
@ -80,6 +94,7 @@
"label.filesharing": "文件共享", "label.filesharing": "文件共享",
"label.participants": "参与者", "label.participants": "参与者",
"label.shareFile": "共享文件", "label.shareFile": "共享文件",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "不支持文件共享", "label.fileSharingUnsupported": "不支持文件共享",
"label.unknown": "未知", "label.unknown": "未知",
"label.democratic": "民主视图", "label.democratic": "民主视图",
@ -90,6 +105,12 @@
"label.veryHigh": "非常高 (FHD)", "label.veryHigh": "非常高 (FHD)",
"label.ultra": "超高 (UHD)", "label.ultra": "超高 (UHD)",
"label.close": "关闭", "label.close": "关闭",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "设置", "settings.settings": "设置",
"settings.camera": "视频设备", "settings.camera": "视频设备",
@ -98,12 +119,17 @@
"settings.audio": "音频设备", "settings.audio": "音频设备",
"settings.selectAudio": "选择音频设备", "settings.selectAudio": "选择音频设备",
"settings.cantSelectAudio": "无法选择音频设备", "settings.cantSelectAudio": "无法选择音频设备",
"settings.audioOutput": "音频输出设备",
"settings.selectAudioOutput": "选择音频输出设备",
"settings.cantSelectAudioOutput": "无法选择音频输出设备",
"settings.resolution": "选择视频分辨率", "settings.resolution": "选择视频分辨率",
"settings.layout": "房间布局", "settings.layout": "房间布局",
"settings.selectRoomLayout": "选择房间布局", "settings.selectRoomLayout": "选择房间布局",
"settings.advancedMode": "高级模式", "settings.advancedMode": "高级模式",
"settings.permanentTopBar": "永久顶吧", "settings.permanentTopBar": "永久顶吧",
"settings.lastn": "可见视频数量", "settings.lastn": "可见视频数量",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "无法保存文件", "filesharing.saveFileError": "无法保存文件",
"filesharing.startingFileShare": "正在尝试共享文件", "filesharing.startingFileShare": "正在尝试共享文件",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "麦克风已断开", "devices.microphoneDisconnected": "麦克风已断开",
"devices.microphoneError": "麦克风发生错误", "devices.microphoneError": "麦克风发生错误",
"devices.microPhoneMute": "麦克风静音", "devices.microphoneMute": "麦克风静音",
"devices.micophoneUnMute": "取消麦克风静音", "devices.microphoneUnMute": "取消麦克风静音",
"devices.microphoneEnable": "启用了麦克风", "devices.microphoneEnable": "启用了麦克风",
"devices.microphoneMuteError": "无法使麦克风静音", "devices.microphoneMuteError": "无法使麦克风静音",
"devices.microphoneUnMuteError": "无法取消麦克风静音", "devices.microphoneUnMuteError": "无法取消麦克风静音",
@ -143,5 +169,10 @@
"devices.screenSharingError": "访问屏幕时发生错误", "devices.screenSharingError": "访问屏幕时发生错误",
"devices.cameraDisconnected": "相机已断开连接", "devices.cameraDisconnected": "相机已断开连接",
"devices.cameraError": "访问相机时发生错误" "devices.cameraError": "访问相机时发生错误",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -51,10 +51,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Přihlášení", "tooltip.login": "Přihlášení",
"tooltip.logout": "Odhlášení", "tooltip.logout": "Odhlášení",
"tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti", "tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti",
@ -64,6 +74,12 @@
"tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)", "tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)",
"tooltip.lobby": "Ukázat Přijímací místnost", "tooltip.lobby": "Ukázat Přijímací místnost",
"tooltip.settings": "Zobrazit nastavení", "tooltip.settings": "Zobrazit nastavení",
"tooltip.participants": null,
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Jméno místnosti", "label.roomName": "Jméno místnosti",
"label.chooseRoomButton": "Pokračovat", "label.chooseRoomButton": "Pokračovat",
@ -77,6 +93,7 @@
"label.filesharing": "Sdílení souborů", "label.filesharing": "Sdílení souborů",
"label.participants": "Účastníci", "label.participants": "Účastníci",
"label.shareFile": "Sdílet soubor", "label.shareFile": "Sdílet soubor",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Sdílení souborů není podporováno", "label.fileSharingUnsupported": "Sdílení souborů není podporováno",
"label.unknown": "Neznámý", "label.unknown": "Neznámý",
"label.democratic": "Rozvržení: Demokratické", "label.democratic": "Rozvržení: Demokratické",
@ -87,6 +104,12 @@
"label.veryHigh": "Velmi vysoké (FHD)", "label.veryHigh": "Velmi vysoké (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Zavřít", "label.close": "Zavřít",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Nastavení", "settings.settings": "Nastavení",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -95,10 +118,17 @@
"settings.audio": "Audio zařízení", "settings.audio": "Audio zařízení",
"settings.selectAudio": "Vyberte audio zařízení", "settings.selectAudio": "Vyberte audio zařízení",
"settings.cantSelectAudio": "Není možno vybrat audio zařízení", "settings.cantSelectAudio": "Není možno vybrat audio zařízení",
"settings.audioOutput": "Audio output zařízení",
"settings.selectAudioOutput": "Vyberte audio output zařízení",
"settings.cantSelectAudioOutput": "Není možno vybrat audio output zařízení",
"settings.resolution": "Vyberte rozlišení vašeho videa", "settings.resolution": "Vyberte rozlišení vašeho videa",
"settings.layout": "Rozvržení místnosti", "settings.layout": "Rozvržení místnosti",
"settings.selectRoomLayout": "Vyberte rozvržení místnosti", "settings.selectRoomLayout": "Vyberte rozvržení místnosti",
"settings.advancedMode": "Pokočilý mód", "settings.advancedMode": "Pokočilý mód",
"settings.permanentTopBar": null,
"settings.lastn": null,
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Není možné uložit soubor", "filesharing.saveFileError": "Není možné uložit soubor",
"filesharing.startingFileShare": "Pokouším se sdílet soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor",
@ -128,8 +158,8 @@
"devices.microphoneDisconnected": "Mikrofon odpojen", "devices.microphoneDisconnected": "Mikrofon odpojen",
"devices.microphoneError": "Při přístupu k vašemu mikrofonu se vyskytla chyba", "devices.microphoneError": "Při přístupu k vašemu mikrofonu se vyskytla chyba",
"devices.microPhoneMute": "Mikrofon ztišen", "devices.microphoneMute": "Mikrofon ztišen",
"devices.micophoneUnMute": "Ztišení mikrofonu zrušeno", "devices.microphoneUnMute": "Ztišení mikrofonu zrušeno",
"devices.microphoneEnable": "Mikrofon povolen", "devices.microphoneEnable": "Mikrofon povolen",
"devices.microphoneMuteError": "Není možné ztišit váš mikrofon", "devices.microphoneMuteError": "Není možné ztišit váš mikrofon",
"devices.microphoneUnMuteError": "Není možné zrušit ztišení vašeho mikrofonu", "devices.microphoneUnMuteError": "Není možné zrušit ztišení vašeho mikrofonu",
@ -138,5 +168,10 @@
"devices.screenSharingError": "Při přístupu k vaší obrazovce se vyskytla chyba", "devices.screenSharingError": "Při přístupu k vaší obrazovce se vyskytla chyba",
"devices.cameraDisconnected": "Kamera odpojena", "devices.cameraDisconnected": "Kamera odpojena",
"devices.cameraError": "Při přístupu k vaší kameře se vyskytla chyba" "devices.cameraError": "Při přístupu k vaší kameře se vyskytla chyba",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -1,46 +1,46 @@
{ {
"socket.disconnected": "Verbindung unterbrochen", "socket.disconnected": "Verbindung unterbrochen",
"socket.reconnecting": "Verbindung unterbrochen, versuche neu zu verbinden", "socket.reconnecting": "Verbindung unterbrochen, versuche neu zu verbinden",
"socket.reconnected": "Verbindung wieder herges|tellt", "socket.reconnected": "Verbindung wiederhergestellt",
"socket.requestError": "Fehler bei Serveranfrage", "socket.requestError": "Fehler bei Serveranfrage",
"room.chooseRoom": null, "room.chooseRoom": "Wähle den Raum aus, den du betreten möchtest",
"room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen", "room.cookieConsent": "Diese Seite verwendet Cookies, um die Benutzerfreundlichkeit zu erhöhen",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "Verstanden",
"room.joined": "Konferenzraum betreten", "room.joined": "Du bist dem Raum beigetreten",
"room.cantJoin": "Betreten des Raumes nicht möglich", "room.cantJoin": "Betreten des Raumes nicht möglich",
"room.youLocked": "Raum wurde abgeschlossen", "room.youLocked": "Du hast den Raum abgeschlossen",
"room.cantLock": "Abschließen des Raumes nicht möglich", "room.cantLock": "Abschließen des Raumes nicht möglich",
"room.youUnLocked": "Raum geöffnet", "room.youUnLocked": "Du hast den Raum geöffnet",
"room.cantUnLock": "Öffnen des Raumes nicht möglich", "room.cantUnLock": "Öffnen des Raumes nicht möglich",
"room.locked": "Raum wurde abgeschlossen", "room.locked": "Raum wurde abgeschlossen",
"room.unlocked": "Raum wurde geöffnet", "room.unlocked": "Raum wurde geöffnet",
"room.newLobbyPeer": "Neuer Teilnehmer im Empfangsraum", "room.newLobbyPeer": "Neuer Teilnehmer im Warteraum",
"room.lobbyPeerLeft": "Teilnehmer hat Empfangsraum verlassen", "room.lobbyPeerLeft": "Ein Teilnehmer hat den Warteraum verlassen",
"room.lobbyPeerChangedDisplayName": "Teilnehmer im Empfangsraum hat seinen Namen geändert: {displayName}", "room.lobbyPeerChangedDisplayName": "Ein Teilnehmer im Warteraum hat seinen Namen geändert zu: {displayName}",
"room.lobbyPeerChangedPicture": "Teilnehmer in Empfangsraum hat sein Avatar geändert", "room.lobbyPeerChangedPicture": "Ein Teilnehmer im Warteraum hat seinen Avatar geändert",
"room.setAccessCode": "Zugangskode für den Raum geändert", "room.setAccessCode": "Zugangscode für den Raum geändert",
"room.accessCodeOn": "Zugangskode aktiviert", "room.accessCodeOn": "Zugangscode aktiviert",
"room.accessCodeOff": "Zugangskode deaktiviert", "room.accessCodeOff": "Zugangscode deaktiviert",
"room.peerChangedDisplayName": "{oldDisplayName} heißt jetzt {displayName}", "room.peerChangedDisplayName": "{oldDisplayName} heißt jetzt {displayName}",
"room.newPeer": "{displayName} hat den Raum betreten", "room.newPeer": "{displayName} hat den Raum betreten",
"room.newFile": "Neue Datei verfügbar", "room.newFile": "Neue Datei verfügbar",
"room.toggleAdvancedMode": "Erweiterter Modus aktiv", "room.toggleAdvancedMode": "Erweiterter Modus aktiv",
"room.setDemocraticView": "Raumlayout demokratisch", "room.setDemocraticView": "Demokratische Ansicht",
"room.setFilmStripView": "Raumlayout Filmstreifen", "room.setFilmStripView": "Filmstreifen-Ansicht",
"room.loggedIn": "Angemeldet", "room.loggedIn": "Angemeldet",
"room.loggedOut": "Abgemeldet", "room.loggedOut": "Abgemeldet",
"room.changedDisplayName": "Dein Name ist jetzt {displayName}", "room.changedDisplayName": "Dein Name ist jetzt {displayName}",
"room.changeDisplayNameError": "Konnte Name nicht ändern", "room.changeDisplayNameError": "Dein Name konnte nicht geändert werden",
"room.chatError": "Konnte Meldung nicht senden", "room.chatError": "Die Chat-Nachricht konnte nicht gesendet werden",
"room.aboutToJoin": "Du bist dabei den Raum zu betreten", "room.aboutToJoin": "Du bist dabei, folgenden Raum zu betreten:",
"room.roomId": "Raum ID: {roomName}", "room.roomId": "Raum ID: {roomName}",
"room.setYourName": "Gib deinen Namen an und wähle wie den Raum betreten willst", "room.setYourName": "Gib deinen Namen an und wähle aus, wie du den Raum betreten willst:",
"room.audioOnly": "Nur Audio", "room.audioOnly": "Nur Audio",
"room.audioVideo": "Audio und Video", "room.audioVideo": "Audio und Video",
"room.youAreReady": "Ok, Du bist bereit", "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.emptyRequireLogin": "Der Raum ist leer. Melde dich an um die Konferenz zu starten oder warte bis der Raum aktiviert wird",
"room.locketWait": "Der Raum ist abgeschlossen, warte bis Dir jemand öffnet", "room.locketWait": "Der Raum ist abgeschlossen - warte, bis dich jemand rein lässt...",
"room.lobbyAdministration": "Warteraum", "room.lobbyAdministration": "Warteraum",
"room.peersInLobby": "Teilnehmer im Warteraum", "room.peersInLobby": "Teilnehmer im Warteraum",
"room.lobbyEmpty": "Der Warteraum ist leer", "room.lobbyEmpty": "Der Warteraum ist leer",
@ -49,37 +49,52 @@
"room.spotlights": "Aktive Teinehmer", "room.spotlights": "Aktive Teinehmer",
"room.passive": "Passive Teilnehmer", "room.passive": "Passive Teilnehmer",
"room.videoPaused": "Video gestoppt", "room.videoPaused": "Video gestoppt",
"room.muteAll": null, "room.muteAll": "Alle stummschalten",
"room.stopAllVideo": null, "room.stopAllVideo": "Alle Videos stoppen",
"room.closeMeeting": null, "room.closeMeeting": "Meeting schließen",
"room.speechUnsupported": null, "room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung",
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Anmelden", "tooltip.login": "Anmelden",
"tooltip.logout": "Abmelden", "tooltip.logout": "Abmelden",
"tooltip.admitFromLobby": "Teilnehmer aktivieren", "tooltip.admitFromLobby": "Teilnehmer reinlassen",
"tooltip.lockRoom": "Raum abschließen", "tooltip.lockRoom": "Raum abschließen",
"tooltip.unLockRoom": "Raum öffnen", "tooltip.unLockRoom": "Raum entsperren",
"tooltip.enterFullscreen": "Vollbild", "tooltip.enterFullscreen": "Vollbild",
"tooltip.leaveFullscreen": "Vollbild verlassen", "tooltip.leaveFullscreen": "Vollbild verlassen",
"tooltip.lobby": "Warteraum", "tooltip.lobby": "Warteraum",
"tooltip.settings": "Einstellungen", "tooltip.settings": "Einstellungen",
"tooltip.participants": "Teilnehmer", "tooltip.participants": "Teilnehmer",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": "Teilnehmer rauswerfen",
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": null, "label.roomName": "Name des Raums",
"label.chooseRoomButton": null, "label.chooseRoomButton": "Weiter",
"label.yourName": "Dein Name", "label.yourName": "Dein Name",
"label.newWindow": "In separatem Fenster öffnen", "label.newWindow": "Neues Fenster",
"label.fullscreen": "Vollbild", "label.fullscreen": "Vollbild",
"label.openDrawer": "Menü", "label.openDrawer": "Menü",
"label.leave": "Ausgang", "label.leave": "Verlassen",
"label.chatInput": "Schreibe Chat...", "label.chatInput": "Schreibe eine Nachricht...",
"label.chat": "Chat", "label.chat": "Chat",
"label.filesharing": "Dateien", "label.filesharing": "Dateien",
"label.participants": "Teilnehmer", "label.participants": "Teilnehmer",
"label.shareFile": "Teile Datai", "label.shareFile": "Datei hochladen",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt", "label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
"label.unknown": "Unbekannt", "label.unknown": "Unbekannt",
"label.democratic": "Demokratisch", "label.democratic": "Demokratisch",
@ -90,58 +105,74 @@
"label.veryHigh": "Sehr hoch (FHD)", "label.veryHigh": "Sehr hoch (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Schließen", "label.close": "Schließen",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Einstellungen", "settings.settings": "Einstellungen",
"settings.camera": "Kamera", "settings.camera": "Kamera",
"settings.selectCamera": "Wähle Videogerät", "settings.selectCamera": "Wähle ein Videogerät",
"settings.cantSelectCamera": "Kann Videogerät nicht aktivieren", "settings.cantSelectCamera": "Kann Videogerät nicht aktivieren",
"settings.audio": "Audiogerät", "settings.audio": "Audiogerät",
"settings.selectAudio": "Wähle Audiogerät", "settings.selectAudio": "Wähle ein Audiogerät",
"settings.cantSelectAudio": "Kann Audiogerät nicht aktivieren", "settings.cantSelectAudio": "Kann Audiogerät nicht aktivieren",
"settings.resolution": "Wähle Auflösung", "settings.audioOutput": "Audioausgabegerät",
"settings.selectAudioOutput": "Wähle ein Audioausgabegerät",
"settings.cantSelectAudioOutput": "Kann Audioausgabegerät nicht aktivieren",
"settings.resolution": "Wähle eine Auflösung",
"settings.layout": "Raumlayout", "settings.layout": "Raumlayout",
"settings.selectRoomLayout": "Wähle Raumlayout", "settings.selectRoomLayout": "Wähle ein Raumlayout",
"settings.advancedMode": "Erweiterter Modus", "settings.advancedMode": "Erweiterter Modus",
"settings.permanentTopBar": "Permanente obere Leiste", "settings.permanentTopBar": "Permanente obere Leiste",
"settings.lastn": "Anzahl der sichtbaren Videos", "settings.lastn": "Anzahl der sichtbaren Videos",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.saveFileError": "Fehler beim Speichern der Datei",
"filesharing.startingFileShare": "Starte Teilen der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei",
"filesharing.successfulFileShare": "Datei wurde geteilt", "filesharing.successfulFileShare": "Datei wurde geteilt",
"filesharing.unableToShare": "Kann Datei nicht teilen", "filesharing.unableToShare": "Datei kann nicht geteilt werden",
"filesharing.error": "Fehler beim Teilen der Datei", "filesharing.error": "Fehler beim Teilen der Datei",
"filesharing.finished": "Datei heruntergeladen", "filesharing.finished": "Datei heruntergeladen",
"filesharing.save": "Speichern", "filesharing.save": "Speichern",
"filesharing.sharedFile": "{displayName} hat eine Datei geteilt", "filesharing.sharedFile": "{displayName} hat eine Datei geteilt",
"filesharing.download": "Herunterladen", "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.", "filesharing.missingSeeds": "Wenn der Download zu lange dauert, ist wahrscheinlich keiner mehr im Raum, der die Datei teilen kann. Die Datei muss erneut hochgeladen werden.",
"devices.devicesChanged": "Mediengeräte wurden aktualisiert und sind in Einstellungen verfügbar", "devices.devicesChanged": "Mediengeräte wurden aktualisiert und sind in den Einstellungen verfügbar",
"device.audioUnsupported": "Audio nicht unterstützt", "device.audioUnsupported": "Audio nicht unterstützt",
"device.activateAudio": "Aktiviere Audio", "device.activateAudio": "Aktiviere Audio",
"device.muteAudio": "stummschalten", "device.muteAudio": "Stummschalten",
"device.unMuteAudio": "Aktiviere Audio", "device.unMuteAudio": "Stummschaltung aufheben",
"device.videoUnsupported": "Video nicht unterstützt", "device.videoUnsupported": "Video nicht unterstützt",
"device.startVideo": "Starte Video", "device.startVideo": "Starte Video",
"device.stopVideo": "Stoppe Video", "device.stopVideo": "Stoppe Video",
"device.screenSharingUnsupported": "Bildschirmteilen nicht unterstützt", "device.screenSharingUnsupported": "Bildschirmfreigabe nicht unterstützt",
"device.startScreenSharing": "Bildschirmteilen", "device.startScreenSharing": "Starte Bildschirmfreigabe",
"device.stopScreenSharing": "Beende Bildschirmteilen", "device.stopScreenSharing": "Beende Bildschirmfreigabe",
"devices.microphoneDisconnected": "Mikrophon nicht verbunden", "devices.microphoneDisconnected": "Mikrofon nicht verbunden",
"devices.microphoneError": "Fehler mit Mikrophon", "devices.microphoneError": "Fehler beim Zugriff auf dein Mikrofon",
"devices.microPhoneMute": "Mikrophon stumm geschaltet", "devices.microphoneMute": "Mikrofon stummgeschaltet",
"devices.micophoneUnMute": "Mikrophon aktiviert", "devices.microphoneUnMute": "Mikrofon aktiviert",
"devices.microphoneEnable": "Mikrofonen aktiviert", "devices.microphoneEnable": "Mikrofon aktiviert",
"devices.microphoneMuteError": "Kann Mikrophon nicht stummschalten", "devices.microphoneMuteError": "Kann Mikrofon nicht stummschalten",
"devices.microphoneUnMuteError": "Kann Mikrophon nicht aktivieren", "devices.microphoneUnMuteError": "Kann Mikrofon nicht aktivieren",
"devices.screenSharingDisconnected" : "Bildschirmteilen unterbrochen", "devices.screenSharingDisconnected" : "Bildschirmfreigabe unterbrochen",
"devices.screenSharingError": "Fehler beim Bildschirmteilen", "devices.screenSharingError": "Fehler bei der Bildschirmfreigabe",
"devices.cameraDisconnected": "Video unterbrochen", "devices.cameraDisconnected": "Kamera getrennt",
"devices.cameraError": "Fehler mit Videogerät" "devices.cameraError": "Fehler mit deiner Kamera",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Log ind", "tooltip.login": "Log ind",
"tooltip.logout": "Log ud", "tooltip.logout": "Log ud",
"tooltip.admitFromLobby": "Giv adgang fra lobbyen", "tooltip.admitFromLobby": "Giv adgang fra lobbyen",
@ -67,6 +77,10 @@
"tooltip.settings": "Vis indstillinger", "tooltip.settings": "Vis indstillinger",
"tooltip.participants": "Vis deltagere", "tooltip.participants": "Vis deltagere",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Værelsesnavn", "label.roomName": "Værelsesnavn",
"label.chooseRoomButton": "Fortsæt", "label.chooseRoomButton": "Fortsæt",
@ -80,6 +94,7 @@
"label.filesharing": "Fildeling", "label.filesharing": "Fildeling",
"label.participants": "Deltagere", "label.participants": "Deltagere",
"label.shareFile": "Del fil", "label.shareFile": "Del fil",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Fildeling er ikke understøttet", "label.fileSharingUnsupported": "Fildeling er ikke understøttet",
"label.unknown": "Ukendt", "label.unknown": "Ukendt",
"label.democracy": "Galleri visning", "label.democracy": "Galleri visning",
@ -90,6 +105,12 @@
"label.veryHigh": "Meget høj (FHD)", "label.veryHigh": "Meget høj (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Luk", "label.close": "Luk",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Indstillinger", "settings.settings": "Indstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -98,12 +119,17 @@
"settings.audio": "Lydenhed", "settings.audio": "Lydenhed",
"settings.selectAudio": "Vælg lydenhed", "settings.selectAudio": "Vælg lydenhed",
"settings.cantSelectAudio": "Kan ikke vælge lydenhed", "settings.cantSelectAudio": "Kan ikke vælge lydenhed",
"settings.audioOutput": "Audio output enhed",
"settings.selectAudioOutput": "Vælg lydudgangsenhed",
"settings.cantSelectAudioOutput": "Kan ikke vælge lydoutputenhed",
"settings.resolution": "Vælg din videoopløsning", "settings.resolution": "Vælg din videoopløsning",
"settings.layout": "Møde visning", "settings.layout": "Møde visning",
"settings.selectRoomLayout": "Vælg møde visning", "settings.selectRoomLayout": "Vælg møde visning",
"settings.advancedMode": "Avanceret tilstand", "settings.advancedMode": "Avanceret tilstand",
"settings.permanentTopBar": "Permanent øverste linje", "settings.permanentTopBar": "Permanent øverste linje",
"settings.lastn": "Antal synlige videoer", "settings.lastn": "Antal synlige videoer",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.saveFileError": "Kan ikke gemme fil",
"filesharing.startingFileShare": "Forsøger at dele filen", "filesharing.startingFileShare": "Forsøger at dele filen",
@ -133,8 +159,8 @@
"device.microphoneDisconnected": "Mikrofon frakoblet", "device.microphoneDisconnected": "Mikrofon frakoblet",
"device.microphoneError": "Der opstod en fejl under adgang til din mikrofon", "device.microphoneError": "Der opstod en fejl under adgang til din mikrofon",
"device.microPhoneMute": "Dæmp din mikrofon", "device.microphoneMute": "Dæmp din mikrofon",
"device.micophoneUnMute": "Slå ikke lyden fra din mikrofon", "device.microphoneUnMute": "Slå ikke lyden fra din mikrofon",
"device.microphoneEnable": "Aktiveret din mikrofon", "device.microphoneEnable": "Aktiveret din mikrofon",
"device.microphoneMuteError": "Kan ikke slå din mikrofon fra", "device.microphoneMuteError": "Kan ikke slå din mikrofon fra",
"device.microphoneUnMuteError": "Kan ikke slå lyden til på din mikrofon", "device.microphoneUnMuteError": "Kan ikke slå lyden til på din mikrofon",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Der opstod en fejl ved adgang til skærmdeling", "devices.screenSharingError": "Der opstod en fejl ved adgang til skærmdeling",
"device.cameraDisconnected": "Kamera frakoblet", "device.cameraDisconnected": "Kamera frakoblet",
"device.cameraError": "Der opstod en fejl ved tilkobling af dit kamera" "device.cameraError": "Der opstod en fejl ved tilkobling af dit kamera",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Σύνδεση", "tooltip.login": "Σύνδεση",
"tooltip.logout": "Αποσύνδεση", "tooltip.logout": "Αποσύνδεση",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -67,6 +77,10 @@
"tooltip.settings": "Εμφάνιση ρυθμίσεων", "tooltip.settings": "Εμφάνιση ρυθμίσεων",
"tooltip.participants": "Εμφάνιση συμμετεχόντων", "tooltip.participants": "Εμφάνιση συμμετεχόντων",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Όνομα δωματίου", "label.roomName": "Όνομα δωματίου",
"label.chooseRoomButton": "Συνέχεια", "label.chooseRoomButton": "Συνέχεια",
@ -80,6 +94,7 @@
"label.filesharing": "Διαμοιρασμοός αρχείου", "label.filesharing": "Διαμοιρασμοός αρχείου",
"label.participants": "Συμμετέχοντες", "label.participants": "Συμμετέχοντες",
"label.shareFile": "Διαμοιραστείτε ένα αρχείο", "label.shareFile": "Διαμοιραστείτε ένα αρχείο",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται", "label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
"label.unknown": "Άγνωστο", "label.unknown": "Άγνωστο",
"label.democratic": null, "label.democratic": null,
@ -90,6 +105,12 @@
"label.veryHigh": "Πολύ υψηλή (FHD)", "label.veryHigh": "Πολύ υψηλή (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Κλείσιμο", "label.close": "Κλείσιμο",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ρυθμίσεις", "settings.settings": "Ρυθμίσεις",
"settings.camera": "Κάμερα", "settings.camera": "Κάμερα",
@ -98,12 +119,17 @@
"settings.audio": "Συσκευή ήχου", "settings.audio": "Συσκευή ήχου",
"settings.selectAudio": "Επιλογή συσκευής ήχου", "settings.selectAudio": "Επιλογή συσκευής ήχου",
"settings.cantSelectAudio": "Αδυναμία επιλογής συσκευής ήχου", "settings.cantSelectAudio": "Αδυναμία επιλογής συσκευής ήχου",
"settings.audioOutput": "Συσκευή εξόδου ήχου",
"settings.selectAudioOutput": "Επιλέξτε συσκευή εξόδου ήχου",
"settings.cantSelectAudioOutput": "Δεν είναι δυνατή η επιλογή συσκευής εξόδου ήχου",
"settings.resolution": "Επιλέξτε την ανάλυση του video", "settings.resolution": "Επιλέξτε την ανάλυση του video",
"settings.layout": "Περιβάλλον δωματίου", "settings.layout": "Περιβάλλον δωματίου",
"settings.selectRoomLayout": "Επιλογή περιβάλλοντος δωματίου", "settings.selectRoomLayout": "Επιλογή περιβάλλοντος δωματίου",
"settings.advancedMode": "Προηγμένη λειτουργία", "settings.advancedMode": "Προηγμένη λειτουργία",
"settings.permanentTopBar": "Μόνιμη μπάρα κορυφής", "settings.permanentTopBar": "Μόνιμη μπάρα κορυφής",
"settings.lastn": "Αριθμός ορατών βίντεο", "settings.lastn": "Αριθμός ορατών βίντεο",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Το μικρόφωνο αποσυνδέθηκε", "devices.microphoneDisconnected": "Το μικρόφωνο αποσυνδέθηκε",
"devices.microphoneError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στο μικρόφωνό σας", "devices.microphoneError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στο μικρόφωνό σας",
"devices.microPhoneMute": "Το μικρόφωνό σας είναι σε σίγαση", "devices.microphoneMute": "Το μικρόφωνό σας είναι σε σίγαση",
"devices.micophoneUnMute": "Ανοίξτε το μικρόφωνό σας", "devices.microphoneUnMute": "Ανοίξτε το μικρόφωνό σας",
"devices.microphoneEnable": "Ενεργοποίησε το μικρόφωνό σας", "devices.microphoneEnable": "Ενεργοποίησε το μικρόφωνό σας",
"devices.microphoneMuteError": "Δεν είναι δυνατή η σίγαση του μικροφώνου σας", "devices.microphoneMuteError": "Δεν είναι δυνατή η σίγαση του μικροφώνου σας",
"devices.microphoneUnMuteError": "Δεν είναι δυνατό το άνοιγμα του μικροφώνου σας", "devices.microphoneUnMuteError": "Δεν είναι δυνατό το άνοιγμα του μικροφώνου σας",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην οθόνη σας", "devices.screenSharingError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην οθόνη σας",
"devices.cameraDisconnected": "Η κάμερα αποσυνδέθηκε", "devices.cameraDisconnected": "Η κάμερα αποσυνδέθηκε",
"devices.cameraError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην κάμερά σας" "devices.cameraError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην κάμερά σας",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": "Mute all", "room.muteAll": "Mute all",
"room.stopAllVideo": "Stop all video", "room.stopAllVideo": "Stop all video",
"room.closeMeeting": "Close meeting", "room.closeMeeting": "Close meeting",
"room.clearChat": "Clear chat",
"room.clearFileSharing": "Clear files",
"room.speechUnsupported": "Your browser does not support speech recognition", "room.speechUnsupported": "Your browser does not support speech recognition",
"room.moderatoractions": "Moderator actions",
"room.raisedHand": "{displayName} raised their hand",
"room.loweredHand": "{displayName} put their hand down",
"room.extraVideo": "Extra video",
"room.overRoomLimit": "The room is full, retry after some time.",
"me.mutedPTT": "You are muted, hold down SPACE-BAR to talk", "me.mutedPTT": "You are muted, hold down SPACE-BAR to talk",
"roles.gotRole": "You got the role: {role}",
"roles.lostRole": "You lost the role: {role}",
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby", "tooltip.admitFromLobby": "Admit from lobby",
@ -67,6 +77,10 @@
"tooltip.settings": "Show settings", "tooltip.settings": "Show settings",
"tooltip.participants": "Show participants", "tooltip.participants": "Show participants",
"tooltip.kickParticipant": "Kick out participant", "tooltip.kickParticipant": "Kick out participant",
"tooltip.muteParticipant": "Mute participant",
"tooltip.muteParticipantVideo": "Mute participant video",
"tooltip.raisedHand": "Raise hand",
"tooltip.muteScreenSharing": "Mute participant share",
"label.roomName": "Room name", "label.roomName": "Room name",
"label.chooseRoomButton": "Continue", "label.chooseRoomButton": "Continue",
@ -80,6 +94,7 @@
"label.filesharing": "File sharing", "label.filesharing": "File sharing",
"label.participants": "Participants", "label.participants": "Participants",
"label.shareFile": "Share file", "label.shareFile": "Share file",
"label.shareGalleryFile": "Share image",
"label.fileSharingUnsupported": "File sharing not supported", "label.fileSharingUnsupported": "File sharing not supported",
"label.unknown": "Unknown", "label.unknown": "Unknown",
"label.democratic": "Democratic view", "label.democratic": "Democratic view",
@ -90,6 +105,12 @@
"label.veryHigh": "Very high (FHD)", "label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Close", "label.close": "Close",
"label.media": "Media",
"label.appearence": "Appearence",
"label.advanced": "Advanced",
"label.addVideo": "Add video",
"label.promoteAllPeers": "Promote all",
"label.moreActions": "More actions",
"settings.settings": "Settings", "settings.settings": "Settings",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -98,12 +119,17 @@
"settings.audio": "Audio device", "settings.audio": "Audio device",
"settings.selectAudio": "Select audio device", "settings.selectAudio": "Select audio device",
"settings.cantSelectAudio": "Unable to select audio device", "settings.cantSelectAudio": "Unable to select audio device",
"settings.audioOutput": "Audio output device",
"settings.selectAudioOutput": "Select audio output device",
"settings.cantSelectAudioOutput": "Unable to select audio output device",
"settings.resolution": "Select your video resolution", "settings.resolution": "Select your video resolution",
"settings.layout": "Room layout", "settings.layout": "Room layout",
"settings.selectRoomLayout": "Select room layout", "settings.selectRoomLayout": "Select room layout",
"settings.advancedMode": "Advanced mode", "settings.advancedMode": "Advanced mode",
"settings.permanentTopBar": "Permanent top bar", "settings.permanentTopBar": "Permanent top bar",
"settings.lastn": "Number of visible videos", "settings.lastn": "Number of visible videos",
"settings.hiddenControls": "Hidden media controls",
"settings.notificationSounds": "Notification sounds",
"filesharing.saveFileError": "Unable to save file", "filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file", "filesharing.startingFileShare": "Attempting to share file",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Microphone disconnected", "devices.microphoneDisconnected": "Microphone disconnected",
"devices.microphoneError": "An error occured while accessing your microphone", "devices.microphoneError": "An error occured while accessing your microphone",
"devices.microPhoneMute": "Muted your microphone", "devices.microphoneMute": "Muted your microphone",
"devices.micophoneUnMute": "Unmuted your microphone", "devices.microphoneUnMute": "Unmuted your microphone",
"devices.microphoneEnable": "Enabled your microphone", "devices.microphoneEnable": "Enabled your microphone",
"devices.microphoneMuteError": "Unable to mute your microphone", "devices.microphoneMuteError": "Unable to mute your microphone",
"devices.microphoneUnMuteError": "Unable to unmute your microphone", "devices.microphoneUnMuteError": "Unable to unmute your microphone",
@ -143,5 +169,10 @@
"devices.screenSharingError": "An error occured while accessing your screen", "devices.screenSharingError": "An error occured while accessing your screen",
"devices.cameraDisconnected": "Camera disconnected", "devices.cameraDisconnected": "Camera disconnected",
"devices.cameraError": "An error occured while accessing your camera" "devices.cameraError": "An error occured while accessing your camera",
"moderator.clearChat": "Moderator cleared the chat",
"moderator.clearFiles": "Moderator cleared the files",
"moderator.muteAudio": "Moderator muted your audio",
"moderator.muteVideo": "Moderator muted your video"
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Salir", "tooltip.logout": "Salir",
"tooltip.admitFromLobby": "Admitir desde la sala de espera", "tooltip.admitFromLobby": "Admitir desde la sala de espera",
@ -67,6 +77,10 @@
"tooltip.settings": "Mostrar ajustes", "tooltip.settings": "Mostrar ajustes",
"tooltip.participants": "Mostrar participantes", "tooltip.participants": "Mostrar participantes",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nombre de la sala", "label.roomName": "Nombre de la sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -80,6 +94,7 @@
"label.filesharing": "Compartir ficheros", "label.filesharing": "Compartir ficheros",
"label.participants": "Participantes", "label.participants": "Participantes",
"label.shareFile": "Compartir fichero", "label.shareFile": "Compartir fichero",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Compartir ficheros no está disponible", "label.fileSharingUnsupported": "Compartir ficheros no está disponible",
"label.unknown": "Desconocido", "label.unknown": "Desconocido",
"label.democratic": "Vista democrática", "label.democratic": "Vista democrática",
@ -90,6 +105,12 @@
"label.veryHigh": "Muy alta (FHD)", "label.veryHigh": "Muy alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Cerrar", "label.close": "Cerrar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ajustes", "settings.settings": "Ajustes",
"settings.camera": "Cámara", "settings.camera": "Cámara",
@ -98,12 +119,17 @@
"settings.audio": "Dispositivo de sonido", "settings.audio": "Dispositivo de sonido",
"settings.selectAudio": "Seleccione dispositivo de sonido", "settings.selectAudio": "Seleccione dispositivo de sonido",
"settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido", "settings.cantSelectAudio": "No ha sido posible seleccionar el dispositivo de sonido",
"settings.audioOutput": "Dispositivo de salida de audio",
"settings.selectAudioOutput": "Seleccionar dispositivo de salida de audio",
"settings.cantSelectAudioOutput": "No se puede seleccionar el dispositivo de salida de audio",
"settings.resolution": "Seleccione su resolución de imagen", "settings.resolution": "Seleccione su resolución de imagen",
"settings.layout": "Disposición de la sala", "settings.layout": "Disposición de la sala",
"settings.selectRoomLayout": "Seleccione la disposición de la sala", "settings.selectRoomLayout": "Seleccione la disposición de la sala",
"settings.advancedMode": "Modo avanzado", "settings.advancedMode": "Modo avanzado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Cantidad de videos visibles", "settings.lastn": "Cantidad de videos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.saveFileError": "No ha sido posible guardar el fichero",
"filesharing.startingFileShare": "Intentando compartir el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Micrófono desconectado", "devices.microphoneDisconnected": "Micrófono desconectado",
"devices.microphoneError": "Hubo un error al acceder a su micrófono", "devices.microphoneError": "Hubo un error al acceder a su micrófono",
"devices.microPhoneMute": "Desactivar micrófono", "devices.microphoneMute": "Desactivar micrófono",
"devices.micophoneUnMute": "Activar micrófono", "devices.microphoneUnMute": "Activar micrófono",
"devices.microphoneEnable": "Micrófono activado", "devices.microphoneEnable": "Micrófono activado",
"devices.microphoneMuteError": "No ha sido posible desactivar su micrófono", "devices.microphoneMuteError": "No ha sido posible desactivar su micrófono",
"devices.microphoneUnMuteError": "No ha sido posible activar su micrófono", "devices.microphoneUnMuteError": "No ha sido posible activar su micrófono",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Hubo un error al acceder a su pantalla", "devices.screenSharingError": "Hubo un error al acceder a su pantalla",
"devices.cameraDisconnected": "Cámara desconectada", "devices.cameraDisconnected": "Cámara desconectada",
"devices.cameraError": "Hubo un error al acceder a su cámara" "devices.cameraError": "Hubo un error al acceder a su cámara",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Connexion", "tooltip.login": "Connexion",
"tooltip.logout": "Déconnexion", "tooltip.logout": "Déconnexion",
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente", "tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
@ -67,6 +77,10 @@
"tooltip.settings": "Afficher les paramètres", "tooltip.settings": "Afficher les paramètres",
"tooltip.participants": "Afficher les participants", "tooltip.participants": "Afficher les participants",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nom de la salle", "label.roomName": "Nom de la salle",
"label.chooseRoomButton": "Continuer", "label.chooseRoomButton": "Continuer",
@ -80,6 +94,7 @@
"label.filesharing": "Partage de fichier", "label.filesharing": "Partage de fichier",
"label.participants": "Participants", "label.participants": "Participants",
"label.shareFile": "Partager un fichier", "label.shareFile": "Partager un fichier",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partage de fichier non supporté", "label.fileSharingUnsupported": "Partage de fichier non supporté",
"label.unknown": "Inconnu", "label.unknown": "Inconnu",
"label.democratic": "Vue démocratique", "label.democratic": "Vue démocratique",
@ -90,6 +105,12 @@
"label.veryHigh": "Très Haute Définition (FHD)", "label.veryHigh": "Très Haute Définition (FHD)",
"label.ultra": "Ultra Haute Définition", "label.ultra": "Ultra Haute Définition",
"label.close": "Fermer", "label.close": "Fermer",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Paramètres", "settings.settings": "Paramètres",
"settings.camera": "Caméra", "settings.camera": "Caméra",
@ -98,12 +119,17 @@
"settings.audio": "Microphone", "settings.audio": "Microphone",
"settings.selectAudio": "Sélectionnez votre microphone", "settings.selectAudio": "Sélectionnez votre microphone",
"settings.cantSelectAudio": "Impossible de sélectionner votre la caméra", "settings.cantSelectAudio": "Impossible de sélectionner votre la caméra",
"settings.audioOutput": "Périphérique de sortie audio",
"settings.selectAudioOutput": "Sélectionnez le périphérique de sortie audio",
"settings.cantSelectAudioOutput": "Impossible de sélectionner le périphérique de sortie audio",
"settings.resolution": "Sélectionnez votre résolution", "settings.resolution": "Sélectionnez votre résolution",
"settings.layout": "Mode d'affichage de la salle", "settings.layout": "Mode d'affichage de la salle",
"settings.selectRoomLayout": "Sélectionnez la présentation de la salle", "settings.selectRoomLayout": "Sélectionnez la présentation de la salle",
"settings.advancedMode": "Mode avancé", "settings.advancedMode": "Mode avancé",
"settings.permanentTopBar": "Barre supérieure permanente", "settings.permanentTopBar": "Barre supérieure permanente",
"settings.lastn": "Nombre de vidéos visibles", "settings.lastn": "Nombre de vidéos visibles",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.saveFileError": "Impossible d'enregistrer le fichier",
"filesharing.startingFileShare": "Début du transfert de fichier", "filesharing.startingFileShare": "Début du transfert de fichier",
@ -132,8 +158,8 @@
"devices.microphoneDisconnected": "Microphone déconnecté", "devices.microphoneDisconnected": "Microphone déconnecté",
"devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone", "devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone",
"devices.microPhoneMute": "Désactiver le microphone", "devices.microphoneMute": "Désactiver le microphone",
"devices.micophoneUnMute": "Réactiver le microphone", "devices.microphoneUnMute": "Réactiver le microphone",
"devices.microphoneEnable": "Activer le microphone", "devices.microphoneEnable": "Activer le microphone",
"devices.microphoneMuteError": "Impossible de désactiver le microphone", "devices.microphoneMuteError": "Impossible de désactiver le microphone",
"devices.microphoneUnMuteError": "Impossible de réactiver le microphone", "devices.microphoneUnMuteError": "Impossible de réactiver le microphone",
@ -142,5 +168,10 @@
"devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran", "devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran",
"devices.cameraDisconnected": "Caméra déconnectée", "devices.cameraDisconnected": "Caméra déconnectée",
"devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra" "devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -6,7 +6,7 @@
"room.chooseRoom": "Izaberite ime sobe u koju se želite prijaviti", "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.cookieConsent": "Ova stranica koristi kolačiće radi poboljšanja korisničkog iskustva",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "Razumijem",
"room.joined": "Prijavljeni ste u sobu", "room.joined": "Prijavljeni ste u sobu",
"room.cantJoin": "Prijava u sobu nije moguća", "room.cantJoin": "Prijava u sobu nije moguća",
"room.youLocked": "Zaključali ste sobu", "room.youLocked": "Zaključali ste sobu",
@ -15,10 +15,10 @@
"room.cantUnLock": "Otključavanje sobe nije moguće", "room.cantUnLock": "Otključavanje sobe nije moguće",
"room.locked": "Soba je sada zaključana", "room.locked": "Soba je sada zaključana",
"room.unlocked": "Soba je sada otključana", "room.unlocked": "Soba je sada otključana",
"room.newLobbyPeer": "U predvorju je novi učesnik", "room.newLobbyPeer": "Novi sudionik čeka u predvorju",
"room.lobbyPeerLeft": "Učesnik je napustio predvorje", "room.lobbyPeerLeft": "Sudionik je napustio predvorje",
"room.lobbyPeerChangedDisplayName": "Učesnik u predvorju je promijenio ime u {displayName}", "room.lobbyPeerChangedDisplayName": "Sudionik u predvorju je promijenio ime u {displayName}",
"room.lobbyPeerChangedPicture": "Učesnik u predvorju je promijenio sliku", "room.lobbyPeerChangedPicture": "Sudionik u predvorju je promijenio sliku",
"room.setAccessCode": "Obnovljena pristupna šifra za sobu", "room.setAccessCode": "Obnovljena pristupna šifra za sobu",
"room.accessCodeOn": "Pristupna šifra sobe je aktivna", "room.accessCodeOn": "Pristupna šifra sobe je aktivna",
"room.accessCodeOff":"Pristupna šifra sobe je neaktivna", "room.accessCodeOff":"Pristupna šifra sobe je neaktivna",
@ -40,21 +40,31 @@
"room.audioVideo": "Zvuk i slika", "room.audioVideo": "Zvuk i slika",
"room.youAreReady": "Spremni ste", "room.youAreReady": "Spremni ste",
"room.emptyRequireLogin": "Soba je trenutno prazna! Prijavite se za pokretanje sastanka, ili sačekajte organizatora" , "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.locketWait": "Soba je zaključana - pričekajte odobrenje...",
"room.lobbyAdministration":"Upravljanje predvorjem", "room.lobbyAdministration":"Upravljanje predvorjem",
"room.peersInLobby":"Učesnici u predvorju", "room.peersInLobby":"Sudionici u predvorju",
"room.lobbyEmpty": "Trenutno nema nikoga u predvorju", "room.lobbyEmpty": "Trenutno nema nikoga u predvorju",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}", "room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Ja", "room.me": "Ja",
"room.spotlights": "Učesnici u fokusu", "room.spotlights": "Sudionici u fokusu",
"room.passive": "Pasivni učesnici", "room.passive": "Pasivni sudionici",
"room.videoPaused": "Video pauziran", "room.videoPaused": "Video pauziran",
"room.muteAll": null, "room.muteAll": "Utišaj sve",
"room.stopAllVideo": null, "room.stopAllVideo": "Ugasi sve kamere",
"room.closeMeeting": null, "room.closeMeeting": "Završi sastanak",
"room.speechUnsupported": null, "room.clearChat": "Izbriši razgovor",
"room.clearFileSharing": "Izbriši dijeljene datoteke",
"room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora",
"room.moderatoractions": "Akcije moderatora",
"room.raisedHand": "{displayName} je podigao ruku",
"room.loweredHand": "{displayName} je spustio ruku",
"room.extraVideo": "Dodatni video",
"room.overRoomLimit": "Soba je popunjena, pokušajte ponovno kasnije.",
"me.mutedPTT": null, "me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
"roles.gotRole": "Dodijeljena vam je uloga: {role}",
"roles.lostRole": "Uloga: {role} je povučena",
"tooltip.login": "Prijava", "tooltip.login": "Prijava",
"tooltip.logout": "Odjava", "tooltip.logout": "Odjava",
@ -65,8 +75,12 @@
"tooltip.leaveFullscreen": "Izađi iz punog ekrana", "tooltip.leaveFullscreen": "Izađi iz punog ekrana",
"tooltip.lobby": "Prikaži predvorje", "tooltip.lobby": "Prikaži predvorje",
"tooltip.settings": "Prikaži postavke", "tooltip.settings": "Prikaži postavke",
"tooltip.participants": "Pokažite sudionike", "tooltip.participants": "Prikaži sudionike",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": "Izbaci sudionika",
"tooltip.muteParticipant": "Utišaj sudionika",
"tooltip.muteParticipantVideo": "Ne primaj video sudionika",
"tooltip.raisedHand": "Podigni ruku",
"tooltip.muteScreenSharing": null,
"label.roomName": "Naziv sobe", "label.roomName": "Naziv sobe",
"label.chooseRoomButton": "Nastavi", "label.chooseRoomButton": "Nastavi",
@ -78,8 +92,9 @@
"label.chatInput":"Uđi u razgovor porukama", "label.chatInput":"Uđi u razgovor porukama",
"label.chat": "Razgovor", "label.chat": "Razgovor",
"label.filesharing": "Dijeljenje datoteka", "label.filesharing": "Dijeljenje datoteka",
"label.participants": "Učesnici", "label.participants": "Sudionici",
"label.shareFile": "Dijeli datoteku", "label.shareFile": "Dijeli datoteku",
"label.shareGalleryFile": "Dijeli sliku",
"label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano", "label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano",
"label.unknown": "Nepoznato", "label.unknown": "Nepoznato",
"label.democratic":"Demokratski prikaz", "label.democratic":"Demokratski prikaz",
@ -90,6 +105,12 @@
"label.veryHigh": "Vrlo visoka (FHD)", "label.veryHigh": "Vrlo visoka (FHD)",
"label.ultra": "Ultra visoka (UHD)", "label.ultra": "Ultra visoka (UHD)",
"label.close": "Zatvori", "label.close": "Zatvori",
"label.media": "Medij",
"label.appearence": "Prikaz",
"label.advanced": "Napredno",
"label.addVideo": "Dodaj video",
"label.promoteAllPeers": "Promoviraj sve",
"label.moreActions": null,
"settings.settings": "Postavke", "settings.settings": "Postavke",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -98,12 +119,17 @@
"settings.audio": "Uređaj za zvuk", "settings.audio": "Uređaj za zvuk",
"settings.selectAudio": "Odaberi uređaj za zvuk", "settings.selectAudio": "Odaberi uređaj za zvuk",
"settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk", "settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk",
"settings.audioOutput": "Uređaj za izlaz zvuka",
"settings.selectAudioOutput": "Odaberite izlazni uređaj za zvuk",
"settings.cantSelectAudioOutput": "Nije moguće odabrati izlazni uređaj za zvuk",
"settings.resolution": "Odaberi video rezoluciju", "settings.resolution": "Odaberi video rezoluciju",
"settings.layout": "Način prikaza", "settings.layout": "Način prikaza",
"settings.selectRoomLayout": "Odaberi način prikaza", "settings.selectRoomLayout": "Odaberi način prikaza",
"settings.advancedMode": "Napredne mogućnosti", "settings.advancedMode": "Napredne mogućnosti",
"settings.permanentTopBar": "Stalna gornja šipka", "settings.permanentTopBar": "Stalna gornja šipka",
"settings.lastn": "Broj vidljivih videozapisa", "settings.lastn": "Broj vidljivih videozapisa",
"settings.hiddenControls": "Skrivene kontrole medija",
"settings.notificationSounds": "Zvuk obavijesti",
"filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.saveFileError": "Nije moguće spremiti datoteku",
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Mikrofon odspojen", "devices.microphoneDisconnected": "Mikrofon odspojen",
"devices.microphoneError": "Greška prilikom pristupa mikrofonu", "devices.microphoneError": "Greška prilikom pristupa mikrofonu",
"devices.microPhoneMute": "Mikrofon utišan", "devices.microphoneMute": "Mikrofon utišan",
"devices.micophoneUnMute": "Mikrofon pojačan", "devices.microphoneUnMute": "Mikrofon pojačan",
"devices.microphoneEnable": "Mikrofon omogućen", "devices.microphoneEnable": "Mikrofon omogućen",
"devices.microphoneMuteError": "Nije moguće utišati mikrofon", "devices.microphoneMuteError": "Nije moguće utišati mikrofon",
"devices.microphoneUnMuteError": "Nije moguće pojačati mikrofon", "devices.microphoneUnMuteError": "Nije moguće pojačati mikrofon",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Greška prilikom pristupa ekranu", "devices.screenSharingError": "Greška prilikom pristupa ekranu",
"devices.cameraDisconnected": "Kamera odspojena", "devices.cameraDisconnected": "Kamera odspojena",
"devices.cameraError": "Greška prilikom pristupa kameri" "devices.cameraError": "Greška prilikom pristupa kameri",
"moderator.clearChat": "Moderator je izbrisao razgovor",
"moderator.clearFiles": "Moderator je izbrisao datoteke",
"moderator.muteAudio": "Moderator je utišao tvoj zvuk",
"moderator.muteVideo": "Moderator je zaustavio tvoj video"
} }

View File

@ -1,24 +1,24 @@
{ {
"socket.disconnected": "A kapcsolat lebomlott", "socket.disconnected": "A kapcsolat lebomlott",
"socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás", "socket.reconnecting": "A kapcsolat lebomlott, újrapróbálkozás",
"socket.reconnected": "Sikeres újarkapcsolódás", "socket.reconnected": "Sikeres újrakapcsolódás",
"socket.requestError": "Sikertelen szerver lekérés", "socket.requestError": "Sikertelen szerver lekérés",
"room.chooseRoom": null, "room.chooseRoom": "Válaszd ki a konferenciaszobát",
"room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ", "room.cookieConsent": "Ez a weblap a felhasználói élmény fokozása miatt sütiket használ",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "Megértettem",
"room.joined": "Csatlakozátál a konferenciához", "room.joined": "Csatlakoztál a konferenciához",
"room.cantJoin": "Sikertelen csatlakozás a konferenciához", "room.cantJoin": "Sikertelen csatlakozás a konferenciához",
"room.youLocked": "A konferenciába való belépés letiltva", "room.youLocked": "A konferenciába való belépés letiltva",
"room.cantLock": "Sikertelen a konferenciaba való belépés letiltása", "room.cantLock": "Sikertelen a konferenciába való belépés letiltása",
"room.youUnLocked": "A konferenciába való belépés engedélyezve", "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.cantUnLock": "Sikertelen a konferenciába való belépés engedélyezése",
"room.locked": "A konferenciába való belépés letiltva", "room.locked": "A konferenciába való belépés letiltva",
"room.unlocked": "A konferenciába való belépés engedélyezve", "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.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.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.lobbyPeerChangedDisplayName": "Az előszobai résztvevő megváltoztatta a nevét: {displayName}",
"room.lobbyPeerChangedPicture": "Az előszobai résztvevő meváltoztatta a képét", "room.lobbyPeerChangedPicture": "Az előszobai résztvevő megváltoztatta a képét",
"room.setAccessCode": "A konferencia hozzáférési kódja megváltozott", "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.accessCodeOn": "A konferencia hozzáférési kódja aktiválva",
"room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva", "room.accessCodeOff": "A konferencia hozzáférési kódka deaktiválva",
@ -39,8 +39,8 @@
"room.audioOnly": "csak Hang", "room.audioOnly": "csak Hang",
"room.audioVideo": "Hang és Videó", "room.audioVideo": "Hang és Videó",
"room.youAreReady": "Ok, kész vagy", "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.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferencia 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.locketWait": "Az automatikus belépés tiltva van - Várj amíg valaki beenged ...",
"room.lobbyAdministration": "Előszoba adminisztráció", "room.lobbyAdministration": "Előszoba adminisztráció",
"room.peersInLobby": "Résztvevők az előszobában", "room.peersInLobby": "Résztvevők az előszobában",
"room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában", "room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában",
@ -49,16 +49,26 @@
"room.spotlights": "Látható résztvevők", "room.spotlights": "Látható résztvevők",
"room.passive": "Passzív résztvevők", "room.passive": "Passzív résztvevők",
"room.videoPaused": "Ez a videóstream szünetel", "room.videoPaused": "Ez a videóstream szünetel",
"room.muteAll": null, "room.muteAll": "Mindenki némítása",
"room.stopAllVideo": null, "room.stopAllVideo": "Mindenki video némítása",
"room.closeMeeting": null, "room.closeMeeting": "Konferencia lebontása",
"room.speechUnsupported": null, "room.clearChat": "Chat történelem kiürítése",
"room.clearFileSharing": "File megosztás kiürítése",
"room.speechUnsupported": "A böngésződ nem támogatja a hangfelismerést",
"room.moderatoractions": "Moderátor funkciók",
"room.raisedHand": "{displayName} jelentkezik",
"room.loweredHand": "{displayName} leeresztette a kezét",
"room.extraVideo": "Kiegészítő videó",
"room.overRoomLimit": "A konferenciaszoba betelt..",
"me.mutedPTT": null, "me.mutedPTT": "Némítva vagy, ha beszélnél nyomd le a szóköz billentyűt",
"roles.gotRole": "{role} szerepet kaptál",
"roles.lostRole": "Elvesztetted a {role} szerepet",
"tooltip.login": "Belépés", "tooltip.login": "Belépés",
"tooltip.logout": "Kilépés", "tooltip.logout": "Kilépés",
"tooltip.admitFromLobby": "Beenegdem az előszobából", "tooltip.admitFromLobby": "Beengedem az előszobából",
"tooltip.lockRoom": "A konferenciába való belépés letiltása", "tooltip.lockRoom": "A konferenciába való belépés letiltása",
"tooltip.unLockRoom": "konferenciába való belépés engedélyezése", "tooltip.unLockRoom": "konferenciába való belépés engedélyezése",
"tooltip.enterFullscreen": "Teljes képernyős mód", "tooltip.enterFullscreen": "Teljes képernyős mód",
@ -66,7 +76,11 @@
"tooltip.lobby": "Az előszobában várakozók listája", "tooltip.lobby": "Az előszobában várakozók listája",
"tooltip.settings": "Beállítások", "tooltip.settings": "Beállítások",
"tooltip.participants": "Résztvevők", "tooltip.participants": "Résztvevők",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": "Résztvevő kirúgása",
"tooltip.muteParticipant": "Résztvevő némítása",
"tooltip.muteParticipantVideo": "Résztvevő videóstreamének némítása",
"tooltip.raisedHand": "Jelentkezés",
"tooltip.muteScreenSharing": "Képernyőmegosztás szüneteltetése",
"label.roomName": "Konferencia", "label.roomName": "Konferencia",
"label.chooseRoomButton": "Tovább", "label.chooseRoomButton": "Tovább",
@ -80,6 +94,7 @@
"label.filesharing": "Fájl megosztás", "label.filesharing": "Fájl megosztás",
"label.participants": "Résztvevők", "label.participants": "Résztvevők",
"label.shareFile": "Fájl megosztása", "label.shareFile": "Fájl megosztása",
"label.shareGalleryFile": "Fájl megosztás galériából",
"label.fileSharingUnsupported": "Fájl megosztás nem támogatott", "label.fileSharingUnsupported": "Fájl megosztás nem támogatott",
"label.unknown": "Ismeretlen", "label.unknown": "Ismeretlen",
"label.democratic": "Egyforma képméretű képkiosztás", "label.democratic": "Egyforma képméretű képkiosztás",
@ -90,25 +105,36 @@
"label.veryHigh": "Nagyon magas (FHD)", "label.veryHigh": "Nagyon magas (FHD)",
"label.ultra": "Ultra magas (UHD)", "label.ultra": "Ultra magas (UHD)",
"label.close": "Bezár", "label.close": "Bezár",
"label.media": "Média",
"label.appearence": "Megjelenés",
"label.advanced": "Részletek",
"label.addVideo": "Videó hozzáadása",
"label.promoteAllPeers": "Mindenkit beengedek",
"label.moreActions": "További műveletek",
"settings.settings": "Beállítások", "settings.settings": "Beállítások",
"settings.camera": "Kamera", "settings.camera": "Kamera",
"settings.selectCamera": "Válasz videóeszközt", "settings.selectCamera": "Válassz videoeszközt",
"settings.cantSelectCamera": "Nem lehet a videó eszközt kiválasztani", "settings.cantSelectCamera": "Nem lehet a videoeszközt kiválasztani",
"settings.audio": "Hang eszköz", "settings.audio": "Hang eszköz",
"settings.selectAudio": "Válasz hangeszközt", "settings.selectAudio": "Válassz hangeszközt",
"settings.cantSelectAudio": "Nem lehet a hang eszközt kiválasztani", "settings.cantSelectAudio": "Nem sikerült a hangeszközt kiválasztani",
"settings.resolution": "Válaszd ki a videóeszközöd felbontását", "settings.audioOutput": "Kimenti hangeszköz",
"settings.selectAudioOutput": "Válassz kimenti hangeszközt",
"settings.cantSelectAudioOutput": "Nem sikerült a kimeneti hangeszközt kiválasztani",
"settings.resolution": "Válaszd ki a videoeszközöd felbontását",
"settings.layout": "A konferencia képkiosztása", "settings.layout": "A konferencia képkiosztása",
"settings.selectRoomLayout": "Válaszd ki a konferencia képkiosztását", "settings.selectRoomLayout": "Válaszd ki a konferencia képkiosztását",
"settings.advancedMode": "Részletes információk", "settings.advancedMode": "Részletes információk",
"settings.permanentTopBar": "Állandó felső sáv", "settings.permanentTopBar": "Állandó felső sáv",
"settings.lastn": "A látható videók száma", "settings.lastn": "A látható videók száma",
"settings.hiddenControls": "Média Gombok automatikus elrejtése",
"settings.notificationSounds": "Értesítések hangjelzéssel",
"filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.saveFileError": "A file-t nem sikerült elmenteni",
"filesharing.startingFileShare": "Fájl megosztása", "filesharing.startingFileShare": "Fájl megosztása",
"filesharing.successfulFileShare": "A fájl sikeresen megosztva", "filesharing.successfulFileShare": "A fájl sikeresen megosztva",
"filesharing.unableToShare": "Sikereteln fájl megosztás", "filesharing.unableToShare": "Sikertelen fájl megosztás",
"filesharing.error": "Hiba a fájlmegosztás során", "filesharing.error": "Hiba a fájlmegosztás során",
"filesharing.finished": "A fájl letöltés befejeződött", "filesharing.finished": "A fájl letöltés befejeződött",
"filesharing.save": "Mentés", "filesharing.save": "Mentés",
@ -118,7 +144,7 @@
"devices.devicesChanged": "Az eszközei megváltoztak, konfiguráld őket be a beállítások menüben", "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.audioUnsupported": "A hang nem támogatott",
"device.activateAudio": "Hang aktiválása", "device.activateAudio": "Hang aktiválása",
"device.muteAudio": "Hang némítása", "device.muteAudio": "Hang némítása",
"device.unMuteAudio": "Hang némítás kikapcsolása", "device.unMuteAudio": "Hang némítás kikapcsolása",
@ -129,12 +155,12 @@
"device.screenSharingUnsupported": "A képernyő megosztás nem támogatott", "device.screenSharingUnsupported": "A képernyő megosztás nem támogatott",
"device.startScreenSharing": "Képernyőmegosztás indítása", "device.startScreenSharing": "Képernyőmegosztás indítása",
"device.stopScreenSharing": "Képernyőmegosztás leáłłítása", "device.stopScreenSharing": "Képernyőmegosztás leállítása",
"devices.microphoneDisconnected": "Microphone kapcsolat bontva", "devices.microphoneDisconnected": "Mikrofon kapcsolat bontva",
"devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben", "devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben",
"devices.microPhoneMute": "A mikrofon némítva lett", "devices.microphoneMute": "A mikrofon némítva lett",
"devices.micophoneUnMute": "A mikrofon némítása ki lett kapocsolva", "devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva",
"devices.microphoneEnable": "A mikrofon engedéylezve", "devices.microphoneEnable": "A mikrofon engedéylezve",
"devices.microphoneMuteError": "Nem sikerült a mikrofonod némítása", "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.microphoneUnMuteError": "Nem sikerült a mikrofonod némításának kikapcsolása",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Hiba történt a képernyőd megosztása során", "devices.screenSharingError": "Hiba történt a képernyőd megosztása során",
"devices.cameraDisconnected": "A kamera kapcsolata lebomlott", "devices.cameraDisconnected": "A kamera kapcsolata lebomlott",
"devices.cameraError": "Hiba történt a kamera elérése során" "devices.cameraError": "Hiba történt a kamera elérése során",
"moderator.clearChat": "A moderátor kiürítette a chat történelmet",
"moderator.clearFiles": "A moderátor kiürítette a file megosztás történelmet",
"moderator.muteAudio": "A moderátor elnémította a hangod",
"moderator.muteVideo": "A moderátor elnémította a videód"
} }

View File

@ -49,12 +49,22 @@
"room.spotlights": "Partecipanti in Evidenza", "room.spotlights": "Partecipanti in Evidenza",
"room.passive": "Participanti Passivi", "room.passive": "Participanti Passivi",
"room.videoPaused": "Il video è in pausa", "room.videoPaused": "Il video è in pausa",
"room.muteAll": null, "room.muteAll": "Muta tutti",
"room.stopAllVideo": null, "room.stopAllVideo": "Ferma tutti i video",
"room.closeMeeting": null, "room.closeMeeting": "Termina meeting",
"room.speechUnsupported": null, "room.clearChat": "Pulisci chat",
"room.clearFileSharing": "Pulisci file sharing",
"room.speechUnsupported": "Il tuo browser non supporta il riconoscimento vocale",
"room.moderatoractions": "Azioni moderatore",
"room.raisedHand": "{displayName} ha alzato la mano",
"room.loweredHand": "{displayName} ha abbassato la mano",
"room.extraVideo": "Video extra",
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": "Sei mutato, tieni premuto SPAZIO per parlare",
"roles.gotRole": "Hai ottenuto il ruolo: {role}",
"roles.lostRole": "Hai perso il ruolo: {role}",
"tooltip.login": "Log in", "tooltip.login": "Log in",
"tooltip.logout": "Log out", "tooltip.logout": "Log out",
@ -66,6 +76,10 @@
"tooltip.lobby": "Mostra lobby", "tooltip.lobby": "Mostra lobby",
"tooltip.settings": "Mostra impostazioni", "tooltip.settings": "Mostra impostazioni",
"tooltip.participants": "Mostra partecipanti", "tooltip.participants": "Mostra partecipanti",
"tooltip.muteParticipant": "Muta partecipante",
"tooltip.muteParticipantVideo": "Ferma video partecipante",
"tooltip.raisedHand": "Mano alzata",
"tooltip.muteScreenSharing": null,
"label.roomName": "Nome della stanza", "label.roomName": "Nome della stanza",
"label.chooseRoomButton": "Continua", "label.chooseRoomButton": "Continua",
@ -79,6 +93,7 @@
"label.filesharing": "Condivisione file", "label.filesharing": "Condivisione file",
"label.participants": "Partecipanti", "label.participants": "Partecipanti",
"label.shareFile": "Condividi file", "label.shareFile": "Condividi file",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Condivisione file non supportata", "label.fileSharingUnsupported": "Condivisione file non supportata",
"label.unknown": "Sconosciuto", "label.unknown": "Sconosciuto",
"label.democratic": "Vista Democratica", "label.democratic": "Vista Democratica",
@ -89,6 +104,12 @@
"label.veryHigh": "Molto alta (FHD)", "label.veryHigh": "Molto alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Chiudi", "label.close": "Chiudi",
"label.media": "Media",
"label.appearence": "Aspetto",
"label.advanced": "Avanzate",
"label.addVideo": "Aggiungi video",
"label.promoteAllPeers": "Promuovi tutti",
"label.moreActions": null,
"settings.settings": "Impostazioni", "settings.settings": "Impostazioni",
"settings.camera": "Videocamera", "settings.camera": "Videocamera",
@ -97,12 +118,17 @@
"settings.audio": "Dispositivo audio", "settings.audio": "Dispositivo audio",
"settings.selectAudio": "Seleziona dispositivo audio", "settings.selectAudio": "Seleziona dispositivo audio",
"settings.cantSelectAudio": "Impossibile selezionare dispositivo audio", "settings.cantSelectAudio": "Impossibile selezionare dispositivo audio",
"settings.audioOutput": "Dispositivo di uscita audio",
"settings.selectAudioOutput": "Seleziona il dispositivo di uscita audio",
"settings.cantSelectAudioOutput": "Impossibile selezionare il dispositivo di uscita audio",
"settings.resolution": "Seleziona risoluzione", "settings.resolution": "Seleziona risoluzione",
"settings.layout": "Aspetto stanza", "settings.layout": "Aspetto stanza",
"settings.selectRoomLayout": "Seleziona aspetto stanza", "settings.selectRoomLayout": "Seleziona aspetto stanza",
"settings.advancedMode": "Modalità avanzata", "settings.advancedMode": "Modalità avanzata",
"settings.permanentTopBar": "Barra superiore permanente", "settings.permanentTopBar": "Barra superiore permanente",
"settings.lastn": "Numero di video visibili", "settings.lastn": "Numero di video visibili",
"settings.hiddenControls": "Controlli media nascosti",
"settings.notificationSounds": "Suoni di notifica",
"filesharing.saveFileError": "Impossibile salvare file", "filesharing.saveFileError": "Impossibile salvare file",
"filesharing.startingFileShare": "Tentativo di condivisione file", "filesharing.startingFileShare": "Tentativo di condivisione file",
@ -132,8 +158,8 @@
"devices.microphoneDisconnected": "Microfono scollegato", "devices.microphoneDisconnected": "Microfono scollegato",
"devices.microphoneError": "Errore con l'accesso al microfono", "devices.microphoneError": "Errore con l'accesso al microfono",
"devices.microPhoneMute": "Microfono silenziato", "devices.microphoneMute": "Microfono silenziato",
"devices.micophoneUnMute": "Microfono riattivato", "devices.microphoneUnMute": "Microfono riattivato",
"devices.microphoneEnable": "Microfono attivo", "devices.microphoneEnable": "Microfono attivo",
"devices.microphoneMuteError": "Impossibile silenziare il microfono", "devices.microphoneMuteError": "Impossibile silenziare il microfono",
"devices.microphoneUnMuteError": "Impossibile riattivare il microfono", "devices.microphoneUnMuteError": "Impossibile riattivare il microfono",
@ -142,5 +168,10 @@
"devices.screenSharingError": "Errore con l'accesso al tuo schermo", "devices.screenSharingError": "Errore con l'accesso al tuo schermo",
"devices.cameraDisconnected": "Videocamera scollegata", "devices.cameraDisconnected": "Videocamera scollegata",
"devices.cameraError": "Errore con l'accesso alla videocamera" "devices.cameraError": "Errore con l'accesso alla videocamera",
"moderator.clearChat": "Il moderatore ha pulito la chat",
"moderator.clearFiles": "Il moderatore ha pulito i file",
"moderator.muteAudio": "Il moderatore ha mutato il tuo audio",
"moderator.muteVideo": "Il moderatore ha fermato il tuo video"
} }

View File

@ -0,0 +1,172 @@
{
"socket.disconnected": "Esat bezsaistē",
"socket.reconnecting": "Esat bezsaistē, tiek mēģināts pievienoties",
"socket.reconnected": "Esat atkārtoti pievienojies",
"socket.requestError": "Kļūme servera pieprasījumā",
"room.chooseRoom": "Ievadiet sapulces telpas nosaukumu (ID), kurai vēlaties pievienoties",
"room.cookieConsent": "Lai uzlabotu lietotāja pieredzi, šī vietne izmanto sīkfailus",
"room.consentUnderstand": "Es saprotu un piekrītu",
"room.joined": "Jūs esiet pievienojies sapulces telpai",
"room.cantJoin": "Nav iespējams pievienoties sapulces telpai",
"room.youLocked": "Jūs aizslēdzāt sapulces telpu",
"room.cantLock": "Nav iespējams aizslēgt sapulces telpu",
"room.youUnLocked": "Jūs atslēdzāt sapulces telpu",
"room.cantUnLock": "Nav iespējams atslēgt sapulces telpu",
"room.locked": "Sapulces telpa tagad ir AIZSLĒGTA",
"room.unlocked": "Sapulces telpa tagad ir ATSLĒGTA",
"room.newLobbyPeer": "Jauns dalībnieks ienācis uzgaidāmajā telpā",
"room.lobbyPeerLeft": "Dalībnieks uzgaidāmo telpu pameta",
"room.lobbyPeerChangedDisplayName": "Dalībnieks uzgaidāmajā telpā nomainīja vārdu uz {displayName}",
"room.lobbyPeerChangedPicture": "Dalībnieks uzgaidāmajā telpā nomainīja pašattēlu",
"room.setAccessCode": "Pieejas kods sapulces telpai aktualizēts",
"room.accessCodeOn": "Pieejas kods sapulces telpai tagad ir aktivēts",
"room.accessCodeOff": "Pieejas kods sapulces telpai tagad ir deaktivēts (atslēgts)",
"room.peerChangedDisplayName": "{oldDisplayName} pārsaucās par {displayName}",
"room.newPeer": "{displayName} pievienojās sapulces telpai",
"room.newFile": "Pieejams jauns fails",
"room.toggleAdvancedMode": "Pārslēgt uz advancēto režīmu",
"room.setDemocraticView": "Nomainīts izkārtojums uz demokrātisko skatu",
"room.setFilmStripView": "Nomainīts izkārtojums uz diapozitīvu (filmstrip) skatu",
"room.loggedIn": "Jūs esat ierakstījies (sistēmā)",
"room.loggedOut": "Jūs esat izrakstījies (no sistēmas)",
"room.changedDisplayName": "Jūsu vārds mainīts uz {displayName}",
"room.changeDisplayNameError": "Gadījās ķibele ar Jūsu vārda nomaiņu",
"room.chatError": "Nav iespējams nosūtīt tērziņa ziņu",
"room.aboutToJoin": "Jūs grasāties pievienoties sapulcei",
"room.roomId": "Sapulces telpas nosaukums (ID): {roomName}",
"room.setYourName": "Norādiet savu dalības vārdu un izvēlieties kā vēlaties pievienoties sapulcei:",
"room.audioOnly": "Vienīgi audio",
"room.audioVideo": "Audio & video",
"room.youAreReady": "Ok, Jūs esiet gatavi!",
"room.emptyRequireLogin": "Sapulces telpa ir tukša! Jūs varat Ierakstīties sistēmā, lai uzsāktu vadīt sapulci vai pagaidīt kamēr pievienojas sapulces rīkotājs/vadītājs",
"room.locketWait": "Sapulce telpa ir slēgta. Jūs atrodaties tās uzgaidāmajā telpā. Uzkavējieties, kamēr kāds Jūs sapulcē ielaiž ...",
"room.lobbyAdministration": "Uzgaidāmās telpas administrēšana",
"room.peersInLobby": "Dalībnieki uzgaidāmajā telpā",
"room.lobbyEmpty": "Pašreiz uzgaidāmajā telpā neviena nav",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Es",
"room.spotlights": "Aktīvie (referējošie) dalībnieki",
"room.passive": "Pasīvie dalībnieki",
"room.videoPaused": "Šis video ir pauzēts",
"room.muteAll": "Noklusināt visus dalībnieku mikrofonus",
"room.stopAllVideo": "Izslēgt visu dalībnieku kameras",
"room.closeMeeting": "Beigt sapulci",
"room.clearChat": "Nodzēst visus tērziņus",
"room.clearFileSharing": "Notīrīt visus kopīgotos failus",
"room.speechUnsupported": "Jūsu pārlūks neatbalsta balss atpazīšanu",
"room.moderatoractions": "Moderatora rīcība",
"room.raisedHand": "{displayName} pacēla roku",
"room.loweredHand": "{displayName} nolaida roku",
"room.extraVideo": "Papildus video",
"me.mutedPTT": "Jūs esat noklusināts. Turiet taustiņu SPACE-BAR, lai runātu",
"roles.gotRole": "Jūs ieguvāt lomu: {role}",
"roles.lostRole": "Jūs zaudējāt lomu: {role}",
"tooltip.login": "Ierakstīties",
"tooltip.logout": "Izrakstīties",
"tooltip.admitFromLobby": "Ielaist no uzgaidāmās telpas",
"tooltip.lockRoom": "Aizslēgt sapulces telpu",
"tooltip.unLockRoom": "Atlēgt sapulces telpu",
"tooltip.enterFullscreen": "Aktivēt pilnekrāna režīmu",
"tooltip.leaveFullscreen": "Pamest pilnekrānu",
"tooltip.lobby": "Parādīt uzgaidāmo telpu",
"tooltip.settings": "Parādīt iestatījumus",
"tooltip.participants": "Parādīt dalībniekus",
"tooltip.kickParticipant": "Izvadīt (izspert) dalībnieku",
"tooltip.muteParticipant": "Noklusināt dalībnieku",
"tooltip.muteParticipantVideo": "Atslēgt dalībnieka video",
"tooltip.raisedHand": "Pacelt roku",
"tooltip.muteScreenSharing": null,
"label.roomName": "Sapulces telpas nosaukums (ID)",
"label.chooseRoomButton": "Turpināt",
"label.yourName": "Jūu vārds",
"label.newWindow": "Jauns logs",
"label.fullscreen": "Pilnekrāns",
"label.openDrawer": "Atvērt atvilkni",
"label.leave": "Pamest",
"label.chatInput": "Rakstiet tērziņa ziņu...",
"label.chat": "Tērzētava",
"label.filesharing": "Failu koplietošana",
"label.participants": "Dalībnieki",
"label.shareFile": "Koplietot failu",
"label.fileSharingUnsupported": "Failu koplietošana netiek atbalstīta",
"label.unknown": "Nezināms",
"label.democratic": "Demokrātisks skats",
"label.filmstrip": "Diapozitīvu (filmstrip) skats",
"label.low": "Zema",
"label.medium": "Vidēja",
"label.high": "Augsta (HD)",
"label.veryHigh": "Ļoti augsta (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Aizvērt",
"label.media": "Mediji",
"label.appearence": "Izskats",
"label.advanced": "Advancēts",
"label.addVideo": "Pievienot video",
"label.moreActions": null,
"settings.settings": "Iestatījumi",
"settings.camera": "Kamera",
"settings.selectCamera": "Izvēlieties kameru (video ierīci)",
"settings.cantSelectCamera": "Nav iespējams lietot šo kameru (video ierīci)",
"settings.audio": "Skaņas ierīce",
"settings.selectAudio": "Izvēlieties skaņas ierīci",
"settings.cantSelectAudio": "Nav iespējams lietot šo skaņas (audio) ierīci",
"settings.resolution": "Iestatiet jūsu video izšķirtspēju",
"settings.layout": "Sapulces telpas izkārtojums",
"settings.selectRoomLayout": "Iestatiet sapulces telpas izkārtojumu",
"settings.advancedMode": "Advancētais režīms",
"settings.permanentTopBar": "Pastāvīga augšējā (ekrānaugšas) josla",
"settings.lastn": "Jums redzamo video/kameru skaits",
"settings.hiddenControls": "Slēpto mediju vadība",
"settings.notificationSounds": "Paziņojumu skaņas",
"filesharing.saveFileError": "Nav iespējams saglabāt failu",
"filesharing.startingFileShare": "Tiek mēģināts kopīgot failu",
"filesharing.successfulFileShare": "Fails sekmīgi kopīgots",
"filesharing.unableToShare": "Nav iespējams kopīgot failu",
"filesharing.error": "Atgadījās faila kopīgošanas kļūme",
"filesharing.finished": "Fails ir lejupielādēts",
"filesharing.save": "Saglabāt",
"filesharing.sharedFile": "{displayName} kopīgoja failu",
"filesharing.download": "Lejuplādēt",
"filesharing.missingSeeds": "Ja šis process aizņem ilgu laiku, iespējams nav neviena, kas sēklo (seed) šo torentu. Mēģiniet palūgt kādu atkārtoti augšuplādēt Jūsu gribēto failu.",
"devices.devicesChanged": "Jūsu ierīces pamainījās. Iestatījumu izvēlnē (dialogā) iestatiet jaunās ierīces.",
"device.audioUnsupported": "Skaņa (audio) netiek atbalstīta",
"device.activateAudio": "Iespējot/aktivēt mikrofonu (izejošo skaņu)",
"device.muteAudio": "Atslēgt/noklusināt mikrofonu (izejošo skaņu) ",
"device.unMuteAudio": "Ieslēgt mikrofonu (izejošo skaņu)",
"device.videoUnsupported": "Kamera (izejošais video) netiek atbalstīta",
"device.startVideo": "Ieslēgt kameru (izejošo video)",
"device.stopVideo": "Izslēgt kameru (izejošo video)",
"device.screenSharingUnsupported": "Ekrāna kopīgošana netiek atbalstīta",
"device.startScreenSharing": "Sākt ekrāna kopīgošanu",
"device.stopScreenSharing": "Beigt ekrāna kopīgošanu",
"devices.microphoneDisconnected": "Mikrofons atvienots",
"devices.microphoneError": "Atgadījās kļūme, piekļūstot jūsu mikrofonam",
"devices.microPhoneMute": "Mikrofons izslēgts/noklusināts",
"devices.micophoneUnMute": "Mikrofons ieslēgts",
"devices.microphoneEnable": "Mikrofons iespējots",
"devices.microphoneMuteError": "Nav iespējams izslēgt Jūsu mikrofonu",
"devices.microphoneUnMuteError": "Nav iespējams ieslēgt Jūsu mikrofonu",
"devices.screenSharingDisconnected" : "Ekrāna kopīgošana nenotiek (atvienota)",
"devices.screenSharingError": "Atgadījās kļūme, piekļūstot Jūsu ekrānam",
"devices.cameraDisconnected": "Kamera atvienota",
"devices.cameraError": "Atgadījās kļūme, piekļūstot Jūsu kamerai",
"moderator.clearChat": "Moderators nodzēsa tērziņus",
"moderator.clearFiles": "Moderators notīrīja failus",
"moderator.muteAudio": "Moderators noklusināja jūsu mikrofonu",
"moderator.muteVideo": "Moderators atslēdza jūsu kameru"
}

View File

@ -52,10 +52,20 @@
"room.muteAll": "Demp alle", "room.muteAll": "Demp alle",
"room.stopAllVideo": "Stopp all video", "room.stopAllVideo": "Stopp all video",
"room.closeMeeting": "Avslutt møte", "room.closeMeeting": "Avslutt møte",
"room.clearChat": "Tøm chat",
"room.clearFileSharing": "Fjern filer",
"room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning", "room.speechUnsupported": "Din nettleser støtter ikke stemmegjenkjenning",
"room.moderatoractions": "Moderatorhandlinger",
"room.raisedHand": "{displayName} rakk opp hånden",
"room.loweredHand": "{displayName} tok ned hånden",
"room.extraVideo": "Ekstra video",
"room.overRoomLimit": "Rommet er fullt, prøv igjen om litt.",
"me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke", "me.mutedPTT": "Du er dempet, hold nede SPACE for å snakke",
"roles.gotRole": "Du fikk rollen: {role}",
"roles.lostRole": "Du mistet rollen: {role}",
"tooltip.login": "Logg in", "tooltip.login": "Logg in",
"tooltip.logout": "Logg ut", "tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby", "tooltip.admitFromLobby": "Slipp inn fra lobby",
@ -67,6 +77,10 @@
"tooltip.settings": "Vis innstillinger", "tooltip.settings": "Vis innstillinger",
"tooltip.participants": "Vis deltakere", "tooltip.participants": "Vis deltakere",
"tooltip.kickParticipant": "Spark ut deltaker", "tooltip.kickParticipant": "Spark ut deltaker",
"tooltip.muteParticipant": "Demp deltaker",
"tooltip.muteParticipantVideo": "Demp deltakervideo",
"tooltip.raisedHand": "Rekk opp hånden",
"tooltip.muteScreenSharing": "Demp deltaker skjermdeling",
"label.roomName": "Møtenavn", "label.roomName": "Møtenavn",
"label.chooseRoomButton": "Fortsett", "label.chooseRoomButton": "Fortsett",
@ -80,6 +94,7 @@
"label.filesharing": "Fildeling", "label.filesharing": "Fildeling",
"label.participants": "Deltakere", "label.participants": "Deltakere",
"label.shareFile": "Del fil", "label.shareFile": "Del fil",
"label.shareGalleryFile": "Del bilde",
"label.fileSharingUnsupported": "Fildeling ikke støttet", "label.fileSharingUnsupported": "Fildeling ikke støttet",
"label.unknown": "Ukjent", "label.unknown": "Ukjent",
"label.democratic": "Demokratisk", "label.democratic": "Demokratisk",
@ -90,6 +105,12 @@
"label.veryHigh": "Veldig høy (FHD)", "label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Lukk", "label.close": "Lukk",
"label.media": "Media",
"label.appearence": "Utseende",
"label.advanced": "Avansert",
"label.addVideo": "Legg til video",
"label.promoteAllPeers": "Slipp inn alle",
"label.moreActions": "Flere handlinger",
"settings.settings": "Innstillinger", "settings.settings": "Innstillinger",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -98,12 +119,17 @@
"settings.audio": "Lydenhet", "settings.audio": "Lydenhet",
"settings.selectAudio": "Velg lydenhet", "settings.selectAudio": "Velg lydenhet",
"settings.cantSelectAudio": "Kan ikke velge lydenhet", "settings.cantSelectAudio": "Kan ikke velge lydenhet",
"settings.audioOutput": "Lydutgangsenhet",
"settings.selectAudioOutput": "Velg lydutgangsenhet",
"settings.cantSelectAudioOutput": "Kan ikke velge lydutgangsenhet",
"settings.resolution": "Velg oppløsning", "settings.resolution": "Velg oppløsning",
"settings.layout": "Møtelayout", "settings.layout": "Møtelayout",
"settings.selectRoomLayout": "Velg møtelayout", "settings.selectRoomLayout": "Velg møtelayout",
"settings.advancedMode": "Avansert modus", "settings.advancedMode": "Avansert modus",
"settings.permanentTopBar": "Permanent topplinje", "settings.permanentTopBar": "Permanent topplinje",
"settings.lastn": "Antall videoer synlig", "settings.lastn": "Antall videoer synlig",
"settings.hiddenControls": "Skjul media knapper",
"settings.notificationSounds": "Varslingslyder",
"filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling", "filesharing.startingFileShare": "Starter fildeling",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Mikrofon koblet fra", "devices.microphoneDisconnected": "Mikrofon koblet fra",
"devices.microphoneError": "Det skjedde noe feil med mikrofonen din", "devices.microphoneError": "Det skjedde noe feil med mikrofonen din",
"devices.microPhoneMute": "Dempet mikrofonen", "devices.microphoneMute": "Dempet mikrofonen",
"devices.micophoneUnMute": "Aktiverte mikrofonen", "devices.microphoneUnMute": "Aktiverte mikrofonen",
"devices.microphoneEnable": "Aktiverte mikrofonen", "devices.microphoneEnable": "Aktiverte mikrofonen",
"devices.microphoneMuteError": "Klarte ikke å dempe mikrofonen", "devices.microphoneMuteError": "Klarte ikke å dempe mikrofonen",
"devices.microphoneUnMuteError": "Klarte ikke å aktivere mikrofonen", "devices.microphoneUnMuteError": "Klarte ikke å aktivere mikrofonen",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Det skjedde noe feil med skjermdelingen din", "devices.screenSharingError": "Det skjedde noe feil med skjermdelingen din",
"devices.cameraDisconnected": "Kamera koblet fra", "devices.cameraDisconnected": "Kamera koblet fra",
"devices.cameraError": "Det skjedde noe feil med kameraet ditt" "devices.cameraError": "Det skjedde noe feil med kameraet ditt",
"moderator.clearChat": "Moderator tømte chatten",
"moderator.clearFiles": "Moderator fjernet filer",
"moderator.muteAudio": "Moderator mutet lyden din",
"moderator.muteVideo": "Moderator mutet videoen din"
} }

View File

@ -6,7 +6,7 @@
"room.chooseRoom": "Wybór konferencji", "room.chooseRoom": "Wybór konferencji",
"room.cookieConsent": "Ta strona internetowa wykorzystuje pliki cookie w celu zwiększenia wygody użytkowania.", "room.cookieConsent": "Ta strona internetowa wykorzystuje pliki cookie w celu zwiększenia wygody użytkowania.",
"room.consentUnderstand": "I understand", "room.consentUnderstand": "Rozumiem",
"room.joined": "Podłączono do konferencji", "room.joined": "Podłączono do konferencji",
"room.cantJoin": "Brak możliwości dołączenia do pokoju", "room.cantJoin": "Brak możliwości dołączenia do pokoju",
"room.youLocked": "Zakluczono pokój", "room.youLocked": "Zakluczono pokój",
@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Zaloguj", "tooltip.login": "Zaloguj",
"tooltip.logout": "Wyloguj", "tooltip.logout": "Wyloguj",
"tooltip.admitFromLobby": "Przejście z poczekalni", "tooltip.admitFromLobby": "Przejście z poczekalni",
@ -67,6 +77,10 @@
"tooltip.settings": "Pokaż ustawienia", "tooltip.settings": "Pokaż ustawienia",
"tooltip.participants": "Pokaż uczestników", "tooltip.participants": "Pokaż uczestników",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nazwa konferencji", "label.roomName": "Nazwa konferencji",
"label.chooseRoomButton": "Kontynuuj", "label.chooseRoomButton": "Kontynuuj",
@ -80,6 +94,7 @@
"label.filesharing": "Udostępnianie plików", "label.filesharing": "Udostępnianie plików",
"label.participants": "Uczestnicy", "label.participants": "Uczestnicy",
"label.shareFile": "Udostępnij plik", "label.shareFile": "Udostępnij plik",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane", "label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane",
"label.unknown": "Nieznane", "label.unknown": "Nieznane",
"label.democratic": "Układ demokratyczny", "label.democratic": "Układ demokratyczny",
@ -90,6 +105,12 @@
"label.veryHigh": "Bardzo wysoka (FHD)", "label.veryHigh": "Bardzo wysoka (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Zamknij", "label.close": "Zamknij",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ustawienia", "settings.settings": "Ustawienia",
"settings.camera": "Kamera", "settings.camera": "Kamera",
@ -98,12 +119,17 @@
"settings.audio": "Urządzenie audio", "settings.audio": "Urządzenie audio",
"settings.selectAudio": "Wybór urządzenia audio", "settings.selectAudio": "Wybór urządzenia audio",
"settings.cantSelectAudio": "Nie można wybrać urządzenia audio", "settings.cantSelectAudio": "Nie można wybrać urządzenia audio",
"settings.audioOutput": "Urządzenie wyjściowe audio",
"settings.selectAudioOutput": "Wybierz urządzenie wyjściowe audio",
"settings.cantSelectAudioOutput": "Nie można wybrać urządzenia wyjściowego audio",
"settings.resolution": "Wybór rozdzielczości wideo", "settings.resolution": "Wybór rozdzielczości wideo",
"settings.layout": "Układ konferencji", "settings.layout": "Układ konferencji",
"settings.selectRoomLayout": "Ustawienia układu konferencji", "settings.selectRoomLayout": "Ustawienia układu konferencji",
"settings.advancedMode": "Tryb zaawansowany", "settings.advancedMode": "Tryb zaawansowany",
"settings.permanentTopBar": "Stały górny pasek", "settings.permanentTopBar": "Stały górny pasek",
"settings.lastn": "Liczba widocznych filmów", "settings.lastn": "Liczba widocznych uczestników (zdalnych)",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.saveFileError": "Nie można zapisać pliku",
"filesharing.startingFileShare": "Próba udostępnienia pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Odłączono mikrofon", "devices.microphoneDisconnected": "Odłączono mikrofon",
"devices.microphoneError": "Błąd dostępu do mikrofonu", "devices.microphoneError": "Błąd dostępu do mikrofonu",
"devices.microPhoneMute": "Wyciszenie mikrofonu włączone", "devices.microphoneMute": "Wyciszenie mikrofonu włączone",
"devices.micophoneUnMute": "Wyciszenie mikrofonu wyłączone", "devices.microphoneUnMute": "Wyciszenie mikrofonu wyłączone",
"devices.microphoneEnable": "Włączono mikrofon", "devices.microphoneEnable": "Włączono mikrofon",
"devices.microphoneMuteError": "Nie można wyciszyć mikrofonu", "devices.microphoneMuteError": "Nie można wyciszyć mikrofonu",
"devices.microphoneUnMuteError": "Nie można wyłączyć wyciszenia mikrofonu.", "devices.microphoneUnMuteError": "Nie można wyłączyć wyciszenia mikrofonu.",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Wystąpił błąd podczas uzyskiwania dostępu do ekranu", "devices.screenSharingError": "Wystąpił błąd podczas uzyskiwania dostępu do ekranu",
"devices.cameraDisconnected": "Kamera odłączona", "devices.cameraDisconnected": "Kamera odłączona",
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery" "devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Entrar", "tooltip.login": "Entrar",
"tooltip.logout": "Sair", "tooltip.logout": "Sair",
"tooltip.admitFromLobby": "Admitir da sala de espera", "tooltip.admitFromLobby": "Admitir da sala de espera",
@ -67,6 +77,10 @@
"tooltip.settings": "Apresentar definições", "tooltip.settings": "Apresentar definições",
"tooltip.participants": "Apresentar participantes", "tooltip.participants": "Apresentar participantes",
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Nome da sala", "label.roomName": "Nome da sala",
"label.chooseRoomButton": "Continuar", "label.chooseRoomButton": "Continuar",
@ -80,6 +94,7 @@
"label.filesharing": "Partilha de ficheiro", "label.filesharing": "Partilha de ficheiro",
"label.participants": "Participantes", "label.participants": "Participantes",
"label.shareFile": "Partilhar ficheiro", "label.shareFile": "Partilhar ficheiro",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partilha de ficheiro não disponível", "label.fileSharingUnsupported": "Partilha de ficheiro não disponível",
"label.unknown": "Desconhecido", "label.unknown": "Desconhecido",
"label.democratic": "Vista democrática", "label.democratic": "Vista democrática",
@ -90,6 +105,12 @@
"label.veryHigh": "Muito alta (FHD)", "label.veryHigh": "Muito alta (FHD)",
"label.ultra": "Ultra (UHD)", "label.ultra": "Ultra (UHD)",
"label.close": "Fechar", "label.close": "Fechar",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Definições", "settings.settings": "Definições",
"settings.camera": "Camera", "settings.camera": "Camera",
@ -98,12 +119,17 @@
"settings.audio": "Dispositivo Áudio", "settings.audio": "Dispositivo Áudio",
"settings.selectAudio": "Selecione o seu dispositivo de áudio", "settings.selectAudio": "Selecione o seu dispositivo de áudio",
"settings.cantSelectAudio": "Impossível selecionar o seu dispositivo de áudio", "settings.cantSelectAudio": "Impossível selecionar o seu dispositivo de áudio",
"settings.audioOutput": "Dispositivo de saída de áudio",
"settings.selectAudioOutput": "Selecionar dispositivo de saída de áudio",
"settings.cantSelectAudioOutput": "Não foi possível selecionar o dispositivo de saída de áudio",
"settings.resolution": "Selecione a sua resolução de vídeo", "settings.resolution": "Selecione a sua resolução de vídeo",
"settings.layout": "Disposição da sala", "settings.layout": "Disposição da sala",
"settings.selectRoomLayout": "Seleccione a disposição da sala", "settings.selectRoomLayout": "Seleccione a disposição da sala",
"settings.advancedMode": "Modo avançado", "settings.advancedMode": "Modo avançado",
"settings.permanentTopBar": "Barra superior permanente", "settings.permanentTopBar": "Barra superior permanente",
"settings.lastn": "Número de vídeos visíveis", "settings.lastn": "Número de vídeos visíveis",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.saveFileError": "Impossível de gravar o ficheiro",
"filesharing.startingFileShare": "Tentando partilha de ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Microfone desiligado", "devices.microphoneDisconnected": "Microfone desiligado",
"devices.microphoneError": "Ocorreu um erro no acesso ao microfone", "devices.microphoneError": "Ocorreu um erro no acesso ao microfone",
"devices.microPhoneMute": "Som microfone desativado", "devices.microphoneMute": "Som microfone desativado",
"devices.micophoneUnMute": "Som mmicrofone ativado", "devices.microphoneUnMute": "Som mmicrofone ativado",
"devices.microphoneEnable": "Microfone ativado", "devices.microphoneEnable": "Microfone ativado",
"devices.microphoneMuteError": "Não foi possível cortar o som do microfone", "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.microphoneUnMuteError": "Não foi possível ativar o som do microfone",
@ -143,5 +169,10 @@
"devices.screenSharingError": "Ocorreu um erro no acesso ao seu ecrã", "devices.screenSharingError": "Ocorreu um erro no acesso ao seu ecrã",
"devices.cameraDisconnected": "Câmara desconectada", "devices.cameraDisconnected": "Câmara desconectada",
"devices.cameraError": "Ocorreu um erro no acesso à sua câmara" "devices.cameraError": "Ocorreu um erro no acesso à sua câmara",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -52,10 +52,20 @@
"room.muteAll": null, "room.muteAll": null,
"room.stopAllVideo": null, "room.stopAllVideo": null,
"room.closeMeeting": null, "room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null, "room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null, "me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Intră în cont", "tooltip.login": "Intră în cont",
"tooltip.logout": "Deconectare", "tooltip.logout": "Deconectare",
"tooltip.admitFromLobby": "Admite din hol", "tooltip.admitFromLobby": "Admite din hol",
@ -67,6 +77,10 @@
"tooltip.settings": "Arată setăile", "tooltip.settings": "Arată setăile",
"tooltip.participants": null, "tooltip.participants": null,
"tooltip.kickParticipant": null, "tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Numele camerei", "label.roomName": "Numele camerei",
"label.chooseRoomButton": "Continuare", "label.chooseRoomButton": "Continuare",
@ -80,6 +94,7 @@
"label.filesharing": "Partajarea fișierelor", "label.filesharing": "Partajarea fișierelor",
"label.participants": "Participanți", "label.participants": "Participanți",
"label.shareFile": "Partajează fișierul", "label.shareFile": "Partajează fișierul",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată", "label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată",
"label.unknown": "Necunoscut", "label.unknown": "Necunoscut",
"label.democratic": "Distribuție egală a dimensiunii imaginii", "label.democratic": "Distribuție egală a dimensiunii imaginii",
@ -90,6 +105,12 @@
"label.veryHigh": "Rezoluție foarte înaltă (FHD)", "label.veryHigh": "Rezoluție foarte înaltă (FHD)",
"label.ultra": "Rezoluție ultra înaltă (UHD)", "label.ultra": "Rezoluție ultra înaltă (UHD)",
"label.close": "Închide", "label.close": "Închide",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Setări", "settings.settings": "Setări",
"settings.camera": "Cameră video", "settings.camera": "Cameră video",
@ -98,12 +119,17 @@
"settings.audio": "Dispozitivul audio", "settings.audio": "Dispozitivul audio",
"settings.selectAudio": "Selectarea dispozitivul audio", "settings.selectAudio": "Selectarea dispozitivul audio",
"settings.cantSelectAudio": "Încercarea de a selecta dispozitivul audio a eșuat", "settings.cantSelectAudio": "Încercarea de a selecta dispozitivul audio a eșuat",
"settings.audioOutput": "Dispozitiv de ieșire audio",
"settings.selectAudioOutput": "Selectați dispozitivul de ieșire audio",
"settings.cantSelectAudioOutput": "Imposibil de selectat dispozitivul de ieșire audio",
"settings.resolution": "Selectează rezoluția video", "settings.resolution": "Selectează rezoluția video",
"settings.layout": "Aspectul camerei video", "settings.layout": "Aspectul camerei video",
"settings.selectRoomLayout": "Selectează spectul camerei video", "settings.selectRoomLayout": "Selectează spectul camerei video",
"settings.advancedMode": "Mod avansat", "settings.advancedMode": "Mod avansat",
"settings.permanentTopBar": "Bara de sus permanentă", "settings.permanentTopBar": "Bara de sus permanentă",
"settings.lastn": "Numărul de videoclipuri vizibile", "settings.lastn": "Numărul de videoclipuri vizibile",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
"filesharing.startingFileShare": "Partajarea fișierului", "filesharing.startingFileShare": "Partajarea fișierului",
@ -133,8 +159,8 @@
"devices.microphoneDisconnected": "Microfonul e deconectat", "devices.microphoneDisconnected": "Microfonul e deconectat",
"devices.microphoneError": "A apărut o eroare la accesarea microfonului", "devices.microphoneError": "A apărut o eroare la accesarea microfonului",
"devices.microPhoneMute": "Microfonul e dezactivat", "devices.microphoneMute": "Microfonul e dezactivat",
"devices.micophoneUnMute": "Retragerea dezactivării microfonului", "devices.microphoneUnMute": "Retragerea dezactivării microfonului",
"devices.microphoneEnable": "Microfonul e activat", "devices.microphoneEnable": "Microfonul e activat",
"devices.microphoneMuteError": "Încercarea de a dezactiva microfonului a eșuat", "devices.microphoneMuteError": "Încercarea de a dezactiva microfonului a eșuat",
"devices.microphoneUnMuteError": "Încercarea de a retrage dezactivarea microfonului a eșuat", "devices.microphoneUnMuteError": "Încercarea de a retrage dezactivarea microfonului a eșuat",
@ -143,5 +169,10 @@
"devices.screenSharingError": "A apărut o eroare la accesarea ecranului", "devices.screenSharingError": "A apărut o eroare la accesarea ecranului",
"devices.cameraDisconnected": "Camera video e disconectată", "devices.cameraDisconnected": "Camera video e disconectată",
"devices.cameraError": "A apărut o eroare la accesarea camerei video" "devices.cameraError": "A apărut o eroare la accesarea camerei video",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
} }

View File

@ -0,0 +1,170 @@
{
"socket.disconnected": "Bağlantınız Kesildi",
"socket.reconnecting": "Bağlantınız kesildi, yeniden bağlanmaya çalışılıyor",
"socket.reconnected": "Yeniden bağlandınız",
"socket.requestError": "Sunucu isteğinde hata",
"room.chooseRoom": "Katılmak istediğiniz odanın adını seçin",
"room.cookieConsent": "Bu web sayfası kullanıcı deneyimini geliştirmek için çerezleri kullanmaktadır",
"room.consentUnderstand": "Anladım",
"room.joined": "Odaya katıldın",
"room.cantJoin": "Odaya katılamadın",
"room.youLocked": "Odayı kilitledin",
"room.cantLock": "Oda kilitlenemiyor",
"room.youUnLocked": "Odanın kilidini açtın",
"room.cantUnLock": "Odanın kilidi açılamıyor",
"room.locked": "Oda kilitlendi",
"room.unlocked": "Oda kilidi açıldı",
"room.newLobbyPeer": "Lobiye yeni katılımcı girdi",
"room.lobbyPeerLeft": "Lobiden katılımcı ayrıldı",
"room.lobbyPeerChangedDisplayName": "Lobideki katılımcı adını {displayName} olarak değiştirdi",
"room.lobbyPeerChangedPicture": "Lobideki katılımcı resim değiştirdi",
"room.setAccessCode": "Oda için erişim kodu güncellendi",
"room.accessCodeOn": "Oda erişim kodu etkinleştirildi",
"room.accessCodeOff": "Oda erişim kodu devre dışı",
"room.peerChangedDisplayName": "{oldDisplayName}, {displayName} olarak değiştirildi",
"room.newPeer": "{displayName} odaya katıldı",
"room.newFile": "Yeni dosya mevcut",
"room.toggleAdvancedMode": "Gelişmiş moda geçiş",
"room.setDemocraticView": "Demokratik görünüme geçtiniz",
"room.setFilmStripView": "Filmşeridi görünümüne geçtiniz",
"room.loggedIn": "Giriş yaptınız",
"room.loggedOut": ıkış yaptınız",
"room.changedDisplayName": "Adınız {displayName} olarak değiştirildi",
"room.changeDisplayNameError": "Adınız değiştirilirken bir hata oluştu",
"room.chatError": "Sohbet mesajı gönderilemiyor",
"room.aboutToJoin": "Toplantıya katılmak üzeresiniz",
"room.roomId": "Oda ID: {roomName}",
"room.setYourName": "Katılım için adınızı belirleyin ve nasıl katılmak istediğinizi seçin:",
"room.audioOnly": "Sadece ses",
"room.audioVideo": "Ses ve Video",
"room.youAreReady": "Tamam, hazırsın",
"room.emptyRequireLogin": "Oda boş! Toplantıyı başlatmak için oturum açabilirsiniz veya toplantı sahibi katılana kadar bekleyebilirsiniz",
"room.locketWait": "Oda kilitli - birisi içeri alana kadar bekleyiniz ...",
"room.lobbyAdministration": "Lobi Yöneticisi",
"room.peersInLobby": "Lobideki katılımcılar",
"room.lobbyEmpty": "Lobide katılımcı yok",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Ben",
"room.spotlights": "Gündemdeki Katılımcılar",
"room.passive": "Pasif Katılımcılar",
"room.videoPaused": "Video duraklatıldı",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Giriş",
"tooltip.logout": ıkış",
"tooltip.admitFromLobby": "Lobiden içeri al",
"tooltip.lockRoom": "Oda kilitle",
"tooltip.unLockRoom": "Oda kilidini aç",
"tooltip.enterFullscreen": "Tam Ekrana Geç",
"tooltip.leaveFullscreen": "Tam Ekrandan Çık",
"tooltip.lobby": "Lobiyi göster",
"tooltip.settings": "Ayarları göster",
"tooltip.participants": "Katılımcıları göster",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"label.roomName": "Oda adı",
"label.chooseRoomButton": "Devam",
"label.yourName": "Adınız",
"label.newWindow": "Yeni pencere",
"label.fullscreen": "Tam Ekran",
"label.openDrawer": "Çiziciyi aç",
"label.leave": "Ayrıl",
"label.chatInput": "Sohbet mesajı gir...",
"label.chat": "Sohbet",
"label.filesharing": "Dosya paylaşım",
"label.participants": "Katılımcı",
"label.shareFile": "Dosya paylaş",
"label.shareGalleryFile": null,
"label.fileSharingUnsupported": "Dosya paylaşımı desteklenmiyor",
"label.unknown": "Bilinmeyen",
"label.democratic": "Demokratik görünüm",
"label.filmstrip": "Filmşeridi görünüm",
"label.low": "Düşük",
"label.medium": "Orta",
"label.high": "Yüksek (HD)",
"label.veryHigh": "Çok Yüksek (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Kapat",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Ayarlar",
"settings.camera": "Kamera",
"settings.selectCamera": "Video aygıtını seç",
"settings.cantSelectCamera": "Video aygıtı seçilemiyor",
"settings.audio": "Ses aygıtı",
"settings.selectAudio": "Ses aygıtını seç",
"settings.cantSelectAudio": "Ses aygıtı seçilemiyor",
"settings.resolution": "Video çözünürlüğü ayarla",
"settings.layout": "Oda düzeni",
"settings.selectRoomLayout": "Oda düzeni seç",
"settings.advancedMode": "Detaylı mod",
"settings.permanentTopBar": "Üst barı kalıcı yap",
"settings.lastn": "İzlenebilir video sayısı",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"filesharing.saveFileError": "Dosya kaydedilemiyor",
"filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor",
"filesharing.successfulFileShare": "Dosya başarıyla paylaşıldı",
"filesharing.unableToShare": "Dosya paylaşılamıyor",
"filesharing.error": "Dosya paylaşım hatası",
"filesharing.finished": "Dosya indirilmesi tamamlandı",
"filesharing.save": "Kaydet",
"filesharing.sharedFile": "{displayName} bir dosya paylaştı",
"filesharing.download": "İndir",
"filesharing.missingSeeds": "İşlem uzun zaman alıyorsa, bu torrent'i paylaşan kimse olmayabilir. İlgili dosyayı yeniden yüklemesini isteyin.",
"devices.devicesChanged": "Cihazlarınız değişti, ayarlar kutusundan cihazlarınızı yapılandırın",
"device.audioUnsupported": "Ses desteklenmiyor",
"device.activateAudio": "Sesi aktif et",
"device.muteAudio": "Sesi kıs",
"device.unMuteAudio": "Sesi aç",
"device.videoUnsupported": "Video desteklenmiyor",
"device.startVideo": "Video başlat",
"device.stopVideo": "Video durdur",
"device.screenSharingUnsupported": "Ekran paylaşımı desteklenmiyor",
"device.startScreenSharing": "Ekran paylaşımını başlat",
"device.stopScreenSharing": "Ekran paylaşımını durdur",
"devices.microphoneDisconnected": "Mikrofon bağlı değil",
"devices.microphoneError": "Mikrofononuza erişilirken bir hata oluştu",
"devices.microPhoneMute": "Mikrofonumu kıs",
"devices.micophoneUnMute": "Mikrofonumu aç",
"devices.microphoneEnable": "Mikrofonumu aktif et",
"devices.microphoneMuteError": "Mikrofonunuz kısılamıyor",
"devices.microphoneUnMuteError": "Mikrofonunuz açılamıyor",
"devices.screenSharingDisconnected" : "Ekran paylaşımı bağlı değil",
"devices.screenSharingError": "Ekranınıza erişilirken bir hata oluştu",
"devices.cameraDisconnected": "Kamera bağlı değil",
"devices.cameraError": "Kameranıza erişilirken bir hata oluştu"
}

View File

@ -0,0 +1,178 @@
{
"socket.disconnected": "Ви відключені",
"socket.reconnecting": "Ви від'єдналися, намагаєтесь знову підключитися",
"socket.reconnected": "Ви знову підключилися",
"socket.requestError": "Помилка при запиті сервера",
"room.chooseRoom": "Виберіть назву кімнати, до якої хочете приєднатися",
"room.cookieConsent": "Цей веб-сайт використовує файли cookie для поліпшення роботи користувачів",
"room.consentUnderstand": "Я розумію",
"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.setDemocratView": "Змінено макет на демократичний вигляд",
"room.setFilmStripView": "Змінено макет на вид фільму",
"room.loggedIn": "Ви ввійшли в систему",
"room.loggedOut": "Ви вийшли з системи",
"room.changedDisplayName": "Відображуване ім’я змінено на {displayName}",
"room.changeDisplayNameError": "Сталася помилка під час зміни вашого відображуваного імені",
"room.chatError": "Не вдається надіслати повідомлення в чаті",
"room.aboutToJoin": "Ви збираєтесь приєднатися до зустрічі",
"room.roomId": "Ідентифікатор кімнати: {roomName}",
"room.setYourName": "Встановіть своє ім'я для участі та виберіть, як ви хочете приєднатися:",
"room.audioOnly": "Тільки аудіо",
"room.audioVideo": "Аудіо та відео",
"room.youAreReady": "Добре, ви готові",
"room.emptyRequireLogin": "Кімната порожня! Ви можете увійти, щоб розпочати зустріч або чекати, поки хост приєднається",
"room.locketWait": "Кімната заблокована - дочекайтеся, поки хтось не впустить вас у ...",
"room.lobbyAdministration": "Адміністрація залу очікування",
"room.peersInLobby": "Учасники залу очікувань",
"room.lobbyEmpty": "Наразі у залі очікувань немає нікого",
"room.hiddenPeers": "{hiddenPeersCount, множина, один {учасник} інший {учасників}}",
"room.me": "Я",
"room.spotlights": "Учасники у центрі уваги",
"room.passive": "Пасивні учасники",
"room.videoPaused": "Це відео призупинено",
"room.muteAll": null,
"room.stopAllVideo": null,
"room.closeMeeting": null,
"room.clearChat": null,
"room.clearFileSharing": null,
"room.speechUnsupported": null,
"room.moderatoractions": null,
"room.raisedHand": null,
"room.loweredHand": null,
"room.extraVideo": null,
"room.overRoomLimit": null,
"me.mutedPTT": null,
"roles.gotRole": null,
"roles.lostRole": null,
"tooltip.login": "Увійти",
"tooltip.logout": "Вихід",
"tooltip.admitFromLobby": "Вхід із залу очікувань",
"tooltip.lockRoom": "Заблокувати кімнату",
"tooltip.unLockRoom": "Розблокувати кімнату",
"tooltip.enterFullscreen": "Вивести повний екран",
"tooltip.leaveFullscreen": "Залишити повноекранний екран",
"tooltip.lobby": "Показати зал очікувань",
"tooltip.settings": "Показати налаштування",
"tooltip.participants": "Показати учасників",
"tooltip.kickParticipant": null,
"tooltip.muteParticipant": null,
"tooltip.muteParticipantVideo": null,
"tooltip.raisedHand": null,
"tooltip.muteScreenSharing": null,
"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.shareGalleryFile": null,
"label.fileSharingUnsupported": "Обмін файлами не підтримується",
"label.unknown": "Невідомо",
"label.democrat": "Демократичний вигляд",
"label.filmstrip": "У вигляді кінострічки",
"label.low": "Низький",
"label.medium": "Середній",
"label.high": "Високий (HD)",
"label.veryHigh": "Дуже високий (FHD)",
"label.ultra": "Ультра (UHD)",
"label.close": "Закрити",
"label.media": null,
"label.appearence": null,
"label.advanced": null,
"label.addVideo": null,
"label.promoteAllPeers": null,
"label.moreActions": null,
"settings.settings": "Налаштування",
"settings.camera": "Камера",
"settings.selectCamera": "Вибрати відеопристрій",
"settings.cantSelectCamera": "Неможливо вибрати відеопристрій",
"settings.audio": "Аудіопристрій",
"settings.selectAudio": "Вибрати аудіопристрій",
"settings.cantSelectAudio": "Неможливо вибрати аудіопристрій",
"settings.audioOutput": "Пристрій аудіовиходу",
"settings.selectAudioOutput": "Виберіть пристрій аудіовиходу",
"settings.cantSelectAudioOutput": "Неможливо вибрати аудіо вихідний пристрій",
"settings.resolution": "Виберіть роздільну здатність відео",
"settings.layout": "Розміщення кімнати",
"settings.selectRoomLayout": "Вибір розташування кімнати",
"settings.advancedMode": "Розширений режим",
"settings.permanentTopBar": "Постійний верхній рядок",
"settings.lastn": "Кількість видимих ​​відео",
"settings.hiddenControls": null,
"settings.notificationSounds": null,
"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": "Під час доступу до камери сталася помилка",
"moderator.clearChat": null,
"moderator.clearFiles": null,
"moderator.muteAudio": null,
"moderator.muteVideo": null
}

View File

@ -42,7 +42,7 @@ fi
if [ "$1" = "config" ]; then if [ "$1" = "config" ]; then
echo 'graph_title MM stats' echo 'graph_title MM stats'
#echo 'graph_args --base 1000 -l 0' #echo 'graph_args --base 1000 -l 0'
echo 'graph_vlabel Actual Seesion Count' echo 'graph_vlabel Actual Session Count'
echo 'graph_category other' echo 'graph_category other'
echo 'graph_info This graph shows the mm stats.' echo 'graph_info This graph shows the mm stats.'
echo 'rooms.label rooms' echo 'rooms.label rooms'

55
prom.md 100644
View File

@ -0,0 +1,55 @@
# Prometheus exporter
The goal of this version is to offer a few basic metrics for
initial testing. The set of supported metrics can be extended.
The current implementation is partly
[unconventional](https://prometheus.io/docs/instrumenting/writing_exporters)
in that it creates new metrics each time but does not register a
custom collector. Reasons are that the exporter should
[clear out metrics](https://github.com/prometheus/client_python/issues/182)
for closed connections but that `prom-client`
[does not yet support](https://github.com/siimon/prom-client/issues/241)
custom collectors.
This version has been ported from an earlier Python version that was not part
of `multiparty-meeting` but connected as an interactive client.
## Configuration
See `prometheus` in `server/config/config.example.js` for options and
applicable defaults.
If `multiparty-meeting` was installed with
[`mm-absible`](https://github.com/misi/mm-ansible)
it may be necessary to open the `iptables` firewall for incoming TCP traffic
on the allocated port (see `/etc/ferm/ferm.conf`).
## Metrics
| metric | value |
|--------|-------|
| `edumeet_peers`| |
| `edumeet_rooms`| |
| `mediasoup_consumer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
| `mediasoup_consumer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
| `mediasoup_producer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
| `mediasoup_producer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
## Architecture
```
+-----------+ +---------------------------------------------+
| workers | | server observer API |
| | sock | +------o------+----o-----+
| +------+ | int. server | exporter |
| | | | | |
| mediasoup | | express socket.io | net | express |
+-----+-----+ +----+---------+-----+-----+-------+-----+----+
^ min-max ^ 443 ^ 443 ^ sock ^ PROM_PORT
| RTP | HTTPS | ws | | HTTP
| | | | |
| +-+---------+-+ +------+------+ +---+--------+
+---------------+ app | | int. client | | Prometheus |
+-------------+ +-------------+ +------------+
```

View File

@ -1,5 +1,7 @@
const os = require('os'); const os = require('os');
const userRoles = require('../userRoles'); const userRoles = require('../userRoles');
// const AwaitQueue = require('awaitqueue');
// const axios = require('axios');
module.exports = module.exports =
{ {
@ -34,8 +36,15 @@ module.exports =
}, },
*/ */
// URI and key for requesting geoip-based TURN server closest to the client // URI and key for requesting geoip-based TURN server closest to the client
turnAPIKey : 'examplekey', turnAPIKey : 'examplekey',
turnAPIURI : 'https://example.com/api/turn', turnAPIURI : 'https://example.com/api/turn',
turnAPIparams : {
'uri_schema' : 'turn',
'transport' : 'tcp',
'ip_ver' : 'ipv4',
'servercount' : '2'
},
// Backup turnservers if REST fails or is not configured // Backup turnservers if REST fails or is not configured
backupTurnServers : [ backupTurnServers : [
{ {
@ -46,15 +55,21 @@ module.exports =
credential : 'example' credential : 'example'
} }
], ],
fileTracker : 'wss://tracker.lab.vvc.niif.hu:443',
redisOptions : {}, redisOptions : {},
// session cookie secret // session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e', cookieSecret : 'T0P-S3cR3t_cook!e',
cookieName : 'multiparty-meeting.sid', cookieName : 'multiparty-meeting.sid',
// if you use encrypted private key the set the passphrase
tls : tls :
{ {
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
// passphrase: 'key_password'
key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem`
}, },
// listening Host or IP
// If omitted listens on every IP. ("0.0.0.0" and "::")
// listeningHost: 'localhost',
// Listening port for https server. // Listening port for https server.
listeningPort : 443, listeningPort : 443,
// Any http request is redirected to https. // Any http request is redirected to https.
@ -64,6 +79,44 @@ module.exports =
// listeningRedirectPort disabled // listeningRedirectPort disabled
// use case: loadbalancer backend // use case: loadbalancer backend
httpOnly : false, httpOnly : false,
// WebServer/Express trust proxy config for httpOnly mode
// You can find more info:
// - https://expressjs.com/en/guide/behind-proxies.html
// - https://www.npmjs.com/package/proxy-addr
// use case: loadbalancer backend
trustProxy : '',
// This logger class will have the log function
// called every time there is a room created or destroyed,
// or peer created or destroyed. This would then be able
// to log to a file or external service.
/* StatusLogger : class
{
constructor()
{
this._queue = new AwaitQueue();
}
// rooms: rooms object
// peers: peers object
// eslint-disable-next-line no-unused-vars
async log({ rooms, peers })
{
this._queue.push(async () =>
{
// Do your logging in here, use queue to keep correct order
// eslint-disable-next-line no-console
console.log('Number of rooms: ', rooms.size);
// eslint-disable-next-line no-console
console.log('Number of peers: ', peers.size);
})
.catch((error) =>
{
// eslint-disable-next-line no-console
console.log('error in log', error);
});
}
}, */
// This function will be called on successful login through oidc. // This function will be called on successful login through oidc.
// Use this function to map your oidc userinfo to the Peer object. // Use this function to map your oidc userinfo to the Peer object.
// The roomId is equal to the room name. // The roomId is equal to the room name.
@ -124,6 +177,7 @@ module.exports =
peer.addRole(userRoles.AUTHENTICATED); peer.addRole(userRoles.AUTHENTICATED);
}, },
*/ */
// eslint-disable-next-line no-unused-vars
userMapping : async ({ peer, roomId, userinfo }) => userMapping : async ({ peer, roomId, userinfo }) =>
{ {
if (userinfo.picture != null) if (userinfo.picture != null)
@ -153,20 +207,53 @@ module.exports =
peer.email = userinfo.email; peer.email = userinfo.email;
} }
}, },
// Required roles for Access. All users have the role "ALL" by default. // All users have the role "NORMAL" by default. Other roles need to be
// Other roles need to be added in the "userMapping" function. This // added in the "userMapping" function. The following accesses and
// is an Array of roles. userRoles.ADMIN have all priveleges and access // permissions are arrays of roles. Roles can be changed in userRoles.js
// always.
// //
// Example: // Example:
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
// This will allow all MODERATOR and AUTHENTICATED users access. accessFromRoles : {
requiredRolesForAccess : [ userRoles.ALL ], // The role(s) will gain access to the room
// even if it is locked (!)
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
// The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ]
},
permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
// The role(s) have permission to promote a peer from the lobby
PROMOTE_PEER : [ userRoles.NORMAL ],
// The role(s) have permission to send chat messages
SEND_CHAT : [ userRoles.NORMAL ],
// The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen
SHARE_SCREEN : [ userRoles.NORMAL ],
// The role(s) have permission to produce extra video
EXTRA_VIDEO : [ userRoles.NORMAL ],
// The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files
MODERATE_FILES : [ userRoles.MODERATOR ],
// The role(s) have permission to moderate room (e.g. kick user)
MODERATE_ROOM : [ userRoles.MODERATOR ]
},
// When truthy, the room will be open to all users when as long as there // When truthy, the room will be open to all users when as long as there
// are allready users in the room // are allready users in the room
activateOnHostJoin : true, activateOnHostJoin : true,
// When set, maxUsersPerRoom defines how many users can join
// a single room. If not set, there is no limit.
// maxUsersPerRoom : 20,
// Room size before spreading to new router
routerScaleSize : 40,
// Mediasoup settings // Mediasoup settings
mediasoup : mediasoup :
{ {
numWorkers : Object.keys(os.cpus()).length, numWorkers : Object.keys(os.cpus()).length,
// mediasoup Worker settings. // mediasoup Worker settings.
@ -247,11 +334,12 @@ module.exports =
{ {
listenIps : listenIps :
[ [
// change ip to your servers IP address! // change 192.0.2.1 IPv4 to your server's IPv4 address!!
{ ip: '0.0.0.0', announcedIp: null } { ip: '192.0.2.1', announcedIp: null }
// Can have multiple listening interfaces // Can have multiple listening interfaces
// { ip: '::/0', announcedIp: null } // change 2001:DB8::1 IPv6 to your server's IPv6 address!!
// { ip: '2001:DB8::1', announcedIp: null }
], ],
initialAvailableOutgoingBitrate : 1000000, initialAvailableOutgoingBitrate : 1000000,
minimumAvailableOutgoingBitrate : 600000, minimumAvailableOutgoingBitrate : 600000,
@ -259,4 +347,13 @@ module.exports =
maxIncomingBitrate : 1500000 maxIncomingBitrate : 1500000
} }
} }
// Prometheus exporter
/*
prometheus: {
deidentify: false, // deidentify IP addresses
numeric: false, // show numeric IP addresses
port: 8889, // allocated port
quiet: false // include fewer labels
}
*/
}; };

View File

@ -47,7 +47,8 @@ class Lobby extends EventEmitter
return Object.values(this._peers).map((peer) => return Object.values(this._peers).map((peer) =>
({ ({
peerId : peer.id, peerId : peer.id,
displayName : peer.displayName displayName : peer.displayName,
picture : peer.picture
})); }));
} }
@ -62,8 +63,8 @@ class Lobby extends EventEmitter
for (const peer in this._peers) for (const peer in this._peers)
{ {
if (peer.socket) if (!this._peers[peer].closed)
this.promotePeer(peer.id); this.promotePeer(peer);
} }
} }

View File

@ -23,9 +23,15 @@ class Peer extends EventEmitter
this._joined = false; this._joined = false;
this._joinedTimestamp = null;
this._inLobby = false; this._inLobby = false;
this._roles = [ userRoles.ALL ]; this._authenticated = false;
this._authenticatedTimestamp = null;
this._roles = [ userRoles.NORMAL ];
this._displayName = false; this._displayName = false;
@ -33,10 +39,14 @@ class Peer extends EventEmitter
this._email = null; this._email = null;
this._routerId = null;
this._rtpCapabilities = null; this._rtpCapabilities = null;
this._raisedHand = false; this._raisedHand = false;
this._raisedHandTimestamp = null;
this._transports = new Map(); this._transports = new Map();
this._producers = new Map(); this._producers = new Map();
@ -133,9 +143,18 @@ class Peer extends EventEmitter
set joined(joined) set joined(joined)
{ {
joined ?
this._joinedTimestamp = Date.now() :
this._joinedTimestamp = null;
this._joined = joined; this._joined = joined;
} }
get joinedTimestamp()
{
return this._joinedTimestamp;
}
get inLobby() get inLobby()
{ {
return this._inLobby; return this._inLobby;
@ -146,6 +165,32 @@ class Peer extends EventEmitter
this._inLobby = inLobby; this._inLobby = inLobby;
} }
get authenticated()
{
return this._authenticated;
}
set authenticated(authenticated)
{
if (authenticated !== this._authenticated)
{
authenticated ?
this._authenticatedTimestamp = Date.now() :
this._authenticatedTimestamp = null;
const oldAuthenticated = this._authenticated;
this._authenticated = authenticated;
this.emit('authenticationChanged', { oldAuthenticated });
}
}
get authenticatedTimestamp()
{
return this._authenticatedTimestamp;
}
get roles() get roles()
{ {
return this._roles; return this._roles;
@ -195,6 +240,16 @@ class Peer extends EventEmitter
this._email = email; this._email = email;
} }
get routerId()
{
return this._routerId;
}
set routerId(routerId)
{
this._routerId = routerId;
}
get rtpCapabilities() get rtpCapabilities()
{ {
return this._rtpCapabilities; return this._rtpCapabilities;
@ -212,9 +267,18 @@ class Peer extends EventEmitter
set raisedHand(raisedHand) set raisedHand(raisedHand)
{ {
raisedHand ?
this._raisedHandTimestamp = Date.now() :
this._raisedHandTimestamp = null;
this._raisedHand = raisedHand; this._raisedHand = raisedHand;
} }
get raisedHandTimestamp()
{
return this._raisedHandTimestamp;
}
get transports() get transports()
{ {
return this._transports; return this._transports;
@ -314,10 +378,12 @@ class Peer extends EventEmitter
{ {
const peerInfo = const peerInfo =
{ {
id : this.id, id : this.id,
displayName : this.displayName, displayName : this.displayName,
picture : this.picture, picture : this.picture,
roles : this.roles roles : this.roles,
raisedHand : this.raisedHand,
raisedHandTimestamp : this.raisedHandTimestamp
}; };
return peerInfo; return peerInfo;

View File

@ -2,11 +2,37 @@ const EventEmitter = require('events').EventEmitter;
const axios = require('axios'); const axios = require('axios');
const Logger = require('./Logger'); const Logger = require('./Logger');
const Lobby = require('./Lobby'); const Lobby = require('./Lobby');
const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken');
const userRoles = require('../userRoles'); const userRoles = require('../userRoles');
const config = require('../config/config'); const config = require('../config/config');
const logger = new Logger('Room'); const logger = new Logger('Room');
// In case they are not configured properly
const accessFromRoles =
{
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
BYPASS_LOBBY : [ userRoles.NORMAL ],
...config.accessFromRoles
};
const permissionsFromRoles =
{
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ],
PROMOTE_PEER : [ userRoles.NORMAL ],
SEND_CHAT : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ],
...config.permissionsFromRoles
};
const ROUTER_SCALE_SIZE = config.routerScaleSize || 40;
class Room extends EventEmitter class Room extends EventEmitter
{ {
/** /**
@ -14,38 +40,57 @@ class Room extends EventEmitter
* *
* @async * @async
* *
* @param {mediasoup.Worker} mediasoupWorker - The mediasoup Worker in which a new * @param {mediasoup.Worker} mediasoupWorkers - The mediasoup Worker in which a new
* mediasoup Router must be created. * mediasoup Router must be created.
* @param {String} roomId - Id of the Room instance. * @param {String} roomId - Id of the Room instance.
*/ */
static async create({ mediasoupWorker, roomId }) static async create({ mediasoupWorkers, roomId })
{ {
logger.info('create() [roomId:"%s"]', roomId); logger.info('create() [roomId:"%s"]', roomId);
// Shuffle workers to get random cores
let shuffledWorkers = mediasoupWorkers.sort(() => Math.random() - 0.5);
// Router media codecs. // Router media codecs.
const mediaCodecs = config.mediasoup.router.mediaCodecs; const mediaCodecs = config.mediasoup.router.mediaCodecs;
// Create a mediasoup Router. const mediasoupRouters = new Map();
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
// Create a mediasoup AudioLevelObserver. let firstRouter = null;
const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
for (const worker of shuffledWorkers)
{
const router = await worker.createRouter({ mediaCodecs });
if (!firstRouter)
firstRouter = router;
mediasoupRouters.set(router.id, router);
}
// Create a mediasoup AudioLevelObserver on first router
const audioLevelObserver = await firstRouter.createAudioLevelObserver(
{ {
maxEntries : 1, maxEntries : 1,
threshold : -80, threshold : -80,
interval : 800 interval : 800
}); });
return new Room({ roomId, mediasoupRouter, audioLevelObserver }); firstRouter = null;
shuffledWorkers = null;
return new Room({ roomId, mediasoupRouters, audioLevelObserver });
} }
constructor({ roomId, mediasoupRouter, audioLevelObserver }) constructor({ roomId, mediasoupRouters, audioLevelObserver })
{ {
logger.info('constructor() [roomId:"%s"]', roomId); logger.info('constructor() [roomId:"%s"]', roomId);
super(); super();
this.setMaxListeners(Infinity); this.setMaxListeners(Infinity);
this._uuid = uuidv4();
// Room ID. // Room ID.
this._roomId = roomId; this._roomId = roomId;
@ -55,12 +100,6 @@ class Room extends EventEmitter
// Locked flag. // Locked flag.
this._locked = false; this._locked = false;
// Required roles to access
this._requiredRoles = [ userRoles.ALL ];
if ('requiredRolesForAccess' in config)
this._requiredRoles = config.requiredRolesForAccess;
// if true: accessCode is a possibility to open the room // if true: accessCode is a possibility to open the room
this._joinByAccesCode = true; this._joinByAccesCode = true;
@ -78,8 +117,13 @@ class Room extends EventEmitter
this._peers = {}; this._peers = {};
// mediasoup Router instance. // Array of mediasoup Router instances.
this._mediasoupRouter = mediasoupRouter; this._mediasoupRouters = mediasoupRouters;
// The router we are currently putting peers in
this._routerIterator = this._mediasoupRouters.values();
this._currentRouter = this._routerIterator.next().value;
// mediasoup AudioLevelObserver. // mediasoup AudioLevelObserver.
this._audioLevelObserver = audioLevelObserver; this._audioLevelObserver = audioLevelObserver;
@ -102,8 +146,14 @@ class Room extends EventEmitter
this._closed = true; this._closed = true;
this._chatHistory = null;
this._fileHistory = null;
this._lobby.close(); this._lobby.close();
this._lobby = null;
// Close the peers. // Close the peers.
for (const peer in this._peers) for (const peer in this._peers)
{ {
@ -113,41 +163,86 @@ class Room extends EventEmitter
this._peers = null; this._peers = null;
// Close the mediasoup Router. // Close the mediasoup Routers.
this._mediasoupRouter.close(); for (const router of this._mediasoupRouters.values())
{
router.close();
}
this._routerIterator = null;
this._currentRouter = null;
this._mediasoupRouters.clear();
this._audioLevelObserver = null;
// Emit 'close' event. // Emit 'close' event.
this.emit('close'); this.emit('close');
} }
handlePeer(peer) verifyPeer({ id, token })
{ {
logger.info('handlePeer() [peer:"%s", roles:"%s"]', peer.id, peer.roles); try
{
const decoded = jwt.verify(token, this._uuid);
// Allow reconnections, remove old peer logger.info('verifyPeer() [decoded:"%o"]', decoded);
return decoded.id === id;
}
catch (err)
{
logger.warn('verifyPeer() | invalid token');
}
return false;
}
handlePeer({ peer, returning })
{
logger.info('handlePeer() [peer:"%s", roles:"%s", returning:"%s"]', peer.id, peer.roles, returning);
// Should not happen
if (this._peers[peer.id]) if (this._peers[peer.id])
{ {
logger.warn( logger.warn(
'handleConnection() | there is already a peer with same peerId [peer:"%s"]', 'handleConnection() | there is already a peer with same peerId [peer:"%s"]',
peer.id); peer.id);
this._peers[peer.id].close();
} }
// Always let ADMIN in, even if locked // Returning user
if (peer.roles.includes(userRoles.ADMIN)) if (returning)
this._peerJoining(peer, true);
else if ( // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
)
this._peerJoining(peer); this._peerJoining(peer);
else if (
'maxUsersPerRoom' in config &&
(
Object.keys(this._peers).length +
this._lobby.peerList().length
) >= config.maxUsersPerRoom)
{
this._handleOverRoomLimit(peer);
}
else if (this._locked) else if (this._locked)
this._parkPeer(peer); this._parkPeer(peer);
else else
{ {
// If the user has a role in config.requiredRolesForAccess, let them in // Has a role that is allowed to bypass lobby
peer.roles.some((role) => this._requiredRoles.includes(role)) ? peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ?
this._peerJoining(peer) : this._peerJoining(peer) :
this._handleGuest(peer); this._handleGuest(peer);
} }
} }
_handleOverRoomLimit(peer)
{
this._notification(peer.socket, 'overRoomLimit');
}
_handleGuest(peer) _handleGuest(peer)
{ {
if (config.activateOnHostJoin && !this.checkEmpty()) if (config.activateOnHostJoin && !this.checkEmpty())
@ -169,7 +264,11 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer); this._peerJoining(promotedPeer);
for (const peer of this._getJoinedPeers()) for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
} }
@ -177,18 +276,18 @@ class Room extends EventEmitter
this._lobby.on('peerRolesChanged', (peer) => this._lobby.on('peerRolesChanged', (peer) =>
{ {
// Always let admin in, even if locked if ( // Has a role that is allowed to bypass room lock
if (peer.roles.includes(userRoles.ADMIN)) peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
)
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
return; return;
} }
// If the user has a role in config.requiredRolesForAccess, let them in if ( // Has a role that is allowed to bypass lobby
if (
!this._locked && !this._locked &&
peer.roles.some((role) => this._requiredRoles.includes(role)) peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role))
) )
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -201,7 +300,11 @@ class Room extends EventEmitter
{ {
const { id, displayName } = changedPeer; const { id, displayName } = changedPeer;
for (const peer of this._getJoinedPeers()) for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
} }
@ -211,7 +314,11 @@ class Room extends EventEmitter
{ {
const { id, picture } = changedPeer; const { id, picture } = changedPeer;
for (const peer of this._getJoinedPeers()) for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
} }
@ -223,7 +330,11 @@ class Room extends EventEmitter
const { id } = closedPeer; const { id } = closedPeer;
for (const peer of this._getJoinedPeers()) for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
} }
@ -326,13 +437,17 @@ class Room extends EventEmitter
{ {
this._lobby.parkPeer(parkPeer); this._lobby.parkPeer(parkPeer);
for (const peer of this._getJoinedPeers()) for (
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
} }
} }
async _peerJoining(peer) async _peerJoining(peer, returning = false)
{ {
peer.socket.join(this._roomId); peer.socket.join(this._roomId);
@ -341,47 +456,60 @@ class Room extends EventEmitter
this._peers[peer.id] = peer; this._peers[peer.id] = peer;
// Assign routerId
peer.routerId = await this._getRouterId();
this._handlePeer(peer); this._handlePeer(peer);
let turnServers; if (returning)
if ('turnAPIURI' in config)
{ {
try this._notification(peer.socket, 'roomBack');
{
const { data } = await axios.get(
config.turnAPIURI,
{
params : {
'uri_schema' : 'turn',
'transport' : 'tcp',
'ip_ver' : 'ipv4',
'servercount' : '2',
'api_key' : config.turnAPIKey,
'ip' : peer.socket.request.connection.remoteAddress
}
});
turnServers = [ {
urls : data.uris,
username : data.username,
credential : data.password
} ];
}
catch (error)
{
if ('backupTurnServers' in config)
turnServers = config.backupTurnServers;
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
}
} }
else if ('backupTurnServers' in config) else
{ {
turnServers = config.backupTurnServers; const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true });
}
this._notification(peer.socket, 'roomReady', { turnServers }); peer.socket.handshake.session.token = token;
peer.socket.handshake.session.save();
let turnServers;
if ('turnAPIURI' in config)
{
try
{
const { data } = await axios.get(
config.turnAPIURI,
{
params : {
...config.turnAPIparams,
'api_key' : config.turnAPIKey,
'ip' : peer.socket.request.connection.remoteAddress
}
});
turnServers = [ {
urls : data.uris,
username : data.username,
credential : data.password
} ];
}
catch (error)
{
if ('backupTurnServers' in config)
turnServers = config.backupTurnServers;
logger.error('_peerJoining() | error on REST turn [error:"%o"]', error);
}
}
else if ('backupTurnServers' in config)
{
turnServers = config.backupTurnServers;
}
this._notification(peer.socket, 'roomReady', { turnServers });
}
} }
_handlePeer(peer) _handlePeer(peer)
@ -461,6 +589,17 @@ class Room extends EventEmitter
peerId : peer.id, peerId : peer.id,
role : newRole role : newRole
}, true, true); }, true, true);
// Got permission to promote peers, notify peer of
// peers in lobby
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole))
{
const lobbyPeers = this._lobby.peerList();
lobbyPeers.length > 0 && this._notification(peer.socket, 'parkedPeers', {
lobbyPeers
});
}
}); });
peer.on('lostRole', ({ oldRole }) => peer.on('lostRole', ({ oldRole }) =>
@ -479,11 +618,14 @@ class Room extends EventEmitter
async _handleSocketRequest(peer, request, cb) async _handleSocketRequest(peer, request, cb)
{ {
const router =
this._mediasoupRouters.get(peer.routerId);
switch (request.method) switch (request.method)
{ {
case 'getRouterRtpCapabilities': case 'getRouterRtpCapabilities':
{ {
cb(null, this._mediasoupRouter.rtpCapabilities); cb(null, router.rtpCapabilities);
break; break;
} }
@ -517,9 +659,21 @@ class Room extends EventEmitter
.filter((joinedPeer) => joinedPeer.id !== peer.id) .filter((joinedPeer) => joinedPeer.id !== peer.id)
.map((joinedPeer) => (joinedPeer.peerInfo)); .map((joinedPeer) => (joinedPeer.peerInfo));
const lobbyPeers = this._lobby.peerList();
cb(null, { cb(null, {
roles : peer.roles, roles : peer.roles,
peers : peerInfos peers : peerInfos,
tracker : config.fileTracker,
authenticated : peer.authenticated,
permissionsFromRoles : permissionsFromRoles,
userRoles : userRoles,
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}); });
// Mark the new Peer as joined. // Mark the new Peer as joined.
@ -580,7 +734,7 @@ class Room extends EventEmitter
webRtcTransportOptions.enableTcp = true; webRtcTransportOptions.enableTcp = true;
} }
const transport = await this._mediasoupRouter.createWebRtcTransport( const transport = await router.createWebRtcTransport(
webRtcTransportOptions webRtcTransportOptions
); );
@ -646,12 +800,27 @@ class Room extends EventEmitter
case 'produce': case 'produce':
{ {
let { appData } = request.data;
if (
appData.source === 'screen' &&
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
)
throw new Error('peer not authorized');
if (
appData.source === 'extravideo' &&
!peer.roles.some(
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
)
throw new Error('peer not authorized');
// Ensure the Peer is joined. // Ensure the Peer is joined.
if (!peer.joined) if (!peer.joined)
throw new Error('Peer not yet joined'); throw new Error('Peer not yet joined');
const { transportId, kind, rtpParameters } = request.data; const { transportId, kind, rtpParameters } = request.data;
let { appData } = request.data;
const transport = peer.getTransport(transportId); const transport = peer.getTransport(transportId);
if (!transport) if (!transport)
@ -664,6 +833,19 @@ class Room extends EventEmitter
const producer = const producer =
await transport.produce({ kind, rtpParameters, appData }); await transport.produce({ kind, rtpParameters, appData });
const pipeRouters = this._getRoutersToPipeTo(peer.routerId);
for (const [ routerId, destinationRouter ] of this._mediasoupRouters)
{
if (pipeRouters.includes(routerId))
{
await router.pipeToRouter({
producerId : producer.id,
router : destinationRouter
});
}
}
// Store the Producer into the Peer data Object. // Store the Producer into the Peer data Object.
peer.addProducer(producer.id, producer); peer.addProducer(producer.id, producer);
@ -910,16 +1092,10 @@ class Room extends EventEmitter
throw new Error('Peer not yet joined'); throw new Error('Peer not yet joined');
const { displayName } = request.data; const { displayName } = request.data;
const oldDisplayName = peer.displayName;
peer.displayName = displayName; peer.displayName = displayName;
// Spread to others // This will be spread through events from the peer object
this._notification(peer.socket, 'changeDisplayName', {
peerId : peer.id,
displayName : displayName,
oldDisplayName : oldDisplayName
}, true);
// Return no error // Return no error
cb(); cb();
@ -927,7 +1103,7 @@ class Room extends EventEmitter
break; break;
} }
case 'changePicture': /* case 'changePicture':
{ {
// Ensure the Peer is joined. // Ensure the Peer is joined.
if (!peer.joined) if (!peer.joined)
@ -947,10 +1123,15 @@ class Room extends EventEmitter
cb(); cb();
break; break;
} } */
case 'chatMessage': case 'chatMessage':
{ {
if (
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
)
throw new Error('peer not authorized');
const { chatMessage } = request.data; const { chatMessage } = request.data;
this._chatHistory.push(chatMessage); this._chatHistory.push(chatMessage);
@ -967,28 +1148,35 @@ class Room extends EventEmitter
break; break;
} }
case 'serverHistory': case 'moderator:clearChat':
{ {
// Return to sender if (
const lobbyPeers = this._lobby.peerList(); !peer.roles.some(
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
)
)
throw new Error('peer not authorized');
cb( this._chatHistory = [];
null,
{ // Spread to others
chatHistory : this._chatHistory, this._notification(peer.socket, 'moderator:clearChat', null, true);
fileHistory : this._fileHistory,
lastNHistory : this._lastN, // Return no error
locked : this._locked, cb();
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}
);
break; break;
} }
case 'lockRoom': case 'lockRoom':
{ {
if (
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized');
this._locked = true; this._locked = true;
// Spread to others // Spread to others
@ -1004,6 +1192,13 @@ class Room extends EventEmitter
case 'unlockRoom': case 'unlockRoom':
{ {
if (
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized');
this._locked = false; this._locked = false;
// Spread to others // Spread to others
@ -1059,6 +1254,13 @@ class Room extends EventEmitter
case 'promotePeer': case 'promotePeer':
{ {
if (
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
this._lobby.promotePeer(peerId); this._lobby.promotePeer(peerId);
@ -1071,6 +1273,13 @@ class Room extends EventEmitter
case 'promoteAllPeers': case 'promoteAllPeers':
{ {
if (
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized');
this._lobby.promoteAllPeers(); this._lobby.promoteAllPeers();
// Return no error // Return no error
@ -1081,6 +1290,13 @@ class Room extends EventEmitter
case 'sendFile': case 'sendFile':
{ {
if (
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
)
)
throw new Error('peer not authorized');
const { magnetUri } = request.data; const { magnetUri } = request.data;
this._fileHistory.push({ peerId: peer.id, magnetUri: magnetUri }); this._fileHistory.push({ peerId: peer.id, magnetUri: magnetUri });
@ -1097,16 +1313,37 @@ class Room extends EventEmitter
break; break;
} }
case 'raiseHand': case 'moderator:clearFileSharing':
{
if (
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_FILES.includes(role)
)
)
throw new Error('peer not authorized');
this._fileHistory = [];
// Spread to others
this._notification(peer.socket, 'moderator:clearFileSharing', null, true);
// Return no error
cb();
break;
}
case 'raisedHand':
{ {
const { raisedHand } = request.data; const { raisedHand } = request.data;
peer.raisedHand = raisedHand; peer.raisedHand = raisedHand;
// Spread to others // Spread to others
this._notification(peer.socket, 'raiseHand', { this._notification(peer.socket, 'raisedHand', {
peerId : peer.id, peerId : peer.id,
raisedHand : raisedHand raisedHand : raisedHand,
raisedHandTimestamp : peer.raisedHandTimestamp
}, true); }, true);
// Return no error // Return no error
@ -1118,15 +1355,14 @@ class Room extends EventEmitter
case 'moderator:muteAll': case 'moderator:muteAll':
{ {
if ( if (
!peer.hasRole(userRoles.MODERATOR) && !peer.roles.some(
!peer.hasRole(userRoles.ADMIN) (role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
) )
throw new Error('peer does not have moderator priveleges'); throw new Error('peer not authorized');
// Spread to others // Spread to others
this._notification(peer.socket, 'moderator:mute', { this._notification(peer.socket, 'moderator:mute', null, true);
peerId : peer.id
}, true);
cb(); cb();
@ -1136,15 +1372,14 @@ class Room extends EventEmitter
case 'moderator:stopAllVideo': case 'moderator:stopAllVideo':
{ {
if ( if (
!peer.hasRole(userRoles.MODERATOR) && !peer.roles.some(
!peer.hasRole(userRoles.ADMIN) (role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
) )
throw new Error('peer does not have moderator priveleges'); throw new Error('peer not authorized');
// Spread to others // Spread to others
this._notification(peer.socket, 'moderator:stopVideo', { this._notification(peer.socket, 'moderator:stopVideo', null, true);
peerId : peer.id
}, true);
cb(); cb();
@ -1154,17 +1389,13 @@ class Room extends EventEmitter
case 'moderator:closeMeeting': case 'moderator:closeMeeting':
{ {
if ( if (
!peer.hasRole(userRoles.MODERATOR) && !peer.roles.some(
!peer.hasRole(userRoles.ADMIN) (role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
) )
throw new Error('peer does not have moderator priveleges'); throw new Error('peer not authorized');
this._notification( this._notification(peer.socket, 'moderator:kick', null, true);
peer.socket,
'moderator:kick',
null,
true
);
cb(); cb();
@ -1177,10 +1408,11 @@ class Room extends EventEmitter
case 'moderator:kickPeer': case 'moderator:kickPeer':
{ {
if ( if (
!peer.hasRole(userRoles.MODERATOR) && !peer.roles.some(
!peer.hasRole(userRoles.ADMIN) (role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
) )
throw new Error('peer does not have moderator priveleges'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1189,10 +1421,7 @@ class Room extends EventEmitter
if (!kickPeer) if (!kickPeer)
throw new Error(`peer with id "${peerId}" not found`); throw new Error(`peer with id "${peerId}" not found`);
this._notification( this._notification(kickPeer.socket, 'moderator:kick');
kickPeer.socket,
'moderator:kick'
);
kickPeer.close(); kickPeer.close();
@ -1224,6 +1453,8 @@ class Room extends EventEmitter
producer.id producer.id
); );
const router = this._mediasoupRouters.get(producerPeer.routerId);
// Optimization: // Optimization:
// - Create the server-side Consumer. If video, do it paused. // - Create the server-side Consumer. If video, do it paused.
// - Tell its Peer about it and wait for its response. // - Tell its Peer about it and wait for its response.
@ -1234,7 +1465,7 @@ class Room extends EventEmitter
// NOTE: Don't create the Consumer if the remote Peer cannot consume it. // NOTE: Don't create the Consumer if the remote Peer cannot consume it.
if ( if (
!consumerPeer.rtpCapabilities || !consumerPeer.rtpCapabilities ||
!this._mediasoupRouter.canConsume( !router.canConsume(
{ {
producerId : producer.id, producerId : producer.id,
rtpCapabilities : consumerPeer.rtpCapabilities rtpCapabilities : consumerPeer.rtpCapabilities
@ -1369,6 +1600,19 @@ class Room extends EventEmitter
.filter((peer) => peer.joined && peer !== excludePeer); .filter((peer) => peer.joined && peer !== excludePeer);
} }
_getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true })
{
return Object.values(this._peers)
.filter(
(peer) =>
peer.joined === joined &&
peer !== excludePeer &&
peer.roles.some(
(role) => permission.includes(role)
)
);
}
_timeoutCallback(callback) _timeoutCallback(callback)
{ {
let called = false; let called = false;
@ -1433,6 +1677,84 @@ class Room extends EventEmitter
socket.emit('notification', { method, data }); socket.emit('notification', { method, data });
} }
} }
async _pipeProducersToNewRouter()
{
const peersToPipe =
Object.values(this._peers)
.filter((peer) => peer.routerId !== this._currentRouter.id);
for (const peer of peersToPipe)
{
const srcRouter = this._mediasoupRouters.get(peer.routerId);
for (const producerId of peer.producers.keys())
{
await srcRouter.pipeToRouter({
producerId,
router : this._currentRouter
});
}
}
}
async _getRouterId()
{
if (this._currentRouter)
{
const routerLoad =
Object.values(this._peers)
.filter((peer) => peer.routerId === this._currentRouter.id).length;
if (routerLoad >= ROUTER_SCALE_SIZE)
{
this._currentRouter = this._routerIterator.next().value;
if (this._currentRouter)
{
await this._pipeProducersToNewRouter();
return this._currentRouter.id;
}
}
else
{
return this._currentRouter.id;
}
}
return this._getLeastLoadedRouter();
}
// Returns an array of router ids we need to pipe to
_getRoutersToPipeTo(originRouterId)
{
return Object.values(this._peers)
.map((peer) => peer.routerId)
.filter((routerId, index, self) =>
routerId !== originRouterId && self.indexOf(routerId) === index
);
}
_getLeastLoadedRouter()
{
let load = Infinity;
let id;
for (const routerId of this._mediasoupRouters.keys())
{
const routerLoad =
Object.values(this._peers).filter((peer) => peer.routerId === routerId).length;
if (routerLoad < load)
{
id = routerId;
load = routerLoad;
}
}
return id;
}
} }
module.exports = Room; module.exports = Room;

View File

@ -0,0 +1,284 @@
const { Resolver } = require('dns').promises;
const express = require('express');
const mediasoup = require('mediasoup');
const prom = require('prom-client');
const Logger = require('./Logger');
const logger = new Logger('prom');
const resolver = new Resolver();
const workers = new Map();
const labelNames = [
'pid', 'room_id', 'peer_id', 'display_name', 'user_agent', 'transport_id',
'proto', 'local_addr', 'remote_addr', 'id', 'kind', 'codec', 'type'
];
const metadata = {
'byteCount' : { metricType: prom.Counter, unit: 'bytes' },
'score' : { metricType: prom.Gauge }
};
module.exports = async function(rooms, peers, config)
{
const collect = async function(registry)
{
const newMetrics = function(subsystem)
{
const namespace = 'mediasoup';
const metrics = new Map();
for (const key in metadata)
{
if (Object.prototype.hasOwnProperty.call(metadata, key))
{
const value = metadata[key];
const name = key.split(/(?=[A-Z])/).join('_')
.toLowerCase();
const unit = value.unit;
const metricType = value.metricType;
let s = `${namespace}_${subsystem}_${name}`;
if (unit)
{
s += `_${unit}`;
}
const m = new metricType({
name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] });
metrics.set(key, m);
}
}
return metrics;
};
const commonLabels = function(both, fn)
{
for (const roomId of rooms.keys())
{
for (const [ peerId, peer ] of peers)
{
if (fn(peer))
{
const displayName = peer._displayName;
const userAgent = peer._socket.client.request.headers['user-agent'];
const kind = both.kind;
const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1];
return { roomId, peerId, displayName, userAgent, kind, codec };
}
}
}
throw new Error('cannot find common labels');
};
const addr = async function(ip, port)
{
if (config.deidentify)
{
const a = ip.split('.');
for (let i = 0; i < a.length - 2; i++)
{
a[i] = 'xx';
}
return `${a.join('.')}:${port}`;
}
else if (config.numeric)
{
return `${ip}:${port}`;
}
else
{
try
{
const a = await resolver.reverse(ip);
ip = a[0];
}
catch (err)
{
logger.error(`reverse DNS query failed: ${ip} ${err.code}`);
}
return `${ip}:${port}`;
}
};
const quiet = function(s)
{
return config.quiet ? '' : s;
};
const setValue = function(key, m, labels, v)
{
logger.debug(`setValue key=${key} v=${v}`);
switch (metadata[key].metricType)
{
case prom.Counter:
m.inc(labels, v);
break;
case prom.Gauge:
m.set(labels, v);
break;
default:
throw new Error(`unexpected metric: ${m}`);
}
};
logger.debug('collect');
const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] });
mRooms.set(rooms.size);
const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] });
for (const [ roomId, room ] of rooms)
{
mPeers.labels(roomId).set(Object.keys(room._peers).length);
}
const mConsumer = newMetrics('consumer');
const mProducer = newMetrics('producer');
for (const [ pid, worker ] of workers)
{
logger.debug(`visiting worker ${pid}`);
for (const router of worker._routers)
{
logger.debug(`visiting router ${router.id}`);
for (const [ transportId, transport ] of router._transports)
{
logger.debug(`visiting transport ${transportId}`);
const transportJson = await transport.dump();
if (transportJson.iceState != 'completed')
{
logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`);
continue;
}
const iceSelectedTuple = transportJson.iceSelectedTuple;
const proto = iceSelectedTuple.protocol;
const localAddr = await addr(iceSelectedTuple.localIp,
iceSelectedTuple.localPort);
const remoteAddr = await addr(iceSelectedTuple.remoteIp,
iceSelectedTuple.remotePort);
for (const [ producerId, producer ] of transport._producers)
{
logger.debug(`visiting producer ${producerId}`);
const { roomId, peerId, displayName, userAgent, kind, codec } =
commonLabels(producer, (peer) => peer._producers.has(producerId));
const a = await producer.getStats();
for (const x of a)
{
const type = x.type;
const labels = {
'pid' : pid,
'room_id' : roomId,
'peer_id' : peerId,
'display_name' : displayName,
'user_agent' : userAgent,
'transport_id' : quiet(transportId),
'proto' : proto,
'local_addr' : localAddr,
'remote_addr' : remoteAddr,
'id' : quiet(producerId),
'kind' : kind,
'codec' : codec,
'type' : type
};
for (const [ key, m ] of mProducer)
{
setValue(key, m, labels, x[key]);
}
}
}
for (const [ consumerId, consumer ] of transport._consumers)
{
logger.debug(`visiting consumer ${consumerId}`);
const { roomId, peerId, displayName, userAgent, kind, codec } =
commonLabels(consumer, (peer) => peer._consumers.has(consumerId));
const a = await consumer.getStats();
for (const x of a)
{
if (x.type == 'inbound-rtp')
{
continue;
}
const type = x.type;
const labels =
{
'pid' : pid,
'room_id' : roomId,
'peer_id' : peerId,
'display_name' : displayName,
'user_agent' : userAgent,
'transport_id' : quiet(transportId),
'proto' : proto,
'local_addr' : localAddr,
'remote_addr' : remoteAddr,
'id' : quiet(consumerId),
'kind' : kind,
'codec' : codec,
'type' : type
};
for (const [ key, m ] of mConsumer)
{
setValue(key, m, labels, x[key]);
}
}
}
}
}
}
};
try
{
logger.debug(`config.deidentify=${config.deidentify}`);
logger.debug(`config.numeric=${config.numeric}`);
logger.debug(`config.port=${config.port}`);
logger.debug(`config.quiet=${config.quiet}`);
mediasoup.observer.on('newworker', (worker) =>
{
logger.debug(`observing newworker ${worker.pid} #${workers.size}`);
workers.set(worker.pid, worker);
worker.observer.on('close', () =>
{
logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`);
workers.delete(worker.pid);
});
});
const app = express();
app.get('/', async (req, res) =>
{
logger.debug(`GET ${req.originalUrl}`);
const registry = new prom.Registry();
await collect(registry);
res.set('Content-Type', registry.contentType);
const data = registry.metrics();
res.end(data);
});
const server = app.listen(config.port || 8889, () =>
{
const address = server.address();
logger.info(`listening ${address.address}:${address.port}`);
});
}
catch (err)
{
logger.error(err);
}
};

View File

@ -1,6 +1,6 @@
{ {
"name": "multiparty-meeting-server", "name": "multiparty-meeting-server",
"version": "3.1.0", "version": "3.3.0",
"private": true, "private": true,
"description": "multiparty meeting server", "description": "multiparty meeting server",
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>", "author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
@ -8,7 +8,8 @@
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js", "start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node server.js",
"connect": "node connect.js" "connect": "node connect.js",
"lint": "eslint -c .eslintrc.json --ext .js *.js lib/"
}, },
"dependencies": { "dependencies": {
"awaitqueue": "^1.0.0", "awaitqueue": "^1.0.0",
@ -25,13 +26,19 @@
"express-socket.io-session": "^1.3.5", "express-socket.io-session": "^1.3.5",
"helmet": "^3.21.2", "helmet": "^3.21.2",
"ims-lti": "^3.0.2", "ims-lti": "^3.0.2",
"mediasoup": "^3.5.5", "jsonwebtoken": "^8.5.1",
"mediasoup": "^3.5.6",
"openid-client": "^3.7.3", "openid-client": "^3.7.3",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-lti": "0.0.7", "passport-lti": "0.0.7",
"pidusage": "^2.0.17", "pidusage": "^2.0.17",
"prom-client": ">=12.0.0",
"redis": "^2.8.0", "redis": "^2.8.0",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"spdy": "^4.0.1" "spdy": "^4.0.1",
"uuid": "^7.0.2"
},
"devDependencies": {
"eslint": "6.8.0"
} }
} }

View File

@ -34,25 +34,28 @@ const expressSession = require('express-session');
const RedisStore = require('connect-redis')(expressSession); const RedisStore = require('connect-redis')(expressSession);
const sharedSession = require('express-socket.io-session'); const sharedSession = require('express-socket.io-session');
const interactiveServer = require('./lib/interactiveServer'); const interactiveServer = require('./lib/interactiveServer');
const promExporter = require('./lib/promExporter');
const { v4: uuidv4 } = require('uuid');
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log('- process.env.DEBUG:', process.env.DEBUG); console.log('- process.env.DEBUG:', process.env.DEBUG);
console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel); console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel);
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags); console.log('- config.mediasoup.worker.logTags:', config.mediasoup.worker.logTags);
/* eslint-enable no-console */ /* eslint-enable no-console */
const logger = new Logger(); const logger = new Logger();
const queue = new AwaitQueue(); const queue = new AwaitQueue();
let statusLogger = null;
if ('StatusLogger' in config)
statusLogger = new config.StatusLogger();
// mediasoup Workers. // mediasoup Workers.
// @type {Array<mediasoup.Worker>} // @type {Array<mediasoup.Worker>}
const mediasoupWorkers = []; const mediasoupWorkers = [];
// Index of next mediasoup Worker to use.
// @type {Number}
let nextMediasoupWorkerIdx = 0;
// Map of Room instances indexed by roomId. // Map of Room instances indexed by roomId.
const rooms = new Map(); const rooms = new Map();
@ -100,6 +103,11 @@ const session = expressSession({
} }
}); });
if (config.trustProxy)
{
app.set('trust proxy', config.trustProxy);
}
app.use(session); app.use(session);
passport.serializeUser((user, done) => passport.serializeUser((user, done) =>
@ -122,6 +130,12 @@ async function run()
// Open the interactive server. // Open the interactive server.
await interactiveServer(rooms, peers); await interactiveServer(rooms, peers);
// start Prometheus exporter
if (config.prometheus)
{
await promExporter(rooms, peers, config.prometheus);
}
if (typeof(config.auth) === 'undefined') if (typeof(config.auth) === 'undefined')
{ {
logger.warn('Auth is not configured properly!'); logger.warn('Auth is not configured properly!');
@ -140,6 +154,25 @@ async function run()
// Run WebSocketServer. // Run WebSocketServer.
await runWebSocketServer(); await runWebSocketServer();
// eslint-disable-next-line no-unused-vars
function errorHandler(err, req, res, next)
{
const trackingId = uuidv4();
res.status(500).send(
`<h1>Internal Server Error</h1>
<p>If you report this error, please also report this
<i>tracking ID</i> which makes it possible to locate your session
in the logs which are available to the system administrator:
<b>${trackingId}</b></p>`
);
logger.error(
'Express error handler dump with tracking ID: %s, error dump: %o',
trackingId, err);
}
app.use(errorHandler);
// Log rooms status every 30 seconds. // Log rooms status every 30 seconds.
setInterval(() => setInterval(() =>
{ {
@ -159,6 +192,17 @@ async function run()
}, 10000); }, 10000);
} }
function statusLog()
{
if (statusLogger)
{
statusLogger.log({
rooms : rooms,
peers : peers
});
}
}
function setupLTI(ltiConfig) function setupLTI(ltiConfig)
{ {
@ -168,7 +212,7 @@ function setupLTI(ltiConfig)
const ltiStrategy = new LTIStrategy( const ltiStrategy = new LTIStrategy(
ltiConfig, ltiConfig,
function(req, lti, done) (req, lti, done) =>
{ {
// LTI launch parameters // LTI launch parameters
if (lti) if (lti)
@ -178,7 +222,7 @@ function setupLTI(ltiConfig)
if (lti.user_id && lti.custom_room) if (lti.user_id && lti.custom_room)
{ {
user.id = lti.user_id; user.id = lti.user_id;
user._lti = lti; user._userinfo = { 'lti': lti };
} }
if (lti.custom_room) if (lti.custom_room)
@ -219,7 +263,18 @@ function setupOIDC(oidcIssuer)
// redirect_uri defaults to client.redirect_uris[0] // redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code' // response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid' // scope defaults to 'openid'
const params = config.auth.oidc.clientOptions;
/* eslint-disable camelcase */
const params = (({
client_id,
redirect_uri,
scope
}) => ({
client_id,
redirect_uri,
scope
}))(config.auth.oidc.clientOptions);
/* eslint-enable camelcase */
// optional, defaults to false, when true req is passed as a first // optional, defaults to false, when true req is passed as a first
// argument to verify fn // argument to verify fn
@ -234,12 +289,17 @@ function setupOIDC(oidcIssuer)
{ client: oidcClient, params, passReqToCallback, usePKCE }, { client: oidcClient, params, passReqToCallback, usePKCE },
(tokenset, userinfo, done) => (tokenset, userinfo, done) =>
{ {
if (userinfo && tokenset)
{
// eslint-disable-next-line camelcase
userinfo._tokenset_claims = tokenset.claims();
}
const user = const user =
{ {
id : tokenset.claims.sub, id : tokenset.claims.sub,
provider : tokenset.claims.iss, provider : tokenset.claims.iss,
_userinfo : userinfo, _userinfo : userinfo
_claims : tokenset.claims
}; };
return done(null, user); return done(null, user);
@ -289,7 +349,7 @@ async function setupAuth()
// lti launch // lti launch
app.post('/auth/lti', app.post('/auth/lti',
passport.authenticate('lti', { failureRedirect: '/' }), passport.authenticate('lti', { failureRedirect: '/' }),
function(req, res) (req, res) =>
{ {
res.redirect(`/${req.user.room}`); res.redirect(`/${req.user.room}`);
} }
@ -306,13 +366,13 @@ async function setupAuth()
{ {
for (const role of peer.roles) for (const role of peer.roles)
{ {
if (role !== userRoles.ALL) if (role !== userRoles.NORMAL)
peer.removeRole(role); peer.removeRole(role);
} }
} }
req.logout(); req.logout();
res.send(logoutHelper()); req.session.destroy(() => res.send(logoutHelper()));
}); });
// callback // callback
@ -333,10 +393,10 @@ async function setupAuth()
if (!peer) // User has no socket session yet, make temporary if (!peer) // User has no socket session yet, make temporary
peer = new Peer({ id: peerId, roomId }); peer = new Peer({ id: peerId, roomId });
if (peer && peer.roomId !== roomId) // The peer is mischievous if (peer.roomId !== roomId) // The peer is mischievous
throw new Error('peer authenticated with wrong room'); throw new Error('peer authenticated with wrong room');
if (peer && typeof config.userMapping === 'function') if (typeof config.userMapping === 'function')
{ {
await config.userMapping({ await config.userMapping({
peer, peer,
@ -345,6 +405,8 @@ async function setupAuth()
}); });
} }
peer.authenticated = true;
res.send(loginHelper({ res.send(loginHelper({
displayName : peer.displayName, displayName : peer.displayName,
picture : peer.picture picture : peer.picture
@ -361,7 +423,7 @@ async function runHttpsServer()
app.all('*', async (req, res, next) => app.all('*', async (req, res, next) =>
{ {
if (req.secure || config.httpOnly ) if (req.secure || config.httpOnly)
{ {
const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`); const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`);
@ -404,11 +466,17 @@ async function runHttpsServer()
// http // http
const redirectListener = http.createServer(app); const redirectListener = http.createServer(app);
redirectListener.listen(config.listeningRedirectPort); if (config.listeningHost)
redirectListener.listen(config.listeningRedirectPort, config.listeningHost);
else
redirectListener.listen(config.listeningRedirectPort);
} }
// https or http // https or http
mainListener.listen(config.listeningPort); if (config.listeningHost)
mainListener.listen(config.listeningPort, config.listeningHost);
else
mainListener.listen(config.listeningPort);
} }
function isPathAlreadyTaken(url) function isPathAlreadyTaken(url)
@ -464,12 +532,37 @@ async function runWebSocketServer()
queue.push(async () => queue.push(async () =>
{ {
const { token } = socket.handshake.session;
const room = await getOrCreateRoom({ roomId }); const room = await getOrCreateRoom({ roomId });
const peer = new Peer({ id: peerId, roomId, socket });
let peer = peers.get(peerId);
let returning = false;
if (peer && !token)
{ // Don't allow hijacking sessions
socket.disconnect(true);
return;
}
else if (token && room.verifyPeer({ id: peerId, token }))
{ // Returning user, remove if old peer exists
if (peer)
peer.close();
returning = true;
}
peer = new Peer({ id: peerId, roomId, socket });
peers.set(peerId, peer); peers.set(peerId, peer);
peer.on('close', () => peers.delete(peerId)); peer.on('close', () =>
{
peers.delete(peerId);
statusLog();
});
if ( if (
Boolean(socket.handshake.session.passport) && Boolean(socket.handshake.session.passport) &&
@ -484,10 +577,11 @@ async function runWebSocketServer()
_userinfo _userinfo
} = socket.handshake.session.passport.user; } = socket.handshake.session.passport.user;
peer.authId= id; peer.authId = id;
peer.displayName = displayName; peer.displayName = displayName;
peer.picture = picture; peer.picture = picture;
peer.email = email; peer.email = email;
peer.authenticated = true;
if (typeof config.userMapping === 'function') if (typeof config.userMapping === 'function')
{ {
@ -495,7 +589,9 @@ async function runWebSocketServer()
} }
} }
room.handlePeer(peer); room.handlePeer({ peer, returning });
statusLog();
}) })
.catch((error) => .catch((error) =>
{ {
@ -539,19 +635,6 @@ async function runMediasoupWorkers()
} }
} }
/**
* Get next mediasoup Worker.
*/
function getMediasoupWorker()
{
const worker = mediasoupWorkers[nextMediasoupWorkerIdx];
if (++nextMediasoupWorkerIdx === mediasoupWorkers.length)
nextMediasoupWorkerIdx = 0;
return worker;
}
/** /**
* Get a Room instance (or create one if it does not exist). * Get a Room instance (or create one if it does not exist).
*/ */
@ -564,13 +647,20 @@ async function getOrCreateRoom({ roomId })
{ {
logger.info('creating a new Room [roomId:"%s"]', roomId); logger.info('creating a new Room [roomId:"%s"]', roomId);
const mediasoupWorker = getMediasoupWorker(); // const mediasoupWorker = getMediasoupWorker();
room = await Room.create({ mediasoupWorker, roomId }); room = await Room.create({ mediasoupWorkers, roomId });
rooms.set(roomId, room); rooms.set(roomId, room);
room.on('close', () => rooms.delete(roomId)); statusLog();
room.on('close', () =>
{
rooms.delete(roomId);
statusLog();
});
} }
return room; return room;

View File

@ -1,12 +1,11 @@
module.exports = { module.exports = {
// Allowed to enter locked rooms + all other priveleges // These can be changed
ADMIN : 'admin', ADMIN : 'admin',
// Allowed to enter restricted rooms if configured.
// Allowed to moderate users in a room (mute all,
// spotlight video, kick users)
MODERATOR : 'moderator', MODERATOR : 'moderator',
// Same as MODERATOR, but can't moderate users PRESENTER : 'presenter',
AUTHENTICATED : 'authenticated', AUTHENTICATED : 'authenticated',
// No priveleges // Don't change anything after this point
ALL : 'normal'
// All users have this role by default, do not change or remove this role
NORMAL : 'normal'
}; };