Merge pull request #3 from havfo/develop

sync with Develop upstream
auto_join_3.3
Saša Davidović 2020-05-05 16:38:08 +02:00 committed by GitHub
commit 51ff2cb80e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 5749 additions and 1652 deletions

View File

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

1
CONTRIBUTING.md 100644
View File

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

101
HAproxy.md 100644
View File

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

21
LICENSE.md 100644
View File

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

61
LTI/LTI.md 100644
View File

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

BIN
LTI/lti1.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
LTI/lti2.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
LTI/lti3.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
LTI/lti4.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@ -21,10 +21,9 @@ If you want the automatic approach, you can find a docker image [here](https://h
If you want the ansible approach, you can find ansible role [here](https://github.com/misi/mm-ansible/).
[![asciicast](https://asciinema.org/a/311365.svg)](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 Unions Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project.

View File

@ -1,6 +1,6 @@
{
"name": "multiparty-meeting",
"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"
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 }
});
@ -68,6 +75,12 @@ export const setAudioInProgress = (flag) =>
payload : { flag }
});
export const setAudioOutputInProgress = (flag) =>
({
type : 'SET_AUDIO_OUTPUT_IN_PROGRESS',
payload : { flag }
});
export const setWebcamInProgress = (flag) =>
({
type : 'SET_WEBCAM_IN_PROGRESS',
@ -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 }
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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 : () =>
{

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import 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 &&

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import DOMPurify from 'dompurify';
import marked from 'marked';
import 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>
);

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import React from 'react';
import { 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
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
if (prevProps !== this.props)
{
const { audioTrack, audioOutputDevice } = this.props;
this._setTrack(audioTrack);
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
};

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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)));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -34,7 +34,11 @@ import messagesPortuguese from './translations/pt';
import messagesChinese from './translations/cn';
import 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

55
prom.md 100644
View File

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

View File

@ -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,11 +60,16 @@ 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.
@ -67,6 +79,12 @@ module.exports =
// 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
}
*/
};

View File

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

View File

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

View File

@ -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');
cb(
null,
{
chatHistory : this._chatHistory,
fileHistory : this._fileHistory,
lastNHistory : this._lastN,
locked : this._locked,
lobbyPeers : lobbyPeers,
accessCode : this._accessCode
}
);
this._chatHistory = [];
// 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;

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "multiparty-meeting-server",
"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"
}
}

View File

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

View File

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