commit
51ff2cb80e
32
CHANGELOG.md
32
CHANGELOG.md
|
|
@ -1,20 +1,32 @@
|
|||
# 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
|
||||
|
||||
* 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
|
||||
* Add key shortcut `space` for audio mute
|
||||
* Add key shortcut `v` for video mute
|
||||
* Add user configurable LastN
|
||||
* Add option to sticky top bar (sticky by default)
|
||||
* update mediasoup server
|
||||
* Add simulcast options to app config (disabled by default)
|
||||
* Add stats option to get counts of rooms and peers
|
||||
* Add httpOnly option for loadbalancer backend setups
|
||||
* Add option to permananent top bar (permanent by default)
|
||||
* Update mediasoup server
|
||||
* Add `simulcast` options to app config (disabled by default)
|
||||
* Add `stats` option to get counts of rooms and peers
|
||||
* Add `httpOnly` option for loadbalancer backend setups
|
||||
* LTI integration for LMS systems like moodle
|
||||
* Add muted=false search parameter
|
||||
* Add translations (12+1 languages)
|
||||
* Add support IPv6
|
||||
* Many other fixes and refactorings
|
||||
|
|
@ -33,10 +45,10 @@
|
|||
|
||||
* Updated to mediasoup v3
|
||||
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
|
||||
- OpenID Connect discovery
|
||||
- Auth code flow
|
||||
* OpenID Connect discovery
|
||||
* Auth code flow
|
||||
* 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
|
||||
|
||||
## 2.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Source code contributions should pass static code analysis as performed by `npm run lint` in `server` and `app` respectively.
|
||||
|
|
@ -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)
|
||||
[](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
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
#### Setup Activity
|
||||
|
||||
##### Activity setup basic form
|
||||
|
||||
Open fully the settings **Click on show more!!**
|
||||

|
||||
|
||||
##### Empty full form
|
||||
|
||||

|
||||
|
||||
##### Filled out form
|
||||
|
||||

|
||||
|
||||
## moodle plugin
|
||||
|
||||
Alternatively you can use multipartymeeting moodle plugin:
|
||||
[https://github.com/misi/moodle-mod_multipartymeeting](https://github.com/misi/moodle-mod_multipartymeeting)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
23
README.md
23
README.md
|
|
@ -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/).
|
||||
[](https://asciinema.org/a/311365)
|
||||
|
||||
|
||||
## Manual installation
|
||||
* 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).
|
||||
|
||||
```bash
|
||||
|
|
@ -77,7 +76,7 @@ $ npm install
|
|||
$ cd server
|
||||
$ 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`
|
||||
|
||||
## Deploy it in a server
|
||||
|
|
@ -103,12 +102,24 @@ $ systemctl enable multiparty-meeting
|
|||
## Ports and firewall
|
||||
|
||||
* 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`)
|
||||
|
||||
## 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
|
||||
|
||||
* 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
|
||||
|
||||
|
|
@ -123,7 +134,7 @@ This started as a fork of the [work](https://github.com/versatica/mediasoup-demo
|
|||
|
||||
## 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 Union’s 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "multiparty-meeting",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"@material-ui/core": "^4.5.1",
|
||||
"@material-ui/icons": "^4.5.1",
|
||||
"bowser": "^2.7.0",
|
||||
"classnames": "^2.2.6",
|
||||
"create-torrent": "^4.4.1",
|
||||
"dompurify": "^2.0.7",
|
||||
"domready": "^1.0.8",
|
||||
|
|
@ -29,7 +30,8 @@
|
|||
"react-intl": "^3.4.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.0.1",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-wakelock-react16": "0.0.7",
|
||||
"redux": "^4.0.4",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
],
|
||||
"devDependencies": {
|
||||
"electron": "^7.1.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"foreman": "^3.0.1",
|
||||
"redux-mock-store": "^1.5.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// eslint-disable-next-line
|
||||
var config =
|
||||
{
|
||||
loginEnabled : false,
|
||||
developmentPort : 3443,
|
||||
productionPort : 443,
|
||||
loginEnabled : false,
|
||||
developmentPort : 3443,
|
||||
productionPort : 443,
|
||||
|
||||
/**
|
||||
* If defaultResolution is set, it will override user settings when joining:
|
||||
|
|
@ -25,19 +25,35 @@ var config =
|
|||
{ scaleResolutionDownBy: 2 },
|
||||
{ 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
|
||||
requestTimeout : 10000,
|
||||
transportOptions :
|
||||
{
|
||||
tcp : true
|
||||
},
|
||||
lastN : 4,
|
||||
mobileLastN : 1,
|
||||
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,
|
||||
background : 'images/background.jpg',
|
||||
// Add file and uncomment for adding logo to appbar
|
||||
// logo : 'images/logo.svg',
|
||||
title : 'Multiparty meeting',
|
||||
theme :
|
||||
title : 'Multiparty meeting',
|
||||
theme :
|
||||
{
|
||||
palette :
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -225,10 +225,7 @@ export default class ScreenShare
|
|||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
case 'chrome':
|
||||
{
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
case 'msedge':
|
||||
case 'edge':
|
||||
{
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ beforeEach(() =>
|
|||
me : {
|
||||
audioDevices : null,
|
||||
audioInProgress : false,
|
||||
audioOutputDevices : null,
|
||||
audioOutputInProgress : false,
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
canShareFiles : false,
|
||||
|
|
@ -40,8 +42,8 @@ beforeEach(() =>
|
|||
loggedIn : false,
|
||||
loginEnabled : true,
|
||||
picture : null,
|
||||
raiseHand : false,
|
||||
raiseHandInProgress : false,
|
||||
raisedHand : false,
|
||||
raisedHandInProgress : false,
|
||||
screenShareInProgress : false,
|
||||
webcamDevices : null,
|
||||
webcamInProgress : false
|
||||
|
|
@ -72,11 +74,12 @@ beforeEach(() =>
|
|||
windowConsumer : null
|
||||
},
|
||||
settings : {
|
||||
advancedMode : true,
|
||||
displayName : 'Jest Tester',
|
||||
resolution : 'ultra',
|
||||
selectedAudioDevice : 'default',
|
||||
selectedWebcam : 'soifjsiajosjfoi'
|
||||
advancedMode : true,
|
||||
displayName : 'Jest Tester',
|
||||
resolution : 'ultra',
|
||||
selectedAudioDevice : 'default',
|
||||
selectedAudioOutputDevice : 'default',
|
||||
selectedWebcam : 'soifjsiajosjfoi'
|
||||
},
|
||||
toolarea : {
|
||||
currentToolTab : 'chat',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import RoomClient from '../RoomClient';
|
||||
|
||||
describe('new RoomClient() without paramaters throws Error', () =>
|
||||
describe('new RoomClient() without parameters throws Error', () =>
|
||||
{
|
||||
test('Matches the snapshot', () =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,4 +14,9 @@ export const addChatHistory = (chatHistory) =>
|
|||
({
|
||||
type : 'ADD_CHAT_HISTORY',
|
||||
payload : { chatHistory }
|
||||
});
|
||||
|
||||
export const clearChat = () =>
|
||||
({
|
||||
type : 'CLEAR_CHAT'
|
||||
});
|
||||
|
|
@ -32,4 +32,9 @@ export const setFileDone = (magnetUri, sharedFiles) =>
|
|||
({
|
||||
type : 'SET_FILE_DONE',
|
||||
payload : { magnetUri, sharedFiles }
|
||||
});
|
||||
|
||||
export const clearFiles = () =>
|
||||
({
|
||||
type : 'CLEAR_FILES'
|
||||
});
|
||||
|
|
@ -4,9 +4,10 @@ export const setMe = ({ 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) =>
|
||||
|
|
@ -50,15 +51,21 @@ export const setAudioDevices = (devices) =>
|
|||
payload : { devices }
|
||||
});
|
||||
|
||||
export const setAudioOutputDevices = (devices) =>
|
||||
({
|
||||
type : 'SET_AUDIO_OUTPUT_DEVICES',
|
||||
payload : { devices }
|
||||
});
|
||||
|
||||
export const setWebcamDevices = (devices) =>
|
||||
({
|
||||
type : 'SET_WEBCAM_DEVICES',
|
||||
payload : { devices }
|
||||
});
|
||||
|
||||
export const setMyRaiseHandState = (flag) =>
|
||||
export const setRaisedHand = (flag) =>
|
||||
({
|
||||
type : 'SET_MY_RAISE_HAND_STATE',
|
||||
type : 'SET_RAISED_HAND',
|
||||
payload : { flag }
|
||||
});
|
||||
|
||||
|
|
@ -67,6 +74,12 @@ export const setAudioInProgress = (flag) =>
|
|||
type : 'SET_AUDIO_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
});
|
||||
|
||||
export const setAudioOutputInProgress = (flag) =>
|
||||
({
|
||||
type : 'SET_AUDIO_OUTPUT_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
});
|
||||
|
||||
export const setWebcamInProgress = (flag) =>
|
||||
({
|
||||
|
|
@ -80,9 +93,9 @@ export const setScreenShareInProgress = (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 }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ export const setPeerScreenInProgress = (peerId, flag) =>
|
|||
payload : { peerId, flag }
|
||||
});
|
||||
|
||||
export const setPeerRaiseHandState = (peerId, raiseHandState) =>
|
||||
export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) =>
|
||||
({
|
||||
type : 'SET_PEER_RAISE_HAND_STATE',
|
||||
payload : { peerId, raiseHandState }
|
||||
type : 'SET_PEER_RAISED_HAND',
|
||||
payload : { peerId, raisedHand, raisedHandTimestamp }
|
||||
});
|
||||
|
||||
export const setPeerPicture = (peerId, picture) =>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ export const setSignInRequired = (signInRequired) =>
|
|||
payload : { signInRequired }
|
||||
});
|
||||
|
||||
export const setOverRoomLimit = (overRoomLimit) =>
|
||||
({
|
||||
type : 'SET_OVER_ROOM_LIMIT',
|
||||
payload : { overRoomLimit }
|
||||
});
|
||||
|
||||
export const setAccessCode = (accessCode) =>
|
||||
({
|
||||
type : 'SET_ACCESS_CODE',
|
||||
|
|
@ -52,13 +58,25 @@ export const setJoinByAccessCode = (joinByAccessCode) =>
|
|||
payload : { joinByAccessCode }
|
||||
});
|
||||
|
||||
export const setSettingsOpen = ({ settingsOpen }) =>
|
||||
export const setSettingsOpen = (settingsOpen) =>
|
||||
({
|
||||
type : 'SET_SETTINGS_OPEN',
|
||||
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',
|
||||
payload : { lockDialogOpen }
|
||||
|
|
@ -111,6 +129,12 @@ export const toggleConsumerFullscreen = (consumerId) =>
|
|||
payload : { consumerId }
|
||||
});
|
||||
|
||||
export const setLobbyPeersPromotionInProgress = (flag) =>
|
||||
({
|
||||
type : 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
});
|
||||
|
||||
export const setMuteAllInProgress = (flag) =>
|
||||
({
|
||||
type : 'MUTE_ALL_IN_PROGRESS',
|
||||
|
|
@ -129,6 +153,18 @@ export const setCloseMeetingInProgress = (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',
|
||||
|
|
@ -139,4 +175,4 @@ export const setPermissionsFromRoles = (permissionsFromRoles) =>
|
|||
({
|
||||
type : 'SET_PERMISSIONS_FROM_ROLES',
|
||||
payload : { permissionsFromRoles }
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ export const setSelectedAudioDevice = (deviceId) =>
|
|||
payload : { deviceId }
|
||||
});
|
||||
|
||||
export const setSelectedAudioOutputDevice = (deviceId) =>
|
||||
({
|
||||
type : 'CHANGE_AUDIO_OUTPUT_DEVICE',
|
||||
payload : { deviceId }
|
||||
});
|
||||
|
||||
export const setSelectedWebcamDevice = (deviceId) =>
|
||||
({
|
||||
type : 'CHANGE_WEBCAM',
|
||||
|
|
@ -32,6 +38,16 @@ export const togglePermanentTopBar = () =>
|
|||
type : 'TOGGLE_PERMANENT_TOPBAR'
|
||||
});
|
||||
|
||||
export const toggleHiddenControls = () =>
|
||||
({
|
||||
type : 'TOGGLE_HIDDEN_CONTROLS'
|
||||
});
|
||||
|
||||
export const toggleNotificationSounds = () =>
|
||||
({
|
||||
type : 'TOGGLE_NOTIFICATION_SOUNDS'
|
||||
});
|
||||
|
||||
export const setLastN = (lastN) =>
|
||||
({
|
||||
type : 'SET_LAST_N',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const ListLobbyPeer = (props) =>
|
|||
const {
|
||||
roomClient,
|
||||
peer,
|
||||
promotionInProgress,
|
||||
canPromote,
|
||||
classes
|
||||
} = props;
|
||||
|
|
@ -55,7 +56,12 @@ const ListLobbyPeer = (props) =>
|
|||
})}
|
||||
>
|
||||
<IconButton
|
||||
disabled={!canPromote || peer.promotionInProgress}
|
||||
disabled={
|
||||
!canPromote ||
|
||||
peer.promotionInProgress ||
|
||||
promotionInProgress
|
||||
}
|
||||
color='primary'
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
|
|
@ -71,18 +77,20 @@ const ListLobbyPeer = (props) =>
|
|||
|
||||
ListLobbyPeer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : PropTypes.object.isRequired,
|
||||
canPromote : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : PropTypes.object.isRequired,
|
||||
promotionInProgress : PropTypes.bool.isRequired,
|
||||
canPromote : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { id }) =>
|
||||
{
|
||||
return {
|
||||
peer : state.lobbyPeers[id],
|
||||
canPromote :
|
||||
peer : state.lobbyPeers[id],
|
||||
promotionInProgress : state.room.lobbyPeersPromotionInProgress,
|
||||
canPromote :
|
||||
state.me.roles.some((role) =>
|
||||
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
|
||||
};
|
||||
|
|
@ -97,6 +105,8 @@ export default withRoomContext(connect(
|
|||
{
|
||||
return (
|
||||
prev.room.permissionsFromRoles === next.room.permissionsFromRoles &&
|
||||
prev.room.lobbyPeersPromotionInProgress ===
|
||||
next.room.lobbyPeersPromotionInProgress &&
|
||||
prev.me.roles === next.me.roles &&
|
||||
prev.lobbyPeers === next.lobbyPeers
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,14 +15,6 @@ import DialogActions from '@material-ui/core/DialogActions';
|
|||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
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 ListSubheader from '@material-ui/core/ListSubheader';
|
||||
import ListLobbyPeer from './ListLobbyPeer';
|
||||
|
|
@ -59,11 +51,11 @@ const styles = (theme) =>
|
|||
});
|
||||
|
||||
const LockDialog = ({
|
||||
// roomClient,
|
||||
roomClient,
|
||||
room,
|
||||
handleCloseLockDialog,
|
||||
// handleAccessCode,
|
||||
lobbyPeers,
|
||||
canPromote,
|
||||
classes
|
||||
}) =>
|
||||
{
|
||||
|
|
@ -71,7 +63,7 @@ const LockDialog = ({
|
|||
<Dialog
|
||||
className={classes.root}
|
||||
open={room.lockDialogOpen}
|
||||
onClose={() => handleCloseLockDialog({ lockDialogOpen: false })}
|
||||
onClose={() => handleCloseLockDialog(false)}
|
||||
classes={{
|
||||
paper : classes.dialogPaper
|
||||
}}
|
||||
|
|
@ -82,54 +74,6 @@ const LockDialog = ({
|
|||
defaultMessage='Lobby administration'
|
||||
/>
|
||||
</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 ?
|
||||
<List
|
||||
dense
|
||||
|
|
@ -160,7 +104,21 @@ const LockDialog = ({
|
|||
</DialogContent>
|
||||
}
|
||||
<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
|
||||
id='label.close'
|
||||
defaultMessage='Close'
|
||||
|
|
@ -173,11 +131,12 @@ const LockDialog = ({
|
|||
|
||||
LockDialog.propTypes =
|
||||
{
|
||||
// roomClient : PropTypes.any.isRequired,
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
handleCloseLockDialog : PropTypes.func.isRequired,
|
||||
handleAccessCode : PropTypes.func.isRequired,
|
||||
lobbyPeers : PropTypes.array,
|
||||
canPromote : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
|
@ -185,7 +144,10 @@ const mapStateToProps = (state) =>
|
|||
{
|
||||
return {
|
||||
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) =>
|
||||
{
|
||||
return (
|
||||
prev.room.locked === next.room.locked &&
|
||||
prev.room.joinByAccessCode === next.room.joinByAccessCode &&
|
||||
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.room === next.room &&
|
||||
prev.me.roles === next.me.roles &&
|
||||
prev.lobbyPeers === next.lobbyPeers
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const App = (props) =>
|
|||
room
|
||||
} = props;
|
||||
|
||||
const { id } = useParams();
|
||||
const id = useParams().id.toLowerCase();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const DialogTitle = withStyles(styles)((props) =>
|
|||
|
||||
return (
|
||||
<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>
|
||||
</MuiDialogTitle>
|
||||
);
|
||||
|
|
@ -125,7 +125,7 @@ const ChooseRoom = ({
|
|||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
{ window.config && window.config.title ? window.config.title : 'Multiparty meeting' }
|
||||
{ window.config.title ? window.config.title : 'Multiparty meeting' }
|
||||
<hr />
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useIntl, FormattedMessage } from 'react-intl';
|
|||
import VideoView from '../VideoContainers/VideoView';
|
||||
import Volume from './Volume';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
|
|
@ -59,6 +60,19 @@ const styles = (theme) =>
|
|||
margin : theme.spacing(1),
|
||||
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 :
|
||||
{
|
||||
position : 'relative',
|
||||
|
|
@ -78,7 +92,16 @@ const styles = (theme) =>
|
|||
zIndex : 21,
|
||||
touchAction : 'none',
|
||||
pointerEvents : 'none',
|
||||
'& p' :
|
||||
'&.hide' :
|
||||
{
|
||||
transition : 'opacity 0.1s ease-in-out',
|
||||
opacity : 0
|
||||
},
|
||||
'&.hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'& p' :
|
||||
{
|
||||
position : 'absolute',
|
||||
float : 'left',
|
||||
|
|
@ -93,6 +116,10 @@ const styles = (theme) =>
|
|||
'&.hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.smallContainer' :
|
||||
{
|
||||
fontSize : '3em'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -107,7 +134,8 @@ const styles = (theme) =>
|
|||
fontSize : '2vs',
|
||||
backgroundColor : 'rgba(255, 0, 0, 0.5)',
|
||||
margin : '4px',
|
||||
padding : '15px',
|
||||
padding : theme.spacing(2),
|
||||
zIndex : 31,
|
||||
borderRadius : '20px',
|
||||
textAlign : 'center',
|
||||
opacity : 0,
|
||||
|
|
@ -135,11 +163,12 @@ const Me = (props) =>
|
|||
activeSpeaker,
|
||||
spacing,
|
||||
style,
|
||||
smallButtons,
|
||||
smallContainer,
|
||||
advancedMode,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
screenProducer,
|
||||
extraVideoProducers,
|
||||
canShareScreen,
|
||||
classes
|
||||
} = props;
|
||||
|
|
@ -289,8 +318,24 @@ const Me = (props) =>
|
|||
style={spacingStyle}
|
||||
>
|
||||
<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
|
||||
className={classes.controls}
|
||||
className={classnames(
|
||||
classes.controls,
|
||||
settings.hiddenControls ? 'hide' : null,
|
||||
hover ? 'hover' : null
|
||||
)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
|
|
@ -311,124 +356,216 @@ const Me = (props) =>
|
|||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<p className={hover ? 'hover' : null}>
|
||||
<p className={
|
||||
classnames(
|
||||
hover ? 'hover' : null,
|
||||
smallContainer ? 'smallContainer' : null
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='room.me'
|
||||
defaultMessage='ME'
|
||||
/>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
||||
<React.Fragment>
|
||||
<Tooltip title={micTip} placement='left'>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.muteAudio',
|
||||
defaultMessage : 'Mute audio'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={!me.canSendMic || me.audioInProgress}
|
||||
color={micState === 'on' ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
if (micState === 'off')
|
||||
roomClient.enableMic();
|
||||
else if (micState === 'on')
|
||||
roomClient.muteMic();
|
||||
else
|
||||
roomClient.unmuteMic();
|
||||
}}
|
||||
>
|
||||
{ micState === 'on' ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.muteAudio',
|
||||
defaultMessage : 'Mute audio'
|
||||
})}
|
||||
className={classes.smallContainer}
|
||||
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
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.muteAudio',
|
||||
defaultMessage : 'Mute audio'
|
||||
})}
|
||||
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='left'>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.startVideo',
|
||||
defaultMessage : 'Start video'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={!me.canSendWebcam || me.webcamInProgress}
|
||||
color={webcamState === 'on' ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
>
|
||||
{ webcamState === 'on' ?
|
||||
<VideoIcon />
|
||||
:
|
||||
<VideoOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{ !me.isMobile &&
|
||||
<Tooltip title={screenTip} placement='left'>
|
||||
<div>
|
||||
<Fab
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.startScreenSharing',
|
||||
defaultMessage : 'Start screen sharing'
|
||||
id : 'device.startVideo',
|
||||
defaultMessage : 'Start video'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={
|
||||
!canShareScreen ||
|
||||
!me.canShareScreen ||
|
||||
me.screenShareInProgress
|
||||
}
|
||||
color={screenState === 'on' ? 'primary' : 'default'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
className={classes.smallContainer}
|
||||
disabled={!me.canSendWebcam || me.webcamInProgress}
|
||||
color={webcamState === 'on' ? 'primary' : 'secondary'}
|
||||
size='small'
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
roomClient.disableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'off':
|
||||
{
|
||||
roomClient.enableScreenSharing();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
>
|
||||
{ (screenState === 'on' || screenState === 'unsupported') &&
|
||||
<ScreenOffIcon/>
|
||||
{ webcamState === 'on' ?
|
||||
<VideoIcon />
|
||||
:
|
||||
<VideoOffIcon />
|
||||
}
|
||||
{ screenState === 'off' &&
|
||||
<ScreenIcon/>
|
||||
</IconButton>
|
||||
:
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.startVideo',
|
||||
defaultMessage : 'Start 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>
|
||||
{ me.browser.platform !== 'mobile' &&
|
||||
<Tooltip title={screenTip} placement='left'>
|
||||
<div>
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.startScreenSharing',
|
||||
defaultMessage : 'Start screen sharing'
|
||||
})}
|
||||
className={classes.smallContainer}
|
||||
disabled={
|
||||
!canShareScreen ||
|
||||
!me.canShareScreen ||
|
||||
me.screenShareInProgress
|
||||
}
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
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>
|
||||
</Tooltip>
|
||||
}
|
||||
|
|
@ -454,6 +591,132 @@ const Me = (props) =>
|
|||
</VideoView>
|
||||
</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 &&
|
||||
<div
|
||||
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
|
||||
|
|
@ -480,7 +743,11 @@ const Me = (props) =>
|
|||
>
|
||||
<div className={classes.viewContainer} style={style}>
|
||||
<div
|
||||
className={classes.controls}
|
||||
className={classnames(
|
||||
classes.controls,
|
||||
settings.hiddenControls ? 'hide' : null,
|
||||
hover ? 'hover' : null
|
||||
)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onTouchStart={() =>
|
||||
|
|
@ -528,20 +795,21 @@ const Me = (props) =>
|
|||
|
||||
Me.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
settings : PropTypes.object,
|
||||
activeSpeaker : PropTypes.bool,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
spacing : PropTypes.number,
|
||||
style : PropTypes.object,
|
||||
smallButtons : PropTypes.bool,
|
||||
canShareScreen : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
settings : PropTypes.object,
|
||||
activeSpeaker : PropTypes.bool,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
extraVideoProducers : PropTypes.arrayOf(appPropTypes.Producer),
|
||||
spacing : PropTypes.number,
|
||||
style : PropTypes.object,
|
||||
smallContainer : PropTypes.bool,
|
||||
canShareScreen : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import { useIntl, FormattedMessage } from 'react-intl';
|
|||
import VideoView from '../VideoContainers/VideoView';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import VolumeUpIcon from '@material-ui/icons/VolumeUp';
|
||||
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
|
||||
import NewWindowIcon from '@material-ui/icons/OpenInNew';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import Volume from './Volume';
|
||||
|
|
@ -59,6 +60,19 @@ const styles = (theme) =>
|
|||
{
|
||||
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 :
|
||||
{
|
||||
position : 'relative',
|
||||
|
|
@ -121,15 +135,16 @@ const Peer = (props) =>
|
|||
advancedMode,
|
||||
peer,
|
||||
activeSpeaker,
|
||||
isMobile,
|
||||
browser,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
extraVideoConsumers,
|
||||
toggleConsumerFullscreen,
|
||||
toggleConsumerWindow,
|
||||
spacing,
|
||||
style,
|
||||
smallButtons,
|
||||
smallContainer,
|
||||
windowConsumer,
|
||||
classes,
|
||||
theme
|
||||
|
|
@ -235,32 +250,57 @@ const Peer = (props) =>
|
|||
placement={smallScreen ? 'top' : 'left'}
|
||||
>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.muteAudio',
|
||||
defaultMessage : 'Mute audio'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={!micConsumer}
|
||||
color={micEnabled ? 'default' : 'secondary'}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</Fab>
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'device.muteAudio',
|
||||
defaultMessage : 'Mute audio'
|
||||
})}
|
||||
className={classes.smallContainer}
|
||||
disabled={!micConsumer}
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() =>
|
||||
{
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<VolumeUpIcon />
|
||||
:
|
||||
<VolumeOffIcon />
|
||||
}
|
||||
</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>
|
||||
</Tooltip>
|
||||
|
||||
{ !isMobile &&
|
||||
{ browser.platform !== 'mobile' &&
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id : 'label.newWindow',
|
||||
|
|
@ -269,24 +309,46 @@ const Peer = (props) =>
|
|||
placement={smallScreen ? 'top' : 'left'}
|
||||
>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.newWindow',
|
||||
defaultMessage : 'New window'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={
|
||||
!videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<NewWindowIcon />
|
||||
</Fab>
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.newWindow',
|
||||
defaultMessage : 'New window'
|
||||
})}
|
||||
className={classes.smallContainer}
|
||||
disabled={
|
||||
!videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
}
|
||||
size='small'
|
||||
color='primary'
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Tooltip>
|
||||
}
|
||||
|
|
@ -299,21 +361,40 @@ const Peer = (props) =>
|
|||
placement={smallScreen ? 'top' : 'left'}
|
||||
>
|
||||
<div>
|
||||
<Fab
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.fullscreen',
|
||||
defaultMessage : 'Fullscreen'
|
||||
})}
|
||||
className={classes.fab}
|
||||
disabled={!videoVisible}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenIcon />
|
||||
</Fab>
|
||||
{ smallContainer ?
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.fullscreen',
|
||||
defaultMessage : 'Fullscreen'
|
||||
})}
|
||||
className={classes.smallContainer}
|
||||
disabled={!videoVisible}
|
||||
size='small'
|
||||
color='primary'
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -340,6 +421,7 @@ const Peer = (props) =>
|
|||
videoMultiLayer={webcamConsumer && webcamConsumer.type !== 'simple'}
|
||||
videoTrack={webcamConsumer && webcamConsumer.track}
|
||||
videoVisible={videoVisible}
|
||||
audioTrack={micConsumer && micConsumer.track}
|
||||
audioCodec={micConsumer && micConsumer.codec}
|
||||
videoCodec={webcamConsumer && webcamConsumer.codec}
|
||||
audioScore={micConsumer ? micConsumer.score : null}
|
||||
|
|
@ -350,6 +432,202 @@ const Peer = (props) =>
|
|||
</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 &&
|
||||
<div
|
||||
className={classnames(classes.root, 'screen', hover ? 'hover' : null)}
|
||||
|
|
@ -408,7 +686,7 @@ const Peer = (props) =>
|
|||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{ !isMobile &&
|
||||
{ browser.platform !== 'mobile' &&
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id : 'label.newWindow',
|
||||
|
|
@ -427,7 +705,7 @@ const Peer = (props) =>
|
|||
!screenVisible ||
|
||||
(windowConsumer === screenConsumer.id)
|
||||
}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
size={smallContainer ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
|
|
@ -454,7 +732,7 @@ const Peer = (props) =>
|
|||
})}
|
||||
className={classes.fab}
|
||||
disabled={!screenVisible}
|
||||
size={smallButtons ? 'small' : 'large'}
|
||||
size={smallContainer ? 'small' : 'large'}
|
||||
onClick={() =>
|
||||
{
|
||||
toggleConsumerFullscreen(screenConsumer);
|
||||
|
|
@ -490,6 +768,7 @@ const Peer = (props) =>
|
|||
videoTrack={screenConsumer && screenConsumer.track}
|
||||
videoVisible={screenVisible}
|
||||
videoCodec={screenConsumer && screenConsumer.codec}
|
||||
videoScore={screenConsumer ? screenConsumer.score : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -506,12 +785,13 @@ Peer.propTypes =
|
|||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
extraVideoConsumers : PropTypes.arrayOf(appPropTypes.Consumer),
|
||||
windowConsumer : PropTypes.string,
|
||||
activeSpeaker : PropTypes.bool,
|
||||
isMobile : PropTypes.bool,
|
||||
browser : PropTypes.object.isRequired,
|
||||
spacing : PropTypes.number,
|
||||
style : PropTypes.object,
|
||||
smallButtons : PropTypes.bool,
|
||||
smallContainer : PropTypes.bool,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
|
|
@ -529,7 +809,7 @@ const makeMapStateToProps = (initialState, { id }) =>
|
|||
...getPeerConsumers(state, id),
|
||||
windowConsumer : state.room.windowConsumer,
|
||||
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.room.activeSpeakerId === next.room.activeSpeakerId &&
|
||||
prev.room.windowConsumer === next.room.windowConsumer &&
|
||||
prev.me.isMobile === next.me.isMobile
|
||||
prev.me.browser === next.me.browser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,16 +91,6 @@ const SpeakerPeer = (props) =>
|
|||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
let videoProfile;
|
||||
|
||||
if (webcamConsumer)
|
||||
videoProfile = webcamConsumer.profile;
|
||||
|
||||
let screenProfile;
|
||||
|
||||
if (screenConsumer)
|
||||
screenProfile = screenConsumer.profile;
|
||||
|
||||
const spacingStyle =
|
||||
{
|
||||
'margin' : spacing
|
||||
|
|
@ -134,11 +124,27 @@ const SpeakerPeer = (props) =>
|
|||
peer={peer}
|
||||
displayName={peer.displayName}
|
||||
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}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
audioCodec={micConsumer && micConsumer.codec}
|
||||
videoCodec={webcamConsumer && webcamConsumer.codec}
|
||||
audioScore={micConsumer ? micConsumer.score : null}
|
||||
videoScore={webcamConsumer ? webcamConsumer.score : null}
|
||||
>
|
||||
<Volume id={peer.id} />
|
||||
</VideoView>
|
||||
|
|
@ -165,10 +171,29 @@ const SpeakerPeer = (props) =>
|
|||
<VideoView
|
||||
advancedMode={advancedMode}
|
||||
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}
|
||||
videoProfile={screenProfile}
|
||||
videoCodec={screenConsumer ? screenConsumer.codec : null}
|
||||
videoCodec={screenConsumer && screenConsumer.codec}
|
||||
videoScore={screenConsumer ? screenConsumer.score : null}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
lobbyPeersKeySelector,
|
||||
peersLengthSelector
|
||||
peersLengthSelector,
|
||||
raisedHandsSelector
|
||||
} from '../Selectors';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
|
|
@ -13,11 +14,14 @@ import * as toolareaActions from '../../actions/toolareaActions';
|
|||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Badge from '@material-ui/core/Badge';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
|
|
@ -26,6 +30,7 @@ import SecurityIcon from '@material-ui/icons/Security';
|
|||
import PeopleIcon from '@material-ui/icons/People';
|
||||
import LockIcon from '@material-ui/icons/Lock';
|
||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||
import VideoCallIcon from '@material-ui/icons/VideoCall';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
|
|
@ -78,8 +83,20 @@ const styles = (theme) =>
|
|||
},
|
||||
actionButton :
|
||||
{
|
||||
margin : theme.spacing(1),
|
||||
padding : 0
|
||||
margin : theme.spacing(1, 0),
|
||||
padding : theme.spacing(0, 1)
|
||||
},
|
||||
disabledButton :
|
||||
{
|
||||
margin : theme.spacing(1, 0)
|
||||
},
|
||||
green :
|
||||
{
|
||||
color : 'rgba(0, 153, 0, 1)'
|
||||
},
|
||||
moreAction :
|
||||
{
|
||||
margin : theme.spacing(0, 0, 0, 1)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -118,6 +135,18 @@ const TopBar = (props) =>
|
|||
{
|
||||
const intl = useIntl();
|
||||
|
||||
const [ moreActionsElement, setMoreActionsElement ] = useState(null);
|
||||
|
||||
const handleMoreActionsOpen = (event) =>
|
||||
{
|
||||
setMoreActionsElement(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMoreActionsClose = () =>
|
||||
{
|
||||
setMoreActionsElement(null);
|
||||
};
|
||||
|
||||
const {
|
||||
roomClient,
|
||||
room,
|
||||
|
|
@ -131,14 +160,19 @@ const TopBar = (props) =>
|
|||
fullscreen,
|
||||
onFullscreen,
|
||||
setSettingsOpen,
|
||||
setExtraVideoOpen,
|
||||
setLockDialogOpen,
|
||||
toggleToolArea,
|
||||
openUsersTab,
|
||||
unread,
|
||||
canProduceExtraVideo,
|
||||
canLock,
|
||||
canPromote,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
const isMoreActionsMenuOpen = Boolean(moreActionsElement);
|
||||
|
||||
const lockTooltip = room.locked ?
|
||||
intl.formatMessage({
|
||||
id : 'tooltip.unLockRoom',
|
||||
|
|
@ -173,215 +207,264 @@ const TopBar = (props) =>
|
|||
});
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position='fixed'
|
||||
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
|
||||
>
|
||||
<Toolbar>
|
||||
<PulsingBadge
|
||||
color='secondary'
|
||||
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'
|
||||
})}
|
||||
<React.Fragment>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
className={room.toolbarsVisible || permanentTopBar ? classes.show : classes.hide}
|
||||
>
|
||||
<Toolbar>
|
||||
<PulsingBadge
|
||||
color='secondary'
|
||||
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.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.actionButtons}>
|
||||
<IconButton
|
||||
aria-haspopup='true'
|
||||
onClick={handleMoreActionsOpen}
|
||||
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',
|
||||
defaultMessage : 'Show participants'
|
||||
})}
|
||||
color='inherit'
|
||||
onClick={() => openUsersTab()}
|
||||
>
|
||||
<Badge
|
||||
color='primary'
|
||||
badgeContent={peersLength + 1}
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.participants',
|
||||
defaultMessage : 'Show participants'
|
||||
})}
|
||||
color='inherit'
|
||||
onClick={() => openUsersTab()}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id : 'tooltip.settings',
|
||||
defaultMessage : 'Show settings'
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
<Badge
|
||||
color='primary'
|
||||
badgeContent={peersLength + 1}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id : 'tooltip.settings',
|
||||
defaultMessage : 'Show settings'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={() => setSettingsOpen(!room.settingsOpen)}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={lockTooltip}>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
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>
|
||||
</Tooltip>
|
||||
{ lobbyPeers.length > 0 &&
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id : 'tooltip.lobby',
|
||||
defaultMessage : 'Show lobby'
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.lobby',
|
||||
defaultMessage : 'Show lobby'
|
||||
})}
|
||||
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'
|
||||
id : 'tooltip.settings',
|
||||
defaultMessage : 'Show settings'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
color='inherit'
|
||||
onClick={() =>
|
||||
{
|
||||
loggedIn ? roomClient.logout() : roomClient.login();
|
||||
}}
|
||||
onClick={() => setSettingsOpen(!room.settingsOpen)}
|
||||
>
|
||||
{ myPicture ?
|
||||
<Avatar src={myPicture} />
|
||||
:
|
||||
<AccountCircle />
|
||||
}
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
<div className={classes.divider} />
|
||||
<Button
|
||||
<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 className={classes.divider} />
|
||||
<Button
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.leave',
|
||||
defaultMessage : 'Leave'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
onClick={() => roomClient.close()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.leave'
|
||||
defaultMessage='Leave'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Menu
|
||||
anchorEl={moreActionsElement}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
open={isMoreActionsMenuOpen}
|
||||
onClose={handleMoreActionsClose}
|
||||
getContentAnchorEl={null}
|
||||
>
|
||||
<MenuItem
|
||||
dense
|
||||
disabled={!canProduceExtraVideo}
|
||||
onClick={() =>
|
||||
{
|
||||
handleMoreActionsClose();
|
||||
setExtraVideoOpen(!room.extraVideoOpen);
|
||||
}}
|
||||
>
|
||||
<VideoCallIcon
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'label.leave',
|
||||
defaultMessage : 'Leave'
|
||||
id : 'label.addVideo',
|
||||
defaultMessage : 'Add video'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
onClick={() => roomClient.close()}
|
||||
>
|
||||
/>
|
||||
<p className={classes.moreAction}>
|
||||
<FormattedMessage
|
||||
id='label.leave'
|
||||
defaultMessage='Leave'
|
||||
id='label.addVideo'
|
||||
defaultMessage='Add video'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</p>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
TopBar.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
peersLength : PropTypes.number,
|
||||
lobbyPeers : PropTypes.array,
|
||||
permanentTopBar : PropTypes.bool,
|
||||
myPicture : PropTypes.string,
|
||||
loggedIn : PropTypes.bool.isRequired,
|
||||
loginEnabled : PropTypes.bool.isRequired,
|
||||
fullscreenEnabled : PropTypes.bool,
|
||||
fullscreen : PropTypes.bool,
|
||||
onFullscreen : PropTypes.func.isRequired,
|
||||
setToolbarsVisible : PropTypes.func.isRequired,
|
||||
setSettingsOpen : PropTypes.func.isRequired,
|
||||
setLockDialogOpen : PropTypes.func.isRequired,
|
||||
toggleToolArea : PropTypes.func.isRequired,
|
||||
openUsersTab : PropTypes.func.isRequired,
|
||||
unread : PropTypes.number.isRequired,
|
||||
canLock : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
peersLength : PropTypes.number,
|
||||
lobbyPeers : PropTypes.array,
|
||||
permanentTopBar : PropTypes.bool,
|
||||
myPicture : PropTypes.string,
|
||||
loggedIn : PropTypes.bool.isRequired,
|
||||
loginEnabled : PropTypes.bool.isRequired,
|
||||
fullscreenEnabled : PropTypes.bool,
|
||||
fullscreen : PropTypes.bool,
|
||||
onFullscreen : PropTypes.func.isRequired,
|
||||
setToolbarsVisible : PropTypes.func.isRequired,
|
||||
setSettingsOpen : PropTypes.func.isRequired,
|
||||
setExtraVideoOpen : PropTypes.func.isRequired,
|
||||
setLockDialogOpen : PropTypes.func.isRequired,
|
||||
toggleToolArea : PropTypes.func.isRequired,
|
||||
openUsersTab : PropTypes.func.isRequired,
|
||||
unread : PropTypes.number.isRequired,
|
||||
canProduceExtraVideo : PropTypes.bool.isRequired,
|
||||
canLock : PropTypes.bool.isRequired,
|
||||
canPromote : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
|
|
@ -394,10 +477,16 @@ const mapStateToProps = (state) =>
|
|||
loginEnabled : state.me.loginEnabled,
|
||||
myPicture : state.me.picture,
|
||||
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))
|
||||
state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)),
|
||||
canPromote :
|
||||
state.me.roles.some((role) =>
|
||||
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
|
@ -408,11 +497,15 @@ const mapDispatchToProps = (dispatch) =>
|
|||
},
|
||||
setSettingsOpen : (settingsOpen) =>
|
||||
{
|
||||
dispatch(roomActions.setSettingsOpen({ settingsOpen }));
|
||||
dispatch(roomActions.setSettingsOpen(settingsOpen));
|
||||
},
|
||||
setExtraVideoOpen : (extraVideoOpen) =>
|
||||
{
|
||||
dispatch(roomActions.setExtraVideoOpen(extraVideoOpen));
|
||||
},
|
||||
setLockDialogOpen : (lockDialogOpen) =>
|
||||
{
|
||||
dispatch(roomActions.setLockDialogOpen({ lockDialogOpen }));
|
||||
dispatch(roomActions.setLockDialogOpen(lockDialogOpen));
|
||||
},
|
||||
toggleToolArea : () =>
|
||||
{
|
||||
|
|
@ -446,4 +539,4 @@ export default withRoomContext(connect(
|
|||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles, { withTheme: true })(TopBar)));
|
||||
)(withStyles(styles, { withTheme: true })(TopBar)));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { withRoomContext } from '../RoomContext';
|
||||
import classnames from 'classnames';
|
||||
import isElectron from 'is-electron';
|
||||
import * as settingsActions from '../actions/settingsActions';
|
||||
import PropTypes from 'prop-types';
|
||||
|
|
@ -82,6 +83,10 @@ const styles = (theme) =>
|
|||
green :
|
||||
{
|
||||
color : 'rgba(0, 153, 0, 1)'
|
||||
},
|
||||
red :
|
||||
{
|
||||
color : 'rgba(153, 0, 0, 1)'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -128,9 +133,9 @@ const DialogTitle = withStyles(styles)((props) =>
|
|||
|
||||
return (
|
||||
<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>
|
||||
{ window.config && window.config.loginEnabled &&
|
||||
{ window.config.loginEnabled &&
|
||||
<Tooltip
|
||||
onClose={handleTooltipClose}
|
||||
onOpen={handleTooltipOpen}
|
||||
|
|
@ -147,7 +152,9 @@ const DialogTitle = withStyles(styles)((props) =>
|
|||
{ myPicture ?
|
||||
<Avatar src={myPicture} className={classes.largeAvatar} />
|
||||
:
|
||||
<AccountCircle className={classes.largeIcon} />
|
||||
<AccountCircle
|
||||
className={classnames(classes.largeIcon, loggedIn ? classes.green : null)}
|
||||
/>
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
|
@ -217,11 +224,11 @@ const JoinDialog = ({
|
|||
myPicture={myPicture}
|
||||
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 />
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
|
|
@ -278,6 +285,16 @@ const JoinDialog = ({
|
|||
}}
|
||||
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>
|
||||
|
||||
|
|
@ -316,6 +333,7 @@ const JoinDialog = ({
|
|||
className={classes.green}
|
||||
gutterBottom
|
||||
variant='h6'
|
||||
style={{ fontWeight: '600' }}
|
||||
align='center'
|
||||
>
|
||||
<FormattedMessage
|
||||
|
|
@ -324,7 +342,11 @@ const JoinDialog = ({
|
|||
/>
|
||||
</DialogContentText>
|
||||
{ room.signInRequired ?
|
||||
<DialogContentText gutterBottom>
|
||||
<DialogContentText
|
||||
gutterBottom
|
||||
variant='h5'
|
||||
style={{ fontWeight: '600' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='room.emptyRequireLogin'
|
||||
defaultMessage={
|
||||
|
|
@ -334,7 +356,11 @@ const JoinDialog = ({
|
|||
/>
|
||||
</DialogContentText>
|
||||
:
|
||||
<DialogContentText gutterBottom>
|
||||
<DialogContentText
|
||||
gutterBottom
|
||||
variant='h5'
|
||||
style={{ fontWeight: '600' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='room.locketWait'
|
||||
defaultMessage='The room is locked - hang on until somebody lets you in ...'
|
||||
|
|
@ -407,6 +433,7 @@ export default withRoomContext(connect(
|
|||
return (
|
||||
prev.room.inLobby === next.room.inLobby &&
|
||||
prev.room.signInRequired === next.room.signInRequired &&
|
||||
prev.room.overRoomLimit === next.room.overRoomLimit &&
|
||||
prev.settings.displayName === next.settings.displayName &&
|
||||
prev.me.displayNameInProgress === next.me.displayNameInProgress &&
|
||||
prev.me.loginEnabled === next.me.loginEnabled &&
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import ChatModerator from './ChatModerator';
|
||||
import MessageList from './MessageList';
|
||||
import ChatInput from './ChatInput';
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ const Chat = (props) =>
|
|||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<ChatModerator />
|
||||
<MessageList />
|
||||
<ChatInput />
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
@ -6,6 +6,7 @@ import DOMPurify from 'dompurify';
|
|||
import marked from 'marked';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const linkRenderer = new marked.Renderer();
|
||||
|
||||
|
|
@ -55,6 +56,8 @@ const styles = (theme) =>
|
|||
|
||||
const Message = (props) =>
|
||||
{
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
self,
|
||||
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>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { withStyles } from '@material-ui/core/styles';
|
|||
import { withRoomContext } from '../../../RoomContext';
|
||||
import { useIntl } from 'react-intl';
|
||||
import FileList from './FileList';
|
||||
import FileSharingModerator from './FileSharingModerator';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
|
|
@ -24,6 +25,10 @@ const styles = (theme) =>
|
|||
button :
|
||||
{
|
||||
margin : theme.spacing(1)
|
||||
},
|
||||
shareButtonsWrapper :
|
||||
{
|
||||
display : 'flex'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -35,12 +40,13 @@ const FileSharing = (props) =>
|
|||
{
|
||||
if (event.target.files.length > 0)
|
||||
{
|
||||
props.roomClient.shareFiles(event.target.files);
|
||||
await props.roomClient.shareFiles(event.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
canShareFiles,
|
||||
browser,
|
||||
canShare,
|
||||
classes
|
||||
} = props;
|
||||
|
|
@ -56,26 +62,61 @@ const FileSharing = (props) =>
|
|||
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 (
|
||||
<Paper className={classes.root}>
|
||||
<input
|
||||
className={classes.input}
|
||||
type='file'
|
||||
disabled={!canShare}
|
||||
onChange={handleFileChange}
|
||||
id='share-files-button'
|
||||
/>
|
||||
<label htmlFor='share-files-button'>
|
||||
<Button
|
||||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
disabled={!canShareFiles || !canShare}
|
||||
>
|
||||
{buttonDescription}
|
||||
</Button>
|
||||
</label>
|
||||
|
||||
<FileSharingModerator />
|
||||
<div className={classes.shareButtonsWrapper} >
|
||||
<input
|
||||
className={classes.input}
|
||||
type='file'
|
||||
disabled={!canShare}
|
||||
onChange={handleFileChange}
|
||||
// Need to reset to be able to share same file twice
|
||||
onClick={(e) => (e.target.value = null)}
|
||||
id='share-files-button'
|
||||
/>
|
||||
<input
|
||||
className={classes.input}
|
||||
type='file'
|
||||
disabled={!canShare}
|
||||
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 />
|
||||
</Paper>
|
||||
);
|
||||
|
|
@ -83,6 +124,7 @@ const FileSharing = (props) =>
|
|||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
browser : PropTypes.object.isRequired,
|
||||
canShareFiles : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired,
|
||||
canShare : PropTypes.bool.isRequired,
|
||||
|
|
@ -93,6 +135,7 @@ const mapStateToProps = (state) =>
|
|||
{
|
||||
return {
|
||||
canShareFiles : state.me.canShareFiles,
|
||||
browser : state.me.browser,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files',
|
||||
canShare :
|
||||
state.me.roles.some((role) =>
|
||||
|
|
@ -109,6 +152,7 @@ export default withRoomContext(connect(
|
|||
{
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { raisedHandsSelector } from '../Selectors';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as toolareaActions from '../../actions/toolareaActions';
|
||||
|
|
@ -51,6 +52,7 @@ const MeetingDrawer = (props) =>
|
|||
currentToolTab,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
raisedHands,
|
||||
closeDrawer,
|
||||
setToolTab,
|
||||
classes,
|
||||
|
|
@ -93,10 +95,14 @@ const MeetingDrawer = (props) =>
|
|||
}
|
||||
/>
|
||||
<Tab
|
||||
label={intl.formatMessage({
|
||||
id : 'label.participants',
|
||||
defaultMessage : 'Participants'
|
||||
})}
|
||||
label={
|
||||
<Badge color='secondary' badgeContent={raisedHands}>
|
||||
{intl.formatMessage({
|
||||
id : 'label.participants',
|
||||
defaultMessage : 'Participants'
|
||||
})}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
<IconButton onClick={closeDrawer}>
|
||||
|
|
@ -116,16 +122,21 @@ MeetingDrawer.propTypes =
|
|||
setToolTab : PropTypes.func.isRequired,
|
||||
unreadMessages : PropTypes.number.isRequired,
|
||||
unreadFiles : PropTypes.number.isRequired,
|
||||
raisedHands : PropTypes.number.isRequired,
|
||||
closeDrawer : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
unreadMessages : state.toolarea.unreadMessages,
|
||||
unreadFiles : state.toolarea.unreadFiles
|
||||
});
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
unreadMessages : state.toolarea.unreadMessages,
|
||||
unreadFiles : state.toolarea.unreadFiles,
|
||||
raisedHands : raisedHandsSelector(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : toolareaActions.setToolTab
|
||||
|
|
@ -141,7 +152,8 @@ export default connect(
|
|||
return (
|
||||
prev.toolarea.currentToolTab === next.toolarea.currentToolTab &&
|
||||
prev.toolarea.unreadMessages === next.toolarea.unreadMessages &&
|
||||
prev.toolarea.unreadFiles === next.toolarea.unreadFiles
|
||||
prev.toolarea.unreadFiles === next.toolarea.unreadFiles &&
|
||||
prev.peers === next.peers
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,49 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import classnames from 'classnames';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { useIntl } from 'react-intl';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import PanIcon from '@material-ui/icons/PanTool';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
import HandIcon from '../../../images/icon-hand-white.svg';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
display : 'flex'
|
||||
},
|
||||
listPeer :
|
||||
{
|
||||
display : 'flex'
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem'
|
||||
height : '2rem',
|
||||
marginTop : theme.spacing(1)
|
||||
},
|
||||
peerInfo :
|
||||
{
|
||||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : theme.spacing(1),
|
||||
flexGrow : 1,
|
||||
alignItems : 'center'
|
||||
},
|
||||
indicators :
|
||||
green :
|
||||
{
|
||||
left : 0,
|
||||
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
|
||||
}
|
||||
color : 'rgba(0, 153, 0, 1)'
|
||||
}
|
||||
});
|
||||
|
||||
const ListMe = (props) =>
|
||||
{
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
roomClient,
|
||||
me,
|
||||
settings,
|
||||
classes
|
||||
|
|
@ -82,29 +52,39 @@ const ListMe = (props) =>
|
|||
const picture = me.picture || EmptyAvatar;
|
||||
|
||||
return (
|
||||
<li className={classes.root}>
|
||||
<div className={classes.listPeer}>
|
||||
<img alt='My avatar' className={classes.avatar} src={picture} />
|
||||
<div className={classes.root}>
|
||||
<img alt='My avatar' className={classes.avatar} src={picture} />
|
||||
|
||||
<div className={classes.peerInfo}>
|
||||
{settings.displayName}
|
||||
</div>
|
||||
|
||||
<div className={classes.indicators}>
|
||||
{ me.raisedHand &&
|
||||
<div className={classnames(classes.icon, 'raise-hand')} />
|
||||
}
|
||||
</div>
|
||||
<div className={classes.peerInfo}>
|
||||
{settings.displayName}
|
||||
</div>
|
||||
</li>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListMe.propTypes =
|
||||
{
|
||||
me : appPropTypes.Me.isRequired,
|
||||
settings : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
settings : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
|
@ -112,7 +92,7 @@ const mapStateToProps = (state) => ({
|
|||
settings : state.settings
|
||||
});
|
||||
|
||||
export default connect(
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
|
|
@ -125,4 +105,4 @@ export default connect(
|
|||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(ListMe));
|
||||
)(withStyles(styles)(ListMe)));
|
||||
|
|
|
|||
|
|
@ -10,14 +10,7 @@ const styles = (theme) =>
|
|||
({
|
||||
root :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
display : 'flex'
|
||||
},
|
||||
actionButtons :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
display : 'flex'
|
||||
},
|
||||
divider :
|
||||
|
|
@ -43,7 +36,6 @@ const ListModerator = (props) =>
|
|||
id : 'room.muteAll',
|
||||
defaultMessage : 'Mute all'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
disabled={room.muteAllInProgress}
|
||||
|
|
@ -60,7 +52,6 @@ const ListModerator = (props) =>
|
|||
id : 'room.stopAllVideo',
|
||||
defaultMessage : 'Stop all video'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
disabled={room.stopAllVideoInProgress}
|
||||
|
|
@ -77,7 +68,6 @@ const ListModerator = (props) =>
|
|||
id : 'room.closeMeeting',
|
||||
defaultMessage : 'Close meeting'
|
||||
})}
|
||||
className={classes.actionButton}
|
||||
variant='contained'
|
||||
color='secondary'
|
||||
disabled={room.closeMeetingInProgress}
|
||||
|
|
|
|||
|
|
@ -3,42 +3,38 @@ import { connect } from 'react-redux';
|
|||
import { makePeerConsumerSelector } from '../../Selectors';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import { useIntl } from 'react-intl';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import 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 ScreenOffIcon from '@material-ui/icons/StopScreenShare';
|
||||
import ExitIcon from '@material-ui/icons/ExitToApp';
|
||||
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) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
display : 'flex'
|
||||
},
|
||||
listPeer :
|
||||
{
|
||||
display : 'flex'
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem'
|
||||
height : '2rem',
|
||||
marginTop : theme.spacing(1)
|
||||
},
|
||||
peerInfo :
|
||||
{
|
||||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : theme.spacing(1),
|
||||
flexGrow : 1,
|
||||
|
|
@ -46,52 +42,12 @@ const styles = (theme) =>
|
|||
},
|
||||
indicators :
|
||||
{
|
||||
left : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
transition : 'opacity 0.3s'
|
||||
display : 'flex',
|
||||
padding : theme.spacing(1.5)
|
||||
},
|
||||
icon :
|
||||
green :
|
||||
{
|
||||
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
|
||||
},
|
||||
'&.on' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.off' :
|
||||
{
|
||||
opacity : 0.2
|
||||
},
|
||||
'&.raise-hand' :
|
||||
{
|
||||
backgroundImage : `url(${HandIcon})`
|
||||
}
|
||||
},
|
||||
controls :
|
||||
{
|
||||
float : 'right',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center'
|
||||
color : 'rgba(0, 153, 0, 1)'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -104,11 +60,18 @@ const ListPeer = (props) =>
|
|||
isModerator,
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
children,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
const webcamEnabled = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
|
|
@ -131,78 +94,97 @@ const ListPeer = (props) =>
|
|||
{peer.displayName}
|
||||
</div>
|
||||
<div className={classes.indicators}>
|
||||
{ peer.raiseHandState &&
|
||||
<div className={
|
||||
classnames(
|
||||
classes.icon, 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
{ peer.raisedHand &&
|
||||
<PanIcon className={classes.green} />
|
||||
}
|
||||
</div>
|
||||
{children}
|
||||
<div className={classes.controls}>
|
||||
{ screenConsumer &&
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.muteScreenSharing',
|
||||
defaultMessage : 'Mute participant share'
|
||||
})}
|
||||
color={screenVisible ? 'primary' : 'secondary'}
|
||||
disabled={peer.peerScreenInProgress}
|
||||
onClick={() =>
|
||||
{
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
|
||||
}}
|
||||
>
|
||||
{ screenVisible ?
|
||||
<ScreenIcon />
|
||||
:
|
||||
<ScreenOffIcon />
|
||||
}
|
||||
</IconButton>
|
||||
}
|
||||
{ screenConsumer &&
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.muteParticipant',
|
||||
defaultMessage : 'Mute participant'
|
||||
id : 'tooltip.muteScreenSharing',
|
||||
defaultMessage : 'Mute participant share'
|
||||
})}
|
||||
color={micEnabled ? 'primary' : 'secondary'}
|
||||
disabled={peer.peerAudioInProgress}
|
||||
onClick={() =>
|
||||
color={screenVisible ? 'primary' : 'secondary'}
|
||||
disabled={peer.peerScreenInProgress}
|
||||
onClick={(e) =>
|
||||
{
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
e.stopPropagation();
|
||||
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'screen', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<MicIcon />
|
||||
{ screenVisible ?
|
||||
<ScreenIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
<ScreenOffIcon />
|
||||
}
|
||||
</IconButton>
|
||||
{ isModerator &&
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.kickParticipant',
|
||||
defaultMessage : 'Kick out participant'
|
||||
})}
|
||||
disabled={peer.peerKickInProgress}
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.kickPeer(peer.id);
|
||||
}}
|
||||
>
|
||||
<ExitIcon />
|
||||
</IconButton>
|
||||
}
|
||||
<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 />
|
||||
}
|
||||
</div>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.muteParticipant',
|
||||
defaultMessage : 'Mute participant'
|
||||
})}
|
||||
color={micEnabled ? 'primary' : 'secondary'}
|
||||
disabled={peer.peerAudioInProgress}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<VolumeUpIcon />
|
||||
:
|
||||
<VolumeOffIcon />
|
||||
}
|
||||
</IconButton>
|
||||
{ isModerator &&
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id : 'tooltip.kickParticipant',
|
||||
defaultMessage : 'Kick out participant'
|
||||
})}
|
||||
disabled={peer.peerKickInProgress}
|
||||
color='secondary'
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
|
||||
roomClient.kickPeer(peer.id);
|
||||
}}
|
||||
>
|
||||
<ExitIcon />
|
||||
</IconButton>
|
||||
}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,12 +31,10 @@ const styles = (theme) =>
|
|||
},
|
||||
listheader :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
fontWeight : 'bolder'
|
||||
},
|
||||
listItem :
|
||||
{
|
||||
padding : theme.spacing(1),
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'pointer',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ import Peer from '../Containers/Peer';
|
|||
import SpeakerPeer from '../Containers/SpeakerPeer';
|
||||
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 = () =>
|
||||
({
|
||||
root :
|
||||
|
|
@ -20,24 +26,22 @@ const styles = () =>
|
|||
width : '100%',
|
||||
display : 'grid',
|
||||
gridTemplateColumns : '1fr',
|
||||
gridTemplateRows : '1.6fr minmax(0, 0.4fr)'
|
||||
gridTemplateRows : '1fr 0.25fr'
|
||||
},
|
||||
speaker :
|
||||
{
|
||||
gridArea : '1 / 1 / 2 / 2',
|
||||
gridArea : '1 / 1 / 1 / 1',
|
||||
display : 'flex',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
paddingTop : 40
|
||||
alignItems : 'center'
|
||||
},
|
||||
filmStrip :
|
||||
{
|
||||
gridArea : '2 / 1 / 3 / 2'
|
||||
gridArea : '2 / 1 / 2 / 1'
|
||||
},
|
||||
filmItem :
|
||||
{
|
||||
display : 'flex',
|
||||
marginLeft : '6px',
|
||||
border : 'var(--peer-border)',
|
||||
'&.selected' :
|
||||
{
|
||||
|
|
@ -45,8 +49,18 @@ const styles = () =>
|
|||
},
|
||||
'&.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.rootContainer = React.createRef();
|
||||
|
||||
this.activePeerContainer = React.createRef();
|
||||
|
||||
this.filmStripContainer = React.createRef();
|
||||
|
|
@ -105,24 +121,38 @@ class Filmstrip extends React.PureComponent
|
|||
{
|
||||
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;
|
||||
|
||||
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()))
|
||||
{
|
||||
speakerWidth /= 2;
|
||||
speakerHeight = (speakerWidth / 4) * 3;
|
||||
speakerHeight = speakerWidth / RATIO;
|
||||
}
|
||||
|
||||
if (speakerHeight > (speaker.clientHeight - 60))
|
||||
if (speakerHeight > (availableSpeakerHeight - PADDING_V))
|
||||
{
|
||||
speakerHeight = (speaker.clientHeight - 60);
|
||||
speakerWidth = (speakerHeight / 3) * 4;
|
||||
speakerHeight = (availableSpeakerHeight - PADDING_V);
|
||||
speakerWidth = speakerHeight * RATIO;
|
||||
}
|
||||
|
||||
newState.speakerWidth = speakerWidth;
|
||||
|
|
@ -133,14 +163,18 @@ class Filmstrip extends React.PureComponent
|
|||
|
||||
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;
|
||||
filmStripHeight = (filmStripWidth / 4) * 3;
|
||||
filmStripWidth = (availableWidth - FILMSTRING_PADDING_H) /
|
||||
this.props.boxes;
|
||||
filmStripHeight = filmStripWidth / RATIO;
|
||||
}
|
||||
|
||||
newState.filmStripWidth = filmStripWidth;
|
||||
|
|
@ -172,27 +206,21 @@ class Filmstrip extends React.PureComponent
|
|||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -205,6 +233,8 @@ class Filmstrip extends React.PureComponent
|
|||
myId,
|
||||
advancedMode,
|
||||
spotlights,
|
||||
toolbarsVisible,
|
||||
permanentTopBar,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -223,7 +253,14 @@ class Filmstrip extends React.PureComponent
|
|||
};
|
||||
|
||||
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}>
|
||||
{ peers[activePeerId] &&
|
||||
<SpeakerPeer
|
||||
|
|
@ -245,7 +282,7 @@ class Filmstrip extends React.PureComponent
|
|||
<Me
|
||||
advancedMode={advancedMode}
|
||||
style={peerStyle}
|
||||
smallButtons
|
||||
smallContainer
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
|
|
@ -268,7 +305,7 @@ class Filmstrip extends React.PureComponent
|
|||
advancedMode={advancedMode}
|
||||
id={peerId}
|
||||
style={peerStyle}
|
||||
smallButtons
|
||||
smallContainer
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
|
|
@ -296,6 +333,8 @@ Filmstrip.propTypes = {
|
|||
selectedPeerId : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired,
|
||||
boxes : PropTypes.number,
|
||||
toolbarsVisible : PropTypes.bool.isRequired,
|
||||
permanentTopBar : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
|
@ -308,7 +347,9 @@ const mapStateToProps = (state) =>
|
|||
consumers : state.consumers,
|
||||
myId : state.me.id,
|
||||
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 (
|
||||
prev.room.activeSpeakerId === next.room.activeSpeakerId &&
|
||||
prev.room.selectedPeerId === next.room.selectedPeerId &&
|
||||
prev.room.toolbarsVisible === next.room.toolbarsVisible &&
|
||||
prev.settings.permanentTopBar === next.settings.permanentTopBar &&
|
||||
prev.peers === next.peers &&
|
||||
prev.consumers === next.consumers &&
|
||||
prev.room.spotlights === next.room.spotlights &&
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { micConsumerSelector } from '../Selectors';
|
||||
import { passiveMicConsumerSelector } from '../Selectors';
|
||||
import PropTypes from 'prop-types';
|
||||
import PeerAudio from './PeerAudio';
|
||||
|
||||
const AudioPeers = (props) =>
|
||||
{
|
||||
const {
|
||||
micConsumers
|
||||
micConsumers,
|
||||
audioOutputDevice
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
|
@ -19,6 +20,7 @@ const AudioPeers = (props) =>
|
|||
<PeerAudio
|
||||
key={micConsumer.id}
|
||||
audioTrack={micConsumer.track}
|
||||
audioOutputDevice={audioOutputDevice}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
@ -29,12 +31,14 @@ const AudioPeers = (props) =>
|
|||
|
||||
AudioPeers.propTypes =
|
||||
{
|
||||
micConsumers : PropTypes.array
|
||||
micConsumers : PropTypes.array,
|
||||
audioOutputDevice : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
micConsumers : micConsumerSelector(state)
|
||||
micConsumers : passiveMicConsumerSelector(state),
|
||||
audioOutputDevice : state.settings.selectedAudioOutputDevice
|
||||
});
|
||||
|
||||
const AudioPeersContainer = connect(
|
||||
|
|
@ -45,7 +49,10 @@ const AudioPeersContainer = connect(
|
|||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.consumers === next.consumers
|
||||
prev.consumers === next.consumers &&
|
||||
prev.room.spotlights === next.room.spotlights &&
|
||||
prev.settings.selectedAudioOutputDevice ===
|
||||
next.settings.selectedAudioOutputDevice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default class PeerAudio extends React.PureComponent
|
|||
// Latest received audio track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
this._audioOutputDevice = null;
|
||||
}
|
||||
|
||||
render()
|
||||
|
|
@ -24,17 +25,21 @@ export default class PeerAudio extends React.PureComponent
|
|||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack } = this.props;
|
||||
const { audioTrack, audioOutputDevice } = this.props;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
this._setOutputDevice(audioOutputDevice);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps)
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
const { audioTrack } = nextProps;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
if (prevProps !== this.props)
|
||||
{
|
||||
const { audioTrack, audioOutputDevice } = this.props;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
this._setOutputDevice(audioOutputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
_setTrack(audioTrack)
|
||||
|
|
@ -60,9 +65,23 @@ export default class PeerAudio extends React.PureComponent
|
|||
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 =
|
||||
{
|
||||
audioTrack : PropTypes.any
|
||||
audioTrack : PropTypes.any,
|
||||
audioOutputDevice : PropTypes.string
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import VideoWindow from './VideoWindow/VideoWindow';
|
|||
import LockDialog from './AccessControl/LockDialog/LockDialog';
|
||||
import Settings from './Settings/Settings';
|
||||
import TopBar from './Controls/TopBar';
|
||||
import WakeLock from 'react-wakelock-react16';
|
||||
import ExtraVideo from './Controls/ExtraVideo';
|
||||
|
||||
const TIMEOUT = 5 * 1000;
|
||||
|
||||
|
|
@ -138,6 +140,7 @@ class Room extends React.PureComponent
|
|||
{
|
||||
const {
|
||||
room,
|
||||
browser,
|
||||
advancedMode,
|
||||
toolAreaOpen,
|
||||
toggleToolArea,
|
||||
|
|
@ -202,6 +205,10 @@ class Room extends React.PureComponent
|
|||
</Hidden>
|
||||
</nav>
|
||||
|
||||
{ browser.platform === 'mobile' && browser.os !== 'ios' &&
|
||||
<WakeLock />
|
||||
}
|
||||
|
||||
<View advancedMode={advancedMode} />
|
||||
|
||||
{ room.lockDialogOpen &&
|
||||
|
|
@ -211,6 +218,10 @@ class Room extends React.PureComponent
|
|||
{ room.settingsOpen &&
|
||||
<Settings />
|
||||
}
|
||||
|
||||
{ room.extraVideoOpen &&
|
||||
<ExtraVideo />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -219,6 +230,7 @@ class Room extends React.PureComponent
|
|||
Room.propTypes =
|
||||
{
|
||||
room : appPropTypes.Room.isRequired,
|
||||
browser : PropTypes.object.isRequired,
|
||||
advancedMode : PropTypes.bool.isRequired,
|
||||
toolAreaOpen : PropTypes.bool.isRequired,
|
||||
setToolbarsVisible : PropTypes.func.isRequired,
|
||||
|
|
@ -230,6 +242,7 @@ Room.propTypes =
|
|||
const mapStateToProps = (state) =>
|
||||
({
|
||||
room : state.room,
|
||||
browser : state.me.browser,
|
||||
advancedMode : state.settings.advancedMode,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen
|
||||
});
|
||||
|
|
@ -255,6 +268,7 @@ export default connect(
|
|||
{
|
||||
return (
|
||||
prev.room === next.room &&
|
||||
prev.me.browser === next.me.browser &&
|
||||
prev.settings.advancedMode === next.settings.advancedMode &&
|
||||
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ export const screenProducersSelector = createSelector(
|
|||
(producers) => Object.values(producers).filter((producer) => producer.source === 'screen')
|
||||
);
|
||||
|
||||
export const extraVideoProducersSelector = createSelector(
|
||||
producersSelect,
|
||||
(producers) => Object.values(producers).filter((producer) => producer.source === 'extravideo')
|
||||
);
|
||||
|
||||
export const micProducerSelector = createSelector(
|
||||
producersSelect,
|
||||
(producers) => Object.values(producers).find((producer) => producer.source === 'mic')
|
||||
|
|
@ -67,6 +72,33 @@ export const screenConsumerSelector = createSelector(
|
|||
(consumers) => Object.values(consumers).filter((consumer) => consumer.source === 'screen')
|
||||
);
|
||||
|
||||
export const spotlightScreenConsumerSelector = createSelector(
|
||||
spotlightsSelector,
|
||||
consumersSelect,
|
||||
(spotlights, consumers) =>
|
||||
Object.values(consumers).filter(
|
||||
(consumer) => consumer.source === 'screen' && spotlights.includes(consumer.peerId)
|
||||
)
|
||||
);
|
||||
|
||||
export const spotlightExtraVideoConsumerSelector = createSelector(
|
||||
spotlightsSelector,
|
||||
consumersSelect,
|
||||
(spotlights, consumers) =>
|
||||
Object.values(consumers).filter(
|
||||
(consumer) => consumer.source === 'extravideo' && spotlights.includes(consumer.peerId)
|
||||
)
|
||||
);
|
||||
|
||||
export const passiveMicConsumerSelector = createSelector(
|
||||
spotlightsSelector,
|
||||
consumersSelect,
|
||||
(spotlights, consumers) =>
|
||||
Object.values(consumers).filter(
|
||||
(consumer) => consumer.source === 'mic' && !spotlights.includes(consumer.peerId)
|
||||
)
|
||||
);
|
||||
|
||||
export const spotlightsLengthSelector = createSelector(
|
||||
spotlightsSelector,
|
||||
(spotlights) => spotlights.length
|
||||
|
|
@ -82,7 +114,7 @@ export const spotlightSortedPeersSelector = createSelector(
|
|||
spotlightsSelector,
|
||||
peersValueSelector,
|
||||
(spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || '')))
|
||||
);
|
||||
|
||||
export const peersLengthSelector = createSelector(
|
||||
|
|
@ -94,27 +126,44 @@ export const passivePeersSelector = createSelector(
|
|||
peersValueSelector,
|
||||
spotlightsSelector,
|
||||
(peers, spotlights) => peers.filter((peer) => !spotlights.includes(peer.id))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
.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(
|
||||
spotlightsLengthSelector,
|
||||
screenProducersSelector,
|
||||
screenConsumerSelector,
|
||||
(spotlightsLength, screenProducers, screenConsumers) =>
|
||||
spotlightsLength + 1 + screenProducers.length + screenConsumers.length
|
||||
spotlightScreenConsumerSelector,
|
||||
extraVideoProducersSelector,
|
||||
spotlightExtraVideoConsumerSelector,
|
||||
(
|
||||
spotlightsLength,
|
||||
screenProducers,
|
||||
screenConsumers,
|
||||
extraVideoProducers,
|
||||
extraVideoConsumers
|
||||
) =>
|
||||
spotlightsLength + 1 + screenProducers.length +
|
||||
screenConsumers.length + extraVideoProducers.length +
|
||||
extraVideoConsumers.length
|
||||
);
|
||||
|
||||
export const meProducersSelector = createSelector(
|
||||
micProducerSelector,
|
||||
webcamProducerSelector,
|
||||
screenProducerSelector,
|
||||
(micProducer, webcamProducer, screenProducer) =>
|
||||
extraVideoProducersSelector,
|
||||
(micProducer, webcamProducer, screenProducer, extraVideoProducers) =>
|
||||
{
|
||||
return {
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
screenProducer
|
||||
screenProducer,
|
||||
extraVideoProducers
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -137,8 +186,10 @@ export const makePeerConsumerSelector = () =>
|
|||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
const extraVideoConsumers =
|
||||
consumersArray.filter((consumer) => consumer.source === 'extravideo');
|
||||
|
||||
return { micConsumer, webcamConsumer, screenConsumer };
|
||||
return { micConsumer, webcamConsumer, screenConsumer, extraVideoConsumers };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
@ -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));
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
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 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 Select from '@material-ui/core/Select';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
setting :
|
||||
{
|
||||
padding : theme.spacing(2)
|
||||
},
|
||||
formControl :
|
||||
{
|
||||
display : 'flex'
|
||||
}
|
||||
});
|
||||
|
||||
const MediaSettings = ({
|
||||
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>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
MediaSettings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
settings : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
settings : state.settings
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.me === next.me &&
|
||||
prev.settings === next.settings
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(MediaSettings)));
|
||||
|
|
@ -1,22 +1,25 @@
|
|||
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 roomActions from '../../actions/roomActions';
|
||||
import * as settingsActions from '../../actions/settingsActions';
|
||||
import PropTypes from 'prop-types';
|
||||
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 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 FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
const tabs =
|
||||
[
|
||||
'media',
|
||||
'appearence',
|
||||
'advanced'
|
||||
];
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
|
|
@ -43,99 +46,27 @@ const styles = (theme) =>
|
|||
width : '90vw'
|
||||
}
|
||||
},
|
||||
setting :
|
||||
tabsHeader :
|
||||
{
|
||||
padding : theme.spacing(2)
|
||||
},
|
||||
formControl :
|
||||
{
|
||||
display : 'flex'
|
||||
flexGrow : 1
|
||||
}
|
||||
});
|
||||
|
||||
const Settings = ({
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
settings,
|
||||
onToggleAdvancedMode,
|
||||
onTogglePermanentTopBar,
|
||||
currentSettingsTab,
|
||||
settingsOpen,
|
||||
handleCloseSettings,
|
||||
handleChangeMode,
|
||||
setSettingsTab,
|
||||
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'
|
||||
})
|
||||
} ];
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
className={classes.root}
|
||||
open={room.settingsOpen}
|
||||
onClose={() => handleCloseSettings({ settingsOpen: false })}
|
||||
open={settingsOpen}
|
||||
onClose={() => handleCloseSettings(false)}
|
||||
classes={{
|
||||
paper : classes.dialogPaper
|
||||
}}
|
||||
|
|
@ -146,201 +77,40 @@ const Settings = ({
|
|||
defaultMessage='Settings'
|
||||
/>
|
||||
</DialogTitle>
|
||||
<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>
|
||||
<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.permanentTopBar} onChange={onTogglePermanentTopBar} value='permanentTopBar' />}
|
||||
label={intl.formatMessage({
|
||||
id : 'settings.permanentTopBar',
|
||||
defaultMessage : 'Permanent top bar'
|
||||
})}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Tabs
|
||||
className={classes.tabsHeader}
|
||||
value={tabs.indexOf(currentSettingsTab)}
|
||||
onChange={(event, value) => setSettingsTab(tabs[value])}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='fullWidth'
|
||||
>
|
||||
<Tab
|
||||
label={
|
||||
intl.formatMessage({
|
||||
id : 'label.media',
|
||||
defaultMessage : 'Media'
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
label={intl.formatMessage({
|
||||
id : 'label.appearence',
|
||||
defaultMessage : 'Appearence'
|
||||
})}
|
||||
/>
|
||||
<Tab
|
||||
label={intl.formatMessage({
|
||||
id : 'label.advanced',
|
||||
defaultMessage : 'Advanced'
|
||||
})}
|
||||
/>
|
||||
</Tabs>
|
||||
{currentSettingsTab === 'media' && <MediaSettings />}
|
||||
{currentSettingsTab === 'appearence' && <AppearenceSettings />}
|
||||
{currentSettingsTab === 'advanced' && <AdvancedSettings />}
|
||||
<DialogActions>
|
||||
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
|
||||
<Button onClick={() => handleCloseSettings(false)} color='primary'>
|
||||
<FormattedMessage
|
||||
id='label.close'
|
||||
defaultMessage='Close'
|
||||
|
|
@ -353,34 +123,25 @@ const Settings = ({
|
|||
|
||||
Settings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
settings : PropTypes.object.isRequired,
|
||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||
onTogglePermanentTopBar : PropTypes.func.isRequired,
|
||||
handleChangeMode : PropTypes.func.isRequired,
|
||||
handleCloseSettings : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
currentSettingsTab : PropTypes.string.isRequired,
|
||||
settingsOpen : PropTypes.bool.isRequired,
|
||||
handleCloseSettings : PropTypes.func.isRequired,
|
||||
setSettingsTab : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
room : state.room,
|
||||
settings : state.settings
|
||||
};
|
||||
};
|
||||
({
|
||||
currentSettingsTab : state.room.currentSettingsTab,
|
||||
settingsOpen : state.room.settingsOpen
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
|
||||
onTogglePermanentTopBar : settingsActions.togglePermanentTopBar,
|
||||
handleChangeMode : roomActions.setDisplayMode,
|
||||
handleCloseSettings : roomActions.setSettingsOpen
|
||||
handleCloseSettings : roomActions.setSettingsOpen,
|
||||
setSettingsTab : roomActions.setSettingsTab
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
null,
|
||||
|
|
@ -388,10 +149,9 @@ export default withRoomContext(connect(
|
|||
areStatesEqual : (next, prev) =>
|
||||
{
|
||||
return (
|
||||
prev.me === next.me &&
|
||||
prev.room === next.room &&
|
||||
prev.settings === next.settings
|
||||
prev.room.currentSettingsTab === next.room.currentSettingsTab &&
|
||||
prev.room.settingsOpen === next.room.settingsOpen
|
||||
);
|
||||
}
|
||||
}
|
||||
)(withStyles(styles)(Settings)));
|
||||
)(withStyles(styles)(Settings));
|
||||
|
|
@ -96,11 +96,6 @@ const FullScreenView = (props) =>
|
|||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.controls}>
|
||||
|
|
@ -121,9 +116,25 @@ const FullScreenView = (props) =>
|
|||
<VideoView
|
||||
advancedMode={advancedMode}
|
||||
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}
|
||||
videoProfile={consumerProfile}
|
||||
videoCodec={consumer && consumer.codec}
|
||||
videoScore={consumer ? consumer.score : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -81,11 +81,14 @@ class FullView extends React.PureComponent
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ import PropTypes from 'prop-types';
|
|||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
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) =>
|
||||
({
|
||||
|
|
@ -88,69 +98,6 @@ const styles = (theme) =>
|
|||
transitionDuration : '0s'
|
||||
}
|
||||
},
|
||||
qualityBar :
|
||||
{
|
||||
height : 6,
|
||||
borderRadius : 2,
|
||||
background : 'rgba(green, 0.65)',
|
||||
transitionProperty : 'height background-color',
|
||||
transitionDuration : '0.25s',
|
||||
'&.score0' :
|
||||
{
|
||||
width : 0,
|
||||
backgroundColor : 'rgba(246, 58, 15, 0.65)'
|
||||
},
|
||||
'&.score1' :
|
||||
{
|
||||
width : '10%',
|
||||
backgroundColor : 'rgba(246, 58, 15, 0.65)'
|
||||
},
|
||||
'&.score2' :
|
||||
{
|
||||
width : '20%',
|
||||
backgroundColor : 'rgba(246, 58, 15, 0.65)'
|
||||
},
|
||||
'&.score3' :
|
||||
{
|
||||
width : '30%',
|
||||
backgroundColor : 'rgba(246, 58, 15, 0.65)'
|
||||
},
|
||||
'&.score4' :
|
||||
{
|
||||
width : '40%',
|
||||
backgroundColor : 'rgba(246, 58, 15, 0.65)'
|
||||
},
|
||||
'&.score5' :
|
||||
{
|
||||
width : '50%',
|
||||
backgroundColor : 'rgba(242, 176, 30, 0.65)'
|
||||
},
|
||||
'&.score6' :
|
||||
{
|
||||
width : '60%',
|
||||
backgroundColor : 'rgba(242, 176, 30, 0.65)'
|
||||
},
|
||||
'&.score7' :
|
||||
{
|
||||
width : '70%',
|
||||
backgroundColor : 'rgba(242, 211, 27, 0.65)'
|
||||
},
|
||||
'&.score8' :
|
||||
{
|
||||
width : '80%',
|
||||
backgroundColor : 'rgba(242, 211, 27, 0.65)'
|
||||
},
|
||||
'&.score9' :
|
||||
{
|
||||
width : '90%',
|
||||
backgroundColor : 'rgba(134, 224, 30, 0.65)'
|
||||
},
|
||||
'&.score10' :
|
||||
{
|
||||
width : '100%',
|
||||
backgroundColor : 'rgba(134, 224, 30, 0.65)'
|
||||
}
|
||||
},
|
||||
peer :
|
||||
{
|
||||
display : 'flex'
|
||||
|
|
@ -190,6 +137,10 @@ class VideoView extends React.PureComponent
|
|||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received audio track
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
|
@ -229,6 +180,62 @@ class VideoView extends React.PureComponent
|
|||
videoHeight
|
||||
} = 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:
|
||||
{
|
||||
quality = <SignalCellular3BarIcon style={{ color: yellow[500] }}/>;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 9:
|
||||
case 10:
|
||||
{
|
||||
quality = <SignalCellularAltIcon style={{ color: green[500] }}/>;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.info}>
|
||||
|
|
@ -254,15 +261,11 @@ class VideoView extends React.PureComponent
|
|||
<p>{videoWidth}x{videoHeight}</p>
|
||||
}
|
||||
</div>
|
||||
{ (audioScore || videoScore) &&
|
||||
{ !isMe &&
|
||||
<div className={classnames(classes.box, 'right')}>
|
||||
<div className={
|
||||
classnames(
|
||||
classes.qualityBar,
|
||||
`score${videoScore ? videoScore.producerScore : audioScore.producerScore}`
|
||||
)
|
||||
{
|
||||
quality
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -296,7 +299,7 @@ class VideoView extends React.PureComponent
|
|||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
ref='videoElement'
|
||||
className={classnames(classes.video, {
|
||||
hidden : !videoVisible,
|
||||
'isMe' : isMe && !isScreen,
|
||||
|
|
@ -304,6 +307,16 @@ class VideoView extends React.PureComponent
|
|||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
controls={false}
|
||||
/>
|
||||
|
||||
<audio
|
||||
ref='audioElement'
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isMe}
|
||||
controls={false}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
|
@ -313,52 +326,87 @@ class VideoView extends React.PureComponent
|
|||
|
||||
componentDidMount()
|
||||
{
|
||||
const { videoTrack } = this.props;
|
||||
const { videoTrack, audioTrack } = this.props;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
this._setTracks(videoTrack, audioTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
|
||||
const { videoElement } = this.refs;
|
||||
|
||||
if (videoElement)
|
||||
{
|
||||
videoElement.oncanplay = null;
|
||||
videoElement.onplay = null;
|
||||
videoElement.onpause = null;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps)
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
const { videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
if (prevProps !== this.props)
|
||||
{
|
||||
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;
|
||||
|
||||
this._videoTrack = videoTrack;
|
||||
this._audioTrack = audioTrack;
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
const { videoElement, audioElement } = this.refs;
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
const stream = new MediaStream();
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
videoElement.srcObject = stream;
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
videoElement.oncanplay = () => this.setState({ videoCanPlay: true });
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,16 +415,19 @@ class VideoView extends React.PureComponent
|
|||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
const { videoElement } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
if (
|
||||
videoElement.videoWidth === videoWidth &&
|
||||
videoElement.videoHeight === videoHeight
|
||||
)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
videoWidth : videoElement.videoWidth,
|
||||
videoHeight : videoElement.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
|
@ -396,6 +447,7 @@ VideoView.propTypes =
|
|||
videoContain : PropTypes.bool,
|
||||
advancedMode : PropTypes.bool,
|
||||
videoTrack : PropTypes.any,
|
||||
audioTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
consumerSpatialLayers : PropTypes.number,
|
||||
consumerTemporalLayers : PropTypes.number,
|
||||
|
|
|
|||
|
|
@ -23,18 +23,29 @@ const VideoWindow = (props) =>
|
|||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<NewWindow onUnload={toggleConsumerWindow}>
|
||||
<FullView
|
||||
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}
|
||||
videoProfile={consumerProfile}
|
||||
videoCodec={consumer && consumer.codec}
|
||||
videoScore={consumer ? consumer.score : null}
|
||||
/>
|
||||
</NewWindow>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ export const Me = PropTypes.shape(
|
|||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen', 'extravideo' ]),
|
||||
paused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
|
|
@ -37,7 +37,7 @@ export const Consumer = PropTypes.shape(
|
|||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
peerId : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ export default function()
|
|||
|
||||
return {
|
||||
flag,
|
||||
name : browser.getBrowserName(),
|
||||
version : browser.getBrowserVersion(),
|
||||
bowser : browser
|
||||
os : browser.getOSName(true), // ios, android, linux...
|
||||
platform : browser.getPlatformType(true), // mobile, desktop, tablet
|
||||
name : browser.getBrowserName(true),
|
||||
version : browser.getBrowserVersion(),
|
||||
bowser : browser
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -34,7 +34,11 @@ import messagesPortuguese from './translations/pt';
|
|||
import messagesChinese from './translations/cn';
|
||||
import messagesSpanish from './translations/es';
|
||||
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';
|
||||
|
||||
|
|
@ -57,7 +61,11 @@ const messages =
|
|||
'zh' : messagesChinese,
|
||||
'es' : messagesSpanish,
|
||||
'hr' : messagesCroatian,
|
||||
'cz' : messagesCzech
|
||||
'cs' : messagesCzech,
|
||||
'it' : messagesItalian,
|
||||
'uk' : messagesUkrainian,
|
||||
'tr' : messagesTurkish,
|
||||
'lv' : messagesLatvian
|
||||
};
|
||||
|
||||
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ const chat = (state = [], action) =>
|
|||
return [ ...state, ...chatHistory ];
|
||||
}
|
||||
|
||||
case 'CLEAR_CHAT':
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ const files = (state = {}, action) =>
|
|||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'CLEAR_FILES':
|
||||
return {};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const lobbyPeers = (state = {}, action) =>
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const initialState =
|
|||
{
|
||||
id : null,
|
||||
picture : null,
|
||||
isMobile : false,
|
||||
browser : null,
|
||||
roles : [ 'normal' ], // Default role
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
|
|
@ -15,8 +15,8 @@ const initialState =
|
|||
screenShareInProgress : false,
|
||||
displayNameInProgress : false,
|
||||
loginEnabled : false,
|
||||
raiseHand : false,
|
||||
raiseHandInProgress : false,
|
||||
raisedHand : false,
|
||||
raisedHandInProgress : false,
|
||||
loggedIn : false,
|
||||
isSpeaking : false
|
||||
};
|
||||
|
|
@ -39,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':
|
||||
|
|
@ -97,6 +99,13 @@ const me = (state = initialState, action) =>
|
|||
return { ...state, audioDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_OUTPUT_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
||||
return { ...state, audioOutputDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
|
@ -125,18 +134,18 @@ const me = (state = initialState, action) =>
|
|||
return { ...state, screenShareInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_MY_RAISE_HAND_STATE':
|
||||
case 'SET_RAISED_HAND':
|
||||
{
|
||||
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;
|
||||
|
||||
return { ...state, raiseHandInProgress: flag };
|
||||
return { ...state, raisedHandInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_NAME_IN_PROGRESS':
|
||||
|
|
|
|||
|
|
@ -20,8 +20,12 @@ const peer = (state = {}, action) =>
|
|||
case 'SET_PEER_KICK_IN_PROGRESS':
|
||||
return { ...state, peerKickInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
return { ...state, raiseHandState: action.payload.raiseHandState };
|
||||
case 'SET_PEER_RAISED_HAND':
|
||||
return {
|
||||
...state,
|
||||
raisedHand : action.payload.raisedHand,
|
||||
raisedHandTimestamp : action.payload.raisedHandTimestamp
|
||||
};
|
||||
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
|
|
@ -86,7 +90,7 @@ const peers = (state = {}, action) =>
|
|||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
case 'SET_PEER_RAISED_HAND':
|
||||
case 'SET_PEER_PICTURE':
|
||||
case 'ADD_CONSUMER':
|
||||
case 'ADD_PEER_ROLE':
|
||||
|
|
|
|||
|
|
@ -1,34 +1,46 @@
|
|||
const initialState =
|
||||
{
|
||||
name : '',
|
||||
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||
locked : false,
|
||||
inLobby : false,
|
||||
signInRequired : false,
|
||||
accessCode : '', // access code to the room if locked and joinByAccessCode == true
|
||||
joinByAccessCode : true, // if true: accessCode is a possibility to open the room
|
||||
activeSpeakerId : null,
|
||||
torrentSupport : false,
|
||||
showSettings : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : 'democratic',
|
||||
selectedPeerId : null,
|
||||
spotlights : [],
|
||||
settingsOpen : false,
|
||||
lockDialogOpen : false,
|
||||
joined : false,
|
||||
muteAllInProgress : false,
|
||||
stopAllVideoInProgress : false,
|
||||
closeMeetingInProgress : false,
|
||||
userRoles : { NORMAL: 'normal' }, // Default role
|
||||
permissionsFromRoles : {
|
||||
name : '',
|
||||
// new/connecting/connected/disconnected/closed,
|
||||
state : 'new',
|
||||
locked : false,
|
||||
inLobby : false,
|
||||
signInRequired : false,
|
||||
overRoomLimit : false,
|
||||
// access code to the room if locked and joinByAccessCode == true
|
||||
accessCode : '',
|
||||
// if true: accessCode is a possibility to open the room
|
||||
joinByAccessCode : true,
|
||||
activeSpeakerId : null,
|
||||
torrentSupport : false,
|
||||
showSettings : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : window.config.defaultLayout || 'democratic',
|
||||
selectedPeerId : null,
|
||||
spotlights : [],
|
||||
settingsOpen : 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 : []
|
||||
}
|
||||
};
|
||||
|
|
@ -77,7 +89,12 @@ const room = (state = initialState, action) =>
|
|||
|
||||
return { ...state, signInRequired };
|
||||
}
|
||||
case 'SET_OVER_ROOM_LIMIT':
|
||||
{
|
||||
const { overRoomLimit } = action.payload;
|
||||
|
||||
return { ...state, overRoomLimit };
|
||||
}
|
||||
case 'SET_ACCESS_CODE':
|
||||
{
|
||||
const { accessCode } = action.payload;
|
||||
|
|
@ -106,6 +123,20 @@ const room = (state = initialState, action) =>
|
|||
return { ...state, settingsOpen };
|
||||
}
|
||||
|
||||
case 'SET_EXTRA_VIDEO_OPEN':
|
||||
{
|
||||
const { extraVideoOpen } = action.payload;
|
||||
|
||||
return { ...state, extraVideoOpen };
|
||||
}
|
||||
|
||||
case 'SET_SETTINGS_TAB':
|
||||
{
|
||||
const { tab } = action.payload;
|
||||
|
||||
return { ...state, currentSettingsTab: tab };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_ACTIVE_SPEAKER':
|
||||
{
|
||||
const { peerId } = action.payload;
|
||||
|
|
@ -175,6 +206,9 @@ const room = (state = initialState, action) =>
|
|||
return { ...state, spotlights };
|
||||
}
|
||||
|
||||
case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS':
|
||||
return { ...state, lobbyPeersPromotionInProgress: action.payload.flag };
|
||||
|
||||
case 'MUTE_ALL_IN_PROGRESS':
|
||||
return { ...state, muteAllInProgress: action.payload.flag };
|
||||
|
||||
|
|
@ -184,6 +218,12 @@ const room = (state = initialState, action) =>
|
|||
case 'CLOSE_MEETING_IN_PROGRESS':
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ const initialState =
|
|||
selectedWebcam : null,
|
||||
selectedAudioDevice : null,
|
||||
advancedMode : false,
|
||||
resolution : 'medium', // low, medium, high, veryhigh, ultra
|
||||
// low, medium, high, veryhigh, ultra
|
||||
resolution : window.config.defaultResolution || 'medium',
|
||||
lastN : 4,
|
||||
permanentTopBar : true
|
||||
permanentTopBar : true,
|
||||
hiddenControls : false,
|
||||
notificationSounds : true
|
||||
};
|
||||
|
||||
const settings = (state = initialState, action) =>
|
||||
|
|
@ -23,6 +26,11 @@ const settings = (state = initialState, action) =>
|
|||
return { ...state, selectedAudioDevice: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'CHANGE_AUDIO_OUTPUT_DEVICE':
|
||||
{
|
||||
return { ...state, selectedAudioOutputDevice: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_NAME':
|
||||
{
|
||||
const { displayName } = action.payload;
|
||||
|
|
@ -51,6 +59,20 @@ const settings = (state = initialState, action) =>
|
|||
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':
|
||||
{
|
||||
const { resolution } = action.payload;
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "从大厅允许",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "显示设置",
|
||||
"tooltip.participants": "显示参加者",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "房间名称",
|
||||
"label.chooseRoomButton": "继续",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "文件共享",
|
||||
"label.participants": "参与者",
|
||||
"label.shareFile": "共享文件",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "不支持文件共享",
|
||||
"label.unknown": "未知",
|
||||
"label.democratic": "民主视图",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "非常高 (FHD)",
|
||||
"label.ultra": "超高 (UHD)",
|
||||
"label.close": "关闭",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "设置",
|
||||
"settings.camera": "视频设备",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"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": "正在尝试共享文件",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "麦克风已断开",
|
||||
"devices.microphoneError": "麦克风发生错误",
|
||||
"devices.microPhoneMute": "麦克风静音",
|
||||
"devices.micophoneUnMute": "取消麦克风静音",
|
||||
"devices.microphoneMute": "麦克风静音",
|
||||
"devices.microphoneUnMute": "取消麦克风静音",
|
||||
"devices.microphoneEnable": "启用了麦克风",
|
||||
"devices.microphoneMuteError": "无法使麦克风静音",
|
||||
"devices.microphoneUnMuteError": "无法取消麦克风静音",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "访问屏幕时发生错误",
|
||||
|
||||
"devices.cameraDisconnected": "相机已断开连接",
|
||||
"devices.cameraError": "访问相机时发生错误"
|
||||
"devices.cameraError": "访问相机时发生错误",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
@ -51,10 +51,20 @@
|
|||
"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": "Přihlášení",
|
||||
"tooltip.logout": "Odhlášení",
|
||||
"tooltip.admitFromLobby": "Povolit uživatele z Přijímací místnosti",
|
||||
|
|
@ -64,6 +74,11 @@
|
|||
"tooltip.leaveFullscreen": "Vypnout režim celé obrazovky (fullscreen)",
|
||||
"tooltip.lobby": "Ukázat Přijímací místnost",
|
||||
"tooltip.settings": "Zobrazit nastavení",
|
||||
"tooltip.participants": null,
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Jméno místnosti",
|
||||
"label.chooseRoomButton": "Pokračovat",
|
||||
|
|
@ -77,6 +92,7 @@
|
|||
"label.filesharing": "Sdílení souborů",
|
||||
"label.participants": "Účastníci",
|
||||
"label.shareFile": "Sdílet soubor",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Sdílení souborů není podporováno",
|
||||
"label.unknown": "Neznámý",
|
||||
"label.democratic": "Rozvržení: Demokratické",
|
||||
|
|
@ -87,6 +103,11 @@
|
|||
"label.veryHigh": "Velmi vysoké (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Zavřít",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Nastavení",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -95,10 +116,17 @@
|
|||
"settings.audio": "Audio zařízení",
|
||||
"settings.selectAudio": "Vyberte 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.layout": "Rozvržení místnosti",
|
||||
"settings.selectRoomLayout": "Vyberte rozvržení místnosti",
|
||||
"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.startingFileShare": "Pokouším se sdílet soubor",
|
||||
|
|
@ -128,8 +156,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Mikrofon odpojen",
|
||||
"devices.microphoneError": "Při přístupu k vašemu mikrofonu se vyskytla chyba",
|
||||
"devices.microPhoneMute": "Mikrofon ztišen",
|
||||
"devices.micophoneUnMute": "Ztišení mikrofonu zrušeno",
|
||||
"devices.microphoneMute": "Mikrofon ztišen",
|
||||
"devices.microphoneUnMute": "Ztišení mikrofonu zrušeno",
|
||||
"devices.microphoneEnable": "Mikrofon povolen",
|
||||
"devices.microphoneMuteError": "Není možné ztišit váš mikrofon",
|
||||
"devices.microphoneUnMuteError": "Není možné zrušit ztišení vašeho mikrofonu",
|
||||
|
|
@ -138,5 +166,10 @@
|
|||
"devices.screenSharingError": "Při přístupu k vaší obrazovce se vyskytla chyba",
|
||||
|
||||
"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
|
||||
}
|
||||
|
|
@ -52,9 +52,19 @@
|
|||
"room.muteAll": "Alle stummschalten",
|
||||
"room.stopAllVideo": "Alle Videos stoppen",
|
||||
"room.closeMeeting": "Meeting schließen",
|
||||
"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": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
|
||||
"me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen",
|
||||
|
||||
"roles.gotRole": null,
|
||||
"roles.lostRole": null,
|
||||
|
||||
"tooltip.login": "Anmelden",
|
||||
"tooltip.logout": "Abmelden",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Einstellungen",
|
||||
"tooltip.participants": "Teilnehmer",
|
||||
"tooltip.kickParticipant": "Teilnehmer rauswerfen",
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Name des Raums",
|
||||
"label.chooseRoomButton": "Weiter",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Dateien",
|
||||
"label.participants": "Teilnehmer",
|
||||
"label.shareFile": "Datei hochladen",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt",
|
||||
"label.unknown": "Unbekannt",
|
||||
"label.democratic": "Demokratisch",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Sehr hoch (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Schließen",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Einstellungen",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Audiogerät",
|
||||
"settings.selectAudio": "Wähle ein Audiogerät",
|
||||
"settings.cantSelectAudio": "Kann Audiogerät nicht aktivieren",
|
||||
"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.selectRoomLayout": "Wähle ein Raumlayout",
|
||||
"settings.advancedMode": "Erweiterter Modus",
|
||||
"settings.permanentTopBar": "Permanente obere Leiste",
|
||||
"settings.lastn": "Anzahl der sichtbaren Videos",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Fehler beim Speichern der Datei",
|
||||
"filesharing.startingFileShare": "Starte Teilen der Datei",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Mikrofon nicht verbunden",
|
||||
"devices.microphoneError": "Fehler beim Zugriff auf dein Mikrofon",
|
||||
"devices.microPhoneMute": "Mikrofon stummgeschaltet",
|
||||
"devices.micophoneUnMute": "Mikrofon aktiviert",
|
||||
"devices.microphoneMute": "Mikrofon stummgeschaltet",
|
||||
"devices.microphoneUnMute": "Mikrofon aktiviert",
|
||||
"devices.microphoneEnable": "Mikrofon aktiviert",
|
||||
"devices.microphoneMuteError": "Kann Mikrofon nicht stummschalten",
|
||||
"devices.microphoneUnMuteError": "Kann Mikrofon nicht aktivieren",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Fehler bei der Bildschirmfreigabe",
|
||||
|
||||
"devices.cameraDisconnected": "Kamera getrennt",
|
||||
"devices.cameraError": "Fehler mit deiner Kamera"
|
||||
"devices.cameraError": "Fehler mit deiner Kamera",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Log ind",
|
||||
"tooltip.logout": "Log ud",
|
||||
"tooltip.admitFromLobby": "Giv adgang fra lobbyen",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Vis indstillinger",
|
||||
"tooltip.participants": "Vis deltagere",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Værelsesnavn",
|
||||
"label.chooseRoomButton": "Fortsæt",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Fildeling",
|
||||
"label.participants": "Deltagere",
|
||||
"label.shareFile": "Del fil",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Fildeling er ikke understøttet",
|
||||
"label.unknown": "Ukendt",
|
||||
"label.democracy": "Galleri visning",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Meget høj (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Luk",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Indstillinger",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Lydenhed",
|
||||
"settings.selectAudio": "Vælg 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.layout": "Møde visning",
|
||||
"settings.selectRoomLayout": "Vælg møde visning",
|
||||
"settings.advancedMode": "Avanceret tilstand",
|
||||
"settings.permanentTopBar": "Permanent øverste linje",
|
||||
"settings.lastn": "Antal synlige videoer",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Kan ikke gemme fil",
|
||||
"filesharing.startingFileShare": "Forsøger at dele filen",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"device.microphoneDisconnected": "Mikrofon frakoblet",
|
||||
"device.microphoneError": "Der opstod en fejl under adgang til din mikrofon",
|
||||
"device.microPhoneMute": "Dæmp din mikrofon",
|
||||
"device.micophoneUnMute": "Slå ikke lyden fra din mikrofon",
|
||||
"device.microphoneMute": "Dæmp din mikrofon",
|
||||
"device.microphoneUnMute": "Slå ikke lyden fra din mikrofon",
|
||||
"device.microphoneEnable": "Aktiveret din mikrofon",
|
||||
"device.microphoneMuteError": "Kan ikke slå din mikrofon fra",
|
||||
"device.microphoneUnMuteError": "Kan ikke slå lyden til på din mikrofon",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Der opstod en fejl ved adgang til skærmdeling",
|
||||
|
||||
"device.cameraDisconnected": "Kamera frakoblet",
|
||||
"device.cameraError": "Der opstod en fejl ved tilkobling af dit kamera"
|
||||
"device.cameraError": "Der opstod en fejl ved tilkobling af dit kamera",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Admit from lobby",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Εμφάνιση ρυθμίσεων",
|
||||
"tooltip.participants": "Εμφάνιση συμμετεχόντων",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Όνομα δωματίου",
|
||||
"label.chooseRoomButton": "Συνέχεια",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Διαμοιρασμοός αρχείου",
|
||||
"label.participants": "Συμμετέχοντες",
|
||||
"label.shareFile": "Διαμοιραστείτε ένα αρχείο",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Ο διαμοιρασμός αρχείων δεν υποστηρίζεται",
|
||||
"label.unknown": "Άγνωστο",
|
||||
"label.democratic": null,
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Πολύ υψηλή (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Κλείσιμο",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Ρυθμίσεις",
|
||||
"settings.camera": "Κάμερα",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Συσκευή ήχου",
|
||||
"settings.selectAudio": "Επιλογή συσκευής ήχου",
|
||||
"settings.cantSelectAudio": "Αδυναμία επιλογής συσκευής ήχου",
|
||||
"settings.audioOutput": "Συσκευή εξόδου ήχου",
|
||||
"settings.selectAudioOutput": "Επιλέξτε συσκευή εξόδου ήχου",
|
||||
"settings.cantSelectAudioOutput": "Δεν είναι δυνατή η επιλογή συσκευής εξόδου ήχου",
|
||||
"settings.resolution": "Επιλέξτε την ανάλυση του video",
|
||||
"settings.layout": "Περιβάλλον δωματίου",
|
||||
"settings.selectRoomLayout": "Επιλογή περιβάλλοντος δωματίου",
|
||||
"settings.advancedMode": "Προηγμένη λειτουργία",
|
||||
"settings.permanentTopBar": "Μόνιμη μπάρα κορυφής",
|
||||
"settings.lastn": "Αριθμός ορατών βίντεο",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου",
|
||||
"filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Το μικρόφωνο αποσυνδέθηκε",
|
||||
"devices.microphoneError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στο μικρόφωνό σας",
|
||||
"devices.microPhoneMute": "Το μικρόφωνό σας είναι σε σίγαση",
|
||||
"devices.micophoneUnMute": "Ανοίξτε το μικρόφωνό σας",
|
||||
"devices.microphoneMute": "Το μικρόφωνό σας είναι σε σίγαση",
|
||||
"devices.microphoneUnMute": "Ανοίξτε το μικρόφωνό σας",
|
||||
"devices.microphoneEnable": "Ενεργοποίησε το μικρόφωνό σας",
|
||||
"devices.microphoneMuteError": "Δεν είναι δυνατή η σίγαση του μικροφώνου σας",
|
||||
"devices.microphoneUnMuteError": "Δεν είναι δυνατό το άνοιγμα του μικροφώνου σας",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην οθόνη σας",
|
||||
|
||||
"devices.cameraDisconnected": "Η κάμερα αποσυνδέθηκε",
|
||||
"devices.cameraError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην κάμερά σας"
|
||||
"devices.cameraError": "Παρουσιάστηκε σφάλμα κατά την πρόσβαση στην κάμερά σας",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
@ -52,10 +52,20 @@
|
|||
"room.muteAll": "Mute all",
|
||||
"room.stopAllVideo": "Stop all video",
|
||||
"room.closeMeeting": "Close meeting",
|
||||
"room.clearChat": "Clear chat",
|
||||
"room.clearFileSharing": "Clear files",
|
||||
"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",
|
||||
|
||||
"roles.gotRole": "You got the role: {role}",
|
||||
"roles.lostRole": "You lost the role: {role}",
|
||||
|
||||
"tooltip.login": "Log in",
|
||||
"tooltip.logout": "Log out",
|
||||
"tooltip.admitFromLobby": "Admit from lobby",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Show settings",
|
||||
"tooltip.participants": "Show participants",
|
||||
"tooltip.kickParticipant": "Kick out participant",
|
||||
"tooltip.muteParticipant": "Mute participant",
|
||||
"tooltip.muteParticipantVideo": "Mute participant video",
|
||||
"tooltip.raisedHand": "Raise hand",
|
||||
|
||||
"label.roomName": "Room name",
|
||||
"label.chooseRoomButton": "Continue",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "File sharing",
|
||||
"label.participants": "Participants",
|
||||
"label.shareFile": "Share file",
|
||||
"label.shareGalleryFile": "Share image",
|
||||
"label.fileSharingUnsupported": "File sharing not supported",
|
||||
"label.unknown": "Unknown",
|
||||
"label.democratic": "Democratic view",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Very high (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Close",
|
||||
"label.media": "Media",
|
||||
"label.appearence": "Appearence",
|
||||
"label.advanced": "Advanced",
|
||||
"label.addVideo": "Add video",
|
||||
"label.promoteAllPeers": "Promote all",
|
||||
|
||||
"settings.settings": "Settings",
|
||||
"settings.camera": "Camera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Audio device",
|
||||
"settings.selectAudio": "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.layout": "Room layout",
|
||||
"settings.selectRoomLayout": "Select room layout",
|
||||
"settings.advancedMode": "Advanced mode",
|
||||
"settings.permanentTopBar": "Permanent top bar",
|
||||
"settings.lastn": "Number of visible videos",
|
||||
"settings.hiddenControls": "Hidden media controls",
|
||||
"settings.notificationSounds": "Notification sounds",
|
||||
|
||||
"filesharing.saveFileError": "Unable to save file",
|
||||
"filesharing.startingFileShare": "Attempting to share file",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microphone disconnected",
|
||||
"devices.microphoneError": "An error occured while accessing your microphone",
|
||||
"devices.microPhoneMute": "Muted your microphone",
|
||||
"devices.micophoneUnMute": "Unmuted your microphone",
|
||||
"devices.microphoneMute": "Muted your microphone",
|
||||
"devices.microphoneUnMute": "Unmuted your microphone",
|
||||
"devices.microphoneEnable": "Enabled your microphone",
|
||||
"devices.microphoneMuteError": "Unable to mute your microphone",
|
||||
"devices.microphoneUnMuteError": "Unable to unmute your microphone",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "An error occured while accessing your screen",
|
||||
|
||||
"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"
|
||||
}
|
||||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Entrar",
|
||||
"tooltip.logout": "Salir",
|
||||
"tooltip.admitFromLobby": "Admitir desde la sala de espera",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Mostrar ajustes",
|
||||
"tooltip.participants": "Mostrar participantes",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Nombre de la sala",
|
||||
"label.chooseRoomButton": "Continuar",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Compartir ficheros",
|
||||
"label.participants": "Participantes",
|
||||
"label.shareFile": "Compartir fichero",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Compartir ficheros no está disponible",
|
||||
"label.unknown": "Desconocido",
|
||||
"label.democratic": "Vista democrática",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Muy alta (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Cerrar",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Ajustes",
|
||||
"settings.camera": "Cámara",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Dispositivo de sonido",
|
||||
"settings.selectAudio": "Seleccione 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.layout": "Disposición de la sala",
|
||||
"settings.selectRoomLayout": "Seleccione la disposición de la sala",
|
||||
"settings.advancedMode": "Modo avanzado",
|
||||
"settings.permanentTopBar": "Barra superior permanente",
|
||||
"settings.lastn": "Cantidad de videos visibles",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "No ha sido posible guardar el fichero",
|
||||
"filesharing.startingFileShare": "Intentando compartir el fichero",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Micrófono desconectado",
|
||||
"devices.microphoneError": "Hubo un error al acceder a su micrófono",
|
||||
"devices.microPhoneMute": "Desactivar micrófono",
|
||||
"devices.micophoneUnMute": "Activar micrófono",
|
||||
"devices.microphoneMute": "Desactivar micrófono",
|
||||
"devices.microphoneUnMute": "Activar micrófono",
|
||||
"devices.microphoneEnable": "Micrófono activado",
|
||||
"devices.microphoneMuteError": "No ha sido posible desactivar su micrófono",
|
||||
"devices.microphoneUnMuteError": "No ha sido posible activar su micrófono",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Hubo un error al acceder a su pantalla",
|
||||
|
||||
"devices.cameraDisconnected": "Cámara desconectada",
|
||||
"devices.cameraError": "Hubo un error al acceder a su cámara"
|
||||
"devices.cameraError": "Hubo un error al acceder a su cámara",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Connexion",
|
||||
"tooltip.logout": "Déconnexion",
|
||||
"tooltip.admitFromLobby": "Autorisé depuis la salle d'attente",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Afficher les paramètres",
|
||||
"tooltip.participants": "Afficher les participants",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Nom de la salle",
|
||||
"label.chooseRoomButton": "Continuer",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Partage de fichier",
|
||||
"label.participants": "Participants",
|
||||
"label.shareFile": "Partager un fichier",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Partage de fichier non supporté",
|
||||
"label.unknown": "Inconnu",
|
||||
"label.democratic": "Vue démocratique",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Très Haute Définition (FHD)",
|
||||
"label.ultra": "Ultra Haute Définition",
|
||||
"label.close": "Fermer",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Paramètres",
|
||||
"settings.camera": "Caméra",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Microphone",
|
||||
"settings.selectAudio": "Sélectionnez votre microphone",
|
||||
"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.layout": "Mode d'affichage de la salle",
|
||||
"settings.selectRoomLayout": "Sélectionnez la présentation de la salle",
|
||||
"settings.advancedMode": "Mode avancé",
|
||||
"settings.permanentTopBar": "Barre supérieure permanente",
|
||||
"settings.lastn": "Nombre de vidéos visibles",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Impossible d'enregistrer le fichier",
|
||||
"filesharing.startingFileShare": "Début du transfert de fichier",
|
||||
|
|
@ -132,8 +156,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microphone déconnecté",
|
||||
"devices.microphoneError": "Une erreur est apparue lors de l'accès à votre microphone",
|
||||
"devices.microPhoneMute": "Désactiver le microphone",
|
||||
"devices.micophoneUnMute": "Réactiver le microphone",
|
||||
"devices.microphoneMute": "Désactiver le microphone",
|
||||
"devices.microphoneUnMute": "Réactiver le microphone",
|
||||
"devices.microphoneEnable": "Activer le microphone",
|
||||
"devices.microphoneMuteError": "Impossible de désactiver le microphone",
|
||||
"devices.microphoneUnMuteError": "Impossible de réactiver le microphone",
|
||||
|
|
@ -142,5 +166,10 @@
|
|||
"devices.screenSharingError": "Une erreur est apparue lors de l'accès à votre partage d'écran",
|
||||
|
||||
"devices.cameraDisconnected": "Caméra déconnectée",
|
||||
"devices.cameraError": "Une erreur est apparue lors de l'accès à votre caméra"
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"room.chooseRoom": "Izaberite ime sobe u koju se želite prijaviti",
|
||||
"room.cookieConsent": "Ova stranica koristi kolačiće radi poboljšanja korisničkog iskustva",
|
||||
"room.consentUnderstand": "I understand",
|
||||
"room.consentUnderstand": "Razumijem",
|
||||
"room.joined": "Prijavljeni ste u sobu",
|
||||
"room.cantJoin": "Prijava u sobu nije moguća",
|
||||
"room.youLocked": "Zaključali ste sobu",
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
"room.cantUnLock": "Otključavanje sobe nije moguće",
|
||||
"room.locked": "Soba je sada zaključana",
|
||||
"room.unlocked": "Soba je sada otključana",
|
||||
"room.newLobbyPeer": "U predvorju je novi učesnik",
|
||||
"room.lobbyPeerLeft": "Učesnik je napustio predvorje",
|
||||
"room.lobbyPeerChangedDisplayName": "Učesnik u predvorju je promijenio ime u {displayName}",
|
||||
"room.lobbyPeerChangedPicture": "Učesnik u predvorju je promijenio sliku",
|
||||
"room.newLobbyPeer": "Novi sudionik čeka u predvorju",
|
||||
"room.lobbyPeerLeft": "Sudionik je napustio predvorje",
|
||||
"room.lobbyPeerChangedDisplayName": "Sudionik u predvorju je promijenio ime u {displayName}",
|
||||
"room.lobbyPeerChangedPicture": "Sudionik u predvorju je promijenio sliku",
|
||||
"room.setAccessCode": "Obnovljena pristupna šifra za sobu",
|
||||
"room.accessCodeOn": "Pristupna šifra sobe je aktivna",
|
||||
"room.accessCodeOff":"Pristupna šifra sobe je neaktivna",
|
||||
|
|
@ -40,21 +40,31 @@
|
|||
"room.audioVideo": "Zvuk i slika",
|
||||
"room.youAreReady": "Spremni ste",
|
||||
"room.emptyRequireLogin": "Soba je trenutno prazna! Prijavite se za pokretanje sastanka, ili sačekajte organizatora" ,
|
||||
"room.locketWait": "Soba je zaključana - pričekajte odobrenje ...",
|
||||
"room.locketWait": "Soba je zaključana - pričekajte odobrenje...",
|
||||
"room.lobbyAdministration":"Upravljanje predvorjem",
|
||||
"room.peersInLobby":"Učesnici u predvorju",
|
||||
"room.peersInLobby":"Sudionici u predvorju",
|
||||
"room.lobbyEmpty": "Trenutno nema nikoga u predvorju",
|
||||
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
|
||||
"room.me": "Ja",
|
||||
"room.spotlights": "Učesnici u fokusu",
|
||||
"room.passive": "Pasivni učesnici",
|
||||
"room.spotlights": "Sudionici u fokusu",
|
||||
"room.passive": "Pasivni sudionici",
|
||||
"room.videoPaused": "Video pauziran",
|
||||
"room.muteAll": null,
|
||||
"room.stopAllVideo": null,
|
||||
"room.closeMeeting": null,
|
||||
"room.speechUnsupported": null,
|
||||
"room.muteAll": "Utišaj sve",
|
||||
"room.stopAllVideo": "Ugasi sve kamere",
|
||||
"room.closeMeeting": "Završi sastanak",
|
||||
"room.clearChat": null,
|
||||
"room.clearFileSharing": null,
|
||||
"room.speechUnsupported": "Vaš preglednik ne podržava prepoznavanje govora",
|
||||
"room.moderatoractions": null,
|
||||
"room.raisedHand": null,
|
||||
"room.loweredHand": null,
|
||||
"room.extraVideo": null,
|
||||
"room.overRoomLimit": null,
|
||||
|
||||
"me.mutedPTT": null,
|
||||
"me.mutedPTT": "Utišani ste, pritisnite i držite SPACE tipku za razgovor",
|
||||
|
||||
"roles.gotRole": null,
|
||||
"roles.lostRole": null,
|
||||
|
||||
"tooltip.login": "Prijava",
|
||||
"tooltip.logout": "Odjava",
|
||||
|
|
@ -66,7 +76,10 @@
|
|||
"tooltip.lobby": "Prikaži predvorje",
|
||||
"tooltip.settings": "Prikaži postavke",
|
||||
"tooltip.participants": "Pokažite sudionike",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.kickParticipant": "Izbaci sudionika",
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Naziv sobe",
|
||||
"label.chooseRoomButton": "Nastavi",
|
||||
|
|
@ -78,8 +91,9 @@
|
|||
"label.chatInput":"Uđi u razgovor porukama",
|
||||
"label.chat": "Razgovor",
|
||||
"label.filesharing": "Dijeljenje datoteka",
|
||||
"label.participants": "Učesnici",
|
||||
"label.participants": "Sudionici",
|
||||
"label.shareFile": "Dijeli datoteku",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Dijeljenje datoteka nije podržano",
|
||||
"label.unknown": "Nepoznato",
|
||||
"label.democratic":"Demokratski prikaz",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Vrlo visoka (FHD)",
|
||||
"label.ultra": "Ultra visoka (UHD)",
|
||||
"label.close": "Zatvori",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Postavke",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Uređaj za zvuk",
|
||||
"settings.selectAudio": "Odaberi uređaj za zvuk",
|
||||
"settings.cantSelectAudio": "Nije moguće odabrati uređaj za zvuk",
|
||||
"settings.audioOutput": "Uređaj za izlaz zvuka",
|
||||
"settings.selectAudioOutput": "Odaberite audio izlazni uređaj",
|
||||
"settings.cantSelectAudioOutput": "Nije moguće odabrati audio izlazni uređaj",
|
||||
"settings.resolution": "Odaberi video rezoluciju",
|
||||
"settings.layout": "Način prikaza",
|
||||
"settings.selectRoomLayout": "Odaberi način prikaza",
|
||||
"settings.advancedMode": "Napredne mogućnosti",
|
||||
"settings.permanentTopBar": "Stalna gornja šipka",
|
||||
"settings.lastn": "Broj vidljivih videozapisa",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Nije moguće spremiti datoteku",
|
||||
"filesharing.startingFileShare": "Pokušaj dijeljenja datoteke",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Mikrofon odspojen",
|
||||
"devices.microphoneError": "Greška prilikom pristupa mikrofonu",
|
||||
"devices.microPhoneMute": "Mikrofon utišan",
|
||||
"devices.micophoneUnMute": "Mikrofon pojačan",
|
||||
"devices.microphoneMute": "Mikrofon utišan",
|
||||
"devices.microphoneUnMute": "Mikrofon pojačan",
|
||||
"devices.microphoneEnable": "Mikrofon omogućen",
|
||||
"devices.microphoneMuteError": "Nije moguće utišati mikrofon",
|
||||
"devices.microphoneUnMuteError": "Nije moguće pojačati mikrofon",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Greška prilikom pristupa ekranu",
|
||||
|
||||
"devices.cameraDisconnected": "Kamera odspojena",
|
||||
"devices.cameraError": "Greška prilikom pristupa kameri"
|
||||
"devices.cameraError": "Greška prilikom pristupa kameri",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
"socket.reconnected": "Sikeres újarkapcsolódá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.consentUnderstand": "I understand",
|
||||
"room.consentUnderstand": "Megértettem",
|
||||
"room.joined": "Csatlakozátál a konferenciához",
|
||||
"room.cantJoin": "Sikertelen csatlakozás a konferenciához",
|
||||
"room.youLocked": "A konferenciába való belépés letiltva",
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
"room.audioVideo": "Hang és Videó",
|
||||
"room.youAreReady": "Ok, kész vagy",
|
||||
"room.emptyRequireLogin": "A konferencia üres! Be kell lépned a konferecnia elkezdéséhez, vagy várnod kell amíg a házigazda becsatlakozik.",
|
||||
"room.locketWait": "A konferencia szobába a a belépés tilos - Várj amíg valaki be nem enged ...",
|
||||
"room.locketWait": "Az automatikus belépés tiltva van - Várj amíg valaki beenged ...",
|
||||
"room.lobbyAdministration": "Előszoba adminisztráció",
|
||||
"room.peersInLobby": "Résztvevők az előszobában",
|
||||
"room.lobbyEmpty": "Épp senki sincs a konferencia előszobájában",
|
||||
|
|
@ -49,12 +49,22 @@
|
|||
"room.spotlights": "Látható résztvevők",
|
||||
"room.passive": "Passzív résztvevők",
|
||||
"room.videoPaused": "Ez a videóstream szünetel",
|
||||
"room.muteAll": null,
|
||||
"room.stopAllVideo": null,
|
||||
"room.closeMeeting": null,
|
||||
"room.speechUnsupported": null,
|
||||
"room.muteAll": "Mindenki némítása",
|
||||
"room.stopAllVideo": "Mindenki video némítása",
|
||||
"room.closeMeeting": "Konferencia lebontása",
|
||||
"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 hangszint",
|
||||
"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.logout": "Kilépés",
|
||||
|
|
@ -66,7 +76,10 @@
|
|||
"tooltip.lobby": "Az előszobában várakozók listája",
|
||||
"tooltip.settings": "Beállítások",
|
||||
"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ő video némítása",
|
||||
"tooltip.raisedHand": "Jelentkezés",
|
||||
|
||||
"label.roomName": "Konferencia",
|
||||
"label.chooseRoomButton": "Tovább",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Fájl megosztás",
|
||||
"label.participants": "Résztvevők",
|
||||
"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.unknown": "Ismeretlen",
|
||||
"label.democratic": "Egyforma képméretű képkiosztás",
|
||||
|
|
@ -90,20 +104,30 @@
|
|||
"label.veryHigh": "Nagyon magas (FHD)",
|
||||
"label.ultra": "Ultra magas (UHD)",
|
||||
"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",
|
||||
|
||||
"settings.settings": "Beállítások",
|
||||
"settings.camera": "Kamera",
|
||||
"settings.selectCamera": "Válasz videóeszközt",
|
||||
"settings.cantSelectCamera": "Nem lehet a videó eszközt kiválasztani",
|
||||
"settings.selectCamera": "Válassz videoeszközt",
|
||||
"settings.cantSelectCamera": "Nem lehet a videoeszközt kiválasztani",
|
||||
"settings.audio": "Hang eszköz",
|
||||
"settings.selectAudio": "Válasz hangeszközt",
|
||||
"settings.cantSelectAudio": "Nem lehet a hang eszközt kiválasztani",
|
||||
"settings.resolution": "Válaszd ki a videóeszközöd felbontását",
|
||||
"settings.selectAudio": "Válassz hangeszközt",
|
||||
"settings.cantSelectAudio": "Nem sikerült a hangeszközt kiválasztani",
|
||||
"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.selectRoomLayout": "Válaszd ki a konferencia képkiosztását",
|
||||
"settings.advancedMode": "Részletes információk",
|
||||
"settings.permanentTopBar": "Állandó felső sáv",
|
||||
"settings.lastn": "A látható videók száma",
|
||||
"settings.hiddenControls": "Média Gombok automatikus elrejtése",
|
||||
"settings.notificationSounds": "Értesítések hangjelzjéssel",
|
||||
|
||||
"filesharing.saveFileError": "A file-t nem sikerült elmenteni",
|
||||
"filesharing.startingFileShare": "Fájl megosztása",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microphone kapcsolat bontva",
|
||||
"devices.microphoneError": "Hiba történt a mikrofon hangeszköz elérése közben",
|
||||
"devices.microPhoneMute": "A mikrofon némítva lett",
|
||||
"devices.micophoneUnMute": "A mikrofon némítása ki lett kapocsolva",
|
||||
"devices.microphoneMute": "A mikrofon némítva lett",
|
||||
"devices.microphoneUnMute": "A mikrofon némítása ki lett kapocsolva",
|
||||
"devices.microphoneEnable": "A mikrofon engedéylezve",
|
||||
"devices.microphoneMuteError": "Nem sikerült a mikrofonod némítása",
|
||||
"devices.microphoneUnMuteError": "Nem sikerült a mikrofonod némításának kikapcsolása",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Hiba történt a képernyőd megosztása során",
|
||||
|
||||
"devices.cameraDisconnected": "A kamera kapcsolata lebomlott",
|
||||
"devices.cameraError": "Hiba történt a kamera elérése során"
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Log in",
|
||||
"tooltip.logout": "Log out",
|
||||
"tooltip.admitFromLobby": "Ammetti dalla lobby",
|
||||
|
|
@ -66,6 +76,9 @@
|
|||
"tooltip.lobby": "Mostra lobby",
|
||||
"tooltip.settings": "Mostra impostazioni",
|
||||
"tooltip.participants": "Mostra partecipanti",
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Nome della stanza",
|
||||
"label.chooseRoomButton": "Continua",
|
||||
|
|
@ -79,6 +92,7 @@
|
|||
"label.filesharing": "Condivisione file",
|
||||
"label.participants": "Partecipanti",
|
||||
"label.shareFile": "Condividi file",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Condivisione file non supportata",
|
||||
"label.unknown": "Sconosciuto",
|
||||
"label.democratic": "Vista Democratica",
|
||||
|
|
@ -89,6 +103,11 @@
|
|||
"label.veryHigh": "Molto alta (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Chiudi",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Impostazioni",
|
||||
"settings.camera": "Videocamera",
|
||||
|
|
@ -97,12 +116,17 @@
|
|||
"settings.audio": "Dispositivo audio",
|
||||
"settings.selectAudio": "Seleziona 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.layout": "Aspetto stanza",
|
||||
"settings.selectRoomLayout": "Seleziona aspetto stanza",
|
||||
"settings.advancedMode": "Modalità avanzata",
|
||||
"settings.permanentTopBar": "Barra superiore permanente",
|
||||
"settings.lastn": "Numero di video visibili",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Impossibile salvare file",
|
||||
"filesharing.startingFileShare": "Tentativo di condivisione file",
|
||||
|
|
@ -132,8 +156,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microfono scollegato",
|
||||
"devices.microphoneError": "Errore con l'accesso al microfono",
|
||||
"devices.microPhoneMute": "Microfono silenziato",
|
||||
"devices.micophoneUnMute": "Microfono riattivato",
|
||||
"devices.microphoneMute": "Microfono silenziato",
|
||||
"devices.microphoneUnMute": "Microfono riattivato",
|
||||
"devices.microphoneEnable": "Microfono attivo",
|
||||
"devices.microphoneMuteError": "Impossibile silenziare il microfono",
|
||||
"devices.microphoneUnMuteError": "Impossibile riattivare il microfono",
|
||||
|
|
@ -142,5 +166,10 @@
|
|||
"devices.screenSharingError": "Errore con l'accesso al tuo schermo",
|
||||
|
||||
"devices.cameraDisconnected": "Videocamera scollegata",
|
||||
"devices.cameraError": "Errore con l'accesso alla videocamera"
|
||||
"devices.cameraError": "Errore con l'accesso alla videocamera",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
{
|
||||
"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",
|
||||
|
||||
"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",
|
||||
|
||||
"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"
|
||||
}
|
||||
|
|
@ -52,10 +52,20 @@
|
|||
"room.muteAll": "Demp alle",
|
||||
"room.stopAllVideo": "Stopp all video",
|
||||
"room.closeMeeting": "Avslutt møte",
|
||||
"room.clearChat": "Tøm chat",
|
||||
"room.clearFileSharing": "Fjern filer",
|
||||
"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",
|
||||
|
||||
"roles.gotRole": "Du fikk rollen: {role}",
|
||||
"roles.lostRole": "Du mistet rollen: {role}",
|
||||
|
||||
"tooltip.login": "Logg in",
|
||||
"tooltip.logout": "Logg ut",
|
||||
"tooltip.admitFromLobby": "Slipp inn fra lobby",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Vis innstillinger",
|
||||
"tooltip.participants": "Vis deltakere",
|
||||
"tooltip.kickParticipant": "Spark ut deltaker",
|
||||
"tooltip.muteParticipant": "Demp deltaker",
|
||||
"tooltip.muteParticipantVideo": "Demp deltakervideo",
|
||||
"tooltip.raisedHand": "Rekk opp hånden",
|
||||
|
||||
"label.roomName": "Møtenavn",
|
||||
"label.chooseRoomButton": "Fortsett",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Fildeling",
|
||||
"label.participants": "Deltakere",
|
||||
"label.shareFile": "Del fil",
|
||||
"label.shareGalleryFile": "Del bilde",
|
||||
"label.fileSharingUnsupported": "Fildeling ikke støttet",
|
||||
"label.unknown": "Ukjent",
|
||||
"label.democratic": "Demokratisk",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Veldig høy (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Lukk",
|
||||
"label.media": "Media",
|
||||
"label.appearence": "Utseende",
|
||||
"label.advanced": "Avansert",
|
||||
"label.addVideo": "Legg til video",
|
||||
"label.promoteAllPeers": "Slipp inn alle",
|
||||
|
||||
"settings.settings": "Innstillinger",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Lydenhet",
|
||||
"settings.selectAudio": "Velg 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.layout": "Møtelayout",
|
||||
"settings.selectRoomLayout": "Velg møtelayout",
|
||||
"settings.advancedMode": "Avansert modus",
|
||||
"settings.permanentTopBar": "Permanent topplinje",
|
||||
"settings.lastn": "Antall videoer synlig",
|
||||
"settings.hiddenControls": "Skjul media knapper",
|
||||
"settings.notificationSounds": "Varslingslyder",
|
||||
|
||||
"filesharing.saveFileError": "Klarte ikke å lagre fil",
|
||||
"filesharing.startingFileShare": "Starter fildeling",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Mikrofon koblet fra",
|
||||
"devices.microphoneError": "Det skjedde noe feil med mikrofonen din",
|
||||
"devices.microPhoneMute": "Dempet mikrofonen",
|
||||
"devices.micophoneUnMute": "Aktiverte mikrofonen",
|
||||
"devices.microphoneMute": "Dempet mikrofonen",
|
||||
"devices.microphoneUnMute": "Aktiverte mikrofonen",
|
||||
"devices.microphoneEnable": "Aktiverte mikrofonen",
|
||||
"devices.microphoneMuteError": "Klarte ikke å dempe mikrofonen",
|
||||
"devices.microphoneUnMuteError": "Klarte ikke å aktivere mikrofonen",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Det skjedde noe feil med skjermdelingen din",
|
||||
|
||||
"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"
|
||||
}
|
||||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Zaloguj",
|
||||
"tooltip.logout": "Wyloguj",
|
||||
"tooltip.admitFromLobby": "Przejście z poczekalni",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Pokaż ustawienia",
|
||||
"tooltip.participants": "Pokaż uczestników",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Nazwa konferencji",
|
||||
"label.chooseRoomButton": "Kontynuuj",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Udostępnianie plików",
|
||||
"label.participants": "Uczestnicy",
|
||||
"label.shareFile": "Udostępnij plik",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane",
|
||||
"label.unknown": "Nieznane",
|
||||
"label.democratic": "Układ demokratyczny",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Bardzo wysoka (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Zamknij",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Ustawienia",
|
||||
"settings.camera": "Kamera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Urządzenie audio",
|
||||
"settings.selectAudio": "Wybór 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.layout": "Układ konferencji",
|
||||
"settings.selectRoomLayout": "Ustawienia układu konferencji",
|
||||
"settings.advancedMode": "Tryb zaawansowany",
|
||||
"settings.permanentTopBar": "Stały górny pasek",
|
||||
"settings.lastn": "Liczba widocznych uczestników (zdalnych)",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Nie można zapisać pliku",
|
||||
"filesharing.startingFileShare": "Próba udostępnienia pliku",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Odłączono mikrofon",
|
||||
"devices.microphoneError": "Błąd dostępu do mikrofonu",
|
||||
"devices.microPhoneMute": "Wyciszenie mikrofonu włączone",
|
||||
"devices.micophoneUnMute": "Wyciszenie mikrofonu wyłączone",
|
||||
"devices.microphoneMute": "Wyciszenie mikrofonu włączone",
|
||||
"devices.microphoneUnMute": "Wyciszenie mikrofonu wyłączone",
|
||||
"devices.microphoneEnable": "Włączono mikrofon",
|
||||
"devices.microphoneMuteError": "Nie można wyciszyć mikrofonu",
|
||||
"devices.microphoneUnMuteError": "Nie można wyłączyć wyciszenia mikrofonu.",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Wystąpił błąd podczas uzyskiwania dostępu do ekranu",
|
||||
|
||||
"devices.cameraDisconnected": "Kamera odłączona",
|
||||
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery"
|
||||
"devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Entrar",
|
||||
"tooltip.logout": "Sair",
|
||||
"tooltip.admitFromLobby": "Admitir da sala de espera",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Apresentar definições",
|
||||
"tooltip.participants": "Apresentar participantes",
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Nome da sala",
|
||||
"label.chooseRoomButton": "Continuar",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Partilha de ficheiro",
|
||||
"label.participants": "Participantes",
|
||||
"label.shareFile": "Partilhar ficheiro",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Partilha de ficheiro não disponível",
|
||||
"label.unknown": "Desconhecido",
|
||||
"label.democratic": "Vista democrática",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Muito alta (FHD)",
|
||||
"label.ultra": "Ultra (UHD)",
|
||||
"label.close": "Fechar",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Definições",
|
||||
"settings.camera": "Camera",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Dispositivo Áudio",
|
||||
"settings.selectAudio": "Selecione 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.layout": "Disposição da sala",
|
||||
"settings.selectRoomLayout": "Seleccione a disposição da sala",
|
||||
"settings.advancedMode": "Modo avançado",
|
||||
"settings.permanentTopBar": "Barra superior permanente",
|
||||
"settings.lastn": "Número de vídeos visíveis",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Impossível de gravar o ficheiro",
|
||||
"filesharing.startingFileShare": "Tentando partilha de ficheiro",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microfone desiligado",
|
||||
"devices.microphoneError": "Ocorreu um erro no acesso ao microfone",
|
||||
"devices.microPhoneMute": "Som microfone desativado",
|
||||
"devices.micophoneUnMute": "Som mmicrofone ativado",
|
||||
"devices.microphoneMute": "Som microfone desativado",
|
||||
"devices.microphoneUnMute": "Som mmicrofone ativado",
|
||||
"devices.microphoneEnable": "Microfone ativado",
|
||||
"devices.microphoneMuteError": "Não foi possível cortar o som do microfone",
|
||||
"devices.microphoneUnMuteError": "Não foi possível ativar o som do microfone",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "Ocorreu um erro no acesso ao seu ecrã",
|
||||
|
||||
"devices.cameraDisconnected": "Câmara desconectada",
|
||||
"devices.cameraError": "Ocorreu um erro no acesso à sua câmara"
|
||||
"devices.cameraError": "Ocorreu um erro no acesso à sua câmara",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,20 @@
|
|||
"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": "Intră în cont",
|
||||
"tooltip.logout": "Deconectare",
|
||||
"tooltip.admitFromLobby": "Admite din hol",
|
||||
|
|
@ -67,6 +77,9 @@
|
|||
"tooltip.settings": "Arată setăile",
|
||||
"tooltip.participants": null,
|
||||
"tooltip.kickParticipant": null,
|
||||
"tooltip.muteParticipant": null,
|
||||
"tooltip.muteParticipantVideo": null,
|
||||
"tooltip.raisedHand": null,
|
||||
|
||||
"label.roomName": "Numele camerei",
|
||||
"label.chooseRoomButton": "Continuare",
|
||||
|
|
@ -80,6 +93,7 @@
|
|||
"label.filesharing": "Partajarea fișierelor",
|
||||
"label.participants": "Participanți",
|
||||
"label.shareFile": "Partajează fișierul",
|
||||
"label.shareGalleryFile": null,
|
||||
"label.fileSharingUnsupported": "Partajarea fișierelor nu este acceptată",
|
||||
"label.unknown": "Necunoscut",
|
||||
"label.democratic": "Distribuție egală a dimensiunii imaginii",
|
||||
|
|
@ -90,6 +104,11 @@
|
|||
"label.veryHigh": "Rezoluție foarte înaltă (FHD)",
|
||||
"label.ultra": "Rezoluție ultra înaltă (UHD)",
|
||||
"label.close": "Închide",
|
||||
"label.media": null,
|
||||
"label.appearence": null,
|
||||
"label.advanced": null,
|
||||
"label.addVideo": null,
|
||||
"label.promoteAllPeers": null,
|
||||
|
||||
"settings.settings": "Setări",
|
||||
"settings.camera": "Cameră video",
|
||||
|
|
@ -98,12 +117,17 @@
|
|||
"settings.audio": "Dispozitivul audio",
|
||||
"settings.selectAudio": "Selectarea dispozitivul audio",
|
||||
"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.layout": "Aspectul camerei video",
|
||||
"settings.selectRoomLayout": "Selectează spectul camerei video",
|
||||
"settings.advancedMode": "Mod avansat",
|
||||
"settings.permanentTopBar": "Bara de sus permanentă",
|
||||
"settings.lastn": "Numărul de videoclipuri vizibile",
|
||||
"settings.hiddenControls": null,
|
||||
"settings.notificationSounds": null,
|
||||
|
||||
"filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat",
|
||||
"filesharing.startingFileShare": "Partajarea fișierului",
|
||||
|
|
@ -133,8 +157,8 @@
|
|||
|
||||
"devices.microphoneDisconnected": "Microfonul e deconectat",
|
||||
"devices.microphoneError": "A apărut o eroare la accesarea microfonului",
|
||||
"devices.microPhoneMute": "Microfonul e dezactivat",
|
||||
"devices.micophoneUnMute": "Retragerea dezactivării microfonului",
|
||||
"devices.microphoneMute": "Microfonul e dezactivat",
|
||||
"devices.microphoneUnMute": "Retragerea dezactivării microfonului",
|
||||
"devices.microphoneEnable": "Microfonul e activat",
|
||||
"devices.microphoneMuteError": "Încercarea de a dezactiva microfonului a eșuat",
|
||||
"devices.microphoneUnMuteError": "Încercarea de a retrage dezactivarea microfonului a eșuat",
|
||||
|
|
@ -143,5 +167,10 @@
|
|||
"devices.screenSharingError": "A apărut o eroare la accesarea ecranului",
|
||||
|
||||
"devices.cameraDisconnected": "Camera video e disconectată",
|
||||
"devices.cameraError": "A apărut o eroare la accesarea camerei video"
|
||||
"devices.cameraError": "A apărut o eroare la accesarea camerei video",
|
||||
|
||||
"moderator.clearChat": null,
|
||||
"moderator.clearFiles": null,
|
||||
"moderator.muteAudio": null,
|
||||
"moderator.muteVideo": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
{
|
||||
"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,
|
||||
|
||||
"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,
|
||||
|
||||
"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"
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"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,
|
||||
|
||||
"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,
|
||||
|
||||
"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
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ fi
|
|||
if [ "$1" = "config" ]; then
|
||||
echo 'graph_title MM stats'
|
||||
#echo 'graph_args --base 1000 -l 0'
|
||||
echo 'graph_vlabel Actual Seesion Count'
|
||||
echo 'graph_vlabel Actual Session Count'
|
||||
echo 'graph_category other'
|
||||
echo 'graph_info This graph shows the mm stats.'
|
||||
echo 'rooms.label rooms'
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
+-------------+ +-------------+ +------------+
|
||||
```
|
||||
|
|
@ -36,8 +36,15 @@ module.exports =
|
|||
},
|
||||
*/
|
||||
// URI and key for requesting geoip-based TURN server closest to the client
|
||||
turnAPIKey : 'examplekey',
|
||||
turnAPIURI : 'https://example.com/api/turn',
|
||||
turnAPIKey : 'examplekey',
|
||||
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
|
||||
backupTurnServers : [
|
||||
{
|
||||
|
|
@ -53,20 +60,31 @@ module.exports =
|
|||
// session cookie secret
|
||||
cookieSecret : 'T0P-S3cR3t_cook!e',
|
||||
cookieName : 'multiparty-meeting.sid',
|
||||
// if you use encrypted private key the set the passphrase
|
||||
tls :
|
||||
{
|
||||
cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`,
|
||||
// passphrase: 'key_password'
|
||||
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.
|
||||
listeningPort : 443,
|
||||
// Any http request is redirected to https.
|
||||
// Listening port for http server.
|
||||
// Listening port for http server.
|
||||
listeningRedirectPort : 80,
|
||||
// Listens only on http, only on listeningPort
|
||||
// listeningRedirectPort disabled
|
||||
// use case: loadbalancer backend
|
||||
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
|
||||
|
|
@ -99,12 +117,6 @@ module.exports =
|
|||
});
|
||||
}
|
||||
}, */
|
||||
// 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 function will be called on successful login through oidc.
|
||||
// Use this function to map your oidc userinfo to the Peer object.
|
||||
// The roomId is equal to the room name.
|
||||
|
|
@ -201,7 +213,7 @@ module.exports =
|
|||
//
|
||||
// Example:
|
||||
// [ userRoles.MODERATOR, userRoles.AUTHENTICATED ]
|
||||
accessFromRoles : {
|
||||
accessFromRoles : {
|
||||
// The role(s) will gain access to the room
|
||||
// even if it is locked (!)
|
||||
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ],
|
||||
|
|
@ -212,25 +224,36 @@ module.exports =
|
|||
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
|
||||
BYPASS_LOBBY : [ userRoles.NORMAL ]
|
||||
},
|
||||
permissionsFromRoles : {
|
||||
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
|
||||
// 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 :
|
||||
mediasoup :
|
||||
{
|
||||
numWorkers : Object.keys(os.cpus()).length,
|
||||
// mediasoup Worker settings.
|
||||
|
|
@ -311,11 +334,12 @@ module.exports =
|
|||
{
|
||||
listenIps :
|
||||
[
|
||||
// change ip to your servers IP address!
|
||||
{ ip: '0.0.0.0', announcedIp: null }
|
||||
// change 192.0.2.1 IPv4 to your server's IPv4 address!!
|
||||
{ ip: '192.0.2.1', announcedIp: null }
|
||||
|
||||
// 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,
|
||||
minimumAvailableOutgoingBitrate : 600000,
|
||||
|
|
@ -323,4 +347,13 @@ module.exports =
|
|||
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
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ class Lobby extends EventEmitter
|
|||
return Object.values(this._peers).map((peer) =>
|
||||
({
|
||||
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)
|
||||
{
|
||||
if (peer.socket)
|
||||
this.promotePeer(peer.id);
|
||||
if (!this._peers[peer].closed)
|
||||
this.promotePeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,14 @@ class Peer extends EventEmitter
|
|||
|
||||
this._joined = false;
|
||||
|
||||
this._joinedTimestamp = null;
|
||||
|
||||
this._inLobby = false;
|
||||
|
||||
this._authenticated = false;
|
||||
|
||||
this._authenticatedTimestamp = null;
|
||||
|
||||
this._roles = [ userRoles.NORMAL ];
|
||||
|
||||
this._displayName = false;
|
||||
|
|
@ -35,10 +39,14 @@ class Peer extends EventEmitter
|
|||
|
||||
this._email = null;
|
||||
|
||||
this._routerId = null;
|
||||
|
||||
this._rtpCapabilities = null;
|
||||
|
||||
this._raisedHand = false;
|
||||
|
||||
this._raisedHandTimestamp = null;
|
||||
|
||||
this._transports = new Map();
|
||||
|
||||
this._producers = new Map();
|
||||
|
|
@ -135,9 +143,18 @@ class Peer extends EventEmitter
|
|||
|
||||
set joined(joined)
|
||||
{
|
||||
joined ?
|
||||
this._joinedTimestamp = Date.now() :
|
||||
this._joinedTimestamp = null;
|
||||
|
||||
this._joined = joined;
|
||||
}
|
||||
|
||||
get joinedTimestamp()
|
||||
{
|
||||
return this._joinedTimestamp;
|
||||
}
|
||||
|
||||
get inLobby()
|
||||
{
|
||||
return this._inLobby;
|
||||
|
|
@ -157,6 +174,10 @@ class Peer extends EventEmitter
|
|||
{
|
||||
if (authenticated !== this._authenticated)
|
||||
{
|
||||
authenticated ?
|
||||
this._authenticatedTimestamp = Date.now() :
|
||||
this._authenticatedTimestamp = null;
|
||||
|
||||
const oldAuthenticated = this._authenticated;
|
||||
|
||||
this._authenticated = authenticated;
|
||||
|
|
@ -165,6 +186,11 @@ class Peer extends EventEmitter
|
|||
}
|
||||
}
|
||||
|
||||
get authenticatedTimestamp()
|
||||
{
|
||||
return this._authenticatedTimestamp;
|
||||
}
|
||||
|
||||
get roles()
|
||||
{
|
||||
return this._roles;
|
||||
|
|
@ -214,6 +240,16 @@ class Peer extends EventEmitter
|
|||
this._email = email;
|
||||
}
|
||||
|
||||
get routerId()
|
||||
{
|
||||
return this._routerId;
|
||||
}
|
||||
|
||||
set routerId(routerId)
|
||||
{
|
||||
this._routerId = routerId;
|
||||
}
|
||||
|
||||
get rtpCapabilities()
|
||||
{
|
||||
return this._rtpCapabilities;
|
||||
|
|
@ -231,9 +267,18 @@ class Peer extends EventEmitter
|
|||
|
||||
set raisedHand(raisedHand)
|
||||
{
|
||||
raisedHand ?
|
||||
this._raisedHandTimestamp = Date.now() :
|
||||
this._raisedHandTimestamp = null;
|
||||
|
||||
this._raisedHand = raisedHand;
|
||||
}
|
||||
|
||||
get raisedHandTimestamp()
|
||||
{
|
||||
return this._raisedHandTimestamp;
|
||||
}
|
||||
|
||||
get transports()
|
||||
{
|
||||
return this._transports;
|
||||
|
|
@ -333,10 +378,12 @@ class Peer extends EventEmitter
|
|||
{
|
||||
const peerInfo =
|
||||
{
|
||||
id : this.id,
|
||||
displayName : this.displayName,
|
||||
picture : this.picture,
|
||||
roles : this.roles
|
||||
id : this.id,
|
||||
displayName : this.displayName,
|
||||
picture : this.picture,
|
||||
roles : this.roles,
|
||||
raisedHand : this.raisedHand,
|
||||
raisedHandTimestamp : this.raisedHandTimestamp
|
||||
};
|
||||
|
||||
return peerInfo;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,30 @@ const config = require('../config/config');
|
|||
|
||||
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
|
||||
{
|
||||
/**
|
||||
|
|
@ -16,32 +40,49 @@ class Room extends EventEmitter
|
|||
*
|
||||
* @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.
|
||||
* @param {String} roomId - Id of the Room instance.
|
||||
*/
|
||||
static async create({ mediasoupWorker, roomId })
|
||||
static async create({ mediasoupWorkers, roomId })
|
||||
{
|
||||
logger.info('create() [roomId:"%s"]', roomId);
|
||||
|
||||
// Shuffle workers to get random cores
|
||||
let shuffledWorkers = mediasoupWorkers.sort(() => Math.random() - 0.5);
|
||||
|
||||
// Router media codecs.
|
||||
const mediaCodecs = config.mediasoup.router.mediaCodecs;
|
||||
|
||||
// Create a mediasoup Router.
|
||||
const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs });
|
||||
const mediasoupRouters = new Map();
|
||||
|
||||
// Create a mediasoup AudioLevelObserver.
|
||||
const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver(
|
||||
let firstRouter = null;
|
||||
|
||||
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,
|
||||
threshold : -80,
|
||||
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);
|
||||
|
||||
|
|
@ -76,8 +117,13 @@ class Room extends EventEmitter
|
|||
|
||||
this._peers = {};
|
||||
|
||||
// mediasoup Router instance.
|
||||
this._mediasoupRouter = mediasoupRouter;
|
||||
// Array of mediasoup Router instances.
|
||||
this._mediasoupRouters = mediasoupRouters;
|
||||
|
||||
// The router we are currently putting peers in
|
||||
this._routerIterator = this._mediasoupRouters.values();
|
||||
|
||||
this._currentRouter = this._routerIterator.next().value;
|
||||
|
||||
// mediasoup AudioLevelObserver.
|
||||
this._audioLevelObserver = audioLevelObserver;
|
||||
|
|
@ -100,8 +146,14 @@ class Room extends EventEmitter
|
|||
|
||||
this._closed = true;
|
||||
|
||||
this._chatHistory = null;
|
||||
|
||||
this._fileHistory = null;
|
||||
|
||||
this._lobby.close();
|
||||
|
||||
this._lobby = null;
|
||||
|
||||
// Close the peers.
|
||||
for (const peer in this._peers)
|
||||
{
|
||||
|
|
@ -111,8 +163,19 @@ class Room extends EventEmitter
|
|||
|
||||
this._peers = null;
|
||||
|
||||
// Close the mediasoup Router.
|
||||
this._mediasoupRouter.close();
|
||||
// Close the mediasoup Routers.
|
||||
for (const router of this._mediasoupRouters.values())
|
||||
{
|
||||
router.close();
|
||||
}
|
||||
|
||||
this._routerIterator = null;
|
||||
|
||||
this._currentRouter = null;
|
||||
|
||||
this._mediasoupRouters.clear();
|
||||
|
||||
this._audioLevelObserver = null;
|
||||
|
||||
// Emit 'close' event.
|
||||
this.emit('close');
|
||||
|
|
@ -152,20 +215,34 @@ class Room extends EventEmitter
|
|||
if (returning)
|
||||
this._peerJoining(peer, true);
|
||||
else if ( // Has a role that is allowed to bypass room lock
|
||||
peer.roles.some((role) => config.accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
)
|
||||
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)
|
||||
this._parkPeer(peer);
|
||||
else
|
||||
{
|
||||
// Has a role that is allowed to bypass lobby
|
||||
peer.roles.some((role) => config.accessFromRoles.BYPASS_LOBBY.includes(role)) ?
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ?
|
||||
this._peerJoining(peer) :
|
||||
this._handleGuest(peer);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOverRoomLimit(peer)
|
||||
{
|
||||
this._notification(peer.socket, 'overRoomLimit');
|
||||
}
|
||||
|
||||
_handleGuest(peer)
|
||||
{
|
||||
if (config.activateOnHostJoin && !this.checkEmpty())
|
||||
|
|
@ -187,7 +264,11 @@ class Room extends EventEmitter
|
|||
|
||||
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 });
|
||||
}
|
||||
|
|
@ -196,7 +277,7 @@ class Room extends EventEmitter
|
|||
this._lobby.on('peerRolesChanged', (peer) =>
|
||||
{
|
||||
if ( // Has a role that is allowed to bypass room lock
|
||||
peer.roles.some((role) => config.accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role))
|
||||
)
|
||||
{
|
||||
this._lobby.promotePeer(peer.id);
|
||||
|
|
@ -206,7 +287,7 @@ class Room extends EventEmitter
|
|||
|
||||
if ( // Has a role that is allowed to bypass lobby
|
||||
!this._locked &&
|
||||
peer.roles.some((role) => config.accessFromRoles.BYPASS_LOBBY.includes(role))
|
||||
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role))
|
||||
)
|
||||
{
|
||||
this._lobby.promotePeer(peer.id);
|
||||
|
|
@ -219,7 +300,11 @@ class Room extends EventEmitter
|
|||
{
|
||||
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 });
|
||||
}
|
||||
|
|
@ -229,7 +314,11 @@ class Room extends EventEmitter
|
|||
{
|
||||
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 });
|
||||
}
|
||||
|
|
@ -241,7 +330,11 @@ class Room extends EventEmitter
|
|||
|
||||
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 });
|
||||
}
|
||||
|
|
@ -344,7 +437,11 @@ class Room extends EventEmitter
|
|||
{
|
||||
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 });
|
||||
}
|
||||
|
|
@ -359,6 +456,9 @@ class Room extends EventEmitter
|
|||
|
||||
this._peers[peer.id] = peer;
|
||||
|
||||
// Assign routerId
|
||||
peer.routerId = await this._getRouterId();
|
||||
|
||||
this._handlePeer(peer);
|
||||
|
||||
if (returning)
|
||||
|
|
@ -383,12 +483,9 @@ class Room extends EventEmitter
|
|||
config.turnAPIURI,
|
||||
{
|
||||
params : {
|
||||
'uri_schema' : 'turn',
|
||||
'transport' : 'tcp',
|
||||
'ip_ver' : 'ipv4',
|
||||
'servercount' : '2',
|
||||
'api_key' : config.turnAPIKey,
|
||||
'ip' : peer.socket.request.connection.remoteAddress
|
||||
...config.turnAPIparams,
|
||||
'api_key' : config.turnAPIKey,
|
||||
'ip' : peer.socket.request.connection.remoteAddress
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -492,6 +589,17 @@ class Room extends EventEmitter
|
|||
peerId : peer.id,
|
||||
role : newRole
|
||||
}, 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 }) =>
|
||||
|
|
@ -510,11 +618,14 @@ class Room extends EventEmitter
|
|||
|
||||
async _handleSocketRequest(peer, request, cb)
|
||||
{
|
||||
const router =
|
||||
this._mediasoupRouters.get(peer.routerId);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'getRouterRtpCapabilities':
|
||||
{
|
||||
cb(null, this._mediasoupRouter.rtpCapabilities);
|
||||
cb(null, router.rtpCapabilities);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -548,13 +659,21 @@ class Room extends EventEmitter
|
|||
.filter((joinedPeer) => joinedPeer.id !== peer.id)
|
||||
.map((joinedPeer) => (joinedPeer.peerInfo));
|
||||
|
||||
const lobbyPeers = this._lobby.peerList();
|
||||
|
||||
cb(null, {
|
||||
roles : peer.roles,
|
||||
peers : peerInfos,
|
||||
tracker : config.fileTracker,
|
||||
authenticated : peer.authenticated,
|
||||
permissionsFromRoles : config.permissionsFromRoles,
|
||||
userRoles : userRoles
|
||||
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.
|
||||
|
|
@ -615,7 +734,7 @@ class Room extends EventEmitter
|
|||
webRtcTransportOptions.enableTcp = true;
|
||||
}
|
||||
|
||||
const transport = await this._mediasoupRouter.createWebRtcTransport(
|
||||
const transport = await router.createWebRtcTransport(
|
||||
webRtcTransportOptions
|
||||
);
|
||||
|
||||
|
|
@ -685,9 +804,17 @@ class Room extends EventEmitter
|
|||
|
||||
if (
|
||||
appData.source === 'screen' &&
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.SHARE_SCREEN.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
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.
|
||||
if (!peer.joined)
|
||||
|
|
@ -706,6 +833,19 @@ class Room extends EventEmitter
|
|||
const producer =
|
||||
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.
|
||||
peer.addProducer(producer.id, producer);
|
||||
|
||||
|
|
@ -988,7 +1128,7 @@ class Room extends EventEmitter
|
|||
case 'chatMessage':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.SEND_CHAT.includes(role))
|
||||
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1008,22 +1148,22 @@ class Room extends EventEmitter
|
|||
break;
|
||||
}
|
||||
|
||||
case 'serverHistory':
|
||||
case 'moderator:clearChat':
|
||||
{
|
||||
// Return to sender
|
||||
const lobbyPeers = this._lobby.peerList();
|
||||
if (
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._chatHistory = [];
|
||||
|
||||
cb(
|
||||
null,
|
||||
{
|
||||
chatHistory : this._chatHistory,
|
||||
fileHistory : this._fileHistory,
|
||||
lastNHistory : this._lastN,
|
||||
locked : this._locked,
|
||||
lobbyPeers : lobbyPeers,
|
||||
accessCode : this._accessCode
|
||||
}
|
||||
);
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:clearChat', null, true);
|
||||
|
||||
// Return no error
|
||||
cb();
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -1031,7 +1171,9 @@ class Room extends EventEmitter
|
|||
case 'lockRoom':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1051,7 +1193,9 @@ class Room extends EventEmitter
|
|||
case 'unlockRoom':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1111,7 +1255,9 @@ class Room extends EventEmitter
|
|||
case 'promotePeer':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.PROMOTE_PEER.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1128,7 +1274,9 @@ class Room extends EventEmitter
|
|||
case 'promoteAllPeers':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.PROMOTE_PEER.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1143,7 +1291,9 @@ class Room extends EventEmitter
|
|||
case 'sendFile':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.SHARE_FILE.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1163,16 +1313,37 @@ class Room extends EventEmitter
|
|||
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;
|
||||
|
||||
peer.raisedHand = raisedHand;
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'raiseHand', {
|
||||
peerId : peer.id,
|
||||
raisedHand : raisedHand
|
||||
this._notification(peer.socket, 'raisedHand', {
|
||||
peerId : peer.id,
|
||||
raisedHand : raisedHand,
|
||||
raisedHandTimestamp : peer.raisedHandTimestamp
|
||||
}, true);
|
||||
|
||||
// Return no error
|
||||
|
|
@ -1184,14 +1355,14 @@ class Room extends EventEmitter
|
|||
case 'moderator:muteAll':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:mute', {
|
||||
peerId : peer.id
|
||||
}, true);
|
||||
this._notification(peer.socket, 'moderator:mute', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
|
|
@ -1201,14 +1372,14 @@ class Room extends EventEmitter
|
|||
case 'moderator:stopAllVideo':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
// Spread to others
|
||||
this._notification(peer.socket, 'moderator:stopVideo', {
|
||||
peerId : peer.id
|
||||
}, true);
|
||||
this._notification(peer.socket, 'moderator:stopVideo', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
|
|
@ -1218,16 +1389,13 @@ class Room extends EventEmitter
|
|||
case 'moderator:closeMeeting':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
this._notification(
|
||||
peer.socket,
|
||||
'moderator:kick',
|
||||
null,
|
||||
true
|
||||
);
|
||||
this._notification(peer.socket, 'moderator:kick', null, true);
|
||||
|
||||
cb();
|
||||
|
||||
|
|
@ -1240,7 +1408,9 @@ class Room extends EventEmitter
|
|||
case 'moderator:kickPeer':
|
||||
{
|
||||
if (
|
||||
!peer.roles.some((role) => config.permissionsFromRoles.MODERATE_ROOM.includes(role))
|
||||
!peer.roles.some(
|
||||
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
|
||||
)
|
||||
)
|
||||
throw new Error('peer not authorized');
|
||||
|
||||
|
|
@ -1251,10 +1421,7 @@ class Room extends EventEmitter
|
|||
if (!kickPeer)
|
||||
throw new Error(`peer with id "${peerId}" not found`);
|
||||
|
||||
this._notification(
|
||||
kickPeer.socket,
|
||||
'moderator:kick'
|
||||
);
|
||||
this._notification(kickPeer.socket, 'moderator:kick');
|
||||
|
||||
kickPeer.close();
|
||||
|
||||
|
|
@ -1286,6 +1453,8 @@ class Room extends EventEmitter
|
|||
producer.id
|
||||
);
|
||||
|
||||
const router = this._mediasoupRouters.get(producerPeer.routerId);
|
||||
|
||||
// Optimization:
|
||||
// - Create the server-side Consumer. If video, do it paused.
|
||||
// - Tell its Peer about it and wait for its response.
|
||||
|
|
@ -1296,7 +1465,7 @@ class Room extends EventEmitter
|
|||
// NOTE: Don't create the Consumer if the remote Peer cannot consume it.
|
||||
if (
|
||||
!consumerPeer.rtpCapabilities ||
|
||||
!this._mediasoupRouter.canConsume(
|
||||
!router.canConsume(
|
||||
{
|
||||
producerId : producer.id,
|
||||
rtpCapabilities : consumerPeer.rtpCapabilities
|
||||
|
|
@ -1431,6 +1600,19 @@ class Room extends EventEmitter
|
|||
.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)
|
||||
{
|
||||
let called = false;
|
||||
|
|
@ -1495,6 +1677,84 @@ class Room extends EventEmitter
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "multiparty-meeting-server",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting server",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"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": {
|
||||
"awaitqueue": "^1.0.0",
|
||||
|
|
@ -31,9 +32,13 @@
|
|||
"passport": "^0.4.0",
|
||||
"passport-lti": "0.0.7",
|
||||
"pidusage": "^2.0.17",
|
||||
"prom-client": ">=12.0.0",
|
||||
"redis": "^2.8.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"spdy": "^4.0.1",
|
||||
"uuid": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "6.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ const expressSession = require('express-session');
|
|||
const RedisStore = require('connect-redis')(expressSession);
|
||||
const sharedSession = require('express-socket.io-session');
|
||||
const interactiveServer = require('./lib/interactiveServer');
|
||||
const promExporter = require('./lib/promExporter');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||
console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
||||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||
console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel);
|
||||
console.log('- config.mediasoup.worker.logTags:', config.mediasoup.worker.logTags);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const logger = new Logger();
|
||||
|
|
@ -54,10 +56,6 @@ if ('StatusLogger' in config)
|
|||
// @type {Array<mediasoup.Worker>}
|
||||
const mediasoupWorkers = [];
|
||||
|
||||
// Index of next mediasoup Worker to use.
|
||||
// @type {Number}
|
||||
let nextMediasoupWorkerIdx = 0;
|
||||
|
||||
// Map of Room instances indexed by roomId.
|
||||
const rooms = new Map();
|
||||
|
||||
|
|
@ -132,6 +130,12 @@ async function run()
|
|||
// Open the interactive server.
|
||||
await interactiveServer(rooms, peers);
|
||||
|
||||
// start Prometheus exporter
|
||||
if (config.prometheus)
|
||||
{
|
||||
await promExporter(rooms, peers, config.prometheus);
|
||||
}
|
||||
|
||||
if (typeof(config.auth) === 'undefined')
|
||||
{
|
||||
logger.warn('Auth is not configured properly!');
|
||||
|
|
@ -150,6 +154,25 @@ async function run()
|
|||
// Run WebSocketServer.
|
||||
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.
|
||||
setInterval(() =>
|
||||
{
|
||||
|
|
@ -189,7 +212,7 @@ function setupLTI(ltiConfig)
|
|||
|
||||
const ltiStrategy = new LTIStrategy(
|
||||
ltiConfig,
|
||||
function(req, lti, done)
|
||||
(req, lti, done) =>
|
||||
{
|
||||
// LTI launch parameters
|
||||
if (lti)
|
||||
|
|
@ -199,7 +222,7 @@ function setupLTI(ltiConfig)
|
|||
if (lti.user_id && lti.custom_room)
|
||||
{
|
||||
user.id = lti.user_id;
|
||||
user._lti = lti;
|
||||
user._userinfo = { 'lti': lti };
|
||||
}
|
||||
|
||||
if (lti.custom_room)
|
||||
|
|
@ -240,7 +263,18 @@ function setupOIDC(oidcIssuer)
|
|||
// redirect_uri defaults to client.redirect_uris[0]
|
||||
// response type defaults to client.response_types[0], then 'code'
|
||||
// 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
|
||||
// argument to verify fn
|
||||
|
|
@ -255,12 +289,17 @@ function setupOIDC(oidcIssuer)
|
|||
{ client: oidcClient, params, passReqToCallback, usePKCE },
|
||||
(tokenset, userinfo, done) =>
|
||||
{
|
||||
if (userinfo && tokenset)
|
||||
{
|
||||
// eslint-disable-next-line camelcase
|
||||
userinfo._tokenset_claims = tokenset.claims();
|
||||
}
|
||||
|
||||
const user =
|
||||
{
|
||||
id : tokenset.claims.sub,
|
||||
provider : tokenset.claims.iss,
|
||||
_userinfo : userinfo,
|
||||
_claims : tokenset.claims
|
||||
_userinfo : userinfo
|
||||
};
|
||||
|
||||
return done(null, user);
|
||||
|
|
@ -310,7 +349,7 @@ async function setupAuth()
|
|||
// lti launch
|
||||
app.post('/auth/lti',
|
||||
passport.authenticate('lti', { failureRedirect: '/' }),
|
||||
function(req, res)
|
||||
(req, res) =>
|
||||
{
|
||||
res.redirect(`/${req.user.room}`);
|
||||
}
|
||||
|
|
@ -333,7 +372,7 @@ async function setupAuth()
|
|||
}
|
||||
|
||||
req.logout();
|
||||
res.send(logoutHelper());
|
||||
req.session.destroy(() => res.send(logoutHelper()));
|
||||
});
|
||||
|
||||
// callback
|
||||
|
|
@ -427,11 +466,17 @@ async function runHttpsServer()
|
|||
// http
|
||||
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
|
||||
mainListener.listen(config.listeningPort);
|
||||
if (config.listeningHost)
|
||||
mainListener.listen(config.listeningPort, config.listeningHost);
|
||||
else
|
||||
mainListener.listen(config.listeningPort);
|
||||
}
|
||||
|
||||
function isPathAlreadyTaken(url)
|
||||
|
|
@ -590,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).
|
||||
*/
|
||||
|
|
@ -615,9 +647,9 @@ async function getOrCreateRoom({ 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ module.exports = {
|
|||
// Don't change anything after this point
|
||||
|
||||
// All users have this role by default, do not change or remove this role
|
||||
NORMAL : 'normal'
|
||||
NORMAL : 'normal'
|
||||
};
|
||||
Loading…
Reference in New Issue