From 76cf5490497735b503d0b9bd7bbeb802870d6492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Sat, 13 Apr 2019 08:08:07 +0200 Subject: [PATCH 01/44] Add placeholder file to server/public --- server/public/readme.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 server/public/readme.md diff --git a/server/public/readme.md b/server/public/readme.md new file mode 100644 index 0000000..acd45d4 --- /dev/null +++ b/server/public/readme.md @@ -0,0 +1,2 @@ +## Webroot +This is the webroot, so the place of the app. From 14d1de60c02ef0d964ba64e7ec53edec20b6f8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 6 May 2019 09:08:08 +0200 Subject: [PATCH 02/44] move app config to directory --- app/public/{ => config}/config.example.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/public/{ => config}/config.example.js (100%) diff --git a/app/public/config.example.js b/app/public/config/config.example.js similarity index 100% rename from app/public/config.example.js rename to app/public/config/config.example.js From 26389993a3050c1245e2739ba930cc1bdb1aa70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 6 May 2019 09:37:01 +0200 Subject: [PATCH 03/44] change config location --- app/public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/public/index.html b/app/public/index.html index dc9e2f3..2c07c0b 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -16,7 +16,7 @@ Multiparty Meeting - + From 88386e569778dea1bfcb61e4d376f980e3107f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Fri, 10 May 2019 21:13:57 +0200 Subject: [PATCH 04/44] update Readme: config.js moved into config dir --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 88bb10b..a657a05 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,16 @@ $ git clone https://github.com/havfo/multiparty-meeting.git $ cd multiparty-meeting ``` -* Copy `server/config.example.js` to `server/config.js` : +* Copy `server/config/config.example.js` to `server/config/config.js` : ```bash -$ cp server/config.example.js server/config.js +$ cp server/config/config.example.js server/config/config.js ``` -* Copy `app/public/config.example.js` to `app/public/config.js` : +* Copy `app/public/config/config.example.js` to `app/public/config/config.js` : ```bash -$ cp app/public/config.example.js app/public/config.js +$ cp app/public/config/config.example.js app/public/config/config.js ``` * Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc). From 28f9e72612619a0011fe150fab658dd68e7a0228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 13 May 2019 11:41:34 +0200 Subject: [PATCH 05/44] use openid-client with passport --- app/src/RoomClient.js | 4 +- server/config/config.example.js | 20 +- server/package.json | 4 +- server/server.js | 376 ++++++++++++++++++++++---------- 4 files changed, 282 insertions(+), 122 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 4a90984..105578f 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -253,14 +253,14 @@ export default class RoomClient login() { - const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; + const url = `/auth/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; this._loginWindow = window.open(url, 'loginWindow'); } logout() { - window.location = '/logout'; + window.location = '/auth/logout'; } closeLoginWindow() diff --git a/server/config/config.example.js b/server/config/config.example.js index 9b2fab1..3a6a228 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,11 +1,23 @@ module.exports = { // oAuth2 conf - oauth2 : + auth : { - clientID : '', - clientSecret : '', - callbackURL : 'https://mYDomainName:port/auth-callback' + /* + The issuer URL for OpenID Connect discovery + The OpenID Provider Configuration Document + could be discovered on: + issuerURL + '/.well-known/openid-configuration' + */ + issuerURL : 'https://example.com' + clientOptions : + { + client_id : '', + client_secret : '', + scope : 'openid email profile' + // where client.example.com is your multiparty meeting server + redirect_uri : 'https://client.example.com/auth/callback' + } }, // Listening hostname for `gulp live|open`. domain : 'localhost', diff --git a/server/package.json b/server/package.json index 0ad8bc0..229316c 100644 --- a/server/package.json +++ b/server/package.json @@ -12,8 +12,10 @@ "compression": "^1.7.3", "debug": "^4.1.0", "express": "^4.16.3", + "express-session": "^1.16.1", "mediasoup": "^2.6.11", - "passport-dataporten": "^1.3.0", + "openid-client": "^2.5.0", + "passport": "^0.4.0", "socket.io": "^2.1.1" }, "devDependencies": { diff --git a/server/server.js b/server/server.js index de42cdc..413cff3 100755 --- a/server/server.js +++ b/server/server.js @@ -12,9 +12,12 @@ const express = require('express'); const compression = require('compression'); const Logger = require('./lib/Logger'); const Room = require('./lib/Room'); -const Dataporten = require('passport-dataporten'); const utils = require('./util'); const base64 = require('base-64'); +// auth +const passport = require('passport'); +const { Issuer, Strategy } = require('openid-client'); +const session = require('express-session') /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); @@ -37,148 +40,291 @@ const tls = key : fs.readFileSync(config.tls.key) }; -const app = express(); +let app = express(); +let httpsServer; +let oidcClient; +let oidcStrategy; -app.use(compression()); - -const dataporten = new Dataporten.Setup(config.oauth2); - -app.all('*', (req, res, next) => +passport.serializeUser(function(user, done) { - if (req.secure) + done(null, user); +}); + +passport.deserializeUser(function(user, done) +{ + done(null, user); +}); + +const auth=config.auth; + +function setupAuth(oidcIssuer) +{ + oidcClient = new oidcIssuer.Client(auth.clientOptions); + const params = { - return next(); - } + ...auth.clientOptions + // ... any authorization request parameters go here + // client_id defaults to client.client_id + // redirect_uri defaults to client.redirect_uris[0] + // response type defaults to client.response_types[0], then 'code' + // scope defaults to 'openid' + }; + + // optional, defaults to false, when true req is passed as a first + // argument to verify fn + const passReqToCallback = false; + + // optional, defaults to false, when true the code_challenge_method will be + // resolved from the issuer configuration, instead of true you may provide + // any of the supported values directly, i.e. "S256" (recommended) or "plain" + const usePKCE = false; + const client=oidcClient; - res.redirect(`https://${req.hostname}${req.url}`); -}); - -app.use(dataporten.passport.initialize()); -app.use(dataporten.passport.session()); - -app.get('/login', (req, res, next) => -{ - dataporten.passport.authenticate('dataporten', { - state : base64.encode(JSON.stringify({ - roomId : req.query.roomId, - peerName : req.query.peerName, - code : utils.random(10) - })) - - })(req, res, next); -}); - -dataporten.setupLogout(app, '/logout'); - -app.get('/', (req, res) => -{ - res.sendFile(`${__dirname}/public/chooseRoom.html`); -}); - -app.get( - '/auth-callback', - dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }), - (req, res) => - { - const state = JSON.parse(base64.decode(req.query.state)); - - if (rooms.has(state.roomId)) - { - const data = - { - peerName : state.peerName, - name : req.user.data.displayName, - picture : req.user.data.photos[0] + oidcStrategy = new Strategy( + { client, params, passReqToCallback, usePKCE }, + (tokenset, userinfo, done) => + { + console.log('tokenset', tokenset); + console.log('access_token', tokenset.access_token); + console.log('id_token', tokenset.id_token); + console.log('claims', tokenset.claims); + console.log('userinfo', userinfo); + let user = { + id : tokenset.claims.sub, + provider : tokenset.claims.iss, + _userinfo : userinfo, + _claims : tokenset.claims, + //tokenset : tokenset, + //idtoken : tokenset.id_token, + // eslint-disable-next-line camelcase + //access_token : tokenset.access_token }; + - const room = rooms.get(state.roomId); + if ( typeof(userinfo.picture) !== 'undefined' ){ + if ( ! userinfo.picture.match(/^http/g) ) { + user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ]; + } else { + user.Photos= [ { value: userinfo.picture } ]; + } + } + + if ( typeof(userinfo.nickname) !== 'undefined' ){ + user.displayName=userinfo.nickname; + } - room.authCallback(data); + if ( typeof(userinfo.name) !== 'undefined' ){ + user.displayName=userinfo.name; + } + + if ( typeof(userinfo.email) !== 'undefined' ){ + user.emails=[{value: userinfo.email}]; + } + + if ( typeof(userinfo.given_name) !== 'undefined' ){ + user.name={givenName: userinfo.given_name}; + } + + if ( typeof(userinfo.family_name) !== 'undefined' ){ + user.name={familyName: userinfo.family_name}; + } + + if ( typeof(userinfo.middle_name) !== 'undefined' ){ + user.name={middleName: userinfo.middle_name}; + } + + + return done(null, user); + } + ); + passport.use('oidc', oidcStrategy); + + app.use(session({ + secret: 'keyboard cat', + resave: true, + saveUninitialized: true, + //cookie : { secure: true } + })); + + app.use(passport.initialize()); + app.use(passport.session()); + + // login + app.get('/auth/login', (req, res, next) => + { + passport.authenticate('oidc', { + state : base64.encode(JSON.stringify({ + roomId : req.query.roomId, + peerName : req.query.peerName, + code : utils.random(10) + })) + })(req, res, next); + }); + + // logout + app.get('/auth/logout', function(req, res) + { + req.logout(); + res.redirect('/'); + } + ); + // callback + app.get( + '/auth/callback', + passport.authenticate('oidc', { failureRedirect: '/auth/login' }), + (req, res) => + { + const state = JSON.parse(base64.decode(req.query.state)); + + if (rooms.has(state.roomId)) + { + let displayName,photo + if (typeof(req.user) !== 'undefined'){ + if (typeof(req.user.displayName) !== 'undefined') displayName=req.user.displayName; + else displayName=""; + if (typeof(req.user.Photos[0].value) !== 'undefined') photo=req.user.Photos[0].value + else photo="/static/media/buddy.403cb9f6.svg"; + } + + const data = + { + peerName : state.peerName, + name : displayName, + picture : photo + }; + + const room = rooms.get(state.roomId); + + room.authCallback(data); + } + + res.send(''); + } + ); +} + +function setupWebServer() { + app.use(compression()); + + app.all('*', (req, res, next) => + { + if (req.secure) + { + return next(); } - res.send(''); - } -); + res.redirect(`https://${req.hostname}${req.url}`); + }); -// Serve all files in the public folder as static files. -app.use(express.static('public')); - -app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`)); - -const httpsServer = https.createServer(tls, app); - -httpsServer.listen(config.listeningPort, '0.0.0.0', () => -{ - logger.info('Server running on port: ', config.listeningPort); -}); - -const httpServer = http.createServer(app); - -httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () => -{ - logger.info('Server redirecting port: ', config.listeningRedirectPort); -}); - -const io = require('socket.io')(httpsServer); - -// Handle connections from clients. -io.on('connection', (socket) => -{ - const { roomId, peerName } = socket.handshake.query; - - if (!roomId || !peerName) + app.get('/', (req, res) => { - logger.warn('connection request without roomId and/or peerName'); + res.sendFile(`${__dirname}/public/chooseRoom.html`); + }); - socket.disconnect(true); + // Serve all files in the public folder as static files. + app.use(express.static('public')); - return; - } + app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`)); - logger.info( - 'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName); + httpsServer = https.createServer(tls, app); - let room; - - // If an unknown roomId, create a new Room. - if (!rooms.has(roomId)) + httpsServer.listen(config.listeningPort, '0.0.0.0', () => { - logger.info('creating a new Room [roomId:"%s"]', roomId); + logger.info('Server running on port: ', config.listeningPort); + }); - try - { - room = new Room(roomId, mediaServer, io); + const httpServer = http.createServer(app); - global.APP_ROOM = room; - } - catch (error) + httpServer.listen(config.listeningRedirectPort, '0.0.0.0', () => + { + logger.info('Server redirecting port: ', config.listeningRedirectPort); + }); +}; + +function setupSocketIO(){ + const io = require('socket.io')(httpsServer); + + // Handle connections from clients. + io.on('connection', (socket) => + { + const { roomId, peerName } = socket.handshake.query; + + if (!roomId || !peerName) { - logger.error('error creating a new Room: %s', error); + logger.warn('connection request without roomId and/or peerName'); socket.disconnect(true); return; } - const logStatusTimer = setInterval(() => - { - room.logStatus(); - }, 30000); + logger.info( + 'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName); - rooms.set(roomId, room); + let room; - room.on('close', () => + // If an unknown roomId, create a new Room. + if (!rooms.has(roomId)) { - rooms.delete(roomId); - clearInterval(logStatusTimer); - }); - } - else + logger.info('creating a new Room [roomId:"%s"]', roomId); + + try + { + room = new Room(roomId, mediaServer, io); + + global.APP_ROOM = room; + } + catch (error) + { + logger.error('error creating a new Room: %s', error); + + socket.disconnect(true); + + return; + } + + const logStatusTimer = setInterval(() => + { + room.logStatus(); + }, 30000); + + rooms.set(roomId, room); + + room.on('close', () => + { + rooms.delete(roomId); + clearInterval(logStatusTimer); + }); + } + else + { + room = rooms.get(roomId); + } + + socket.room = roomId; + + room.handleConnection(peerName, socket); + }); +} +if ( + typeof(auth) !== 'undefined' && + typeof(auth.issuerURL) !== 'undefined' && + typeof(auth.clientOptions) !== 'undefined' +) +{ + Issuer.discover(auth.issuerURL).then((oidcIssuer) => { - room = rooms.get(roomId); + setupAuth(oidcIssuer); + setupWebServer(); + setupSocketIO(); + }).catch((err) => { + logger.error(err); } + ); +} else +{ + logger.error('Auth is not configure properly!'); + setupWebServer(); + setupSocketIO(); +} - socket.room = roomId; - - room.handleConnection(peerName, socket); -}); From a5bc1fc3e7861fe0ffe5a5de784971fbbfd6f945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 13 May 2019 14:58:13 +0200 Subject: [PATCH 06/44] move secure session cookie secret to config --- server/config/config.example.js | 2 ++ server/server.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index 3a6a228..4e7f34f 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -19,6 +19,8 @@ module.exports = redirect_uri : 'https://client.example.com/auth/callback' } }, + // session cookie secret + cookieSecret : 'T0P-S3cR3t_cook!e', // Listening hostname for `gulp live|open`. domain : 'localhost', tls : diff --git a/server/server.js b/server/server.js index 413cff3..4b5d177 100755 --- a/server/server.js +++ b/server/server.js @@ -140,10 +140,10 @@ function setupAuth(oidcIssuer) passport.use('oidc', oidcStrategy); app.use(session({ - secret: 'keyboard cat', + secret: config.cookieSecret, resave: true, saveUninitialized: true, - //cookie : { secure: true } + cookie: { secure: true } })); app.use(passport.initialize()); From b209b351134c190628d50650bb6b8221e84d2910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 13 May 2019 14:59:58 +0200 Subject: [PATCH 07/44] Tidy: remove not necesary debug info --- server/server.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/server.js b/server/server.js index 4b5d177..32f976c 100755 --- a/server/server.js +++ b/server/server.js @@ -84,20 +84,11 @@ function setupAuth(oidcIssuer) { client, params, passReqToCallback, usePKCE }, (tokenset, userinfo, done) => { - console.log('tokenset', tokenset); - console.log('access_token', tokenset.access_token); - console.log('id_token', tokenset.id_token); - console.log('claims', tokenset.claims); - console.log('userinfo', userinfo); let user = { id : tokenset.claims.sub, provider : tokenset.claims.iss, _userinfo : userinfo, _claims : tokenset.claims, - //tokenset : tokenset, - //idtoken : tokenset.id_token, - // eslint-disable-next-line camelcase - //access_token : tokenset.access_token }; From 393ec1522155fff5bb4fe03a0c2eab07cf1f00e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Tue, 14 May 2019 08:07:30 +0200 Subject: [PATCH 08/44] extend check of photo --- server/server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 32f976c..3e1e3d4 100755 --- a/server/server.js +++ b/server/server.js @@ -173,7 +173,11 @@ function setupAuth(oidcIssuer) if (typeof(req.user) !== 'undefined'){ if (typeof(req.user.displayName) !== 'undefined') displayName=req.user.displayName; else displayName=""; - if (typeof(req.user.Photos[0].value) !== 'undefined') photo=req.user.Photos[0].value + if ( + typeof(req.user.Photos) !== 'undefined' && + typeof(req.user.Photos[0]) !== 'undefined' && + typeof(req.user.Photos[0].value) !== 'undefined' + ) photo=req.user.Photos[0].value; else photo="/static/media/buddy.403cb9f6.svg"; } From 01967ccffdb59c2030f0c0768be4ae28c5aa4c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Wed, 15 May 2019 10:42:09 +0200 Subject: [PATCH 09/44] https to spdy replacment --- server/package.json | 3 ++- server/server.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/package.json b/server/package.json index 229316c..fb16c02 100644 --- a/server/package.json +++ b/server/package.json @@ -16,7 +16,8 @@ "mediasoup": "^2.6.11", "openid-client": "^2.5.0", "passport": "^0.4.0", - "socket.io": "^2.1.1" + "socket.io": "^2.1.1", + "spdy": "^4.0.0" }, "devDependencies": { "gulp": "^4.0.0", diff --git a/server/server.js b/server/server.js index 3e1e3d4..b74a76c 100755 --- a/server/server.js +++ b/server/server.js @@ -6,8 +6,8 @@ process.title = 'multiparty-meeting-server'; const config = require('./config/config'); const fs = require('fs'); -const https = require('https'); const http = require('http'); +const spdy = require('spdy'); const express = require('express'); const compression = require('compression'); const Logger = require('./lib/Logger'); @@ -221,7 +221,7 @@ function setupWebServer() { app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`)); - httpsServer = https.createServer(tls, app); + httpsServer = spdy.createServer(tls, app); httpsServer.listen(config.listeningPort, '0.0.0.0', () => { From 30f42d6ced06250eccf3fad6c0f2341c6541d25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 3 Jun 2019 11:55:23 +0200 Subject: [PATCH 10/44] Mostly working mediasoup v3 --- .gitignore | 2 +- app/package.json | 10 +- app/src/RoomClient.js | 1818 ++++++++++------- app/src/Spotlights.js | 135 +- app/src/actions/stateActions.js | 92 +- app/src/components/Containers/Me.js | 6 +- app/src/components/Containers/Peer.js | 32 +- app/src/components/Containers/Volume.js | 2 +- app/src/components/Controls/Sidebar.js | 4 +- .../MeetingDrawer/FileSharing/File.js | 16 +- .../MeetingDrawer/FileSharing/FileSharing.js | 18 +- .../MeetingDrawer/ParticipantList/ListPeer.js | 10 +- .../ParticipantList/ParticipantList.js | 42 +- app/src/components/MeetingViews/Democratic.js | 4 +- app/src/components/MeetingViews/Filmstrip.js | 42 +- app/src/components/Selectors.js | 10 +- .../VideoContainers/FullScreenView.js | 7 - app/src/components/appPropTypes.js | 30 +- app/src/deviceInfo.js | 31 + app/src/index.js | 12 +- app/src/reducers/consumers.js | 38 +- app/src/reducers/me.js | 24 +- app/src/reducers/peerVolumes.js | 14 +- app/src/reducers/peers.js | 12 +- app/src/reducers/room.js | 16 +- app/src/urlFactory.js | 4 +- server/config/config.example.js | 120 +- server/lib/Room.js | 1391 ++++++++----- server/lib/homer.js | 4 +- server/package.json | 5 +- server/server.js | 353 ++-- 31 files changed, 2563 insertions(+), 1741 deletions(-) create mode 100644 app/src/deviceInfo.js diff --git a/.gitignore b/.gitignore index 122f774..7f87e51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ node_modules/ /app/build/ -/app/public/config.js +/app/public/config/config.js /app/public/images/logo.* /server/config/ !/server/config/config.example.js diff --git a/app/package.json b/app/package.json index 05a3baf..943109e 100644 --- a/app/package.json +++ b/app/package.json @@ -8,13 +8,14 @@ "dependencies": { "@material-ui/core": "^3.9.2", "@material-ui/icons": "^3.0.2", + "bowser": "^2.4.0", "create-torrent": "^3.33.0", "domready": "^1.0.8", "file-saver": "^2.0.1", "hark": "^1.2.3", "js-cookie": "^2.2.0", "marked": "^0.6.1", - "mediasoup-client": "^2.4.10", + "mediasoup-client": "^3.0.6", "notistack": "^0.5.1", "prop-types": "^15.7.2", "random-string": "^0.2.0", @@ -169,7 +170,12 @@ "no-case-declarations": 2, "no-catch-shadow": 2, "no-class-assign": 2, - "no-confusing-arrow": ["error", {"allowParens": true}], + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], "no-console": 2, "no-const-assign": 2, "no-debugger": 2, diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 105578f..8ce133a 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -20,7 +20,7 @@ const { const logger = new Logger('RoomClient'); -let ROOM_OPTIONS = +const ROOM_OPTIONS = { requestTimeout : requestTimeout, transportOptions : transportOptions, @@ -56,6 +56,13 @@ const VIDEO_CONSTRAINS = } }; +const VIDEO_ENCODINGS = +[ + { maxBitrate: 180000, scaleResolutionDownBy: 4 }, + { maxBitrate: 360000, scaleResolutionDownBy: 2 }, + { maxBitrate: 1500000, scaleResolutionDownBy: 1 } +]; + let store; const AudioContext = window.AudioContext // Default @@ -74,13 +81,13 @@ export default class RoomClient } constructor( - { roomId, peerName, device, useSimulcast, produce }) + { roomId, peerId, device, useSimulcast, produce, consume, forceTcp }) { logger.debug( - 'constructor() [roomId:"%s", peerName:"%s", device:%s]', - roomId, peerName, device.flag); + 'constructor() [roomId: "%s", peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", consume: "%s", forceTcp: "%s"]', + roomId, peerId, device.flag, useSimulcast, produce, consume, forceTcp); - this._signalingUrl = getSignalingUrl(peerName, roomId); + this._signalingUrl = getSignalingUrl(peerId, roomId); // window element to external login site this._loginWindow = null; @@ -91,6 +98,12 @@ export default class RoomClient // Whether we should produce. this._produce = produce; + // Whether we should consume. + this._consume = consume; + + // Wheter we force TCP + this._forceTcp = forceTcp; + // Torrent support this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; @@ -101,7 +114,7 @@ export default class RoomClient this._device = device; // My peer name. - this._peerName = peerName; + this._peerId = peerId; // Alert sound this._soundAlert = new Audio('/sounds/notify.mp3'); @@ -114,10 +127,13 @@ export default class RoomClient // Socket.io peer connection this._signalingSocket = null; - // The mediasoup room instance - this._room = null; + // The room ID this._roomId = roomId; + // mediasoup-client Device instance. + // @type {mediasoupClient.Device} + this._mediasoupDevice = null; + this._doneJoining = false; // Our WebTorrent client @@ -156,6 +172,10 @@ export default class RoomClient this._audioDevices = {}; + // mediasoup Consumers. + // @type {Map} + this._consumers = new Map(); + this._screenSharing = ScreenShare.create(device); this._screenSharingProducer = null; @@ -174,12 +194,14 @@ export default class RoomClient logger.debug('close()'); - // Leave the mediasoup Room. - this._room.leave(); + this._signalingSocket.close(); - // Close signaling Peer (wait a bit so mediasoup-client can send - // the 'leaveRoom' notification). - setTimeout(() => this._signalingSocket.close(), 250); + // Close mediasoup Transports. + if (this._sendTransport) + this._sendTransport.close(); + + if (this._recvTransport) + this._recvTransport.close(); store.dispatch(stateActions.setRoomState('closed')); } @@ -204,28 +226,61 @@ export default class RoomClient case 'a': // Activate advanced mode { store.dispatch(stateActions.toggleAdvancedMode()); - this.notify('Toggled advanced mode.'); + store.dispatch(requestActions.notify( + { + text : 'Toggled advanced mode.' + })); break; } case '1': // Set democratic view { store.dispatch(stateActions.setDisplayMode('democratic')); - this.notify('Changed layout to democratic view.'); + store.dispatch(requestActions.notify( + { + text : 'Changed layout to democratic view.' + })); break; } case '2': // Set filmstrip view { store.dispatch(stateActions.setDisplayMode('filmstrip')); - this.notify('Changed layout to filmstrip view.'); + store.dispatch(requestActions.notify( + { + text : 'Changed layout to filmstrip view.' + })); break; } case 'm': // Toggle microphone { - this.toggleMic(); - this.notify('Muted/unmuted your microphone.'); + if (this._micProducer) + { + if (this._micProducer.paused) + { + this._micProducer.resume(); + + store.dispatch(requestActions.notify( + { + text : 'Unmuted your microphone.' + })); + } + else + { + this._micProducer.pause(); + + store.dispatch(requestActions.notify( + { + text : 'Muted your microphone.' + })); + } + } + else + { + this.enableMic(); + } + break; } @@ -247,13 +302,16 @@ export default class RoomClient await this._updateAudioDevices(); await this._updateWebcams(); - this.notify('Your devices changed, configure your devices in the settings dialog.'); + store.dispatch(requestActions.notify( + { + text : 'Your devices changed, configure your devices in the settings dialog.' + })); }); } login() { - const url = `/auth/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; + const url = `/auth/login?roomId=${this._roomId}&peerId=${this._peerId}`; this._loginWindow = window.open(url, 'loginWindow'); } @@ -324,17 +382,21 @@ export default class RoomClient } else { - this._signalingSocket.emit(method, data, this.timeoutCallback((err, response) => - { - if (err) + this._signalingSocket.emit( + 'request', + { method, data }, + this.timeoutCallback((err, response) => { - reject(err); - } - else - { - resolve(response); - } - })); + if (err) + { + reject(err); + } + else + { + resolve(response); + } + }) + ); } }); } @@ -345,17 +407,24 @@ export default class RoomClient try { - await this.sendRequest('change-display-name', { displayName }); + await this.sendRequest('changeDisplayName', { displayName }); store.dispatch(stateActions.setDisplayName(displayName)); - this.notify(`Your display name changed to ${displayName}.`); + store.dispatch(requestActions.notify( + { + text : `Your display name changed to ${displayName}.` + })); } catch (error) { logger.error('changeDisplayName() | failed: %o', error); - this.notify('An error occured while changing your display name.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while changing your display name.' + })); // We need to refresh the component for it to render the previous // displayName again. @@ -369,7 +438,7 @@ export default class RoomClient try { - await this.sendRequest('change-profile-picture', { picture }); + await this.sendRequest('changeProfilePicture', { picture }); } catch (error) { @@ -386,13 +455,17 @@ export default class RoomClient store.dispatch( stateActions.addUserMessage(chatMessage.text)); - await this.sendRequest('chat-message', { chatMessage }); + await this.sendRequest('chatMessage', { chatMessage }); } catch (error) { logger.error('sendChatMessage() | failed: %o', error); - this.notify('An error occured while sending chat message.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to send chat message.' + })); } } @@ -402,7 +475,11 @@ export default class RoomClient { if (err) { - return this.notify('An error occurred while saving a file'); + return store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to save file.' + })); } saveAs(blob, file.name); @@ -466,15 +543,20 @@ export default class RoomClient async shareFiles(files) { - this.notify('Creating torrent'); + store.dispatch(requestActions.notify( + { + text : 'Starting file share.' + })); createTorrent(files, (err, torrent) => { if (err) { - return this.notify( - 'An error occured while uploading a file' - ); + return store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to upload file.' + })); } const existingTorrent = this._webTorrent.get(torrent); @@ -494,9 +576,10 @@ export default class RoomClient this._webTorrent.seed(files, (newTorrent) => { - this.notify( - 'Torrent successfully created' - ); + store.dispatch(requestActions.notify( + { + text : 'File successfully shared.' + })); const { displayName, picture } = store.getState().settings; const file = { @@ -525,13 +608,17 @@ export default class RoomClient try { - await this.sendRequest('send-file', { file }); + await this.sendRequest('sendFile', { file }); } catch (error) { logger.error('sendFile() | failed: %o', error); - this.notify('An error occurred while sharing file.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to share file.' + })); } } @@ -545,7 +632,7 @@ export default class RoomClient chatHistory, fileHistory, lastN - } = await this.sendRequest('server-history'); + } = await this.sendRequest('serverHistory'); if (chatHistory.length > 0) { @@ -566,7 +653,7 @@ export default class RoomClient logger.debug('Got lastN'); // Remove our self from list - const index = lastN.indexOf(this._peerName); + const index = lastN.indexOf(this._peerId); lastN.splice(index, 1); @@ -577,20 +664,14 @@ export default class RoomClient { logger.error('getServerHistory() | failed: %o', error); - this.notify('An error occured while getting server history.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to retrieve room history.' + })); } } - toggleMic() - { - logger.debug('toggleMic()'); - - if (this._micProducer.locallyPaused) - this.unmuteMic(); - else - this.muteMic(); - } - async muteMic() { logger.debug('muteMic()'); @@ -603,7 +684,11 @@ export default class RoomClient { logger.error('muteMic() | failed: %o', error); - this.notify('An error occured while accessing your microphone.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to access your microphone.' + })); } } @@ -616,7 +701,7 @@ export default class RoomClient if (this._micProducer) this._micProducer.resume(); else if (this._room.canSend('audio')) - await this._setMicProducer(); + await this.enableMic(); else throw new Error('cannot send audio'); } @@ -624,7 +709,11 @@ export default class RoomClient { logger.error('unmuteMic() | failed: %o', error); - this.notify('An error occured while accessing your microphone.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while accessing your microphone.' + })); } } @@ -635,26 +724,17 @@ export default class RoomClient try { - for (const peer of this._room.peers) + for (const consumer of this._consumers.values()) { - if (spotlights.indexOf(peer.name) > -1) // Resume video for speaker + if (consumer.kind === 'video') { - for (const consumer of peer.consumers) + if (spotlights.indexOf(consumer.appData.peerId) > -1) { - if (consumer.kind !== 'video' || !consumer.supported) - continue; - - await consumer.resume(); + await this._resumeConsumer(consumer); } - } - else // Pause video for everybody else - { - for (const consumer of peer.consumers) + else { - if (consumer.kind !== 'video') - continue; - - await consumer.pause('not-speaker'); + await this._pauseConsumer(consumer); } } } @@ -712,78 +792,6 @@ export default class RoomClient }); } - async enableScreenSharing() - { - logger.debug('enableScreenSharing()'); - - store.dispatch(stateActions.setScreenShareInProgress(true)); - - try - { - await this._setScreenShareProducer(); - } - catch (error) - { - logger.error('enableScreenSharing() | failed: %o', error); - } - - store.dispatch(stateActions.setScreenShareInProgress(false)); - } - - async enableWebcam() - { - logger.debug('enableWebcam()'); - - store.dispatch(stateActions.setWebcamInProgress(true)); - - try - { - await this._setWebcamProducer(); - } - catch (error) - { - logger.error('enableWebcam() | failed: %o', error); - } - - store.dispatch(stateActions.setWebcamInProgress(false)); - } - - async disableScreenSharing() - { - logger.debug('disableScreenSharing()'); - - store.dispatch(stateActions.setScreenShareInProgress(true)); - - try - { - await this._screenSharingProducer.close(); - } - catch (error) - { - logger.error('disableScreenSharing() | failed: %o', error); - } - - store.dispatch(stateActions.setScreenShareInProgress(false)); - } - - async disableWebcam() - { - logger.debug('disableWebcam()'); - - store.dispatch(stateActions.setWebcamInProgress(true)); - - try - { - this._webcamProducer.close(); - } - catch (error) - { - logger.error('disableWebcam() | failed: %o', error); - } - - store.dispatch(stateActions.setWebcamInProgress(false)); - } - async changeAudioDevice(deviceId) { logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); @@ -840,7 +848,7 @@ export default class RoomClient if (volume !== this._micProducer.volume) { this._micProducer.volume = volume; - store.dispatch(stateActions.setPeerVolume(this._peerName, volume)); + store.dispatch(stateActions.setPeerVolume(this._peerId, volume)); } }); @@ -878,6 +886,8 @@ export default class RoomClient if (!device) throw new Error('no webcam devices'); + this._webcamProducer.track.stop(); + logger.debug('changeVideoResolution() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( @@ -891,12 +901,10 @@ export default class RoomClient const track = stream.getVideoTracks()[0]; - const newTrack = await this._webcamProducer.replaceTrack(track); - - track.stop(); + await this._webcamProducer.replaceTrack({ track }); store.dispatch( - stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); + stateActions.setProducerTrack(this._webcamProducer.id, track)); store.dispatch(stateActions.setSelectedWebcamDevice(deviceId)); store.dispatch(stateActions.setVideoResolution(resolution)); @@ -931,6 +939,8 @@ export default class RoomClient 'changeWebcam() | new selected webcam [device:%o]', device); + this._webcamProducer.track.stop(); + logger.debug('changeWebcam() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( @@ -944,12 +954,10 @@ export default class RoomClient const track = stream.getVideoTracks()[0]; - const newTrack = await this._webcamProducer.replaceTrack(track); - - track.stop(); + await this._webcamProducer.replaceTrack({ track }); store.dispatch( - stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); + stateActions.setProducerTrack(this._webcamProducer.id, track)); store.dispatch(stateActions.setSelectedWebcamDevice(deviceId)); @@ -964,52 +972,48 @@ export default class RoomClient stateActions.setWebcamInProgress(false)); } - setSelectedPeer(peerName) + setSelectedPeer(peerId) { - logger.debug('setSelectedPeer() [peerName:"%s"]', peerName); + logger.debug('setSelectedPeer() [peerId:"%s"]', peerId); - this._spotlights.setPeerSpotlight(peerName); + this._spotlights.setPeerSpotlight(peerId); store.dispatch( - stateActions.setSelectedPeer(peerName)); + stateActions.setSelectedPeer(peerId)); } // type: mic/webcam/screen // mute: true/false - modifyPeerConsumer(peerName, type, mute) + async modifyPeerConsumer(peerId, type, mute) { logger.debug( - 'modifyPeerConsumer() [peerName:"%s", type:"%s"]', - peerName, + 'modifyPeerConsumer() [peerId:"%s", type:"%s"]', + peerId, type ); if (type === 'mic') store.dispatch( - stateActions.setPeerAudioInProgress(peerName, true)); + stateActions.setPeerAudioInProgress(peerId, true)); else if (type === 'webcam') store.dispatch( - stateActions.setPeerVideoInProgress(peerName, true)); + stateActions.setPeerVideoInProgress(peerId, true)); else if (type === 'screen') store.dispatch( - stateActions.setPeerScreenInProgress(peerName, true)); + stateActions.setPeerScreenInProgress(peerId, true)); try { - for (const peer of this._room.peers) + for (const consumer of this._consumers.values()) { - if (peer.name === peerName) + if (consumer.appData.peerId === peerId && consumer.appData.source === type) { - for (const consumer of peer.consumers) + if (mute) { - if (consumer.appData.source !== type || !consumer.supported) - continue; - - if (mute) - consumer.pause(`mute-${type}`); - else - consumer.resume(); + await this._pauseConsumer(consumer); } + else + await this._resumeConsumer(consumer); } } } @@ -1020,13 +1024,57 @@ export default class RoomClient if (type === 'mic') store.dispatch( - stateActions.setPeerAudioInProgress(peerName, false)); + stateActions.setPeerAudioInProgress(peerId, false)); else if (type === 'webcam') store.dispatch( - stateActions.setPeerVideoInProgress(peerName, false)); + stateActions.setPeerVideoInProgress(peerId, false)); else if (type === 'screen') store.dispatch( - stateActions.setPeerScreenInProgress(peerName, false)); + stateActions.setPeerScreenInProgress(peerId, false)); + } + + async _pauseConsumer(consumer) + { + logger.debug('_pauseConsumer() [consumer: %o]', consumer); + + if (consumer.paused || consumer.closed) + return; + + try + { + await this.sendRequest('pauseConsumer', { consumerId: consumer.id }); + + consumer.pause(); + + store.dispatch( + stateActions.setConsumerPaused(consumer.id, 'local')); + } + catch (error) + { + logger.error('_pauseConsumer() | failed:%o', error); + } + } + + async _resumeConsumer(consumer) + { + logger.debug('_resumeConsumer() [consumer: %o]', consumer); + + if (!consumer.paused || consumer.closed) + return; + + try + { + await this.sendRequest('resumeConsumer', { consumerId: consumer.id }); + + consumer.resume(); + + store.dispatch( + stateActions.setConsumerResumed(consumer.id, 'local')); + } + catch (error) + { + logger.error('_resumeConsumer() | failed:%o', error); + } } async sendRaiseHandState(state) @@ -1038,7 +1086,7 @@ export default class RoomClient try { - await this.sendRequest('raisehand-message', { raiseHandState: state }); + await this.sendRequest('raiseHand', { raiseHandState: state }); store.dispatch( stateActions.setMyRaiseHandState(state)); @@ -1047,7 +1095,11 @@ export default class RoomClient { logger.error('sendRaiseHandState() | failed: %o', error); - this.notify(`An error occured while ${state ? 'raising' : 'lowering'} hand.`); + store.dispatch(requestActions.notify( + { + type : 'error', + text : `An error occured while ${state ? 'raising' : 'lowering'} hand.` + })); // We need to refresh the component for it to render changed state store.dispatch(stateActions.setMyRaiseHandState(!state)); @@ -1076,18 +1128,11 @@ export default class RoomClient } } - join() + async join() { this._signalingSocket = io(this._signalingUrl); - if (this._device.flag === 'firefox') - ROOM_OPTIONS = Object.assign({ iceTransportPolicy: 'relay' }, ROOM_OPTIONS); - - // mediasoup-client Room instance. - this._room = new mediasoupClient.Room(ROOM_OPTIONS); - this._room.roomId = this._roomId; - - this._spotlights = new Spotlights(this._maxSpotlights, this._room); + this._spotlights = new Spotlights(this._maxSpotlights, this._signalingSocket); store.dispatch(stateActions.setRoomState('connecting')); @@ -1096,31 +1141,29 @@ export default class RoomClient logger.debug('signaling Peer "connect" event'); }); - this._signalingSocket.on('room-ready', () => - { - logger.debug('signaling Peer "room-ready" event'); - - this._joinRoom(); - }); - - this._signalingSocket.on('room-locked', () => - { - logger.debug('signaling Peer "room-locked" event'); - - store.dispatch(stateActions.setRoomLockedOut()); - }); - this._signalingSocket.on('disconnect', () => { logger.warn('signaling Peer "disconnect" event'); - this.notify('You are disconnected.'); + store.dispatch(requestActions.notify( + { + text : 'You are disconnected.' + })); - // Leave Room. - try { this._room.remoteClose({ cause: 'signaling disconnected' }); } - catch (error) {} + // Close mediasoup Transports. + if (this._sendTransport) + { + this._sendTransport.close(); + this._sendTransport = null; + } - store.dispatch(stateActions.setRoomState('connecting')); + if (this._recvTransport) + { + this._recvTransport.close(); + this._recvTransport = null; + } + + store.dispatch(stateActions.setRoomState('closed')); }); this._signalingSocket.on('close', () => @@ -1133,153 +1176,386 @@ export default class RoomClient this.close(); }); - this._signalingSocket.on('mediasoup-notification', (data) => + this._signalingSocket.on('request', async (request, cb) => { - const notification = data; + logger.debug( + 'socket "request" event [method:%s, data:%o]', + request.method, request.data); - this._room.receiveNotification(notification); - }); - - this._signalingSocket.on('lock-room', ({ peerName }) => - { - store.dispatch( - stateActions.setRoomLocked()); - - const peer = this._room.getPeerByName(peerName); - - if (peer) + switch (request.method) { - this.notify(`${peer.appData.displayName} locked the room.`); + case 'newConsumer': + { + const { + peerId, + producerId, + id, + kind, + rtpParameters, + type, + appData, + producerPaused + } = request.data; + + let codecOptions; + + if (kind === 'audio') + { + codecOptions = + { + opusStereo : 1 + }; + } + + const consumer = await this._recvTransport.consume( + { + id, + producerId, + kind, + rtpParameters, + codecOptions, + appData : { ...appData, peerId } // Trick. + }); + + // Store in the map. + this._consumers.set(consumer.id, consumer); + + consumer.on('transportclose', () => + { + this._consumers.delete(consumer.id); + }); + + const { spatialLayers, temporalLayers } = + mediasoupClient.parseScalabilityMode( + consumer.rtpParameters.encodings[0].scalabilityMode); + + store.dispatch(stateActions.addConsumer( + { + id : consumer.id, + peerId : peerId, + kind : kind, + type : type, + locallyPaused : false, + remotelyPaused : producerPaused, + rtpParameters : consumer.rtpParameters, + source : consumer.appData.source, + spatialLayers : spatialLayers, + temporalLayers : temporalLayers, + preferredSpatialLayer : spatialLayers - 1, + preferredTemporalLayer : temporalLayers - 1, + codec : consumer.rtpParameters.codecs[0].mimeType.split('/')[1], + track : consumer.track + }, + peerId)); + + // We are ready. Answer the request so the server will + // resume this Consumer (which was paused for now). + cb(null); + + if (consumer.kind === 'audio') + { + const stream = new MediaStream(); + + stream.addTrack(consumer.track); + if (!stream.getAudioTracks()[0]) + throw new Error('request.newConsumer | given stream has no audio track'); + + consumer.hark = hark(stream, { play: false }); + + // eslint-disable-next-line no-unused-vars + consumer.hark.on('volume_change', (dBs, threshold) => + { + // The exact formula to convert from dBs (-100..0) to linear (0..1) is: + // Math.pow(10, dBs / 20) + // However it does not produce a visually useful output, so let exagerate + // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to + // minimize component renderings. + let volume = Math.round(Math.pow(10, dBs / 85) * 10); + + if (volume === 1) + volume = 0; + + volume = Math.round(volume); + + if (volume !== consumer.volume) + { + consumer.volume = volume; + + store.dispatch(stateActions.setPeerVolume(consumer.peerId, volume)); + } + }); + } + + break; + } + + default: + { + logger.error('unknown request.method "%s"', request.method); + + cb(500, `unknown request.method "${request.method}"`); + } } }); - this._signalingSocket.on('unlock-room', ({ peerName }) => + this._signalingSocket.on('notification', async (notification) => { - store.dispatch( - stateActions.setRoomUnLocked()); + logger.debug( + 'socket "notification" event [method:%s, data:%o]', + notification.method, notification.data); - const peer = this._room.getPeerByName(peerName); - - if (peer) + switch (notification.method) { - this.notify(`${peer.appData.displayName} unlocked the room.`); - } - }); + case 'roomReady': + { + await this._joinRoom(); - this._signalingSocket.on('active-speaker', ({ peerName }) => - { - store.dispatch( - stateActions.setRoomActiveSpeaker(peerName)); + break; + } - if (peerName && peerName !== this._peerName) - this._spotlights.handleActiveSpeaker(peerName); - }); + case 'roomLocked': + { + store.dispatch(stateActions.setRoomLockedOut()); - this._signalingSocket.on('display-name-changed', ({ peerName, displayName: name }) => - { - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); + break; + } - if (!peer) - { - logger.error('peer not found'); + case 'lockRoom': + { + store.dispatch( + stateActions.setRoomLocked()); - return; - } + store.dispatch(requestActions.notify( + { + text : 'Room is now locked.' + })); - const oldDisplayName = peer.appData.name; + break; + } - peer.appData.displayName = name; + case 'unlockRoom': + { + store.dispatch( + stateActions.setRoomUnLocked()); + + store.dispatch(requestActions.notify( + { + text : 'Room is now unlocked.' + })); - store.dispatch( - stateActions.setPeerDisplayName(name, peerName)); + break; + } - this.notify(`${oldDisplayName} changed their display name to ${name}.`); - }); + case 'activeSpeaker': + { + const { peerId } = notification.data; - this._signalingSocket.on('profile-picture-changed', ({ peerName, picture }) => - { - store.dispatch(stateActions.setPeerPicture(peerName, picture)); - }); + store.dispatch( + stateActions.setRoomActiveSpeaker(peerId)); + + if (peerId && peerId !== this._peerId) + this._spotlights.handleActiveSpeaker(peerId); - // This means: server wants to change MY user information - this._signalingSocket.on('auth', (data) => - { - logger.debug('got auth event from server', data); + break; + } - this.changeDisplayName(data.name); + case 'changeDisplayName': + { + const { peerId, displayName, oldDisplayName } = notification.data; - this.changeProfilePicture(data.picture); - store.dispatch(stateActions.setPicture(data.picture)); - store.dispatch(stateActions.loggedIn()); + store.dispatch( + stateActions.setPeerDisplayName(displayName, peerId)); - this.notify('You are logged in.'); + store.dispatch(requestActions.notify( + { + text : `${oldDisplayName} is now ${displayName}` + })); - this.closeLoginWindow(); - }); + break; + } - this._signalingSocket.on('raisehand-message', (data) => - { - const { peerName, raiseHandState } = data; + case 'changeProfilePicture': + { + const { peerId, picture } = notification.data; - logger.debug('Got raiseHandState from "%s"', peerName); + store.dispatch(stateActions.setPeerPicture(peerId, picture)); - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); + break; + } - if (!peer) - { - logger.error('peer not found'); + case 'auth': + { + const { displayName, picture } = notification.data; - return; - } + this.changeDisplayName(displayName); - this.notify(`${peer.appData.displayName} ${raiseHandState ? 'raised' : 'lowered'} their hand.`); + this.changeProfilePicture(picture); + store.dispatch(stateActions.setPicture(picture)); + store.dispatch(stateActions.loggedIn()); - store.dispatch( - stateActions.setPeerRaiseHandState(peerName, raiseHandState)); - }); + store.dispatch(requestActions.notify( + { + text : 'You are logged in.' + })); - this._signalingSocket.on('chat-message-receive', (data) => - { - const { peerName, chatMessage } = data; + this.closeLoginWindow(); - logger.debug('Got chat from "%s"', peerName); + break; + } - store.dispatch( - stateActions.addResponseMessage({ ...chatMessage, peerName })); + case 'chatMessage': + { + const { peerId, chatMessage } = notification.data; - if (!store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'chat')) // Make sound - { - this._soundNotification(); - } - }); + store.dispatch( + stateActions.addResponseMessage({ ...chatMessage, peerId })); - this._signalingSocket.on('file-receive', (data) => - { - const { peerName, file } = data; + if ( + !store.getState().toolarea.toolAreaOpen || + (store.getState().toolarea.toolAreaOpen && + store.getState().toolarea.currentToolTab !== 'chat') + ) // Make sound + { + this._soundNotification(); + } - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); + break; + } - if (!peer) - { - logger.error('peer not found'); + case 'sendFile': + { + const { file } = notification.data; - return; - } + store.dispatch(stateActions.addFile(file)); - store.dispatch(stateActions.addFile(file)); + store.dispatch(requestActions.notify( + { + text : 'New file available.' + })); - this.notify(`${peer.appData.displayName} shared a file.`); + if ( + !store.getState().toolarea.toolAreaOpen || + (store.getState().toolarea.toolAreaOpen && + store.getState().toolarea.currentToolTab !== 'files') + ) // Make sound + { + this._soundNotification(); + } - if (!store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'files')) // Make sound - { - this._soundNotification(); + break; + } + + case 'producerScore': + { + const { producerId, score } = notification.data; + + store.dispatch( + stateActions.setProducerScore(producerId, score)); + + break; + } + + case 'newPeer': + { + const { id, displayName, device } = notification.data; + + store.dispatch( + stateActions.addPeer({ id, displayName, device, consumers: [] })); + + store.dispatch(requestActions.notify( + { + text : `${displayName} joined the room.` + })); + + break; + } + + case 'peerClosed': + { + const { peerId } = notification.data; + + store.dispatch( + stateActions.removePeer(peerId)); + + break; + } + + case 'consumerClosed': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + consumer.close(); + this._consumers.delete(consumerId); + + const { peerId } = consumer.appData; + + store.dispatch( + stateActions.removeConsumer(consumerId, peerId)); + + break; + } + + case 'consumerPaused': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch( + stateActions.setConsumerPaused(consumerId, 'remote')); + + break; + } + + case 'consumerResumed': + { + const { consumerId } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch( + stateActions.setConsumerResumed(consumerId, 'remote')); + + break; + } + + case 'consumerLayersChanged': + { + const { consumerId, spatialLayer, temporalLayer } = notification.data; + const consumer = this._consumers.get(consumerId); + + if (!consumer) + break; + + store.dispatch(stateActions.setConsumerCurrentLayers( + consumerId, spatialLayer, temporalLayer)); + + break; + } + + case 'consumerScore': + { + const { consumerId, score } = notification.data; + + store.dispatch( + stateActions.setConsumerScore(consumerId, score)); + + break; + } + + default: + { + logger.error( + 'unknown notification.method "%s"', notification.method); + } } }); } @@ -1288,113 +1564,152 @@ export default class RoomClient { logger.debug('_joinRoom()'); - // NOTE: We allow rejoining (room.join()) the same mediasoup Room when - // WebSocket re-connects, so we must clean existing event listeners. Otherwise - // they will be called twice after the reconnection. - this._room.removeAllListeners(); - - this._room.on('close', (originator, appData) => - { - if (originator === 'remote') - { - logger.warn('mediasoup Peer/Room remotely closed [appData:%o]', appData); - - store.dispatch(stateActions.setRoomState('closed')); - - return; - } - }); - - this._room.on('request', (request, callback, errback) => - { - logger.debug( - 'sending mediasoup request [method:%s]:%o', request.method, request); - - this.sendRequest('mediasoup-request', request) - .then(callback) - .catch(errback); - }); - - this._room.on('notify', (notification) => - { - logger.debug( - 'sending mediasoup notification [method:%s]:%o', - notification.method, notification); - - this.sendRequest('mediasoup-notification', notification) - .catch((error) => - { - logger.warn('could not send mediasoup notification:%o', error); - }); - }); - - this._room.on('newpeer', (peer) => - { - logger.debug( - 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); - - this._soundNotification(); - - if (this._doneJoining) - { - this._handlePeer(peer); - } - }); + const { displayName } = store.getState().settings; try { - const { displayName } = store.getState().settings; + this._mediasoupDevice = new mediasoupClient.Device(); - await this._room.join( - this._peerName, - { - displayName : displayName, - device : this._device - } - ); + const routerRtpCapabilities = + await this.sendRequest('getRouterRtpCapabilities'); - store.dispatch( - stateActions.setFileSharingSupported(this._torrentSupport)); + await this._mediasoupDevice.load({ routerRtpCapabilities }); - this._sendTransport = - this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' }); - - this._sendTransport.on('close', (originator) => + if (this._produce) { - logger.debug( - 'Transport "close" event [originator:%s]', originator); - }); + const transportInfo = await this.sendRequest( + 'createWebRtcTransport', + { + forceTcp : this._forceTcp, + producing : true, + consuming : false + }); - // Create Transport for receiving. - this._recvTransport = - this._room.createTransport('recv', { media: 'RECV' }); + const { + id, + iceParameters, + iceCandidates, + dtlsParameters + } = transportInfo; - this._recvTransport.on('close', (originator) => + this._sendTransport = this._mediasoupDevice.createSendTransport( + { + id, + iceParameters, + iceCandidates, + dtlsParameters + }); + + this._sendTransport.on( + 'connect', ({ dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow + { + this.sendRequest( + 'connectWebRtcTransport', + { + transportId : this._sendTransport.id, + dtlsParameters + }) + .then(callback) + .catch(errback); + }); + + this._sendTransport.on( + 'produce', ({ kind, rtpParameters, appData }, callback, errback) => + { + this.sendRequest( + 'produce', + { + transportId : this._sendTransport.id, + kind, + rtpParameters, + appData + }) + .then(callback) + .catch(errback); + }); + } + + if (this._consume) { - logger.debug( - 'receiving Transport "close" event [originator:%s]', originator); - }); + const transportInfo = await this.sendRequest( + 'createWebRtcTransport', + { + forceTcp : this._forceTcp, + producing : false, + consuming : true + }); + + const { + id, + iceParameters, + iceCandidates, + dtlsParameters + } = transportInfo; + + this._recvTransport = this._mediasoupDevice.createRecvTransport( + { + id, + iceParameters, + iceCandidates, + dtlsParameters + }); + + this._recvTransport.on( + 'connect', ({ dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow + { + this.sendRequest( + 'connectWebRtcTransport', + { + transportId : this._recvTransport.id, + dtlsParameters + }) + .then(callback) + .catch(errback); + }); + } // Set our media capabilities. store.dispatch(stateActions.setMediaCapabilities( { - canSendMic : this._room.canSend('audio'), - canSendWebcam : this._room.canSend('video') - })); - store.dispatch(stateActions.setScreenCapabilities( - { - canShareScreen : this._room.canSend('video') && + canSendMic : this._mediasoupDevice.canProduce('audio'), + canSendWebcam : this._mediasoupDevice.canProduce('video'), + canShareScreen : this._mediasoupDevice.canProduce('video') && this._screenSharing.isScreenShareAvailable(), - needExtension : this._screenSharing.needExtension() + needExtension : this._screenSharing.needExtension(), + canShareFiles : this._torrentSupport })); + const { peers } = await this.sendRequest( + 'join', + { + displayName : displayName, + device : this._device, + rtpCapabilities : this._consume + ? this._mediasoupDevice.rtpCapabilities + : undefined + }); + + for (const peer of peers) + { + store.dispatch( + stateActions.addPeer({ ...peer, consumers: [] })); + } + + this._spotlights.addPeers(peers); + + this._spotlights.on('spotlights-updated', (spotlights) => + { + store.dispatch(stateActions.setSpotlights(spotlights)); + this.updateSpotlights(spotlights); + }); + // Don't produce if explicitely requested to not to do it. if (this._produce) { - if (this._room.canSend('audio')) - await this._setMicProducer(); + if (this._mediasoupDevice.canProduce('audio')) + await this.enableMic(); - if (this._room.canSend('video')) + if (this._mediasoupDevice.canProduce('video')) await this.enableWebcam(); } @@ -1405,22 +1720,10 @@ export default class RoomClient this.getServerHistory(); - this.notify('You have joined the room.'); - - this._spotlights.on('spotlights-updated', (spotlights) => - { - store.dispatch(stateActions.setSpotlights(spotlights)); - this.updateSpotlights(spotlights); - }); - - const peers = this._room.peers; - - for (const peer of peers) - { - this._handlePeer(peer, { notify: false }); - } - - this._doneJoining = true; + store.dispatch(requestActions.notify( + { + text : 'You have joined the room.' + })); this._spotlights.start(); } @@ -1428,7 +1731,11 @@ export default class RoomClient { logger.error('_joinRoom() failed:%o', error); - this.notify('An error occured while joining the room.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to join the room.' + })); this.close(); } @@ -1440,14 +1747,24 @@ export default class RoomClient try { - await this.sendRequest('lock-room'); + await this.sendRequest('lockRoom'); store.dispatch( stateActions.setRoomLocked()); - this.notify('You locked the room.'); + + store.dispatch(requestActions.notify( + { + text : 'You locked the room.' + })); } catch (error) { + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to lock the room.' + })); + logger.error('lockRoom() | failed: %o', error); } } @@ -1458,27 +1775,44 @@ export default class RoomClient try { - await this.sendRequest('unlock-room'); + await this.sendRequest('unlockRoom'); store.dispatch( stateActions.setRoomUnLocked()); - this.notify('You unlocked the room.'); + + store.dispatch(requestActions.notify( + { + text : 'You unlocked the room.' + })); } catch (error) { + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Unable to unlock the room.' + })); + logger.error('unlockRoom() | failed: %o', error); } } - async _setMicProducer() + async enableMic() { - if (!this._room.canSend('audio')) - throw new Error('cannot send audio'); - if (this._micProducer) - throw new Error('mic Producer already exists'); + return; - let producer; + if (!this._mediasoupDevice.canProduce('audio')) + { + logger.error('enableMic() | cannot produce audio'); + + return; + } + + let track; + + store.dispatch( + stateActions.setAudioInProgress(true)); try { @@ -1490,10 +1824,10 @@ export default class RoomClient throw new Error('no audio devices'); logger.debug( - '_setMicProducer() | new selected audio device [device:%o]', + 'enableMic() | new selected audio device [device:%o]', device); - logger.debug('_setMicProducer() | calling getUserMedia()'); + logger.debug('enableMic() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { @@ -1503,76 +1837,60 @@ export default class RoomClient } ); - const track = stream.getAudioTracks()[0]; + track = stream.getAudioTracks()[0]; - producer = this._room.createProducer(track, null, { source: 'mic' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._micProducer = producer; + this._micProducer = await this._sendTransport.produce( + { + track, + codecOptions : + { + opusStereo : 1, + opusDtx : 1 + }, + appData : + { source: 'mic' } + }); store.dispatch(stateActions.addProducer( { - id : producer.id, - source : 'mic', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name + id : this._micProducer.id, + source : 'mic', + paused : this._micProducer.paused, + track : this._micProducer.track, + rtpParameters : this._micProducer.rtpParameters, + codec : this._micProducer.rtpParameters.codecs[0].mimeType.split('/')[1] })); store.dispatch(stateActions.setSelectedAudioDevice(deviceId)); await this._updateAudioDevices(); - producer.on('close', (originator) => + this._micProducer.on('transportclose', () => { - logger.debug( - 'mic Producer "close" event [originator:%s]', originator); - this._micProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); }); - producer.on('pause', (originator) => + this._micProducer.on('trackended', () => { - logger.debug( - 'mic Producer "pause" event [originator:%s]', originator); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Microphone disconnected!' + })); - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'mic Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('mic Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('mic Producer "unhandled" event'); + this.disableMic() + .catch(() => {}); }); const harkStream = new MediaStream(); - harkStream.addTrack(producer.track); + harkStream.addTrack(track); if (!harkStream.getAudioTracks()[0]) - throw new Error('_setMicProducer(): given stream has no audio track'); - producer.hark = hark(harkStream, { play: false }); + throw new Error('enableMic(): given stream has no audio track'); + this._micProducer.hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars - producer.hark.on('volume_change', (dBs, threshold) => + this._micProducer.hark.on('volume_change', (dBs, threshold) => { // The exact formula to convert from dBs (-100..0) to linear (0..1) is: // Math.pow(10, dBs / 20) @@ -1584,10 +1902,10 @@ export default class RoomClient if (volume === 1) volume = 0; volume = Math.round(volume); - if (volume !== producer.volume) + if (this._micProducer && volume !== this._micProducer.volume) { - producer.volume = volume; - store.dispatch(stateActions.setPeerVolume(this._peerName, volume)); + this._micProducer.volume = volume; + store.dispatch(stateActions.setPeerVolume(this._peerId, volume)); } }); @@ -1602,22 +1920,71 @@ export default class RoomClient } catch (error) { - logger.error('_setMicProducer() failed:%o', error); + logger.error('enableMic() failed:%o', error); - this.notify('An error occured while accessing your microphone.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while accessing your microphone.' + })); - if (producer) - producer.close(); + if (track) + track.stop(); } + + store.dispatch( + stateActions.setAudioInProgress(false)); } - async _setScreenShareProducer() + async disableMic() { - if (!this._room.canSend('video')) - throw new Error('cannot send screen'); + logger.debug('disableMic()'); - let producer; + if (!this._micProducer) + return; + + store.dispatch(stateActions.setAudioInProgress(true)); + + this._micProducer.close(); + + store.dispatch( + stateActions.removeProducer(this._micProducer.id)); + + try + { + await this.sendRequest( + 'closeProducer', { producerId: this._micProducer.id }); + } + catch (error) + { + store.dispatch(requestActions.notify( + { + type : 'error', + text : `Error closing server-side mic Producer: ${error}` + })); + } + + this._micProducer = null; + + store.dispatch(stateActions.setAudioInProgress(false)); + } + + async enableScreenSharing() + { + if (this._screenSharingProducer) + return; + + if (!this._mediasoupDevice.canProduce('video')) + { + logger.error('enableScreenSharing() | cannot produce video'); + + return; + } + + let track; + + store.dispatch(stateActions.setScreenShareInProgress(true)); try { @@ -1627,7 +1994,7 @@ export default class RoomClient if (!available) throw new Error('screen sharing not available'); - logger.debug('_setScreenShareProducer() | calling getUserMedia()'); + logger.debug('enableScreenSharing() | calling getUserMedia()'); const stream = await this._screenSharing.start({ width : 1280, @@ -1635,105 +2002,132 @@ export default class RoomClient frameRate : 3 }); - const track = stream.getVideoTracks()[0]; + track = stream.getVideoTracks()[0]; - producer = this._room.createProducer( - track, { simulcast: false }, { source: 'screen' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._screenSharingProducer = producer; + if (this._useSimulcast) + { + this._screenSharingProducer = await this._sendTransport.produce( + { + track, + encodings : VIDEO_ENCODINGS, + codecOptions : + { + videoGoogleStartBitrate : 1000 + }, + appData : + { + source : 'screen' + } + }); + } + else + { + this._screenSharingProducer = await this._sendTransport.produce({ + track, + appData : + { + source : 'screen' + } + }); + } store.dispatch(stateActions.addProducer( { - id : producer.id, - source : 'screen', - deviceLabel : 'screen', - type : 'screen', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name + id : this._screenSharingProducer.id, + deviceLabel : 'screen', + source : 'screen', + paused : this._screenSharingProducer.paused, + track : this._screenSharingProducer.track, + rtpParameters : this._screenSharingProducer.rtpParameters, + codec : this._screenSharingProducer.rtpParameters.codecs[0].mimeType.split('/')[1] })); - producer.on('close', (originator) => + this._screenSharingProducer.on('transportclose', () => { - logger.debug( - 'webcam Producer "close" event [originator:%s]', originator); - this._screenSharingProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); }); - producer.on('trackended', (originator) => + this._screenSharingProducer.on('trackended', () => { - logger.debug( - 'webcam Producer "trackended" event [originator:%s]', originator); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Screen sharing disconnected!' + })); - this.disableScreenSharing(); + this.disableScreenSharing() + .catch(() => {}); }); - producer.on('pause', (originator) => - { - logger.debug( - 'webcam Producer "pause" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'webcam Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('webcam Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('webcam Producer "unhandled" event'); - }); - - logger.debug('_setScreenShareProducer() succeeded'); + logger.debug('enableScreenSharing() succeeded'); } catch (error) { - logger.error('_setScreenShareProducer() failed:%o', error); + logger.error('enableScreenSharing() failed: %o', error); - if (error.name === 'NotAllowedError') // Request to share denied by user - { - this.notify('Request to start sharing your screen was denied.'); - } - else // Some other error - { - this.notify('An error occured while starting to share your screen.'); - } + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while accessing your camera.' + })); - if (producer) - producer.close(); - - throw error; + if (track) + track.stop(); } + + store.dispatch(stateActions.setScreenShareInProgress(false)); } - async _setWebcamProducer() + async disableScreenSharing() + { + logger.debug('disableScreenSharing()'); + + if (!this._screenSharingProducer) + return; + + store.dispatch(stateActions.setScreenShareInProgress(true)); + + this._screenSharingProducer.close(); + + store.dispatch( + stateActions.removeProducer(this._screenSharingProducer.id)); + + try + { + await this.sendRequest( + 'closeProducer', { producerId: this._screenSharingProducer.id }); + } + catch (error) + { + store.dispatch(requestActions.notify( + { + type : 'error', + text : `Error closing server-side screen Producer: ${error}` + })); + } + + this._screenSharingProducer = null; + + store.dispatch(stateActions.setScreenShareInProgress(false)); + } + + async enableWebcam() { - if (!this._room.canSend('video')) - throw new Error('cannot send video'); if (this._webcamProducer) - throw new Error('webcam Producer already exists'); + return; - let producer; + if (!this._mediasoupDevice.canProduce('video')) + { + logger.error('enableWebcam() | cannot produce video'); + + return; + } + + let track; + + store.dispatch( + stateActions.setWebcamInProgress(true)); try { @@ -1760,66 +2154,65 @@ export default class RoomClient } }); - const track = stream.getVideoTracks()[0]; + track = stream.getVideoTracks()[0]; - producer = this._room.createProducer( - track, { simulcast: this._useSimulcast }, { source: 'webcam' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._webcamProducer = producer; + if (this._useSimulcast) + { + this._webcamProducer = await this._sendTransport.produce( + { + track, + encodings : VIDEO_ENCODINGS, + codecOptions : + { + videoGoogleStartBitrate : 1000 + }, + appData : + { + source : 'webcam' + } + }); + } + else + { + this._webcamProducer = await this._sendTransport.produce({ + track, + appData : + { + source : 'webcam' + } + }); + } store.dispatch(stateActions.addProducer( { - id : producer.id, - source : 'webcam', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name + id : this._webcamProducer.id, + deviceLabel : device.label, + source : 'webcam', + paused : this._webcamProducer.paused, + track : this._webcamProducer.track, + rtpParameters : this._webcamProducer.rtpParameters, + codec : this._webcamProducer.rtpParameters.codecs[0].mimeType.split('/')[1] })); store.dispatch(stateActions.setSelectedWebcamDevice(deviceId)); await this._updateWebcams(); - producer.on('close', (originator) => + this._webcamProducer.on('transportclose', () => { - logger.debug( - 'webcam Producer "close" event [originator:%s]', originator); - this._webcamProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); }); - producer.on('pause', (originator) => + this._webcamProducer.on('trackended', () => { - logger.debug( - 'webcam Producer "pause" event [originator:%s]', originator); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'Webcam disconnected!' + })); - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'webcam Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('webcam Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('webcam Producer "unhandled" event'); + this.disableWebcam() + .catch(() => {}); }); logger.debug('_setWebcamProducer() succeeded'); @@ -1828,13 +2221,51 @@ export default class RoomClient { logger.error('_setWebcamProducer() failed:%o', error); - this.notify('An error occured while accessing your camera.'); + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while accessing your camera.' + })); - if (producer) - producer.close(); - - throw error; + if (track) + track.stop(); } + + store.dispatch( + stateActions.setWebcamInProgress(false)); + } + + async disableWebcam() + { + logger.debug('disableWebcam()'); + + if (!this._webcamProducer) + return; + + store.dispatch(stateActions.setWebcamInProgress(true)); + + this._webcamProducer.close(); + + store.dispatch( + stateActions.removeProducer(this._webcamProducer.id)); + + try + { + await this.sendRequest( + 'closeProducer', { producerId: this._webcamProducer.id }); + } + catch (error) + { + store.dispatch(requestActions.notify( + { + type : 'error', + text : `Error closing server-side webcam Producer: ${error}` + })); + } + + this._webcamProducer = null; + + store.dispatch(stateActions.setWebcamInProgress(false)); } async _updateAudioDevices() @@ -1950,167 +2381,4 @@ export default class RoomClient logger.error('_getWebcamDeviceId() failed:%o', error); } } - - _handlePeer(peer, { notify = true } = {}) - { - const displayName = peer.appData.displayName; - - store.dispatch(stateActions.addPeer( - { - name : peer.name, - displayName : displayName, - device : peer.appData.device, - raiseHandState : peer.appData.raiseHandState, - consumers : [] - })); - - if (notify) - { - this.notify(`${displayName} joined the room.`); - } - - for (const consumer of peer.consumers) - { - this._handleConsumer(consumer); - } - - peer.on('close', (originator) => - { - logger.debug( - 'peer "close" event [name:"%s", originator:%s]', - peer.name, originator); - - store.dispatch(stateActions.removePeer(peer.name)); - - if (this._room.joined) - { - this.notify(`${displayName} left the room.`); - } - }); - - peer.on('newconsumer', (consumer) => - { - logger.debug( - 'peer "newconsumer" event [name:"%s", id:%s, consumer:%o]', - peer.name, consumer.id, consumer); - - this._handleConsumer(consumer); - }); - } - - _handleConsumer(consumer) - { - const codec = consumer.rtpParameters.codecs[0]; - - store.dispatch(stateActions.addConsumer( - { - id : consumer.id, - peerName : consumer.peer.name, - source : consumer.appData.source, - supported : consumer.supported, - locallyPaused : consumer.locallyPaused, - remotelyPaused : consumer.remotelyPaused, - track : null, - codec : codec ? codec.name : null - }, - consumer.peer.name) - ); - - consumer.on('close', (originator) => - { - logger.debug( - 'consumer "close" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.removeConsumer( - consumer.id, consumer.peer.name)); - }); - - consumer.on('handled', (originator) => - { - logger.debug( - 'consumer "handled" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - if (consumer.kind === 'audio') - { - const stream = new MediaStream(); - - stream.addTrack(consumer.track); - if (!stream.getAudioTracks()[0]) - throw new Error('consumer.on("handled" | given stream has no audio track'); - - consumer.hark = hark(stream, { play: false }); - - // eslint-disable-next-line no-unused-vars - consumer.hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exagerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - volume = Math.round(volume); - if (volume !== consumer.volume) - { - consumer.volume = volume; - store.dispatch(stateActions.setPeerVolume(consumer.peer.name, volume)); - } - }); - } - }); - - consumer.on('pause', (originator) => - { - logger.debug( - 'consumer "pause" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.setConsumerPaused(consumer.id, originator)); - }); - - consumer.on('resume', (originator) => - { - logger.debug( - 'consumer "resume" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.setConsumerResumed(consumer.id, originator)); - }); - - consumer.on('effectiveprofilechange', (profile) => - { - logger.debug( - 'consumer "effectiveprofilechange" event [id:%s, consumer:%o, profile:%s]', - consumer.id, consumer, profile); - - store.dispatch(stateActions.setConsumerEffectiveProfile(consumer.id, profile)); - }); - - // Receive the consumer (if we can). - if (consumer.supported) - { - if (consumer.kind === 'video' && - !this._spotlights.peerInSpotlights(consumer.peer.name)) - { // Start paused - logger.debug( - 'consumer paused by default'); - consumer.pause('not-speaker'); - } - - consumer.receive(this._recvTransport) - .then((track) => - { - store.dispatch(stateActions.setConsumerTrack(consumer.id, track)); - }) - .catch((error) => - { - logger.error( - 'unexpected error while receiving a new Consumer:%o', error); - }); - } - } } diff --git a/app/src/Spotlights.js b/app/src/Spotlights.js index 8b91e99..c683c34 100644 --- a/app/src/Spotlights.js +++ b/app/src/Spotlights.js @@ -5,11 +5,11 @@ const logger = new Logger('Spotlight'); export default class Spotlights extends EventEmitter { - constructor(maxSpotlights, room) + constructor(maxSpotlights, signalingSocket) { super(); - this._room = room; + this._signalingSocket = signalingSocket; this._maxSpotlights = maxSpotlights; this._peerList = []; this._selectedSpotlights = []; @@ -19,24 +19,25 @@ export default class Spotlights extends EventEmitter start() { - const peers = this._room.peers; - - for (const peer of peers) - { - this._handlePeer(peer); - } - - this._handleRoom(); + this._handleSignaling(); this._started = true; this._spotlightsUpdated(); } - peerInSpotlights(peerName) + addPeers(peers) + { + for (const peer of peers) + { + this._newPeer(peer.id); + } + } + + peerInSpotlights(peerId) { if (this._started) { - return this._currentSpotlights.indexOf(peerName) !== -1; + return this._currentSpotlights.indexOf(peerId) !== -1; } else { @@ -44,11 +45,11 @@ export default class Spotlights extends EventEmitter } } - setPeerSpotlight(peerName) + setPeerSpotlight(peerId) { - logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName); + logger.debug('setPeerSpotlight() [peerId:"%s"]', peerId); - const index = this._selectedSpotlights.indexOf(peerName); + const index = this._selectedSpotlights.indexOf(peerId); if (index !== -1) { @@ -56,13 +57,13 @@ export default class Spotlights extends EventEmitter } else { - this._selectedSpotlights = [ peerName ]; + this._selectedSpotlights = [ peerId ]; } /* if (index === -1) // We don't have this peer in the list, adding { - this._selectedSpotlights.push(peerName); + this._selectedSpotlights.push(peerId); } else // We have this peer, remove { @@ -74,16 +75,65 @@ export default class Spotlights extends EventEmitter this._spotlightsUpdated(); } - _handleRoom() + _handleSignaling() { - this._room.on('newpeer', (peer) => + this._signalingSocket.on('notification', (notification) => { - logger.debug( - 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); - this._handlePeer(peer); + if (notification.method === 'newPeer') + { + const { id } = notification.data; + + this._newPeer(id); + } + + if (notification.method === 'peerClosed') + { + const { peerId } = notification.data; + + this._closePeer(peerId); + } }); } + _newPeer(id) + { + logger.debug( + 'room "newpeer" event [id: "%s"]', id); + + if (this._peerList.indexOf(id) === -1) // We don't have this peer in the list + { + logger.debug('_handlePeer() | adding peer [peerId: "%s"]', id); + + this._peerList.push(id); + + if (this._started) + this._spotlightsUpdated(); + } + } + + _closePeer(id) + { + logger.debug( + 'room "peerClosed" event [peerId:%o]', id); + + let index = this._peerList.indexOf(id); + + if (index !== -1) // We have this peer in the list, remove + { + this._peerList.splice(index, 1); + } + + index = this._selectedSpotlights.indexOf(id); + + if (index !== -1) // We have this peer in the list, remove + { + this._selectedSpotlights.splice(index, 1); + } + + if (this._started) + this._spotlightsUpdated(); + } + addSpeakerList(speakerList) { this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ]; @@ -92,49 +142,16 @@ export default class Spotlights extends EventEmitter this._spotlightsUpdated(); } - _handlePeer(peer) + handleActiveSpeaker(peerId) { - logger.debug('_handlePeer() [peerName:"%s"]', peer.name); + logger.debug('handleActiveSpeaker() [peerId:"%s"]', peerId); - if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list - { - peer.on('close', () => - { - let index = this._peerList.indexOf(peer.name); - - if (index !== -1) // We have this peer in the list, remove - { - this._peerList.splice(index, 1); - } - - index = this._selectedSpotlights.indexOf(peer.name); - - if (index !== -1) // We have this peer in the list, remove - { - this._selectedSpotlights.splice(index, 1); - } - - this._spotlightsUpdated(); - }); - - logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name); - - this._peerList.push(peer.name); - - this._spotlightsUpdated(); - } - } - - handleActiveSpeaker(peerName) - { - logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName); - - const index = this._peerList.indexOf(peerName); + const index = this._peerList.indexOf(peerId); if (index > -1) { this._peerList.splice(index, 1); - this._peerList = [ peerName ].concat(this._peerList); + this._peerList = [ peerId ].concat(this._peerList); this._spotlightsUpdated(); } diff --git a/app/src/actions/stateActions.js b/app/src/actions/stateActions.js index d81d433..26d0baf 100644 --- a/app/src/actions/stateActions.js +++ b/app/src/actions/stateActions.js @@ -14,11 +14,11 @@ export const setRoomState = (state) => }; }; -export const setRoomActiveSpeaker = (peerName) => +export const setRoomActiveSpeaker = (peerId) => { return { type : 'SET_ROOM_ACTIVE_SPEAKER', - payload : { peerName } + payload : { peerId } }; }; @@ -57,19 +57,25 @@ export const setSettingsOpen = ({ settingsOpen }) => payload : { settingsOpen } }); -export const setMe = ({ peerName, device, loginEnabled }) => +export const setMe = ({ peerId, device, loginEnabled }) => { return { type : 'SET_ME', - payload : { peerName, device, loginEnabled } + payload : { peerId, device, loginEnabled } }; }; -export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) => +export const setMediaCapabilities = ({ + canSendMic, + canSendWebcam, + canShareScreen, + needExtension, + canShareFiles +}) => { return { type : 'SET_MEDIA_CAPABILITIES', - payload : { canSendMic, canSendWebcam } + payload : { canSendMic, canSendWebcam, canShareScreen, needExtension, canShareFiles } }; }; @@ -150,27 +156,27 @@ export const setDisplayMode = (mode) => payload : { mode } }); -export const setPeerVideoInProgress = (peerName, flag) => +export const setPeerVideoInProgress = (peerId, flag) => { return { type : 'SET_PEER_VIDEO_IN_PROGRESS', - payload : { peerName, flag } + payload : { peerId, flag } }; }; -export const setPeerAudioInProgress = (peerName, flag) => +export const setPeerAudioInProgress = (peerId, flag) => { return { type : 'SET_PEER_AUDIO_IN_PROGRESS', - payload : { peerName, flag } + payload : { peerId, flag } }; }; -export const setPeerScreenInProgress = (peerName, flag) => +export const setPeerScreenInProgress = (peerId, flag) => { return { type : 'SET_PEER_SCREEN_IN_PROGRESS', - payload : { peerName, flag } + payload : { peerId, flag } }; }; @@ -226,11 +232,11 @@ export const setMyRaiseHandStateInProgress = (flag) => }; }; -export const setPeerRaiseHandState = (peerName, raiseHandState) => +export const setPeerRaiseHandState = (peerId, raiseHandState) => { return { type : 'SET_PEER_RAISE_HAND_STATE', - payload : { peerName, raiseHandState } + payload : { peerId, raiseHandState } }; }; @@ -274,6 +280,14 @@ export const setProducerTrack = (producerId, track) => }; }; +export const setProducerScore = (producerId, score) => +{ + return { + type : 'SET_PRODUCER_SCORE', + payload : { producerId, score } + }; +}; + export const setAudioInProgress = (flag) => { return { @@ -306,35 +320,35 @@ export const addPeer = (peer) => }; }; -export const removePeer = (peerName) => +export const removePeer = (peerId) => { return { type : 'REMOVE_PEER', - payload : { peerName } + payload : { peerId } }; }; -export const setPeerDisplayName = (displayName, peerName) => +export const setPeerDisplayName = (displayName, peerId) => { return { type : 'SET_PEER_DISPLAY_NAME', - payload : { displayName, peerName } + payload : { displayName, peerId } }; }; -export const addConsumer = (consumer, peerName) => +export const addConsumer = (consumer, peerId) => { return { type : 'ADD_CONSUMER', - payload : { consumer, peerName } + payload : { consumer, peerId } }; }; -export const removeConsumer = (consumerId, peerName) => +export const removeConsumer = (consumerId, peerId) => { return { type : 'REMOVE_CONSUMER', - payload : { consumerId, peerName } + payload : { consumerId, peerId } }; }; @@ -354,11 +368,19 @@ export const setConsumerResumed = (consumerId, originator) => }; }; -export const setConsumerEffectiveProfile = (consumerId, profile) => +export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) => { return { - type : 'SET_CONSUMER_EFFECTIVE_PROFILE', - payload : { consumerId, profile } + type : 'SET_CONSUMER_CURRENT_LAYERS', + payload : { consumerId, spatialLayer, temporalLayer } + }; +}; + +export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) => +{ + return { + type : 'SET_CONSUMER_PREFERRED_LAYERS', + payload : { consumerId, spatialLayer, temporalLayer } }; }; @@ -370,11 +392,19 @@ export const setConsumerTrack = (consumerId, track) => }; }; -export const setPeerVolume = (peerName, volume) => +export const setConsumerScore = (consumerId, score) => +{ + return { + type : 'SET_CONSUMER_SCORE', + payload : { consumerId, score } + }; +}; + +export const setPeerVolume = (peerId, volume) => { return { type : 'SET_PEER_VOLUME', - payload : { peerName, volume } + payload : { peerId, volume } }; }; @@ -536,10 +566,10 @@ export const setPicture = (picture) => payload : { picture } }); -export const setPeerPicture = (peerName, picture) => +export const setPeerPicture = (peerId, picture) => ({ type : 'SET_PEER_PICTURE', - payload : { peerName, picture } + payload : { peerId, picture } }); export const loggedIn = () => @@ -547,10 +577,10 @@ export const loggedIn = () => type : 'LOGGED_IN' }); -export const setSelectedPeer = (selectedPeerName) => +export const setSelectedPeer = (selectedpeerId) => ({ type : 'SET_SELECTED_PEER', - payload : { selectedPeerName } + payload : { selectedpeerId } }); export const setSpotlights = (spotlights) => diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 3739582..60d30e8 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -95,7 +95,7 @@ const Me = (props) => roomClient.changeDisplayName(displayName); }} > - + @@ -138,7 +138,7 @@ const mapStateToProps = (state) => me : state.me, ...meProducersSelector(state), settings : state.settings, - activeSpeaker : state.me.name === state.room.activeSpeakerName + activeSpeaker : state.me.id === state.room.activeSpeakerId }; }; @@ -153,7 +153,7 @@ export default withRoomContext(connect( prev.me === next.me && prev.producers === next.producers && prev.settings === next.settings && - prev.room.activeSpeakerName === next.room.activeSpeakerName + prev.room.activeSpeakerId === next.room.activeSpeakerId ); } } diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index d83100c..76ea10b 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -196,13 +196,6 @@ const Peer = (props) => }} >
- { videoVisible && !webcamConsumer.supported ? -
-

incompatible video

-
- :null - } - { !videoVisible ?

this video is paused

@@ -210,7 +203,7 @@ const Peer = (props) => :null } - { videoVisible && webcamConsumer.supported ? + { videoVisible ?
setWebcamHover(true)} @@ -240,8 +233,8 @@ const Peer = (props) => onClick={() => { micEnabled ? - roomClient.modifyPeerConsumer(peer.name, 'mic', true) : - roomClient.modifyPeerConsumer(peer.name, 'mic', false); + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); }} > { micEnabled ? @@ -295,7 +288,7 @@ const Peer = (props) => audioCodec={micConsumer ? micConsumer.codec : null} videoCodec={webcamConsumer ? webcamConsumer.codec : null} > - +
@@ -323,13 +316,6 @@ const Peer = (props) => }, 2000); }} > - { screenVisible && !screenConsumer.supported ? -
-

incompatible video

-
- :null - } - { !screenVisible ?

this video is paused

@@ -337,7 +323,7 @@ const Peer = (props) => :null } - { screenVisible && screenConsumer.supported ? + { screenVisible ?
const mapStateToProps = (state) => { return { - peer : state.peers[props.name], + peer : state.peers[props.id], ...getPeerConsumers(state, props), windowConsumer : state.room.windowConsumer, - activeSpeaker : props.name === state.room.activeSpeakerName + activeSpeaker : props.id === state.room.activeSpeakerId }; }; @@ -470,7 +456,7 @@ export default withRoomContext(connect( return ( prev.peers === next.peers && prev.consumers === next.consumers && - prev.room.activeSpeakerName === next.room.activeSpeakerName && + prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.windowConsumer === next.room.windowConsumer ); } diff --git a/app/src/components/Containers/Volume.js b/app/src/components/Containers/Volume.js index 7488a30..3c13a39 100644 --- a/app/src/components/Containers/Volume.js +++ b/app/src/components/Containers/Volume.js @@ -150,7 +150,7 @@ const makeMapStateToProps = (initialState, props) => const mapStateToProps = (state) => { return { - volume : state.peerVolumes[props.name] + volume : state.peerVolumes[props.id] }; }; diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index 406a9f3..cc9fe86 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -158,8 +158,8 @@ const Sidebar = (props) => onClick={() => { micState === 'on' ? - roomClient.muteMic() : - roomClient.unmuteMic(); + roomClient.disableMic() : + roomClient.enableMic(); }} > { micState === 'on' ? diff --git a/app/src/components/MeetingDrawer/FileSharing/File.js b/app/src/components/MeetingDrawer/FileSharing/File.js index c532ef5..05b92b0 100644 --- a/app/src/components/MeetingDrawer/FileSharing/File.js +++ b/app/src/components/MeetingDrawer/FileSharing/File.js @@ -55,7 +55,7 @@ class File extends React.PureComponent { const { roomClient, - torrentSupport, + canShareFiles, file, classes } = this.props; @@ -105,7 +105,7 @@ class File extends React.PureComponent {magnet.decode(file.magnetUri).dn} - { torrentSupport ? + { canShareFiles ? @@ -80,17 +80,17 @@ class FileSharing extends React.PureComponent } FileSharing.propTypes = { - roomClient : PropTypes.any.isRequired, - torrentSupport : PropTypes.bool.isRequired, - tabOpen : PropTypes.bool.isRequired, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + canShareFiles : PropTypes.bool.isRequired, + tabOpen : PropTypes.bool.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => { return { - torrentSupport : state.room.torrentSupport, - tabOpen : state.toolarea.currentToolTab === 'files' + canShareFiles : state.me.canShareFiles, + tabOpen : state.toolarea.currentToolTab === 'files' }; }; diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 59ab7ec..8715b9d 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -185,8 +185,8 @@ const ListPeer = (props) => { e.stopPropagation(); screenVisible ? - roomClient.modifyPeerConsumer(peer.name, 'screen', true) : - roomClient.modifyPeerConsumer(peer.name, 'screen', false); + roomClient.modifyPeerConsumer(peer.id, 'screen', true) : + roomClient.modifyPeerConsumer(peer.id, 'screen', false); }} > { screenVisible ? @@ -207,8 +207,8 @@ const ListPeer = (props) => { e.stopPropagation(); micEnabled ? - roomClient.modifyPeerConsumer(peer.name, 'mic', true) : - roomClient.modifyPeerConsumer(peer.name, 'mic', false); + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); }} > { micEnabled ? @@ -241,7 +241,7 @@ const makeMapStateToProps = (initialState, props) => const mapStateToProps = (state) => { return { - peer : state.peers[props.name], + peer : state.peers[props.id], ...getPeerConsumers(state, props) }; }; diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index a7d5213..fac3e74 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -76,7 +76,7 @@ class ParticipantList extends React.PureComponent roomClient, advancedMode, passivePeers, - selectedPeerName, + selectedPeerId, spotlightPeers, classes } = this.props; @@ -92,14 +92,14 @@ class ParticipantList extends React.PureComponent
  • Participants in Spotlight:
  • { spotlightPeers.map((peer) => (
  • roomClient.setSelectedPeer(peer.name)} + onClick={() => roomClient.setSelectedPeer(peer.id)} > - - + +
  • ))} @@ -107,15 +107,15 @@ class ParticipantList extends React.PureComponent
    • Passive Participants:
    • - { passivePeers.map((peerName) => ( + { passivePeers.map((peerId) => (
    • roomClient.setSelectedPeer(peerName)} + onClick={() => roomClient.setSelectedPeer(peerId)} > - +
    • ))}
    @@ -126,20 +126,20 @@ class ParticipantList extends React.PureComponent ParticipantList.propTypes = { - roomClient : PropTypes.any.isRequired, - advancedMode : PropTypes.bool, - passivePeers : PropTypes.array, - selectedPeerName : PropTypes.string, - spotlightPeers : PropTypes.array, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + advancedMode : PropTypes.bool, + passivePeers : PropTypes.array, + selectedPeerId : PropTypes.string, + spotlightPeers : PropTypes.array, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => { return { - passivePeers : passivePeersSelector(state), - selectedPeerName : state.room.selectedPeerName, - spotlightPeers : spotlightPeersSelector(state) + passivePeers : passivePeersSelector(state), + selectedPeerId : state.room.selectedPeerId, + spotlightPeers : spotlightPeersSelector(state) }; }; @@ -153,7 +153,7 @@ const ParticipantListContainer = withRoomContext(connect( return ( prev.peers === next.peers && prev.room.spotlights === next.room.spotlights && - prev.room.selectedPeerName === next.room.selectedPeerName + prev.room.selectedPeerId === next.room.selectedPeerId ); } } diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index ca91ad8..828db9b 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -139,9 +139,9 @@ class Democratic extends React.PureComponent { return ( ); diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index 3f76809..91884a2 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -104,11 +104,11 @@ class Filmstrip extends React.PureComponent // Find the name of the peer which is currently speaking. This is either // the latest active speaker, or the manually selected peer, or, if no // person has spoken yet, the first peer in the list of peers. - getActivePeerName = () => + getActivePeerId = () => { - if (this.props.selectedPeerName) + if (this.props.selectedPeerId) { - return this.props.selectedPeerName; + return this.props.selectedPeerId; } if (this.state.lastSpeaker) @@ -116,23 +116,23 @@ class Filmstrip extends React.PureComponent return this.state.lastSpeaker; } - const peerNames = Object.keys(this.props.peers); + const peerIds = Object.keys(this.props.peers); - if (peerNames.length > 0) + if (peerIds.length > 0) { - return peerNames[0]; + return peerIds[0]; } }; - isSharingCamera = (peerName) => this.props.peers[peerName] && - this.props.peers[peerName].consumers.some((consumer) => + isSharingCamera = (peerId) => this.props.peers[peerId] && + this.props.peers[peerId].consumers.some((consumer) => this.props.consumers[consumer].source === 'screen'); getRatio = () => { let ratio = 4 / 3; - if (this.isSharingCamera(this.getActivePeerName())) + if (this.isSharingCamera(this.getActivePeerId())) { ratio *= 2; } @@ -202,12 +202,12 @@ class Filmstrip extends React.PureComponent classes } = this.props; - const activePeerName = this.getActivePeerName(); + const activePeerId = this.getActivePeerId(); return (
    - { peers[activePeerName] ? + { peers[activePeerId] ?
    :null @@ -226,23 +226,23 @@ class Filmstrip extends React.PureComponent
    - { Object.keys(peers).map((peerName) => + { Object.keys(peers).map((peerId) => { - if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) + if (spotlights.find((spotlightsElement) => spotlightsElement === peerId)) { return (
    roomClient.setSelectedPeer(peerName)} + key={peerId} + onClick={() => roomClient.setSelectedPeer(peerId)} className={classnames(classes.film, { - selected : this.props.selectedPeerName === peerName, - active : this.state.lastSpeaker === peerName + selected : this.props.selectedPeerId === peerId, + active : this.state.lastSpeaker === peerId })} >
    @@ -276,7 +276,7 @@ Filmstrip.propTypes = { peers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired, myName : PropTypes.string.isRequired, - selectedPeerName : PropTypes.string, + selectedPeerId : PropTypes.string, spotlightsLength : PropTypes.number, spotlights : PropTypes.array.isRequired, classes : PropTypes.object.isRequired @@ -288,7 +288,7 @@ const mapStateToProps = (state) => return { activeSpeakerName : state.room.activeSpeakerName, - selectedPeerName : state.room.selectedPeerName, + selectedPeerId : state.room.selectedPeerId, peers : state.peers, consumers : state.consumers, myName : state.me.name, diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index c21c21f..95291f4 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -5,7 +5,7 @@ const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; const peersSelector = (state) => state.peers; const getPeerConsumers = (state, props) => - (state.peers[props.name] ? state.peers[props.name].consumers : null); + (state.peers[props.id] ? state.peers[props.id].consumers : null); const getAllConsumers = (state) => state.consumers; const peersKeySelector = createSelector( peersSelector, @@ -66,10 +66,10 @@ export const spotlightPeersSelector = createSelector( spotlightsSelector, peersSelector, (spotlights, peers) => - spotlights.reduce((result, peerName) => + spotlights.reduce((result, peerId) => { - if (peers[peerName]) - result.push(peers[peerName]); + if (peers[peerId]) + result.push(peers[peerId]); return result; }, []) @@ -83,7 +83,7 @@ export const peersLengthSelector = createSelector( export const passivePeersSelector = createSelector( peersKeySelector, spotlightsSelector, - (peers, spotlights) => peers.filter((peerName) => !spotlights.includes(peerName)) + (peers, spotlights) => peers.filter((peerId) => !spotlights.includes(peerId)) ); export const videoBoxesSelector = createSelector( diff --git a/app/src/components/VideoContainers/FullScreenView.js b/app/src/components/VideoContainers/FullScreenView.js index 85a9e0d..cbf2b73 100644 --- a/app/src/components/VideoContainers/FullScreenView.js +++ b/app/src/components/VideoContainers/FullScreenView.js @@ -102,13 +102,6 @@ const FullScreenView = (props) => return (
    - { consumerVisible && !consumer.supported ? -
    -

    incompatible video

    -
    - :null - } -
    =0', chromium: '>=0' })) + flag = 'chrome'; + else if (browser.satisfies({ firefox: '>=0' })) + flag = 'firefox'; + else if (browser.satisfies({ safari: '>=0' })) + flag = 'safari'; + else if (browser.satisfies({ opera: '>=0' })) + flag = 'opera'; + else if (browser.satisfies({ 'microsoft edge': '>=0' })) + flag = 'edge'; + else + flag = 'unknown'; + + return { + flag, + name : browser.getBrowserName(), + version : browser.getBrowserVersion(), + bowser : browser + }; +} diff --git a/app/src/index.js b/app/src/index.js index d82f1d6..b4455d5 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -3,12 +3,12 @@ import UrlParse from 'url-parse'; import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; -import { getDeviceInfo } from 'mediasoup-client'; import randomString from 'random-string'; import Logger from './Logger'; import debug from 'debug'; import RoomClient from './RoomClient'; import RoomContext from './RoomContext'; +import deviceInfo from './deviceInfo'; import * as stateActions from './actions/stateActions'; import Room from './components/Room'; import LoadingView from './components/LoadingView'; @@ -44,13 +44,15 @@ function run() { logger.debug('run() [environment:%s]', process.env.NODE_ENV); - const peerName = randomString({ length: 8 }).toLowerCase(); + const peerId = randomString({ length: 8 }).toLowerCase(); const urlParser = new UrlParse(window.location.href, true); let roomId = (urlParser.pathname).substr(1) ? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase(); const produce = urlParser.query.produce !== 'false'; + const consume = urlParser.query.consume !== 'false'; const useSimulcast = urlParser.query.simulcast === 'true'; + const forceTcp = urlParser.query.forceTcp === 'true'; if (!roomId) { @@ -80,21 +82,21 @@ function run() const roomUrl = roomUrlParser.toString(); // Get current device. - const device = getDeviceInfo(); + const device = deviceInfo(); store.dispatch( stateActions.setRoomUrl(roomUrl)); store.dispatch( stateActions.setMe({ - peerName, + peerId, device, loginEnabled : window.config.loginEnabled }) ); roomClient = new RoomClient( - { roomId, peerName, device, useSimulcast, produce }); + { roomId, peerId, device, useSimulcast, produce, consume, forceTcp }); global.CLIENT = roomClient; diff --git a/app/src/reducers/consumers.js b/app/src/reducers/consumers.js index 758efd2..2c99416 100644 --- a/app/src/reducers/consumers.js +++ b/app/src/reducers/consumers.js @@ -51,11 +51,30 @@ const consumers = (state = initialState, action) => return { ...state, [consumerId]: newConsumer }; } - case 'SET_CONSUMER_EFFECTIVE_PROFILE': + case 'SET_CONSUMER_CURRENT_LAYERS': { - const { consumerId, profile } = action.payload; + const { consumerId, spatialLayer, temporalLayer } = action.payload; const consumer = state[consumerId]; - const newConsumer = { ...consumer, profile }; + const newConsumer = + { + ...consumer, + currentSpatialLayer : spatialLayer, + currentTemporalLayer : temporalLayer + }; + + return { ...state, [consumerId]: newConsumer }; + } + + case 'SET_CONSUMER_PREFERRED_LAYERS': + { + const { consumerId, spatialLayer, temporalLayer } = action.payload; + const consumer = state[consumerId]; + const newConsumer = + { + ...consumer, + preferredSpatialLayer : spatialLayer, + preferredTemporalLayer : temporalLayer + }; return { ...state, [consumerId]: newConsumer }; } @@ -69,6 +88,19 @@ const consumers = (state = initialState, action) => return { ...state, [consumerId]: newConsumer }; } + case 'SET_CONSUMER_SCORE': + { + const { consumerId, score } = action.payload; + const consumer = state[consumerId]; + + if (!consumer) + return state; + + const newConsumer = { ...consumer, score }; + + return { ...state, [consumerId]: newConsumer }; + } + default: return state; } diff --git a/app/src/reducers/me.js b/app/src/reducers/me.js index 4750f0c..948988f 100644 --- a/app/src/reducers/me.js +++ b/app/src/reducers/me.js @@ -1,11 +1,12 @@ const initialState = { - name : null, + id : null, device : null, canSendMic : false, canSendWebcam : false, canShareScreen : false, needExtension : false, + canShareFiles : false, audioDevices : null, webcamDevices : null, webcamInProgress : false, @@ -24,14 +25,14 @@ const me = (state = initialState, action) => case 'SET_ME': { const { - peerName, + peerId, device, loginEnabled } = action.payload; return { ...state, - name : peerName, + id : peerId, device, loginEnabled }; @@ -45,9 +46,22 @@ const me = (state = initialState, action) => case 'SET_MEDIA_CAPABILITIES': { - const { canSendMic, canSendWebcam } = action.payload; + const { + canSendMic, + canSendWebcam, + canShareScreen, + needExtension, + canShareFiles + } = action.payload; - return { ...state, canSendMic, canSendWebcam }; + return { + ...state, + canSendMic, + canSendWebcam, + canShareScreen, + needExtension, + canShareFiles + }; } case 'SET_SCREEN_CAPABILITIES': diff --git a/app/src/reducers/peerVolumes.js b/app/src/reducers/peerVolumes.js index 6b89db8..fafe739 100644 --- a/app/src/reducers/peerVolumes.js +++ b/app/src/reducers/peerVolumes.js @@ -7,33 +7,33 @@ const peerVolumes = (state = initialState, action) => case 'SET_ME': { const { - peerName + peerId } = action.payload; - return { ...state, [peerName]: 0 }; + return { ...state, [peerId]: 0 }; } case 'ADD_PEER': { const { peer } = action.payload; - return { ...state, [peer.name]: 0 }; + return { ...state, [peer.id]: 0 }; } case 'REMOVE_PEER': { - const { peerName } = action.payload; + const { peerId } = action.payload; const newState = { ...state }; - delete newState[peerName]; + delete newState[peerId]; return newState; } case 'SET_PEER_VOLUME': { - const { peerName, volume } = action.payload; + const { peerId, volume } = action.payload; - return { ...state, [peerName]: volume }; + return { ...state, [peerId]: volume }; } default: diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index ea9cb44..b5a3270 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -53,12 +53,12 @@ const peers = (state = {}, action) => { case 'ADD_PEER': { - return { ...state, [action.payload.peer.name]: peer(undefined, action) }; + return { ...state, [action.payload.peer.id]: peer(undefined, action) }; } case 'REMOVE_PEER': { - return omit(state, [ action.payload.peerName ]); + return omit(state, [ action.payload.peerId ]); } case 'SET_PEER_DISPLAY_NAME': @@ -69,25 +69,25 @@ const peers = (state = {}, action) => case 'SET_PEER_PICTURE': case 'ADD_CONSUMER': { - const oldPeer = state[action.payload.peerName]; + const oldPeer = state[action.payload.peerId]; if (!oldPeer) { throw new Error('no Peer found'); } - return { ...state, [oldPeer.name]: peer(oldPeer, action) }; + return { ...state, [oldPeer.id]: peer(oldPeer, action) }; } case 'REMOVE_CONSUMER': { - const oldPeer = state[action.payload.peerName]; + const oldPeer = state[action.payload.peerId]; // NOTE: This means that the Peer was closed before, so it's ok. if (!oldPeer) return state; - return { ...state, [oldPeer.name]: peer(oldPeer, action) }; + return { ...state, [oldPeer.id]: peer(oldPeer, action) }; } default: diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index cdf9c0a..446b350 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -5,14 +5,14 @@ const initialState = locked : false, lockedOut : false, audioSuspended : false, - activeSpeakerName : null, + activeSpeakerId : null, torrentSupport : false, showSettings : false, fullScreenConsumer : null, // ConsumerID windowConsumer : null, // ConsumerID toolbarsVisible : true, mode : 'democratic', - selectedPeerName : null, + selectedPeerId : null, spotlights : [], settingsOpen : false }; @@ -35,7 +35,7 @@ const room = (state = initialState, action) => if (roomState === 'connected') return { ...state, state: roomState }; else - return { ...state, state: roomState, activeSpeakerName: null }; + return { ...state, state: roomState, activeSpeakerId: null }; } case 'SET_ROOM_LOCKED': @@ -69,9 +69,9 @@ const room = (state = initialState, action) => case 'SET_ROOM_ACTIVE_SPEAKER': { - const { peerName } = action.payload; + const { peerId } = action.payload; - return { ...state, activeSpeakerName: peerName }; + return { ...state, activeSpeakerId: peerId }; } case 'FILE_SHARING_SUPPORTED': @@ -119,13 +119,13 @@ const room = (state = initialState, action) => case 'SET_SELECTED_PEER': { - const { selectedPeerName } = action.payload; + const { selectedPeerId } = action.payload; return { ...state, - selectedPeerName : state.selectedPeerName === selectedPeerName ? - null : selectedPeerName + selectedPeerId : state.selectedPeerId === selectedPeerId ? + null : selectedPeerId }; } diff --git a/app/src/urlFactory.js b/app/src/urlFactory.js index fc4a9f5..7c4dbcd 100644 --- a/app/src/urlFactory.js +++ b/app/src/urlFactory.js @@ -1,10 +1,10 @@ -export function getSignalingUrl(peerName, roomId) +export function getSignalingUrl(peerId, roomId) { const hostname = window.location.hostname; const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port; - const url = `wss://${hostname}:${port}/?peerName=${peerName}&roomId=${roomId}`; + const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`; return url; } diff --git a/server/config/config.example.js b/server/config/config.example.js index 4e7f34f..ef41311 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,3 +1,5 @@ +const os = require('os'); + module.exports = { // oAuth2 conf @@ -9,21 +11,21 @@ module.exports = could be discovered on: issuerURL + '/.well-known/openid-configuration' */ - issuerURL : 'https://example.com' - clientOptions : + issuerURL : 'https://example.com', + clientOptions : { - client_id : '', - client_secret : '', - scope : 'openid email profile' + client_id : '', + client_secret : '', + scope : 'openid email profile', // where client.example.com is your multiparty meeting server - redirect_uri : 'https://client.example.com/auth/callback' + redirect_uri : 'https://client.example.com/auth/callback' } }, // session cookie secret - cookieSecret : 'T0P-S3cR3t_cook!e', + cookieSecret : 'T0P-S3cR3t_cook!e', // Listening hostname for `gulp live|open`. - domain : 'localhost', - tls : + domain : 'localhost', + tls : { cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` @@ -33,59 +35,61 @@ module.exports = // Any http request is redirected to https. // Listening port for http server. listeningRedirectPort : 80, - // STUN/TURN + // Mediasoup settings mediasoup : { - // mediasoup Server settings. - logLevel : 'warn', - logTags : - [ - 'info', - 'ice', - 'dtls', - 'rtp', - 'srtp', - 'rtcp', - 'rbe', - 'rtx' - ], - rtcIPv4 : true, - rtcIPv6 : true, - rtcAnnouncedIPv4 : null, - rtcAnnouncedIPv6 : null, - rtcMinPort : 40000, - rtcMaxPort : 49999, - // mediasoup Room codecs. - mediaCodecs : - [ - { - kind : 'audio', - name : 'opus', - clockRate : 48000, - channels : 2, - parameters : + numWorkers : Object.keys(os.cpus()).length, + // mediasoup Worker settings. + worker : + { + logLevel : 'warn', + logTags : + [ + 'info', + 'ice', + 'dtls', + 'rtp', + 'srtp', + 'rtcp' + ], + rtcMinPort : 40000, + rtcMaxPort : 49999 + }, + // mediasoup Router settings. + router : + { + // Router media codecs. + mediaCodecs : + [ { - useinbandfec : 1 - } - }, - // { - // kind : 'video', - // name : 'VP8', - // clockRate : 90000 - // } - { - kind : 'video', - name : 'H264', - clockRate : 90000, - parameters : + kind : 'audio', + mimeType : 'audio/opus', + clockRate : 48000, + channels : 2 + }, { - 'packetization-mode' : 1, - 'profile-level-id' : '42e01f', - 'level-asymmetry-allowed' : 1 + kind : 'video', + mimeType : 'video/h264', + clockRate : 90000, + parameters : + { + 'packetization-mode' : 1, + 'profile-level-id' : '42e01f', + 'level-asymmetry-allowed' : 1, + 'x-google-start-bitrate' : 1000 + } } - } - ], - // mediasoup per Peer max sending bitrate (in bps). - maxBitrate : 500000 + ] + }, + // mediasoup WebRtcTransport settings. + webRtcTransport : + { + listenIps : + [ + { ip: '1.2.3.4', announcedIp: null } + ], + maxIncomingBitrate : 1500000, + initialAvailableOutgoingBitrate : 1000000 + } } }; diff --git a/server/lib/Room.js b/server/lib/Room.js index 741325a..b1fa234 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -4,15 +4,41 @@ const EventEmitter = require('events').EventEmitter; const Logger = require('./Logger'); const config = require('../config/config'); -const MAX_BITRATE = config.mediasoup.maxBitrate || 1000000; -const MIN_BITRATE = Math.min(50000, MAX_BITRATE); -const BITRATE_FACTOR = 0.75; - const logger = new Logger('Room'); class Room extends EventEmitter { - constructor(roomId, mediaServer, io) + /** + * Factory function that creates and returns Room instance. + * + * @async + * + * @param {mediasoup.Worker} mediasoupWorker - 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 }) + { + logger.info('create() [roomId:%s, forceH264:%s]', roomId); + + // Router media codecs. + let mediaCodecs = config.mediasoup.router.mediaCodecs; + + // Create a mediasoup Router. + const mediasoupRouter = await mediasoupWorker.createRouter({ mediaCodecs }); + + // Create a mediasoup AudioLevelObserver. + const audioLevelObserver = await mediasoupRouter.createAudioLevelObserver( + { + maxEntries : 1, + threshold : -80, + interval : 800 + }); + + return new Room({ roomId, mediasoupRouter, audioLevelObserver }); + } + + constructor({ roomId, mediasoupRouter, audioLevelObserver }) { logger.info('constructor() [roomId:"%s"]', roomId); @@ -34,30 +60,51 @@ class Room extends EventEmitter this._lastN = []; - this._io = io; + // this._io = io; - this._signalingPeers = new Map(); + this._peers = new Map(); - try + // mediasoup Router instance. + // @type {mediasoup.Router} + this._mediasoupRouter = mediasoupRouter; + + // mediasoup AudioLevelObserver. + // @type {mediasoup.AudioLevelObserver} + this._audioLevelObserver = audioLevelObserver; + + // Set audioLevelObserver events. + this._audioLevelObserver.on('volumes', (volumes) => { - // mediasoup Room instance. - this._mediaRoom = mediaServer.Room(config.mediasoup.mediaCodecs); - } - catch (error) + const { producer, volume } = volumes[0]; + + // logger.debug( + // 'audioLevelObserver "volumes" event [producerId:%s, volume:%s]', + // producer.id, volume); + + // Notify all Peers. + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'activeSpeaker', { + peerId : producer.appData.peerId, + volume : volume + }); + }); + }); + + this._audioLevelObserver.on('silence', () => { - this.close(); + // logger.debug('audioLevelObserver "silence" event'); - throw error; - } - - // Current max bitrate for all the participants. - this._maxBitrate = MAX_BITRATE; + // Notify all Peers. + this._peers.forEach((peer) => + { + this._notification(peer.socket, 'activeSpeaker', { peerId : null }); + }); + }); // Current active speaker. // @type {mediasoup.Peer} this._currentActiveSpeaker = null; - - this._handleMediaRoom(); } get id() @@ -71,19 +118,20 @@ class Room extends EventEmitter this._closed = true; - // Close the signalingPeers - if (this._signalingPeers) - for (let peer of this._signalingPeers) + // Close the peers + if (this._peers) + { + this._peers.forEach((peer) => { if (peer.socket) peer.socket.disconnect(); - }; + }); + } - this._signalingPeers.clear(); + this._peers.clear(); - // Close the mediasoup Room. - if (this._mediaRoom) - this._mediaRoom.close(); + // Close the mediasoup Router. + this._mediasoupRouter.close(); // Emit 'close' event. this.emit('close'); @@ -91,55 +139,52 @@ class Room extends EventEmitter logStatus() { - if (!this._mediaRoom) - return; - logger.info( - 'logStatus() [room id:"%s", peers:%s, mediasoup peers:%s]', + 'logStatus() [room id:"%s", peers:%s]', this._roomId, - this._signalingPeers.length, - this._mediaRoom.peers.length); + this._peers.size + ); } - handleConnection(peerName, socket) + handleConnection({ peerId, consume, socket }) { - logger.info('handleConnection() [peerName:"%s"]', peerName); + logger.info('handleConnection() [peerId:"%s"]', peerId); // This will allow reconnects to join despite lock - if (this._signalingPeers.has(peerName)) + if (this._peers.has(peerId)) { logger.warn( - 'handleConnection() | there is already a peer with same peerName, ' + - 'closing the previous one [peerName:"%s"]', - peerName); + 'handleConnection() | there is already a peer with same peerId, ' + + 'closing the previous one [peerId:"%s"]', + peerId); - const signalingPeer = this._signalingPeers.get(peerName); + const peer = this._peers.get(peerId); - signalingPeer.socket.disconnect(); - this._signalingPeers.delete(peerName); + peer.socket.disconnect(); + this._peers.delete(peerId); } else if (this._locked) // Don't allow connections to a locked room { - socket.emit('room-locked'); + notification(socket, 'roomLocked'); socket.disconnect(true); return; } socket.join(this._roomId); - const signalingPeer = { peerName : peerName, socket : socket }; + const peer = { id : peerId, socket : socket }; - const index = this._lastN.indexOf(peerName); + const index = this._lastN.indexOf(peerId); if (index === -1) // We don't have this peer, add to end { - this._lastN.push(peerName); + this._lastN.push(peerId); } - this._signalingPeers.set(peerName, signalingPeer); + this._peers.set(peerId, peer); - this._handleSignalingPeer(signalingPeer); - socket.emit('room-ready'); + this._handlePeer({ peer, consume }); + this._notification(socket, 'roomReady'); } isLocked() @@ -152,566 +197,860 @@ class Room extends EventEmitter logger.debug('authCallback()'); const { - peerName, - name, + peerId, + displayName, picture } = data; - const signalingPeer = this._signalingPeers.get(peerName); + const peer = this._peers.get(peerId); - if (signalingPeer) + if (peer) { - signalingPeer.socket.emit('auth', - { - name : name, - picture : picture + this._notification(peer.socket, 'auth', { + displayName : displayName, + picture : picture }); } } - _handleMediaRoom() + _handlePeer({ peer, consume }) { - logger.debug('_handleMediaRoom()'); + logger.debug('_handlePeer() [peer:"%s"]', peer.id); - const activeSpeakerDetector = this._mediaRoom.createActiveSpeakerDetector(); + peer.data = {}; - activeSpeakerDetector.on('activespeakerchange', (activePeer) => + // Not joined after a custom protoo 'join' request is later received. + peer.data.consume = consume; + peer.data.joined = false; + peer.data.displayName = undefined; + peer.data.device = undefined; + peer.data.rtpCapabilities = undefined; + peer.data.raiseHandState = false; + + // Have mediasoup related maps ready even before the Peer joins since we + // allow creating Transports before joining. + peer.data.transports = new Map(); + peer.data.producers = new Map(); + peer.data.consumers = new Map(); + + peer.socket.on('request', (request, cb) => { - if (activePeer) - { - logger.info('new active speaker [peerName:"%s"]', activePeer.name); + logger.debug( + 'Peer "request" event [method:%s, peerId:%s]', + request.method, peer.id); - this._currentActiveSpeaker = activePeer; - - const index = this._lastN.indexOf(activePeer.name); - - if (index > -1) // We have this speaker in the list, move to front + this._handleSocketRequest(peer, request, cb) + .catch((error) => { - this._lastN.splice(index, 1); - this._lastN = [activePeer.name].concat(this._lastN); - } + logger.error('request failed:%o', error); - const activeVideoProducer = activePeer.producers - .find((producer) => producer.kind === 'video'); - - for (const peer of this._mediaRoom.peers) - { - for (const consumer of peer.consumers) - { - if (consumer.kind !== 'video') - continue; - - if (consumer.source === activeVideoProducer) - { - consumer.setPreferredProfile('high'); - } - else - { - consumer.setPreferredProfile('low'); - } - } - } - } - else - { - logger.info('no active speaker'); - - this._currentActiveSpeaker = null; - - for (const peer of this._mediaRoom.peers) - { - for (const consumer of peer.consumers) - { - if (consumer.kind !== 'video') - continue; - - consumer.setPreferredProfile('low'); - } - } - } - - this._io.to(this._roomId).emit('active-speaker', { - peerName : activePeer ? activePeer.name : null + cb(error); }); }); - } - _handleSignalingPeer(signalingPeer) - { - logger.debug('_handleSignalingPeer() [peer:"%s"]', signalingPeer.id); - - signalingPeer.socket.on('mediasoup-request', (request, cb) => + peer.socket.on('disconnect', () => { - const mediasoupRequest = request; - - this._handleMediasoupClientRequest( - signalingPeer, mediasoupRequest, cb); - }); - - signalingPeer.socket.on('mediasoup-notification', (request, cb) => - { - // Return no error - cb(null); - - const mediasoupNotification = request; - - this._handleMediasoupClientNotification( - signalingPeer, mediasoupNotification); - }); - - signalingPeer.socket.on('change-display-name', (request, cb) => - { - // Return no error - cb(null); - - const { displayName } = request; - const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName); - const oldDisplayName = mediaPeer.appData.displayName; - - mediaPeer.appData.displayName = displayName; - - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'display-name-changed', - { - peerName : signalingPeer.peerName, - displayName : displayName, - oldDisplayName : oldDisplayName - } - ); - }); - - signalingPeer.socket.on('change-profile-picture', (request, cb) => - { - // Return no error - cb(null); - - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'profile-picture-changed', - { - peerName : signalingPeer.peerName, - picture : request.picture - } - ); - }); - - signalingPeer.socket.on('chat-message', (request, cb) => - { - // Return no error - cb(null); - - const { chatMessage } = request; - - this._chatHistory.push(chatMessage); - - // Spread to others - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'chat-message-receive', - { - peerName : signalingPeer.peerName, - chatMessage : chatMessage - } - ); - }); - - signalingPeer.socket.on('server-history', (request, cb) => - { - // Return to sender - cb( - null, - { - chatHistory : this._chatHistory, - fileHistory : this._fileHistory, - lastN : this._lastN - } - ); - }); - - signalingPeer.socket.on('lock-room', (request, cb) => - { - // Return no error - cb(null); - - this._locked = true; - - // Spread to others - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'lock-room', - { - peerName : signalingPeer.peerName - } - ); - }); - - signalingPeer.socket.on('unlock-room', (request, cb) => - { - // Return no error - cb(null); - - this._locked = false; - - // Spread to others - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'unlock-room', - { - peerName : signalingPeer.peerName - } - ); - }); - - signalingPeer.socket.on('send-file', (request, cb) => - { - // Return no error - cb(null); - - const fileData = request.file; - - this._fileHistory.push(fileData); - - // Spread to others - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'file-receive', - { - peerName : signalingPeer.peerName, - file : fileData - } - ); - }); - - signalingPeer.socket.on('raisehand-message', (request, cb) => - { - // Return no error - cb(null); - - const { raiseHandState } = request; - const { mediaPeer } = signalingPeer; - - mediaPeer.appData.raiseHandState = raiseHandState; - // Spread to others - signalingPeer.socket.broadcast.to(this._roomId).emit( - 'raisehand-message', - { - peerName : signalingPeer.peerName, - raiseHandState : raiseHandState - }, - ); - }); - - signalingPeer.socket.on('request-consumer-keyframe', (request, cb) => - { - cb(null); - - const { consumerId } = request; - const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName); - const consumer = mediaPeer.consumers - .find((_consumer) => _consumer.id === consumerId); - - if (!consumer) - { - logger.warn('consumer with id "%s" not found', consumerId); - + if (this._closed) return; + + logger.debug('Peer "close" event [peerId:%s]', peer.id); + + // If the Peer was joined, notify all Peers. + if (peer.data.joined) + { + this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); } - - consumer.requestKeyFrame(); - }); - signalingPeer.socket.on('disconnect', () => - { - logger.debug('Peer "close" event [peer:"%s"]', signalingPeer.peerName); - - const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName); - - if (mediaPeer && !mediaPeer.closed) - mediaPeer.close(); - - const index = this._lastN.indexOf(signalingPeer.peerName); + const index = this._lastN.indexOf(peer.id); if (index > -1) // We have this peer in the list, remove { this._lastN.splice(index, 1); } - // If this is the latest peer in the room, close the room. - // However wait a bit (for reconnections). - setTimeout(() => + // Iterate and close all mediasoup Transport associated to this Peer, so all + // its Producers and Consumers will also be closed. + for (const transport of peer.data.transports.values()) { - if (this._mediaRoom && this._mediaRoom.closed) - return; - - if (this._mediaRoom.peers.length === 0) - { - logger.info( - 'last peer in the room left, closing the room [roomId:"%s"]', - this._roomId); - - this.close(); - } - }, 5000); - }); - } - - _handleMediaPeer(signalingPeer, mediaPeer) - { - mediaPeer.on('notify', (notification) => - { - signalingPeer.socket.emit('mediasoup-notification', notification); - }); - - mediaPeer.on('newtransport', (transport) => - { - logger.info( - 'mediaPeer "newtransport" event [id:%s, direction:%s]', - transport.id, transport.direction); - - // Update peers max sending bitrate. - if (transport.direction === 'send') - { - this._updateMaxBitrate(); - - transport.on('close', () => - { - this._updateMaxBitrate(); - }); + transport.close(); } - this._handleMediaTransport(transport); - }); + this._peers.delete(peer.id); - mediaPeer.on('newproducer', (producer) => - { - logger.info('mediaPeer "newproducer" event [id:%s]', producer.id); - - this._handleMediaProducer(producer); - }); - - mediaPeer.on('newconsumer', (consumer) => - { - logger.info('mediaPeer "newconsumer" event [id:%s]', consumer.id); - - this._handleMediaConsumer(consumer); - }); - - // Also handle already existing Consumers. - for (const consumer of mediaPeer.consumers) - { - logger.info('mediaPeer existing "consumer" [id:%s]', consumer.id); - - this._handleMediaConsumer(consumer); - } - - // Notify about the existing active speaker. - if (this._currentActiveSpeaker) - { - signalingPeer.socket.emit( - 'active-speaker', + // If this is the latest Peer in the room, close the room after a while. + if (this._peers.size === 0) + { + setTimeout(() => { - peerName : this._currentActiveSpeaker.name - }); - } - } + if (this._closed) + return; - _handleMediaTransport(transport) - { - transport.on('close', (originator) => - { - logger.info( - 'Transport "close" event [originator:%s]', originator); + if (this._peers.size === 0) + { + logger.info( + 'last Peer in the room left, closing the room [roomId:%s]', + this._roomId); + + this.close(); + } + }, 10000); + } }); } - _handleMediaProducer(producer) + async _handleSocketRequest(peer, request, cb) { - producer.on('close', (originator) => - { - logger.info( - 'Producer "close" event [originator:%s]', originator); - }); - - producer.on('pause', (originator) => - { - logger.info( - 'Producer "pause" event [originator:%s]', originator); - }); - - producer.on('resume', (originator) => - { - logger.info( - 'Producer "resume" event [originator:%s]', originator); - }); - } - - _handleMediaConsumer(consumer) - { - consumer.on('close', (originator) => - { - logger.info( - 'Consumer "close" event [originator:%s]', originator); - }); - - consumer.on('pause', (originator) => - { - logger.info( - 'Consumer "pause" event [originator:%s]', originator); - }); - - consumer.on('resume', (originator) => - { - logger.info( - 'Consumer "resume" event [originator:%s]', originator); - }); - - consumer.on('effectiveprofilechange', (profile) => - { - logger.info( - 'Consumer "effectiveprofilechange" event [profile:%s]', profile); - }); - - // If video, initially make it 'low' profile unless this is for the current - // active speaker. - if (consumer.kind === 'video' && consumer.peer !== this._currentActiveSpeaker) - consumer.setPreferredProfile('low'); - } - - _handleMediasoupClientRequest(signalingPeer, request, cb) - { - logger.debug( - 'mediasoup-client request [method:%s, peer:"%s"]', - request.method, signalingPeer.peerName); - switch (request.method) { - case 'queryRoom': + + case 'getRouterRtpCapabilities': { - this._mediaRoom.receiveRequest(request) - .then((response) => cb(null, response)) - .catch((error) => cb(error.toString())); + cb(null, this._mediasoupRouter.rtpCapabilities); break; } case 'join': { - // TODO: Handle appData. Yes? - const { peerName } = request; + // Ensure the Peer is not already joined. + if (peer.data.joined) + throw new Error('Peer already joined'); - if (peerName !== signalingPeer.peerName) + const { displayName, device, rtpCapabilities } = request.data; + + // Store client data into the protoo Peer data object. + peer.data.displayName = displayName; + peer.data.device = device; + peer.data.rtpCapabilities = rtpCapabilities; + + // Tell the new Peer about already joined Peers. + // And also create Consumers for existing Producers. + + const peerInfos = []; + + this._peers.forEach((joinedPeer) => { - cb('that is not your corresponding mediasoup Peer name'); - - break; - } - else if (signalingPeer.mediaPeer) - { - cb('already have a mediasoup Peer'); - - break; - } - - this._mediaRoom.receiveRequest(request) - .then((response) => + if (joinedPeer.data.joined) { - cb(null, response); + peerInfos.push( + { + id : joinedPeer.id, + displayName : joinedPeer.data.displayName, + device : joinedPeer.data.device + }); + + for (const producer of joinedPeer.data.producers.values()) + { + this._createConsumer( + { + consumerPeer : peer, + producerPeer : joinedPeer, + producer + }); + } + } + }); - // Get the newly created mediasoup Peer. - const mediaPeer = this._mediaRoom.getPeerByName(peerName); + cb(null, { peers: peerInfos }); - signalingPeer.mediaPeer = mediaPeer; + // Mark the new Peer as joined. + peer.data.joined = true; - this._handleMediaPeer(signalingPeer, mediaPeer); - }) - .catch((error) => + this._notification( + peer.socket, + 'newPeer', { - cb(error.toString()); + id : peer.id, + displayName : displayName, + device : device + }, + true + ); + + logger.debug( + 'peer joined [peeerId: %s, displayName: %s, device: %o]', + peer.id, displayName, device); + + break; + } + + case 'createWebRtcTransport': + { + // NOTE: Don't require that the Peer is joined here, so the client can + // initiate mediasoup Transports and be ready when he later joins. + + const { forceTcp, producing, consuming } = request.data; + const { + maxIncomingBitrate, + initialAvailableOutgoingBitrate + } = config.mediasoup.webRtcTransport; + + const transport = await this._mediasoupRouter.createWebRtcTransport( + { + listenIps : config.mediasoup.webRtcTransport.listenIps, + enableUdp : !forceTcp, + enableTcp : true, + preferUdp : true, + initialAvailableOutgoingBitrate, + appData : { producing, consuming } }); + // Store the WebRtcTransport into the protoo Peer data Object. + peer.data.transports.set(transport.id, transport); + + cb( + null, + { + id : transport.id, + iceParameters : transport.iceParameters, + iceCandidates : transport.iceCandidates, + dtlsParameters : transport.dtlsParameters + }); + + // If set, apply max incoming bitrate limit. + if (maxIncomingBitrate) + { + try { await transport.setMaxIncomingBitrate(maxIncomingBitrate); } + catch (error) {} + } + + break; + } + + case 'connectWebRtcTransport': + { + const { transportId, dtlsParameters } = request.data; + const transport = peer.data.transports.get(transportId); + + if (!transport) + throw new Error(`transport with id "${transportId}" not found`); + + await transport.connect({ dtlsParameters }); + + cb(null); + + break; + } + + case 'restartIce': + { + const { transportId } = request.data; + const transport = peer.data.transports.get(transportId); + + if (!transport) + throw new Error(`transport with id "${transportId}" not found`); + + const iceParameters = await transport.restartIce(); + + cb(null, iceParameters); + + break; + } + + case 'produce': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { transportId, kind, rtpParameters } = request.data; + let { appData } = request.data; + const transport = peer.data.transports.get(transportId); + + if (!transport) + throw new Error(`transport with id "${transportId}" not found`); + + // Add peerId into appData to later get the associated Peer during + // the 'loudest' event of the audioLevelObserver. + appData = { ...appData, peerId: peer.id }; + + const producer = + await transport.produce({ kind, rtpParameters, appData }); + + // Store the Producer into the protoo Peer data Object. + peer.data.producers.set(producer.id, producer); + + // Set Producer events. + producer.on('score', (score) => + { + // logger.debug( + // 'producer "score" event [producerId:%s, score:%o]', + // producer.id, score); + + this._notification(peer.socket, 'producerScore', { producerId: producer.id, score }); + }); + + producer.on('videoorientationchange', (videoOrientation) => + { + logger.debug( + 'producer "videoorientationchange" event [producerId:%s, videoOrientation:%o]', + producer.id, videoOrientation); + }); + + cb(null, { id: producer.id }); + + this._peers.forEach((otherPeer) => + { + if (otherPeer.data.joined && otherPeer !== peer) + { + this._createConsumer( + { + consumerPeer : otherPeer, + producerPeer : peer, + producer + }); + } + }); + + // Add into the audioLevelObserver. + if (kind === 'audio') + { + this._audioLevelObserver.addProducer({ producerId: producer.id }) + .catch(() => {}); + } + + break; + } + + case 'closeProducer': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { producerId } = request.data; + const producer = peer.data.producers.get(producerId); + + if (!producer) + throw new Error(`producer with id "${producerId}" not found`); + + producer.close(); + + // Remove from its map. + peer.data.producers.delete(producer.id); + + cb(null); + + break; + } + + case 'pauseProducer': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { producerId } = request.data; + const producer = peer.data.producers.get(producerId); + + if (!producer) + throw new Error(`producer with id "${producerId}" not found`); + + await producer.pause(); + + cb(null); + + break; + } + + case 'resumeProducer': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { producerId } = request.data; + const producer = peer.data.producers.get(producerId); + + if (!producer) + throw new Error(`producer with id "${producerId}" not found`); + + await producer.resume(); + + cb(null); + + break; + } + + case 'pauseConsumer': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { consumerId } = request.data; + const consumer = peer.data.consumers.get(consumerId); + + if (!consumer) + throw new Error(`consumer with id "${consumerId}" not found`); + + await consumer.pause(); + + cb(null); + + break; + } + + case 'resumeConsumer': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { consumerId } = request.data; + const consumer = peer.data.consumers.get(consumerId); + + if (!consumer) + throw new Error(`consumer with id "${consumerId}" not found`); + + await consumer.resume(); + + cb(null); + + break; + } + + case 'setConsumerPreferedLayers': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { consumerId, spatialLayer, temporalLayer } = request.data; + const consumer = peer.data.consumers.get(consumerId); + + if (!consumer) + throw new Error(`consumer with id "${consumerId}" not found`); + + await consumer.setPreferredLayers({ spatialLayer, temporalLayer }); + + cb(null); + + break; + } + + case 'requestConsumerKeyFrame': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { consumerId } = request.data; + const consumer = peer.data.consumers.get(consumerId); + + if (!consumer) + throw new Error(`consumer with id "${consumerId}" not found`); + + await consumer.requestKeyFrame(); + + cb(null); + + break; + } + + case 'getTransportStats': + { + const { transportId } = request.data; + const transport = peer.data.transports.get(transportId); + + if (!transport) + throw new Error(`transport with id "${transportId}" not found`); + + const stats = await transport.getStats(); + + cb(null, stats); + + break; + } + + case 'getProducerStats': + { + const { producerId } = request.data; + const producer = peer.data.producers.get(producerId); + + if (!producer) + throw new Error(`producer with id "${producerId}" not found`); + + const stats = await producer.getStats(); + + cb(null, stats); + + break; + } + + case 'getConsumerStats': + { + const { consumerId } = request.data; + const consumer = peer.data.consumers.get(consumerId); + + if (!consumer) + throw new Error(`consumer with id "${consumerId}" not found`); + + const stats = await consumer.getStats(); + + cb(null, stats); + + break; + } + + case 'changeDisplayName': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { displayName } = request.data; + const oldDisplayName = peer.data.displayName; + + peer.data.displayName = displayName; + + // Spread to others + this._notification(peer.socket, 'changeDisplayName', { + peerId : peer.id, + displayName : displayName, + oldDisplayName : oldDisplayName + }, true); + + // Return no error + cb(null); + + break; + } + + case 'changeProfilePicture': + { + // Ensure the Peer is joined. + if (!peer.data.joined) + throw new Error('Peer not yet joined'); + + const { picture } = request.data; + + // Spread to others + this._notification(peer.socket, 'changeProfilePicture', { + peerId : peer.id, + picture : picture + }, true); + + // Return no error + cb(null); + + break; + } + + case 'chatMessage': + { + const { chatMessage } = request.data; + + this._chatHistory.push(chatMessage); + + // Spread to others + this._notification(peer.socket, 'chatMessage', { + peerId : peer.id, + chatMessage : chatMessage + }, true); + + // Return no error + cb(null); + + break; + } + + case 'serverHistory': + { + // Return to sender + cb( + null, + { + chatHistory : this._chatHistory, + fileHistory : this._fileHistory, + lastN : this._lastN + } + ); + + break; + } + + case 'lockRoom': + { + this._locked = true; + + // Spread to others + this._notification(peer.socket, 'lockRoom', { + peerId : peer.id + }, true); + + // Return no error + cb(null); + + break; + } + + case 'unlockRoom': + { + this._locked = false; + + // Spread to others + this._notification(peer.socket, 'unlockRoom', { + peerId : peer.id + }, true); + + // Return no error + cb(null); + + break; + } + + case 'sendFile': + { + const { file } = request.data; + + this._fileHistory.push(file); + + // Spread to others + this._notification(peer.socket, 'sendFile', { + file : file + }, true); + + // Return no error + cb(null); + + break; + } + + case 'raiseHand': + { + const { raiseHandState } = request.data; + + peer.data.raiseHandState = raiseHandState; + + // Spread to others + this._notification(peer.socket, 'raiseHand', { + peerId : peer.id, + raiseHandState : raiseHandState + }, true); + + // Return no error + cb(null); + break; } default: { - const { mediaPeer } = signalingPeer; + logger.error('unknown request.method "%s"', request.method); - if (!mediaPeer) - { - logger.error( - 'cannot handle mediasoup request, no mediasoup Peer [method:"%s"]', - request.method); - - cb('no mediasoup Peer'); - } - - mediaPeer.receiveRequest(request) - .then((response) => cb(null, response)) - .catch((error) => cb(error.toString())); + cb(500, `unknown request.method "${request.method}"`); } } } - _handleMediasoupClientNotification(signalingPeer, notification) + /** + * Creates a mediasoup Consumer for the given mediasoup Producer. + * + * @async + */ + async _createConsumer({ consumerPeer, producerPeer, producer }) { - logger.debug( - 'mediasoup-client notification [method:%s, peer:"%s"]', - notification.method, signalingPeer.peerName); + // Optimization: + // - Create the server-side Consumer. If video, do it paused. + // - Tell its Peer about it and wait for its response. + // - Upon receipt of the response, resume the server-side Consumer. + // - If video, this will mean a single key frame requested by the + // server-side Consumer (when resuming it). - // NOTE: mediasoup-client just sends notifications with target 'peer', - // so first of all, get the mediasoup Peer. - const { mediaPeer } = signalingPeer; - - if (!mediaPeer) + // NOTE: Don't create the Consumer if the remote Peer cannot consume it. + if ( + !consumerPeer.data.rtpCapabilities || + !this._mediasoupRouter.canConsume( + { + producerId : producer.id, + rtpCapabilities : consumerPeer.data.rtpCapabilities + }) + ) { - logger.error( - 'cannot handle mediasoup notification, no mediasoup Peer [method:"%s"]', - notification.method); + return; + } + + // Must take the Transport the remote Peer is using for consuming. + const transport = Array.from(consumerPeer.data.transports.values()) + .find((t) => t.appData.consuming); + + // This should not happen. + if (!transport) + { + logger.warn('_createConsumer() | Transport for consuming not found'); return; } - mediaPeer.receiveNotification(notification); + // Create the Consumer in paused mode. + let consumer; + + try + { + consumer = await transport.consume( + { + producerId : producer.id, + rtpCapabilities : consumerPeer.data.rtpCapabilities, + paused : producer.kind === 'video' + }); + } + catch (error) + { + logger.warn('_createConsumer() | transport.consume():%o', error); + + return; + } + + // Store the Consumer into the protoo consumerPeer data Object. + consumerPeer.data.consumers.set(consumer.id, consumer); + + // Set Consumer events. + consumer.on('transportclose', () => + { + // Remove from its map. + consumerPeer.data.consumers.delete(consumer.id); + }); + + consumer.on('producerclose', () => + { + // Remove from its map. + consumerPeer.data.consumers.delete(consumer.id); + + this._notification(consumerPeer.socket, 'consumerClosed', { consumerId: consumer.id }); + }); + + consumer.on('producerpause', () => + { + this._notification(consumerPeer.socket, 'consumerPaused', { consumerId: consumer.id }); + }); + + consumer.on('producerresume', () => + { + this._notification(consumerPeer.socket, 'consumerResumed', { consumerId: consumer.id }); + }); + + consumer.on('score', (score) => + { + // logger.debug( + // 'consumer "score" event [consumerId:%s, score:%o]', + // consumer.id, score); + + this._notification(consumerPeer.socket, 'consumerScore', { consumerId: consumer.id, score }); + }); + + consumer.on('layerschange', (layers) => + { + this._notification( + consumerPeer.socket, + 'consumerLayersChanged', + { + consumerId : consumer.id, + spatialLayer : layers ? layers.spatialLayer : null, + temporalLayer : layers ? layers.temporalLayer : null + } + ); + }); + + // Send a protoo request to the remote Peer with Consumer parameters. + try + { + await this._request( + consumerPeer.socket, + 'newConsumer', + { + peerId : producerPeer.id, + kind : producer.kind, + producerId : producer.id, + id : consumer.id, + kind : consumer.kind, + rtpParameters : consumer.rtpParameters, + type : consumer.type, + appData : producer.appData, + producerPaused : consumer.producerPaused + } + ); + + // Now that we got the positive response from the remote Peer and, if + // video, resume the Consumer to ask for an efficient key frame. + if (producer.kind === 'video') + await consumer.resume(); + + this._notification( + consumerPeer.socket, + 'consumerScore', + { + consumerId : consumer.id, + score : consumer.score + } + ); + } + catch (error) + { + logger.warn('_createConsumer() | failed:%o', error); + } } - _updateMaxBitrate() + _timeoutCallback(callback) { - if (this._mediaRoom.closed) - return; + let called = false; - const numPeers = this._mediaRoom.peers.length; - const previousMaxBitrate = this._maxBitrate; - let newMaxBitrate; + const interval = setTimeout( + () => + { + if (called) + return; + called = true; + callback(new Error('Request timeout.')); + }, + 10000 + ); - if (numPeers <= 2) + return (...args) => { - newMaxBitrate = MAX_BITRATE; + if (called) + return; + called = true; + clearTimeout(interval); + + callback(...args); + }; + } + + _request(socket, method, data = {}) + { + return new Promise((resolve, reject) => + { + socket.emit( + 'request', + { method, data }, + this._timeoutCallback((err, response) => + { + if (err) + { + reject(err); + } + else + { + resolve(response); + } + }) + ); + }); + } + + _notification(socket, method, data = {}, broadcast = false) + { + if (broadcast) + { + socket.broadcast.to(this._roomId).emit( + 'notification', { method, data } + ); } else { - newMaxBitrate = Math.round(MAX_BITRATE / ((numPeers - 1) * BITRATE_FACTOR)); - - if (newMaxBitrate < MIN_BITRATE) - newMaxBitrate = MIN_BITRATE; + socket.emit('notification', { method, data }); } - - this._maxBitrate = newMaxBitrate; - - for (const peer of this._mediaRoom.peers) - { - for (const transport of peer.transports) - { - if (transport.direction === 'send') - { - transport.setMaxBitrate(newMaxBitrate) - .catch((error) => - { - logger.error('transport.setMaxBitrate() failed: %s', String(error)); - }); - } - } - } - - logger.info( - '_updateMaxBitrate() [num peers:%s, before:%skbps, now:%skbps]', - numPeers, - Math.round(previousMaxBitrate / 1000), - Math.round(newMaxBitrate / 1000)); } } diff --git a/server/lib/homer.js b/server/lib/homer.js index 83106c2..9efcb36 100644 --- a/server/lib/homer.js +++ b/server/lib/homer.js @@ -54,7 +54,7 @@ function handleRoom(room, stream) Object.assign({}, baseEvent, { event : 'room.newpeer', - peerName : peer.name, + peerId : peer.id, rtpCapabilities : peer.rtpCapabilities }), stream); @@ -67,7 +67,7 @@ function handlePeer(peer, baseEvent, stream) { baseEvent = Object.assign({}, baseEvent, { - peerName : peer.name + peerId : peer.id }); peer.on('close', (originator) => diff --git a/server/package.json b/server/package.json index fb16c02..1762e0c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,19 +1,20 @@ { "name": "multiparty-meeting-server", - "version": "2.0.0", + "version": "3.0.0", "private": true, "description": "multiparty meeting server", "author": "Håvar Aambø Fosstveit ", "license": "MIT", "main": "lib/index.js", "dependencies": { + "awaitqueue": "^1.0.0", "base-64": "^0.1.0", "colors": "^1.1.2", "compression": "^1.7.3", "debug": "^4.1.0", "express": "^4.16.3", "express-session": "^1.16.1", - "mediasoup": "^2.6.11", + "mediasoup": "^3.0.12", "openid-client": "^2.5.0", "passport": "^0.4.0", "socket.io": "^2.1.1", diff --git a/server/server.js b/server/server.js index b74a76c..6c24643 100755 --- a/server/server.js +++ b/server/server.js @@ -10,6 +10,8 @@ const http = require('http'); const spdy = require('spdy'); const express = require('express'); const compression = require('compression'); +const mediasoup = require('mediasoup'); +const AwaitQueue = require('awaitqueue'); const Logger = require('./lib/Logger'); const Room = require('./lib/Room'); const utils = require('./util'); @@ -17,7 +19,7 @@ const base64 = require('base-64'); // auth const passport = require('passport'); const { Issuer, Strategy } = require('openid-client'); -const session = require('express-session') +const session = require('express-session'); /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); @@ -25,11 +27,18 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel); console.log('- config.mediasoup.logTags:', config.mediasoup.logTags); /* eslint-enable no-console */ -// Start the mediasoup server. -const mediaServer = require('./mediasoup'); - const logger = new Logger(); +const queue = new AwaitQueue(); + +// mediasoup Workers. +// @type {Array} +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(); @@ -40,35 +49,84 @@ const tls = key : fs.readFileSync(config.tls.key) }; -let app = express(); +const app = express(); let httpsServer; let oidcClient; let oidcStrategy; -passport.serializeUser(function(user, done) +passport.serializeUser((user, done) => { done(null, user); }); -passport.deserializeUser(function(user, done) +passport.deserializeUser((user, done) => { done(null, user); }); -const auth=config.auth; +const auth = config.auth; -function setupAuth(oidcIssuer) +async function run() +{ + if ( + typeof(auth) !== 'undefined' && + typeof(auth.issuerURL) !== 'undefined' && + typeof(auth.clientOptions) !== 'undefined' + ) + { + Issuer.discover(auth.issuerURL).then( async (oidcIssuer) => + { + // Setup authentication + await setupAuth(oidcIssuer); + + // Run a mediasoup Worker. + await runMediasoupWorkers(); + + // Run HTTPS server. + await runHttpsServer(); + + // Run WebSocketServer. + await runWebSocketServer(); + }) + .catch((err) => + { + logger.error(err); + }); + } + else + { + logger.error('Auth is not configure properly!'); + + // Run a mediasoup Worker. + await runMediasoupWorkers(); + + // Run HTTPS server. + await runHttpsServer(); + + // Run WebSocketServer. + await runWebSocketServer(); + } + + // Log rooms status every 30 seconds. + setInterval(() => + { + for (const room of rooms.values()) + { + room.logStatus(); + } + }, 120000); +} + +async function setupAuth(oidcIssuer) { oidcClient = new oidcIssuer.Client(auth.clientOptions); - const params = - { - ...auth.clientOptions - // ... any authorization request parameters go here - // client_id defaults to client.client_id - // redirect_uri defaults to client.redirect_uris[0] - // response type defaults to client.response_types[0], then 'code' - // scope defaults to 'openid' - }; + + // ... any authorization request parameters go here + // client_id defaults to client.client_id + // redirect_uri defaults to client.redirect_uris[0] + // response type defaults to client.response_types[0], then 'code' + // scope defaults to 'openid' + const params = auth.clientOptions; // optional, defaults to false, when true req is passed as a first // argument to verify fn @@ -78,63 +136,73 @@ function setupAuth(oidcIssuer) // resolved from the issuer configuration, instead of true you may provide // any of the supported values directly, i.e. "S256" (recommended) or "plain" const usePKCE = false; - const client=oidcClient; + const client = oidcClient; oidcStrategy = new Strategy( - { client, params, passReqToCallback, usePKCE }, - (tokenset, userinfo, done) => - { - let user = { - id : tokenset.claims.sub, - provider : tokenset.claims.iss, - _userinfo : userinfo, - _claims : tokenset.claims, + { client, params, passReqToCallback, usePKCE }, + (tokenset, userinfo, done) => + { + const user = + { + id : tokenset.claims.sub, + provider : tokenset.claims.iss, + _userinfo : userinfo, + _claims : tokenset.claims }; - - if ( typeof(userinfo.picture) !== 'undefined' ){ - if ( ! userinfo.picture.match(/^http/g) ) { + if (typeof(userinfo.picture) !== 'undefined') + { + if (!userinfo.picture.match(/^http/g)) + { user.Photos = [ { value: `data:image/jpeg;base64, ${userinfo.picture}` } ]; - } else { - user.Photos= [ { value: userinfo.picture } ]; + } + else + { + user.Photos = [ { value: userinfo.picture } ]; } } - if ( typeof(userinfo.nickname) !== 'undefined' ){ - user.displayName=userinfo.nickname; + if (typeof(userinfo.nickname) !== 'undefined') + { + user.displayName = userinfo.nickname; } - if ( typeof(userinfo.name) !== 'undefined' ){ - user.displayName=userinfo.name; + if (typeof(userinfo.name) !== 'undefined') + { + user.displayName = userinfo.name; } - if ( typeof(userinfo.email) !== 'undefined' ){ - user.emails=[{value: userinfo.email}]; + if (typeof(userinfo.email) !== 'undefined') + { + user.emails = [ { value: userinfo.email } ]; } - if ( typeof(userinfo.given_name) !== 'undefined' ){ - user.name={givenName: userinfo.given_name}; + if (typeof(userinfo.given_name) !== 'undefined') + { + user.name = { givenName: userinfo.given_name }; } - if ( typeof(userinfo.family_name) !== 'undefined' ){ - user.name={familyName: userinfo.family_name}; + if (typeof(userinfo.family_name) !== 'undefined') + { + user.name = { familyName: userinfo.family_name }; } - if ( typeof(userinfo.middle_name) !== 'undefined' ){ - user.name={middleName: userinfo.middle_name}; + if (typeof(userinfo.middle_name) !== 'undefined') + { + user.name = { middleName: userinfo.middle_name }; } - return done(null, user); } ); + passport.use('oidc', oidcStrategy); app.use(session({ - secret: config.cookieSecret, - resave: true, - saveUninitialized: true, - cookie: { secure: true } + secret : config.cookieSecret, + resave : true, + saveUninitialized : true, + cookie : { secure: true } })); app.use(passport.initialize()); @@ -145,20 +213,20 @@ function setupAuth(oidcIssuer) { passport.authenticate('oidc', { state : base64.encode(JSON.stringify({ - roomId : req.query.roomId, - peerName : req.query.peerName, - code : utils.random(10) + roomId : req.query.roomId, + peerId : req.query.peerId, + code : utils.random(10) })) })(req, res, next); }); // logout - app.get('/auth/logout', function(req, res) + app.get('/auth/logout', (req, res) => { req.logout(); res.redirect('/'); - } - ); + }); + // callback app.get( '/auth/callback', @@ -168,22 +236,30 @@ function setupAuth(oidcIssuer) const state = JSON.parse(base64.decode(req.query.state)); if (rooms.has(state.roomId)) - { - let displayName,photo - if (typeof(req.user) !== 'undefined'){ - if (typeof(req.user.displayName) !== 'undefined') displayName=req.user.displayName; - else displayName=""; + { + let displayName; + let photo; + + if (typeof(req.user) !== 'undefined') + { + if (typeof(req.user.displayName) !== 'undefined') + displayName = req.user.displayName; + else + displayName = ''; + if ( typeof(req.user.Photos) !== 'undefined' && typeof(req.user.Photos[0]) !== 'undefined' && typeof(req.user.Photos[0].value) !== 'undefined' - ) photo=req.user.Photos[0].value; - else photo="/static/media/buddy.403cb9f6.svg"; + ) + photo = req.user.Photos[0].value; + else + photo = '/static/media/buddy.403cb9f6.svg'; } - + const data = { - peerName : state.peerName, + peerId : state.peerId, name : displayName, picture : photo }; @@ -198,9 +274,12 @@ function setupAuth(oidcIssuer) ); } -function setupWebServer() { +async function runHttpsServer() +{ app.use(compression()); + app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge')); + app.all('*', (req, res, next) => { if (req.secure) @@ -234,19 +313,23 @@ function setupWebServer() { { logger.info('Server redirecting port: ', config.listeningRedirectPort); }); -}; +} -function setupSocketIO(){ +/** + * Create a protoo WebSocketServer to allow WebSocket connections from browsers. + */ +async function runWebSocketServer() +{ const io = require('socket.io')(httpsServer); // Handle connections from clients. io.on('connection', (socket) => { - const { roomId, peerName } = socket.handshake.query; + const { roomId, peerId } = socket.handshake.query; - if (!roomId || !peerName) + if (!roomId || !peerId) { - logger.warn('connection request without roomId and/or peerName'); + logger.warn('connection request without roomId and/or peerId'); socket.disconnect(true); @@ -254,72 +337,90 @@ function setupSocketIO(){ } logger.info( - 'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName); + 'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId); - let room; - - // If an unknown roomId, create a new Room. - if (!rooms.has(roomId)) + queue.push(async () => { - logger.info('creating a new Room [roomId:"%s"]', roomId); + const room = await getOrCreateRoom({ roomId }); - try - { - room = new Room(roomId, mediaServer, io); - - global.APP_ROOM = room; - } - catch (error) - { - logger.error('error creating a new Room: %s', error); - - socket.disconnect(true); - - return; - } - - const logStatusTimer = setInterval(() => - { - room.logStatus(); - }, 30000); - - rooms.set(roomId, room); - - room.on('close', () => - { - rooms.delete(roomId); - clearInterval(logStatusTimer); - }); - } - else + room.handleConnection({ peerId, socket }); + }) + .catch((error) => { - room = rooms.get(roomId); - } + logger.error('room creation or room joining failed:%o', error); - socket.room = roomId; + socket.disconnect(true); - room.handleConnection(peerName, socket); + return; + }); }); } -if ( - typeof(auth) !== 'undefined' && - typeof(auth.issuerURL) !== 'undefined' && - typeof(auth.clientOptions) !== 'undefined' -) + +/** + * Launch as many mediasoup Workers as given in the configuration file. + */ +async function runMediasoupWorkers() { - Issuer.discover(auth.issuerURL).then((oidcIssuer) => + const { numWorkers } = config.mediasoup; + + logger.info('running %d mediasoup Workers...', numWorkers); + + for (let i = 0; i < numWorkers; ++i) { - setupAuth(oidcIssuer); - setupWebServer(); - setupSocketIO(); - }).catch((err) => { - logger.error(err); + const worker = await mediasoup.createWorker( + { + logLevel : config.mediasoup.worker.logLevel, + logTags : config.mediasoup.worker.logTags, + rtcMinPort : config.mediasoup.worker.rtcMinPort, + rtcMaxPort : config.mediasoup.worker.rtcMaxPort + }); + + worker.on('died', () => + { + logger.error( + 'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid); + + setTimeout(() => process.exit(1), 2000); + }); + + mediasoupWorkers.push(worker); } - ); -} else -{ - logger.error('Auth is not configure properly!'); - setupWebServer(); - setupSocketIO(); } +/** + * 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). + */ +async function getOrCreateRoom({ roomId }) +{ + let room = rooms.get(roomId); + + // If the Room does not exist create a new one. + if (!room) + { + logger.info('creating a new Room [roomId:%s]', roomId); + + const mediasoupWorker = getMediasoupWorker(); + + room = await Room.create({ mediasoupWorker, roomId }); + + rooms.set(roomId, room); + room.on('close', () => rooms.delete(roomId)); + } + + return room; +} + +run(); \ No newline at end of file From bc9deb12006fce66e07bcd0c5b8fb99b0a906233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 3 Jun 2019 12:24:07 +0200 Subject: [PATCH 11/44] Updated handling of audio device change --- app/src/RoomClient.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 8ce133a..26107d0 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -809,6 +809,8 @@ export default class RoomClient logger.debug( 'changeAudioDevice() | new selected webcam [device:%o]', device); + + this._micProducer.track.stop(); logger.debug('changeAudioDevice() | calling getUserMedia()'); @@ -822,14 +824,18 @@ export default class RoomClient const track = stream.getAudioTracks()[0]; - const newTrack = await this._micProducer.replaceTrack(track); + await this._micProducer.replaceTrack({ track }); const harkStream = new MediaStream(); - harkStream.addTrack(newTrack); + harkStream.addTrack(track); + if (!harkStream.getAudioTracks()[0]) throw new Error('changeAudioDevice(): given stream has no audio track'); - if (this._micProducer.hark != null) this._micProducer.hark.stop(); + + if (this._micProducer.hark != null) + this._micProducer.hark.stop(); + this._micProducer.hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars @@ -855,7 +861,7 @@ export default class RoomClient track.stop(); store.dispatch( - stateActions.setProducerTrack(this._micProducer.id, newTrack)); + stateActions.setProducerTrack(this._micProducer.id, track)); store.dispatch(stateActions.setSelectedAudioDevice(deviceId)); @@ -1885,8 +1891,10 @@ export default class RoomClient const harkStream = new MediaStream(); harkStream.addTrack(track); + if (!harkStream.getAudioTracks()[0]) throw new Error('enableMic(): given stream has no audio track'); + this._micProducer.hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars From 65f320cd4889e77cced441333969144005b44d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 3 Jun 2019 13:19:57 +0200 Subject: [PATCH 12/44] Stray line removed --- app/src/RoomClient.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 26107d0..a3398ee 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -858,8 +858,6 @@ export default class RoomClient } }); - track.stop(); - store.dispatch( stateActions.setProducerTrack(this._micProducer.id, track)); From 8a98f9b841aa1fc64b2ff64212f54c89f1fe7bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 3 Jun 2019 14:04:15 +0200 Subject: [PATCH 13/44] Fix for auth because of updated signaling --- server/server.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/server/server.js b/server/server.js index 6c24643..366181e 100755 --- a/server/server.js +++ b/server/server.js @@ -161,33 +161,33 @@ async function setupAuth(oidcIssuer) user.Photos = [ { value: userinfo.picture } ]; } } - - if (typeof(userinfo.nickname) !== 'undefined') + + if (userinfo.nickname != null) { user.displayName = userinfo.nickname; } - if (typeof(userinfo.name) !== 'undefined') + if (userinfo.name != null) { user.displayName = userinfo.name; } - - if (typeof(userinfo.email) !== 'undefined') + + if (userinfo.email != null) { user.emails = [ { value: userinfo.email } ]; } - if (typeof(userinfo.given_name) !== 'undefined') + if (userinfo.given_name != null) { user.name = { givenName: userinfo.given_name }; } - if (typeof(userinfo.family_name) !== 'undefined') + if (userinfo.family_name != null) { user.name = { familyName: userinfo.family_name }; } - if (typeof(userinfo.middle_name) !== 'undefined') + if (userinfo.middle_name != null) { user.name = { middleName: userinfo.middle_name }; } @@ -240,17 +240,17 @@ async function setupAuth(oidcIssuer) let displayName; let photo; - if (typeof(req.user) !== 'undefined') + if (req.user != null) { - if (typeof(req.user.displayName) !== 'undefined') + if (req.user.displayName != null) displayName = req.user.displayName; else displayName = ''; if ( - typeof(req.user.Photos) !== 'undefined' && - typeof(req.user.Photos[0]) !== 'undefined' && - typeof(req.user.Photos[0].value) !== 'undefined' + req.user.Photos != null && + req.user.Photos[0] != null && + req.user.Photos[0].value != null ) photo = req.user.Photos[0].value; else @@ -259,9 +259,9 @@ async function setupAuth(oidcIssuer) const data = { - peerId : state.peerId, - name : displayName, - picture : photo + peerId : state.peerId, + displayName : displayName, + picture : photo }; const room = rooms.get(state.roomId); From 0ceb016d04fcc32c65609073bccd07405373f2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 3 Jun 2019 14:14:06 +0200 Subject: [PATCH 14/44] Updated consumer audio level state change --- app/src/RoomClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index a3398ee..25bed16 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1256,7 +1256,7 @@ export default class RoomClient // resume this Consumer (which was paused for now). cb(null); - if (consumer.kind === 'audio') + if (kind === 'audio') { const stream = new MediaStream(); @@ -1285,7 +1285,7 @@ export default class RoomClient { consumer.volume = volume; - store.dispatch(stateActions.setPeerVolume(consumer.peerId, volume)); + store.dispatch(stateActions.setPeerVolume(peerId, volume)); } }); } From 38627e485de2abff00bde21fcd31c19120f0206e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 09:42:07 +0200 Subject: [PATCH 15/44] Fix to hark. Handle consumer close. --- app/src/RoomClient.js | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 25bed16..df0ffec 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -163,6 +163,9 @@ export default class RoomClient // Local mic mediasoup Producer. this._micProducer = null; + // Local mic hark + this._hark = null; + // Local webcam mediasoup Producer. this._webcamProducer = null; @@ -826,6 +829,8 @@ export default class RoomClient await this._micProducer.replaceTrack({ track }); + this._micProducer.volume = 0; + const harkStream = new MediaStream(); harkStream.addTrack(track); @@ -833,13 +838,13 @@ export default class RoomClient if (!harkStream.getAudioTracks()[0]) throw new Error('changeAudioDevice(): given stream has no audio track'); - if (this._micProducer.hark != null) - this._micProducer.hark.stop(); + if (this._hark != null) + this._hark.stop(); - this._micProducer.hark = hark(harkStream, { play: false }); + this._hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars - this._micProducer.hark.on('volume_change', (dBs, threshold) => + this._hark.on('volume_change', (dBs, threshold) => { // The exact formula to convert from dBs (-100..0) to linear (0..1) is: // Math.pow(10, dBs / 20) @@ -850,10 +855,13 @@ export default class RoomClient if (volume === 1) volume = 0; + volume = Math.round(volume); - if (volume !== this._micProducer.volume) + + if (this._micProducer && volume !== this._micProducer.volume) { this._micProducer.volume = volume; + store.dispatch(stateActions.setPeerVolume(this._peerId, volume)); } }); @@ -1258,9 +1266,12 @@ export default class RoomClient if (kind === 'audio') { + consumer.volume = 0; + const stream = new MediaStream(); stream.addTrack(consumer.track); + if (!stream.getAudioTracks()[0]) throw new Error('request.newConsumer | given stream has no audio track'); @@ -1281,7 +1292,7 @@ export default class RoomClient volume = Math.round(volume); - if (volume !== consumer.volume) + if (consumer && volume !== consumer.volume) { consumer.volume = volume; @@ -1493,6 +1504,10 @@ export default class RoomClient break; consumer.close(); + + if (consumer.hark != null) + consumer.hark.stop(); + this._consumers.delete(consumerId); const { peerId } = consumer.appData; @@ -1886,6 +1901,8 @@ export default class RoomClient .catch(() => {}); }); + this._micProducer.volume = 0; + const harkStream = new MediaStream(); harkStream.addTrack(track); @@ -1893,10 +1910,13 @@ export default class RoomClient if (!harkStream.getAudioTracks()[0]) throw new Error('enableMic(): given stream has no audio track'); - this._micProducer.hark = hark(harkStream, { play: false }); + if (this._hark != null) + this._hark.stop(); + + this._hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars - this._micProducer.hark.on('volume_change', (dBs, threshold) => + this._hark.on('volume_change', (dBs, threshold) => { // The exact formula to convert from dBs (-100..0) to linear (0..1) is: // Math.pow(10, dBs / 20) @@ -1907,10 +1927,13 @@ export default class RoomClient if (volume === 1) volume = 0; + volume = Math.round(volume); + if (this._micProducer && volume !== this._micProducer.volume) { this._micProducer.volume = volume; + store.dispatch(stateActions.setPeerVolume(this._peerId, volume)); } }); From a763d37879426c9003a3af5462e82ac649d0cf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 11:29:00 +0200 Subject: [PATCH 16/44] Fixed sending picture to peers if you have it. --- app/src/RoomClient.js | 6 +++++- server/lib/Room.js | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index df0ffec..cd018b6 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1583,7 +1583,10 @@ export default class RoomClient { logger.debug('_joinRoom()'); - const { displayName } = store.getState().settings; + const { + displayName, + picture + } = store.getState().settings; try { @@ -1702,6 +1705,7 @@ export default class RoomClient 'join', { displayName : displayName, + picture : picture, device : this._device, rtpCapabilities : this._consume ? this._mediasoupDevice.rtpCapabilities diff --git a/server/lib/Room.js b/server/lib/Room.js index b1fa234..8ff7872 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -316,10 +316,16 @@ class Room extends EventEmitter if (peer.data.joined) throw new Error('Peer already joined'); - const { displayName, device, rtpCapabilities } = request.data; + const { + displayName, + picture, + device, + rtpCapabilities + } = request.data; // Store client data into the protoo Peer data object. peer.data.displayName = displayName; + peer.data.picture = picture; peer.data.device = device; peer.data.rtpCapabilities = rtpCapabilities; @@ -336,6 +342,7 @@ class Room extends EventEmitter { id : joinedPeer.id, displayName : joinedPeer.data.displayName, + picture : joinedPeer.data.picture, device : joinedPeer.data.device }); @@ -362,14 +369,15 @@ class Room extends EventEmitter { id : peer.id, displayName : displayName, + picture : picture, device : device }, true ); logger.debug( - 'peer joined [peeerId: %s, displayName: %s, device: %o]', - peer.id, displayName, device); + 'peer joined [peeerId: %s, displayName: %s, picture: %s, device: %o]', + peer.id, displayName, picture, device); break; } From 0aa0aa08b0e5b18b765b7c284773b4eea9859c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 11:42:14 +0200 Subject: [PATCH 17/44] Fixed adding picture from newPeer. --- app/src/RoomClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index cd018b6..8aa36ad 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1472,10 +1472,10 @@ export default class RoomClient case 'newPeer': { - const { id, displayName, device } = notification.data; + const { id, displayName, picture, device } = notification.data; store.dispatch( - stateActions.addPeer({ id, displayName, device, consumers: [] })); + stateActions.addPeer({ id, displayName, picture, device, consumers: [] })); store.dispatch(requestActions.notify( { From a3013bb716be7e189d8ce81515b2d576a7e30943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 12:58:12 +0200 Subject: [PATCH 18/44] Removed screensharing extension. Added some small optimization fixes. --- .gitignore | 1 + CHANGELOG.md | 17 ++ README.md | 1 - app/package.json | 2 +- app/public/index.html | 3 +- app/public/robots.txt | 3 + app/src/RoomClient.js | 51 +----- app/src/ScreenShare.js | 237 +------------------------ app/src/actions/stateActions.js | 11 +- app/src/components/Controls/Sidebar.js | 7 +- app/src/reducers/me.js | 10 -- server/package.json | 2 +- 12 files changed, 31 insertions(+), 314 deletions(-) create mode 100644 app/public/robots.txt diff --git a/.gitignore b/.gitignore index 7f87e51..ded69d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ /server/public/ /server/certs/ !/server/certs/mediasoup-demo.localhost.* +.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f982c1..375f121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +### 2.1 +* Updated to mediasoup v3 +* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client) + - OpenID Connect discovery + - Auth code flow +* Add spdy http2 support. + - Notice it does not supports node 11.x + + ### 2.0 +* Material UI +* Separate settings for lastN for desktop and mobile + + ### 1.2 +* Add Lock Room feature +* Fix suspended Web Audio context / fixed delayed getUsermedia +* Added support for the new getdisplaymedia API in Chrome 72 + ### 1.1 * Moved Filesharing code out from React code to RoomClient * Major cleanup of CSS. Variables for most colors and sizes exposed in :root diff --git a/README.md b/README.md index a657a05..6e69fb1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci * Chat * Screen sharing * File sharing -* Different video layouts There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no. diff --git a/app/package.json b/app/package.json index 943109e..c61504f 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "multiparty-meeting", - "version": "2.0.0", + "version": "2.1.0", "private": true, "description": "multiparty meeting service", "author": "Håvar Aambø Fosstveit ", diff --git a/app/public/index.html b/app/public/index.html index 2c07c0b..86b1003 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -9,8 +9,7 @@ - - + diff --git a/app/public/robots.txt b/app/public/robots.txt new file mode 100644 index 0000000..21fed79 --- /dev/null +++ b/app/public/robots.txt @@ -0,0 +1,3 @@ +# Allow crawling of all content +User-agent: * +Disallow: \ No newline at end of file diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 8aa36ad..217e268 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -748,53 +748,6 @@ export default class RoomClient } } - installExtension() - { - logger.debug('installExtension()'); - - return new Promise((resolve, reject) => - { - window.addEventListener('message', _onExtensionMessage, false); - // eslint-disable-next-line - chrome.webstore.install(null, _successfulInstall, _failedInstall); - function _onExtensionMessage({ data }) - { - if (data.type === 'ScreenShareInjected') - { - logger.debug('installExtension() | installation succeeded'); - - return resolve(); - } - } - - function _failedInstall(reason) - { - window.removeEventListener('message', _onExtensionMessage); - - return reject( - new Error('Failed to install extension: %s', reason)); - } - - function _successfulInstall() - { - logger.debug('installExtension() | installation accepted'); - } - }) - .then(() => - { - // This should be handled better - store.dispatch(stateActions.setScreenCapabilities( - { - canShareScreen : this._room.canSend('video'), - needExtension : false - })); - }) - .catch((error) => - { - logger.error('installExtension() | failed: %o', error); - }); - } - async changeAudioDevice(deviceId) { logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); @@ -1697,7 +1650,6 @@ export default class RoomClient canSendWebcam : this._mediasoupDevice.canProduce('video'), canShareScreen : this._mediasoupDevice.canProduce('video') && this._screenSharing.isScreenShareAvailable(), - needExtension : this._screenSharing.needExtension(), canShareFiles : this._torrentSupport })); @@ -2021,8 +1973,7 @@ export default class RoomClient try { - const available = this._screenSharing.isScreenShareAvailable() && - !this._screenSharing.needExtension(); + const available = this._screenSharing.isScreenShareAvailable(); if (!available) throw new Error('screen sharing not available'); diff --git a/app/src/ScreenShare.js b/app/src/ScreenShare.js index 9df56b4..c9336d1 100644 --- a/app/src/ScreenShare.js +++ b/app/src/ScreenShare.js @@ -1,113 +1,4 @@ -class ChromeScreenShare -{ - constructor() - { - this._stream = null; - } - - start(options = { }) - { - const state = this; - - return new Promise((resolve, reject) => - { - window.addEventListener('message', _onExtensionMessage, false); - window.postMessage({ type: 'getStreamId' }, '*'); - - function _onExtensionMessage({ data }) - { - if (data.type !== 'gotStreamId') - { - return; - } - - const constraints = state._toConstraints(options, data.streamId); - - navigator.mediaDevices.getUserMedia(constraints) - .then((stream) => - { - window.removeEventListener('message', _onExtensionMessage); - - state._stream = stream; - resolve(stream); - }) - .catch((err) => - { - window.removeEventListener('message', _onExtensionMessage); - - reject(err); - }); - } - }); - } - - stop() - { - if (this._stream instanceof MediaStream === false) - { - return; - } - - this._stream.getTracks().forEach((track) => track.stop()); - this._stream = null; - } - - isScreenShareAvailable() - { - if ('__multipartyMeetingScreenShareExtensionAvailable__' in window) - { - return true; - } - - return false; - } - - needExtension() - { - if ('__multipartyMeetingScreenShareExtensionAvailable__' in window) - { - return false; - } - - return true; - } - - _toConstraints(options, streamId) - { - const constraints = { - video : { - mandatory : { - chromeMediaSource : 'desktop', - chromeMediaSourceId : streamId - }, - optional : [ { - googTemporalLayeredScreencast : true - } ] - }, - audio : false - }; - - if (isFinite(options.width)) - { - constraints.video.mandatory.maxWidth = options.width; - constraints.video.mandatory.minWidth = options.width; - } - if (isFinite(options.height)) - { - constraints.video.mandatory.maxHeight = options.height; - constraints.video.mandatory.minHeight = options.height; - } - if (isFinite(options.frameRate)) - { - constraints.video.mandatory.maxFrameRate = options.frameRate; - constraints.video.mandatory.minFrameRate = options.frameRate; - } - - return constraints; - } -} - -class Chrome72ScreenShare +class DisplayMediaScreenShare { constructor() { @@ -143,11 +34,6 @@ class Chrome72ScreenShare return true; } - needExtension() - { - return false; - } - _toConstraints() { const constraints = { @@ -194,11 +80,6 @@ class FirefoxScreenShare return true; } - needExtension() - { - return false; - } - _toConstraints(options) { const constraints = { @@ -238,119 +119,12 @@ class FirefoxScreenShare } } -class Firefox66ScreenShare -{ - constructor() - { - this._stream = null; - } - - start(options = {}) - { - const constraints = this._toConstraints(options); - - return navigator.mediaDevices.getDisplayMedia(constraints) - .then((stream) => - { - this._stream = stream; - - return Promise.resolve(stream); - }); - } - - stop() - { - if (this._stream instanceof MediaStream === false) - { - return; - } - - this._stream.getTracks().forEach((track) => track.stop()); - this._stream = null; - } - - isScreenShareAvailable() - { - return true; - } - - needExtension() - { - return false; - } - - _toConstraints() - { - const constraints = { - video : true - }; - - return constraints; - } -} - -class EdgeScreenShare -{ - constructor() - { - this._stream = null; - } - - start(options = {}) - { - const constraints = this._toConstraints(options); - - return navigator.getDisplayMedia(constraints) - .then((stream) => - { - this._stream = stream; - - return Promise.resolve(stream); - }); - } - - stop() - { - if (this._stream instanceof MediaStream === false) - { - return; - } - - this._stream.getTracks().forEach((track) => track.stop()); - this._stream = null; - } - - isScreenShareAvailable() - { - return true; - } - - needExtension() - { - return false; - } - - _toConstraints() - { - const constraints = { - video : true - }; - - return constraints; - } -} - class DefaultScreenShare { isScreenShareAvailable() { return false; } - - needExtension() - { - return false; - } } export default class ScreenShare @@ -364,18 +138,15 @@ export default class ScreenShare if (device.version < 66.0) return new FirefoxScreenShare(); else - return new Firefox66ScreenShare(); + return new DisplayMediaScreenShare(); } case 'chrome': { - if (device.version < 72.0) - return new ChromeScreenShare(); - else - return new Chrome72ScreenShare(); + return new DisplayMediaScreenShare(); } case 'msedge': { - return new EdgeScreenShare(); + return new DisplayMediaScreenShare(); } default: { diff --git a/app/src/actions/stateActions.js b/app/src/actions/stateActions.js index 26d0baf..98f6fda 100644 --- a/app/src/actions/stateActions.js +++ b/app/src/actions/stateActions.js @@ -69,21 +69,12 @@ export const setMediaCapabilities = ({ canSendMic, canSendWebcam, canShareScreen, - needExtension, canShareFiles }) => { return { type : 'SET_MEDIA_CAPABILITIES', - payload : { canSendMic, canSendWebcam, canShareScreen, needExtension, canShareFiles } - }; -}; - -export const setScreenCapabilities = ({ canShareScreen, needExtension }) => -{ - return { - type : 'SET_SCREEN_CAPABILITIES', - payload : { canShareScreen, needExtension } + payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles } }; }; diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index cc9fe86..fbb11db 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -119,12 +119,7 @@ const Sidebar = (props) => let screenTip; - if (me.needExtension) - { - screenState = 'need-extension'; - screenTip = 'Install screen sharing extension'; - } - else if (!me.canShareScreen) + if (!me.canShareScreen) { screenState = 'unsupported'; screenTip = 'Screen sharing not supported'; diff --git a/app/src/reducers/me.js b/app/src/reducers/me.js index 948988f..8f0969b 100644 --- a/app/src/reducers/me.js +++ b/app/src/reducers/me.js @@ -5,7 +5,6 @@ const initialState = canSendMic : false, canSendWebcam : false, canShareScreen : false, - needExtension : false, canShareFiles : false, audioDevices : null, webcamDevices : null, @@ -50,7 +49,6 @@ const me = (state = initialState, action) => canSendMic, canSendWebcam, canShareScreen, - needExtension, canShareFiles } = action.payload; @@ -59,18 +57,10 @@ const me = (state = initialState, action) => canSendMic, canSendWebcam, canShareScreen, - needExtension, canShareFiles }; } - case 'SET_SCREEN_CAPABILITIES': - { - const { canShareScreen, needExtension } = action.payload; - - return { ...state, canShareScreen, needExtension }; - } - case 'SET_AUDIO_DEVICES': { const { devices } = action.payload; diff --git a/server/package.json b/server/package.json index 1762e0c..4269b56 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "multiparty-meeting-server", - "version": "3.0.0", + "version": "2.1.0", "private": true, "description": "multiparty meeting server", "author": "Håvar Aambø Fosstveit ", From 149c40e89eea7bcaaba0088158ad25778e4d901c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 13:04:00 +0200 Subject: [PATCH 19/44] Remove domain from config. Unused, fixes #97. --- server/config/config.example.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index ef41311..59c4252 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -23,8 +23,6 @@ module.exports = }, // session cookie secret cookieSecret : 'T0P-S3cR3t_cook!e', - // Listening hostname for `gulp live|open`. - domain : 'localhost', tls : { cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, From 2af49fcf41f9dd2b14c054fbd878a483cfbda92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 13:08:58 +0200 Subject: [PATCH 20/44] Removed unused case. --- app/src/components/Controls/Sidebar.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index fbb11db..18276e4 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -206,11 +206,6 @@ const Sidebar = (props) => roomClient.enableScreenSharing(); break; } - case 'need-extension': - { - roomClient.installExtension(); - break; - } default: { break; From c47dc5a3e46d0da6d583cfdb7de841b6116c9541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 4 Jun 2019 13:09:54 +0200 Subject: [PATCH 21/44] Removed unused icon. --- app/src/components/Controls/Sidebar.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index 18276e4..958bc7d 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -15,7 +15,6 @@ import VideoIcon from '@material-ui/icons/Videocam'; import VideoOffIcon from '@material-ui/icons/VideocamOff'; import ScreenIcon from '@material-ui/icons/ScreenShare'; import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; -import ExtensionIcon from '@material-ui/icons/Extension'; import LockIcon from '@material-ui/icons/Lock'; import LockOpenIcon from '@material-ui/icons/LockOpen'; import LeaveIcon from '@material-ui/icons/Cancel'; @@ -221,10 +220,6 @@ const Sidebar = (props) => :null } - { screenState === 'need-extension' ? - - :null - } From 8a51f3c9661f0f09eb281b7ce3799fdb35303ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 5 Jun 2019 08:38:23 +0200 Subject: [PATCH 22/44] Small fix to tooltips error. --- app/src/components/Controls/Sidebar.js | 146 +++++++++++++------------ 1 file changed, 76 insertions(+), 70 deletions(-) diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index 958bc7d..79024ea 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -143,84 +143,90 @@ const Sidebar = (props) => } > - - { - micState === 'on' ? - roomClient.disableMic() : - roomClient.enableMic(); - }} - > - { micState === 'on' ? - - : - - } - +
    + + { + micState === 'on' ? + roomClient.disableMic() : + roomClient.enableMic(); + }} + > + { micState === 'on' ? + + : + + } + +
    - - { - webcamState === 'on' ? - roomClient.disableWebcam() : - roomClient.enableWebcam(); - }} - > - { webcamState === 'on' ? - - : - - } - +
    + + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + +
    - - { - switch (screenState) +
    + { - case 'on': + switch (screenState) { - roomClient.disableScreenSharing(); - break; - } - case 'off': - { - roomClient.enableScreenSharing(); - break; - } - default: - { - break; + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } } + }} + > + { screenState === 'on' || screenState === 'unsupported' ? + + :null } - }} - > - { screenState === 'on' || screenState === 'unsupported' ? - - :null - } - { screenState === 'off' ? - - :null - } - + { screenState === 'off' ? + + :null + } + +
    Date: Wed, 5 Jun 2019 10:40:59 +0200 Subject: [PATCH 23/44] Cleaned up file sharing. --- app/src/RoomClient.js | 38 +++-------- app/src/actions/stateActions.js | 4 +- .../MeetingDrawer/FileSharing/File.js | 35 ++++++---- .../MeetingDrawer/FileSharing/FileList.js | 65 +++++++++++++++++-- app/src/reducers/files.js | 15 ++--- server/lib/Room.js | 37 ++++++----- 6 files changed, 121 insertions(+), 73 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 217e268..f6f4764 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -566,15 +566,7 @@ export default class RoomClient if (existingTorrent) { - const { displayName, picture } = store.getState().settings; - - const file = { - magnetUri : existingTorrent.magnetURI, - displayName, - picture - }; - - return this._sendFile(file); + return this._sendFile(existingTorrent.magnetURI); } this._webTorrent.seed(files, (newTorrent) => @@ -584,34 +576,24 @@ export default class RoomClient text : 'File successfully shared.' })); - const { displayName, picture } = store.getState().settings; - const file = { - magnetUri : newTorrent.magnetURI, - displayName, - picture - }; - store.dispatch(stateActions.addFile( - { - magnetUri : file.magnetUri, - displayName : displayName, - picture : picture, - me : true - })); + this._peerId, + newTorrent.magnetURI + )); - this._sendFile(file); + this._sendFile(newTorrent.magnetURI); }); }); } // { file, name, picture } - async _sendFile(file) + async _sendFile(magnetUri) { - logger.debug('sendFile() [file: %o]', file); + logger.debug('sendFile() [magnetUri: %o]', magnetUri); try { - await this.sendRequest('sendFile', { file }); + await this.sendRequest('sendFile', { magnetUri }); } catch (error) { @@ -1392,9 +1374,9 @@ export default class RoomClient case 'sendFile': { - const { file } = notification.data; + const { peerId, magnetUri } = notification.data; - store.dispatch(stateActions.addFile(file)); + store.dispatch(stateActions.addFile(peerId, magnetUri)); store.dispatch(requestActions.notify( { diff --git a/app/src/actions/stateActions.js b/app/src/actions/stateActions.js index 98f6fda..23639f0 100644 --- a/app/src/actions/stateActions.js +++ b/app/src/actions/stateActions.js @@ -503,11 +503,11 @@ export const dropMessages = () => }; }; -export const addFile = (file) => +export const addFile = (peerId, magnetUri) => { return { type : 'ADD_FILE', - payload : { file } + payload : { peerId, magnetUri } }; }; diff --git a/app/src/components/MeetingDrawer/FileSharing/File.js b/app/src/components/MeetingDrawer/FileSharing/File.js index 05b92b0..72e6fbc 100644 --- a/app/src/components/MeetingDrawer/FileSharing/File.js +++ b/app/src/components/MeetingDrawer/FileSharing/File.js @@ -1,12 +1,12 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import * as appPropTypes from '../../appPropTypes'; import { connect } from 'react-redux'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import magnet from 'magnet-uri'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; -import EmptyAvatar from '../../../images/avatar-empty.jpeg'; const styles = (theme) => ({ @@ -55,15 +55,18 @@ class File extends React.PureComponent { const { roomClient, + displayName, + picture, canShareFiles, + magnetUri, file, classes } = this.props; return (
    - Peer avatar - + Avatar +
    { file.files ? @@ -93,17 +96,13 @@ class File extends React.PureComponent :null } - { file.me ? - 'You shared a file' - : - `${file.displayName} shared a file` - } + { `${displayName} shared a file` } { !file.active && !file.files ?
    - {magnet.decode(file.magnetUri).dn} + { magnet.decode(magnetUri).dn } { canShareFiles ? + + + + ); +}; + +JoinDialog.propTypes = +{ + roomClient : PropTypes.any.isRequired, + classes : PropTypes.object.isRequired +}; + +export default withRoomContext(withStyles(styles)(JoinDialog)); \ No newline at end of file diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 8a503b7..3b36826 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -15,7 +15,6 @@ import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; import Hidden from '@material-ui/core/Hidden'; import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import Avatar from '@material-ui/core/Avatar'; @@ -33,6 +32,7 @@ import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import SettingsIcon from '@material-ui/icons/Settings'; import Settings from './Settings/Settings'; +import JoinDialog from './JoinDialog'; const TIMEOUT = 10 * 1000; @@ -176,10 +176,6 @@ class Room extends React.PureComponent componentDidMount() { - const { roomClient } = this.props; - - roomClient.join(); - if (this.fullscreen.fullscreenEnabled) { this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); @@ -242,29 +238,7 @@ class Room extends React.PureComponent democratic : Democratic }[room.mode]; - if (room.audioSuspended) - { - return ( -
    - - - This webpage required sound and video to play, please click to allow. - - - -
    - ); - } - else if (room.lockedOut) + if (room.lockedOut) { return (
    @@ -274,6 +248,10 @@ class Room extends React.PureComponent
    ); } + else if (!room.joined) + { + return (); + } else { return ( diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 446b350..e7c01a2 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -14,7 +14,8 @@ const initialState = mode : 'democratic', selectedPeerId : null, spotlights : [], - settingsOpen : false + settingsOpen : false, + joined : false }; const room = (state = initialState, action) => @@ -88,6 +89,13 @@ const room = (state = initialState, action) => return { ...state, showSettings }; } + case 'TOGGLE_JOINED': + { + const joined = !state.joined; + + return { ...state, joined }; + } + case 'TOGGLE_FULLSCREEN_CONSUMER': { const { consumerId } = action.payload; From 27a1bf3821bfbbfa998ca62585c0a8ec2c84c54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 12 Jun 2019 15:44:29 +0200 Subject: [PATCH 30/44] Don't wait for local media --- app/src/RoomClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index a88a449..a2b7791 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1637,10 +1637,10 @@ export default class RoomClient if (this._produce) { if (this._mediasoupDevice.canProduce('audio')) - await this.enableMic(); + this.enableMic(); if (joinVideo && this._mediasoupDevice.canProduce('video')) - await this.enableWebcam(); + this.enableWebcam(); } store.dispatch(stateActions.setRoomState('connected')); From 7d831f4bda147f2077dfe5a17bbce2fb27d2cdab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 12 Jun 2019 15:53:48 +0200 Subject: [PATCH 31/44] Fix media device handling. --- app/src/RoomClient.js | 44 ++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index a2b7791..aea8f94 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -652,9 +652,15 @@ export default class RoomClient { logger.debug('muteMic()'); + this._micProducer.pause(); + try { - this._micProducer.pause(); + await this.sendRequest( + 'pauseProducer', { producerId: this._micProducer.id }); + + store.dispatch( + stateActions.setProducerPaused(this._micProducer.id)); } catch (error) { @@ -672,24 +678,32 @@ export default class RoomClient { logger.debug('unmuteMic()'); - try + if (!this._micProducer) { - if (this._micProducer) - this._micProducer.resume(); - else if (this._room.canSend('audio')) - await this.enableMic(); - else - throw new Error('cannot send audio'); + this.enableMic(); } - catch (error) + else { - logger.error('unmuteMic() | failed: %o', error); + this._micProducer.resume(); - store.dispatch(requestActions.notify( - { - type : 'error', - text : 'An error occured while accessing your microphone.' - })); + try + { + await this.sendRequest( + 'resumeProducer', { producerId: this._micProducer.id }); + + store.dispatch( + stateActions.setProducerResumed(this._micProducer.id)); + } + catch (error) + { + logger.error('unmuteMic() | failed: %o', error); + + store.dispatch(requestActions.notify( + { + type : 'error', + text : 'An error occured while accessing your microphone.' + })); + } } } From 4f3387b73c36bbf0dd83c19da432d090e5fd800a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 13 Jun 2019 13:29:52 +0200 Subject: [PATCH 32/44] Remove unused code. --- app/src/actions/stateActions.js | 8 -------- app/src/reducers/room.js | 8 -------- 2 files changed, 16 deletions(-) diff --git a/app/src/actions/stateActions.js b/app/src/actions/stateActions.js index 5690669..fda3ceb 100644 --- a/app/src/actions/stateActions.js +++ b/app/src/actions/stateActions.js @@ -43,14 +43,6 @@ export const setRoomLockedOut = () => }; }; -export const setAudioSuspended = ({ audioSuspended }) => -{ - return { - type : 'SET_AUDIO_SUSPENDED', - payload : { audioSuspended } - }; -}; - export const setSettingsOpen = ({ settingsOpen }) => ({ type : 'SET_SETTINGS_OPEN', diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index e7c01a2..e1444c3 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -4,7 +4,6 @@ const initialState = state : 'new', // new/connecting/connected/disconnected/closed, locked : false, lockedOut : false, - audioSuspended : false, activeSpeakerId : null, torrentSupport : false, showSettings : false, @@ -54,13 +53,6 @@ const room = (state = initialState, action) => return { ...state, lockedOut: true }; } - case 'SET_AUDIO_SUSPENDED': - { - const { audioSuspended } = action.payload; - - return { ...state, audioSuspended }; - } - case 'SET_SETTINGS_OPEN': { const { settingsOpen } = action.payload; From 819bd6494854d49de2ece525c20f65b27799c024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 13 Jun 2019 13:37:39 +0200 Subject: [PATCH 33/44] Wrap dialog in div with styling. --- app/src/components/Room.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 3b36826..afcc75d 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -250,7 +250,11 @@ class Room extends React.PureComponent } else if (!room.joined) { - return (); + return ( +
    + +
    + ); } else { From 2aa6166f6b51732ab25baf04dc16e85d07d1a139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 13 Jun 2019 13:56:31 +0200 Subject: [PATCH 34/44] Fix mute/unmute mic. --- app/src/components/Controls/Sidebar.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js index 79024ea..9ef0583 100644 --- a/app/src/components/Controls/Sidebar.js +++ b/app/src/components/Controls/Sidebar.js @@ -78,11 +78,16 @@ const Sidebar = (props) => let micTip; - if (!me.canSendMic || !micProducer) + if (!me.canSendMic) { micState = 'unsupported'; micTip = 'Audio unsupported'; } + else if (!micProducer) + { + micState = 'off'; + micTip = 'Activate audio'; + } else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) { micState = 'on'; @@ -90,7 +95,7 @@ const Sidebar = (props) => } else { - micState = 'off'; + micState = 'muted'; micTip = 'Unmute audio'; } @@ -152,9 +157,12 @@ const Sidebar = (props) => size={smallScreen ? 'large' : 'medium'} onClick={() => { - micState === 'on' ? - roomClient.disableMic() : + if (micState === 'off') roomClient.enableMic(); + else if (micState === 'on') + roomClient.muteMic(); + else + roomClient.unmuteMic(); }} > { micState === 'on' ? From 66b922d0b374834452aed0b451cb7a5655e98f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 19 Jun 2019 14:33:49 +0200 Subject: [PATCH 35/44] Cleanup of package.json --- app/package.json | 2 +- server/public/readme.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 server/public/readme.md diff --git a/app/package.json b/app/package.json index 0eed6e9..b32625c 100644 --- a/app/package.json +++ b/app/package.json @@ -37,7 +37,7 @@ "analyze-main": "source-map-explorer build/static/js/main.*", "analyze-chunk": "source-map-explorer build/static/js/2.*", "start": "HTTPS=true PORT=4443 react-scripts start", - "build": "react-scripts build && rm -rf ../server/public/* && cp -r build/* ../server/public/", + "build": "react-scripts build && mkdir -p ../server/public && rm -rf ../server/public/* && cp -r build/* ../server/public/", "test": "react-scripts test", "eject": "react-scripts eject" }, diff --git a/server/public/readme.md b/server/public/readme.md deleted file mode 100644 index acd45d4..0000000 --- a/server/public/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -## Webroot -This is the webroot, so the place of the app. From 9acf0056acb67adb42f670f16aa13d82842aac3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 24 Jun 2019 15:23:45 +0200 Subject: [PATCH 36/44] Removed sidebar. Moved buttons to me-view and top-bar. Added text hint on me-view. --- app/src/components/Containers/Me.js | 336 +++++++++++++++++++++++-- app/src/components/Containers/Peer.js | 18 +- app/src/components/Controls/Sidebar.js | 331 ------------------------ app/src/components/Room.js | 34 ++- 4 files changed, 358 insertions(+), 361 deletions(-) delete mode 100644 app/src/components/Controls/Sidebar.js diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 553f2eb..64bf3ee 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -1,19 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; import { connect } from 'react-redux'; import { meProducersSelector } from '../Selectors'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; +import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; import VideoView from '../VideoContainers/VideoView'; import Volume from './Volume'; +import Fab from '@material-ui/core/Fab'; +import Tooltip from '@material-ui/core/Tooltip'; +import MicIcon from '@material-ui/icons/Mic'; +import MicOffIcon from '@material-ui/icons/MicOff'; +import VideoIcon from '@material-ui/icons/Videocam'; +import VideoOffIcon from '@material-ui/icons/VideocamOff'; +import ScreenIcon from '@material-ui/icons/ScreenShare'; +import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; -const styles = () => +const styles = (theme) => ({ root : { - flexDirection : 'row', flex : '0 0 auto', boxShadow : 'var(--peer-shadow)', border : 'var(--peer-border)', @@ -22,27 +30,78 @@ const styles = () => backgroundPosition : 'bottom', backgroundSize : 'auto 85%', backgroundRepeat : 'no-repeat', + '&.webcam' : + { + order : 1 + }, + '&.screen' : + { + order : 2 + }, + '&.hover' : + { + boxShadow : '0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(82, 168, 236, 0.9)' + }, '&.active-speaker' : { borderColor : 'var(--active-speaker-border-color)' } }, + fab : + { + margin : theme.spacing.unit + }, viewContainer : { - position : 'relative', - '&.webcam' : + position : 'relative', + width : '100%', + height : '100%' + }, + controls : + { + position : 'absolute', + width : '100%', + height : '100%', + backgroundColor : 'rgba(0, 0, 0, 0.3)', + display : 'flex', + flexDirection : 'column', + justifyContent : 'center', + alignItems : 'flex-end', + padding : '0.4vmin', + zIndex : 21, + opacity : 0, + transition : 'opacity 0.3s', + touchAction : 'none', + '&.hover' : { - order : 2 + opacity : 1 }, - '&.screen' : + '& p' : { - order : 1 + position : 'absolute', + float : 'left', + top : '50%', + left : '50%', + transform : 'translate(-50%, -50%)', + color : 'rgba(255, 255, 255, 1)', + fontSize : '7em', + margin : 0 } } }); const Me = (props) => { + const [ hover, setHover ] = useState(false); + const [ webcamHover, setWebcamHover ] = useState(false); + const [ screenHover, setScreenHover ] = useState(false); + + let touchTimeout = null; + + let touchWebcamTimeout = null; + + let touchScreenTimeout = null; + const { roomClient, me, @@ -54,7 +113,8 @@ const Me = (props) => micProducer, webcamProducer, screenProducer, - classes + classes, + theme } = props; const videoVisible = ( @@ -69,23 +129,222 @@ const Me = (props) => !screenProducer.remotelyPaused ); + let micState; + + let micTip; + + if (!me.canSendMic) + { + micState = 'unsupported'; + micTip = 'Audio unsupported'; + } + else if (!micProducer) + { + micState = 'off'; + micTip = 'Activate audio'; + } + else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) + { + micState = 'on'; + micTip = 'Mute audio'; + } + else + { + micState = 'muted'; + micTip = 'Unmute audio'; + } + + let webcamState; + + let webcamTip; + + if (!me.canSendWebcam) + { + webcamState = 'unsupported'; + webcamTip = 'Video unsupported'; + } + else if (webcamProducer) + { + webcamState = 'on'; + webcamTip = 'Stop video'; + } + else + { + webcamState = 'off'; + webcamTip = 'Start video'; + } + + let screenState; + + let screenTip; + + if (!me.canShareScreen) + { + screenState = 'unsupported'; + screenTip = 'Screen sharing not supported'; + } + else if (screenProducer) + { + screenState = 'on'; + screenTip = 'Stop screen sharing'; + } + else + { + screenState = 'off'; + screenTip = 'Start screen sharing'; + } + const spacingStyle = { 'margin' : spacing }; + const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); + return (
    setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} style={spacingStyle} > -
    +
    +
    setWebcamHover(true)} + onMouseOut={() => setWebcamHover(false)} + onTouchStart={() => + { + if (touchWebcamTimeout) + clearTimeout(touchWebcamTimeout); + + setWebcamHover(true); + }} + onTouchEnd={() => + { + if (touchWebcamTimeout) + clearTimeout(touchWebcamTimeout); + + touchWebcamTimeout = setTimeout(() => + { + setWebcamHover(false); + }, 2000); + }} + > +

    ME

    + +
    + + { + if (micState === 'off') + roomClient.enableMic(); + else if (micState === 'on') + roomClient.muteMic(); + else + roomClient.unmuteMic(); + }} + > + { micState === 'on' ? + + : + + } + +
    +
    + +
    + + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + +
    +
    + +
    + + { + switch (screenState) + { + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } + } + }} + > + { screenState === 'on' || screenState === 'unsupported' ? + + :null + } + { screenState === 'off' ? + + :null + } + +
    +
    +
    +
    { screenProducer ? -
    -
    +
    setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + style={spacingStyle} + > +
    +
    setScreenHover(true)} + onMouseOut={() => setScreenHover(false)} + onTouchStart={() => + { + if (touchScreenTimeout) + clearTimeout(touchScreenTimeout); + + setScreenHover(true); + }} + onTouchEnd={() => + { + + if (touchScreenTimeout) + clearTimeout(touchScreenTimeout); + + touchScreenTimeout = setTimeout(() => + { + setScreenHover(false); + }, 2000); + }} + > +

    ME

    +
    + @@ -164,4 +472,4 @@ export default withRoomContext(connect( ); } } -)(withStyles(styles)(Me))); +)(withStyles(styles, { withTheme: true })(Me))); diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 0e4875d..2d3a338 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -31,11 +31,11 @@ const styles = (theme) => backgroundRepeat : 'no-repeat', '&.webcam' : { - order : 2 + order : 4 }, '&.screen' : { - order : 1 + order : 3 }, '&.hover' : { @@ -52,17 +52,9 @@ const styles = (theme) => }, viewContainer : { - position : 'relative', - width : '100%', - height : '100%', - '&.webcam' : - { - order : 2 - }, - '&.screen' : - { - order : 1 - } + position : 'relative', + width : '100%', + height : '100%' }, controls : { diff --git a/app/src/components/Controls/Sidebar.js b/app/src/components/Controls/Sidebar.js deleted file mode 100644 index 9ef0583..0000000 --- a/app/src/components/Controls/Sidebar.js +++ /dev/null @@ -1,331 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { meProducersSelector } from '../Selectors'; -import { withStyles } from '@material-ui/core/styles'; -import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery'; -import classnames from 'classnames'; -import * as appPropTypes from '../appPropTypes'; -import { withRoomContext } from '../../RoomContext'; -import Fab from '@material-ui/core/Fab'; -import Tooltip from '@material-ui/core/Tooltip'; -import MicIcon from '@material-ui/icons/Mic'; -import MicOffIcon from '@material-ui/icons/MicOff'; -import VideoIcon from '@material-ui/icons/Videocam'; -import VideoOffIcon from '@material-ui/icons/VideocamOff'; -import ScreenIcon from '@material-ui/icons/ScreenShare'; -import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; -import LockIcon from '@material-ui/icons/Lock'; -import LockOpenIcon from '@material-ui/icons/LockOpen'; -import LeaveIcon from '@material-ui/icons/Cancel'; - -const styles = (theme) => - ({ - root : - { - position : 'fixed', - zIndex : 500, - display : 'flex', - [theme.breakpoints.up('md')] : - { - top : '50%', - transform : 'translate(0%, -50%)', - flexDirection : 'column', - justifyContent : 'center', - alignItems : 'center', - left : '1.0em', - width : '2.6em' - }, - [theme.breakpoints.down('sm')] : - { - flexDirection : 'row', - bottom : '0.5em', - left : '50%', - transform : 'translate(-50%, -0%)' - } - }, - fab : - { - margin : theme.spacing.unit - }, - show : - { - opacity : 1, - transition : 'opacity .5s' - }, - hide : - { - opacity : 0, - transition : 'opacity .5s' - } - }); - -const Sidebar = (props) => -{ - const { - roomClient, - toolbarsVisible, - me, - micProducer, - webcamProducer, - screenProducer, - locked, - classes, - theme - } = props; - - let micState; - - let micTip; - - if (!me.canSendMic) - { - micState = 'unsupported'; - micTip = 'Audio unsupported'; - } - else if (!micProducer) - { - micState = 'off'; - micTip = 'Activate audio'; - } - else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) - { - micState = 'on'; - micTip = 'Mute audio'; - } - else - { - micState = 'muted'; - micTip = 'Unmute audio'; - } - - let webcamState; - - let webcamTip; - - if (!me.canSendWebcam) - { - webcamState = 'unsupported'; - webcamTip = 'Video unsupported'; - } - else if (webcamProducer) - { - webcamState = 'on'; - webcamTip = 'Stop video'; - } - else - { - webcamState = 'off'; - webcamTip = 'Start video'; - } - - let screenState; - - let screenTip; - - if (!me.canShareScreen) - { - screenState = 'unsupported'; - screenTip = 'Screen sharing not supported'; - } - else if (screenProducer) - { - screenState = 'on'; - screenTip = 'Stop screen sharing'; - } - else - { - screenState = 'off'; - screenTip = 'Start screen sharing'; - } - - const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - return ( -
    - -
    - - { - if (micState === 'off') - roomClient.enableMic(); - else if (micState === 'on') - roomClient.muteMic(); - else - roomClient.unmuteMic(); - }} - > - { micState === 'on' ? - - : - - } - -
    -
    - -
    - - { - webcamState === 'on' ? - roomClient.disableWebcam() : - roomClient.enableWebcam(); - }} - > - { webcamState === 'on' ? - - : - - } - -
    -
    - -
    - - { - switch (screenState) - { - case 'on': - { - roomClient.disableScreenSharing(); - break; - } - case 'off': - { - roomClient.enableScreenSharing(); - break; - } - default: - { - break; - } - } - }} - > - { screenState === 'on' || screenState === 'unsupported' ? - - :null - } - { screenState === 'off' ? - - :null - } - -
    -
    - - - - { - if (locked) - { - roomClient.unlockRoom(); - } - else - { - roomClient.lockRoom(); - } - }} - > - { locked ? - - : - - } - - - - { /* roomClient.sendRaiseHandState(!me.raiseHand)} - > - - */ } - - - roomClient.close()} - > - - - -
    - ); -}; - -Sidebar.propTypes = -{ - roomClient : PropTypes.any.isRequired, - toolbarsVisible : PropTypes.bool.isRequired, - me : appPropTypes.Me.isRequired, - micProducer : appPropTypes.Producer, - webcamProducer : appPropTypes.Producer, - screenProducer : appPropTypes.Producer, - locked : PropTypes.bool.isRequired, - classes : PropTypes.object.isRequired, - theme : PropTypes.object.isRequired -}; - -const mapStateToProps = (state) => - ({ - toolbarsVisible : state.room.toolbarsVisible, - ...meProducersSelector(state), - me : state.me, - locked : state.room.locked - }); - -export default withRoomContext(connect( - mapStateToProps, - null, - null, - { - areStatesEqual : (next, prev) => - { - return ( - prev.room.toolbarsVisible === next.room.toolbarsVisible && - prev.room.locked === next.room.locked && - prev.producers === next.producers && - prev.me === next.me - ); - } - } -)(withStyles(styles, { withTheme: true })(Sidebar))); diff --git a/app/src/components/Room.js b/app/src/components/Room.js index afcc75d..c71897e 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -27,10 +27,12 @@ import Filmstrip from './MeetingViews/Filmstrip'; import AudioPeers from './PeerAudio/AudioPeers'; import FullScreenView from './VideoContainers/FullScreenView'; import VideoWindow from './VideoWindow/VideoWindow'; -import Sidebar from './Controls/Sidebar'; import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import SettingsIcon from '@material-ui/icons/Settings'; +import LockIcon from '@material-ui/icons/Lock'; +import LockOpenIcon from '@material-ui/icons/LockOpen'; +import LeaveIcon from '@material-ui/icons/Cancel'; import Settings from './Settings/Settings'; import JoinDialog from './JoinDialog'; @@ -306,6 +308,34 @@ class Room extends React.PureComponent
    + roomClient.close()} + > + + + + { + if (room.locked) + { + roomClient.unlockRoom(); + } + else + { + roomClient.lockRoom(); + } + }} + > + { room.locked ? + + : + + } + { this.fullscreen.fullscreenEnabled ? - -
    ); From f79a45234108b05981f46fec2a5883bc5fdce14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 11:38:03 +0200 Subject: [PATCH 37/44] Updated material-ui and fixed some layout. --- app/package.json | 4 +-- app/src/components/Containers/Me.js | 7 ++++- app/src/components/Containers/Peer.js | 9 ++++++- app/src/components/MeetingViews/Filmstrip.js | 2 ++ app/src/components/Room.js | 27 ++++++++++++++------ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/package.json b/app/package.json index b32625c..9db48a7 100644 --- a/app/package.json +++ b/app/package.json @@ -6,8 +6,8 @@ "author": "Håvar Aambø Fosstveit ", "license": "MIT", "dependencies": { - "@material-ui/core": "^3.9.2", - "@material-ui/icons": "^3.0.2", + "@material-ui/core": "^4.1.2", + "@material-ui/icons": "^4.2.1", "bowser": "^2.4.0", "create-torrent": "^3.33.0", "domready": "^1.0.8", diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 64bf3ee..cdb2251 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { meProducersSelector } from '../Selectors'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; -import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; @@ -109,6 +109,7 @@ const Me = (props) => activeSpeaker, spacing, style, + smallButtons, advancedMode, micProducer, webcamProducer, @@ -264,6 +265,7 @@ const Me = (props) => className={classes.fab} disabled={!me.canSendMic || me.audioInProgress} color={micState === 'on' ? 'default' : 'secondary'} + size={smallButtons ? 'small' : 'large'} onClick={() => { if (micState === 'off') @@ -289,6 +291,7 @@ const Me = (props) => className={classes.fab} disabled={!me.canSendWebcam || me.webcamInProgress} color={webcamState === 'on' ? 'default' : 'secondary'} + size={smallButtons ? 'small' : 'large'} onClick={() => { webcamState === 'on' ? @@ -311,6 +314,7 @@ const Me = (props) => className={classes.fab} disabled={!me.canShareScreen || me.screenShareInProgress} color={screenState === 'on' ? 'primary' : 'default'} + size={smallButtons ? 'small' : 'large'} onClick={() => { switch (screenState) @@ -443,6 +447,7 @@ Me.propTypes = screenProducer : appPropTypes.Producer, spacing : PropTypes.number, style : PropTypes.object, + smallButtons : PropTypes.bool, classes : PropTypes.object.isRequired, theme : PropTypes.object.isRequired }; diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 2d3a338..01bc263 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; -import { unstable_useMediaQuery as useMediaQuery } from '@material-ui/core/useMediaQuery'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import * as stateActions from '../../actions/stateActions'; import VideoView from '../VideoContainers/VideoView'; import Fab from '@material-ui/core/Fab'; @@ -123,6 +123,7 @@ const Peer = (props) => toggleConsumerWindow, spacing, style, + smallButtons, windowConsumer, classes, theme @@ -231,6 +232,7 @@ const Peer = (props) => className={classes.fab} disabled={!micConsumer} color={micEnabled ? 'default' : 'secondary'} + size={smallButtons ? 'small' : 'large'} onClick={() => { micEnabled ? @@ -253,6 +255,7 @@ const Peer = (props) => !videoVisible || (windowConsumer === webcamConsumer.id) } + size={smallButtons ? 'small' : 'large'} onClick={() => { toggleConsumerWindow(webcamConsumer); @@ -267,6 +270,7 @@ const Peer = (props) => aria-label='Fullscreen' className={classes.fab} disabled={!videoVisible} + size={smallButtons ? 'small' : 'large'} onClick={() => { toggleConsumerFullscreen(webcamConsumer); @@ -356,6 +360,7 @@ const Peer = (props) => !screenVisible || (windowConsumer === screenConsumer.id) } + size={smallButtons ? 'small' : 'large'} onClick={() => { toggleConsumerWindow(screenConsumer); @@ -370,6 +375,7 @@ const Peer = (props) => aria-label='Fullscreen' className={classes.fab} disabled={!screenVisible} + size={smallButtons ? 'small' : 'large'} onClick={() => { toggleConsumerFullscreen(screenConsumer); @@ -408,6 +414,7 @@ Peer.propTypes = activeSpeaker : PropTypes.bool, spacing : PropTypes.number, style : PropTypes.object, + smallButtons : PropTypes.bool, toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired, classes : PropTypes.object.isRequired, diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index 2949eff..934db64 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -249,6 +249,7 @@ class Filmstrip extends React.PureComponent
    @@ -271,6 +272,7 @@ class Filmstrip extends React.PureComponent advancedMode={advancedMode} id={peerId} style={peerStyle} + smallButtons />
    diff --git a/app/src/components/Room.js b/app/src/components/Room.js index c71897e..6057865 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -32,7 +32,7 @@ import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import SettingsIcon from '@material-ui/icons/Settings'; import LockIcon from '@material-ui/icons/Lock'; import LockOpenIcon from '@material-ui/icons/LockOpen'; -import LeaveIcon from '@material-ui/icons/Cancel'; +import Button from '@material-ui/core/Button'; import Settings from './Settings/Settings'; import JoinDialog from './JoinDialog'; @@ -127,6 +127,11 @@ const styles = (theme) => { display : 'flex' }, + actionButton : + { + margin : theme.spacing.unit, + padding : 0 + }, meContainer : { position : 'fixed', @@ -308,15 +313,9 @@ class Room extends React.PureComponent
    - roomClient.close()} - > - - { @@ -339,6 +338,7 @@ class Room extends React.PureComponent { this.fullscreen.fullscreenEnabled ? @@ -352,6 +352,7 @@ class Room extends React.PureComponent } setSettingsOpen(!room.settingsOpen)} > @@ -360,6 +361,7 @@ class Room extends React.PureComponent { loginEnabled ? { @@ -374,6 +376,15 @@ class Room extends React.PureComponent :null } +
    From da3233faf3cfde651323c25719d001d2e3a5144e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 13:13:34 +0200 Subject: [PATCH 38/44] Updated API in material-ui. --- app/src/components/Containers/Me.js | 2 +- app/src/components/Containers/Peer.js | 2 +- app/src/components/JoinDialog.js | 2 +- app/src/components/MeetingDrawer/Chat/ChatInput.js | 2 +- app/src/components/MeetingDrawer/Chat/Message.js | 6 +++--- app/src/components/MeetingDrawer/Chat/MessageList.js | 2 +- app/src/components/MeetingDrawer/FileSharing/File.js | 8 ++++---- app/src/components/MeetingDrawer/FileSharing/FileList.js | 2 +- .../components/MeetingDrawer/FileSharing/FileSharing.js | 2 +- .../MeetingDrawer/ParticipantList/ParticipantList.js | 2 +- app/src/components/Room.js | 4 ++-- app/src/components/Settings/Settings.js | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index cdb2251..947a98e 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -49,7 +49,7 @@ const styles = (theme) => }, fab : { - margin : theme.spacing.unit + margin : theme.spacing(1) }, viewContainer : { diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 01bc263..14ff783 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -48,7 +48,7 @@ const styles = (theme) => }, fab : { - margin : theme.spacing.unit + margin : theme.spacing(1) }, viewContainer : { diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index 5a99b3f..ae11771 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -15,7 +15,7 @@ const styles = (theme) => dialogPaper : { width : '20vw', - padding : theme.spacing.unit * 2, + padding : theme.spacing(2), [theme.breakpoints.down('lg')] : { width : '30vw' diff --git a/app/src/components/MeetingDrawer/Chat/ChatInput.js b/app/src/components/MeetingDrawer/Chat/ChatInput.js index 07d7fe5..bafaa20 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatInput.js +++ b/app/src/components/MeetingDrawer/Chat/ChatInput.js @@ -12,7 +12,7 @@ const styles = (theme) => ({ root : { - padding : theme.spacing.unit, + padding : theme.spacing(1), display : 'flex', alignItems : 'center', borderRadius : 0 diff --git a/app/src/components/MeetingDrawer/Chat/Message.js b/app/src/components/MeetingDrawer/Chat/Message.js index f00fa01..856ac92 100644 --- a/app/src/components/MeetingDrawer/Chat/Message.js +++ b/app/src/components/MeetingDrawer/Chat/Message.js @@ -21,8 +21,8 @@ const styles = (theme) => root : { display : 'flex', - marginBottom : theme.spacing.unit, - padding : theme.spacing.unit, + marginBottom : theme.spacing(1), + padding : theme.spacing(1), flexShrink : 0 }, selfMessage : @@ -42,7 +42,7 @@ const styles = (theme) => }, content : { - marginLeft : theme.spacing.unit + marginLeft : theme.spacing(1) }, avatar : { diff --git a/app/src/components/MeetingDrawer/Chat/MessageList.js b/app/src/components/MeetingDrawer/Chat/MessageList.js index 8e174cc..cdc8809 100644 --- a/app/src/components/MeetingDrawer/Chat/MessageList.js +++ b/app/src/components/MeetingDrawer/Chat/MessageList.js @@ -14,7 +14,7 @@ const styles = (theme) => flexDirection : 'column', alignItems : 'center', overflowY : 'auto', - padding : theme.spacing.unit + padding : theme.spacing(1) } }); diff --git a/app/src/components/MeetingDrawer/FileSharing/File.js b/app/src/components/MeetingDrawer/FileSharing/File.js index 5981668..cd3a985 100644 --- a/app/src/components/MeetingDrawer/FileSharing/File.js +++ b/app/src/components/MeetingDrawer/FileSharing/File.js @@ -14,11 +14,11 @@ const styles = (theme) => display : 'flex', alignItems : 'center', width : '100%', - padding : theme.spacing.unit, + padding : theme.spacing(1), boxShadow : '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)', '&:not(:last-child)' : { - marginBottom : theme.spacing.unit + marginBottom : theme.spacing(1) } }, avatar : @@ -29,7 +29,7 @@ const styles = (theme) => text : { margin : 0, - padding : theme.spacing.unit + padding : theme.spacing(1) }, fileContent : { @@ -40,7 +40,7 @@ const styles = (theme) => { display : 'flex', alignItems : 'center', - padding : theme.spacing.unit + padding : theme.spacing(1) }, button : { diff --git a/app/src/components/MeetingDrawer/FileSharing/FileList.js b/app/src/components/MeetingDrawer/FileSharing/FileList.js index c84f265..1dcb905 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileList.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileList.js @@ -15,7 +15,7 @@ const styles = (theme) => flexDirection : 'column', alignItems : 'center', overflowY : 'auto', - padding : theme.spacing.unit + padding : theme.spacing(1) } }); diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js index 94e3966..2c33c4b 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js @@ -22,7 +22,7 @@ const styles = (theme) => }, button : { - margin : theme.spacing.unit + margin : theme.spacing(1) } }); diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index fac3e74..ffa6d8b 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -23,7 +23,7 @@ const styles = (theme) => list : { listStyleType : 'none', - padding : theme.spacing.unit, + padding : theme.spacing(1), boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)', backgroundColor : 'rgba(255, 255, 255, 1)' }, diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 6057865..040511e 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -60,7 +60,7 @@ const styles = (theme) => left : '50%', transform : 'translateX(-50%) translateY(-50%)', width : '30vw', - padding : theme.spacing.unit * 2, + padding : theme.spacing(2), flexDirection : 'column', justifyContent : 'center', alignItems : 'center' @@ -129,7 +129,7 @@ const styles = (theme) => }, actionButton : { - margin : theme.spacing.unit, + margin : theme.spacing(1), padding : 0 }, meContainer : diff --git a/app/src/components/Settings/Settings.js b/app/src/components/Settings/Settings.js index 59b63c7..11411da 100644 --- a/app/src/components/Settings/Settings.js +++ b/app/src/components/Settings/Settings.js @@ -43,7 +43,7 @@ const styles = (theme) => }, setting : { - padding : theme.spacing.unit * 2 + padding : theme.spacing(2) }, formControl : { From bfe6d14b1a5761b5838ddc34f475020f28992eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 14:38:48 +0200 Subject: [PATCH 39/44] Cleanup of CSS. --- app/src/components/Containers/Me.js | 4 +- app/src/components/Containers/Peer.js | 4 +- app/src/components/Containers/SpeakerPeer.js | 4 +- .../MeetingDrawer/ParticipantList/ListMe.js | 6 +- .../MeetingDrawer/ParticipantList/ListPeer.js | 6 +- .../ParticipantList/ParticipantList.js | 6 +- .../VideoContainers/FullScreenView.js | 4 +- .../components/VideoContainers/VideoView.js | 150 ++++++++---------- app/src/components/VideoWindow/NewWindow.js | 4 +- 9 files changed, 87 insertions(+), 101 deletions(-) diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 947a98e..8800b05 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -67,7 +67,7 @@ const styles = (theme) => flexDirection : 'column', justifyContent : 'center', alignItems : 'flex-end', - padding : '0.4vmin', + padding : theme.spacing(1), zIndex : 21, opacity : 0, transition : 'opacity 0.3s', @@ -83,7 +83,7 @@ const styles = (theme) => top : '50%', left : '50%', transform : 'translate(-50%, -50%)', - color : 'rgba(255, 255, 255, 1)', + color : 'rgba(255, 255, 255, 0.5)', fontSize : '7em', margin : 0 } diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 14ff783..edba53d 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -66,7 +66,7 @@ const styles = (theme) => flexDirection : 'column', justifyContent : 'center', alignItems : 'flex-end', - padding : '0.4vmin', + padding : theme.spacing(1), zIndex : 21, opacity : 0, transition : 'opacity 0.3s', @@ -85,7 +85,7 @@ const styles = (theme) => display : 'flex', justifyContent : 'center', alignItems : 'center', - padding : '0.4vmin', + padding : theme.spacing(1), zIndex : 20, '& p' : { diff --git a/app/src/components/Containers/SpeakerPeer.js b/app/src/components/Containers/SpeakerPeer.js index efa576f..85aeb0b 100644 --- a/app/src/components/Containers/SpeakerPeer.js +++ b/app/src/components/Containers/SpeakerPeer.js @@ -8,7 +8,7 @@ import { withStyles } from '@material-ui/core/styles'; import VideoView from '../VideoContainers/VideoView'; import Volume from './Volume'; -const styles = () => +const styles = (theme) => ({ root : { @@ -51,7 +51,7 @@ const styles = () => display : 'flex', justifyContent : 'center', alignItems : 'center', - padding : '0.4vmin', + padding : theme.spacing(1), zIndex : 21, '& p' : { diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index b33ffc3..f506f9c 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -7,11 +7,11 @@ import * as appPropTypes from '../../appPropTypes'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import HandIcon from '../../../images/icon-hand-white.svg'; -const styles = () => +const styles = (theme) => ({ root : { - padding : '0.5rem', + padding : theme.spacing(1), width : '100%', overflow : 'hidden', cursor : 'auto', @@ -31,7 +31,7 @@ const styles = () => fontSize : '1rem', border : 'none', display : 'flex', - paddingLeft : '0.5rem', + paddingLeft : theme.spacing(1), flexGrow : 1, alignItems : 'center' }, diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 8715b9d..82f6840 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -13,11 +13,11 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import HandIcon from '../../../images/icon-hand-white.svg'; -const styles = () => +const styles = (theme) => ({ root : { - padding : '0.5rem', + padding : theme.spacing(1), width : '100%', overflow : 'hidden', cursor : 'auto', @@ -37,7 +37,7 @@ const styles = () => fontSize : '1rem', border : 'none', display : 'flex', - paddingLeft : '0.5rem', + paddingLeft : theme.spacing(1), flexGrow : 1, alignItems : 'center' }, diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index ffa6d8b..a4f66ed 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -18,7 +18,7 @@ const styles = (theme) => { width : '100%', overflowY : 'auto', - padding : 6 + padding : theme.spacing(1) }, list : { @@ -29,12 +29,12 @@ const styles = (theme) => }, listheader : { - padding : '0.5rem', + padding : theme.spacing(1), fontWeight : 'bolder' }, listItem : { - padding : '0.5rem', + padding : theme.spacing(1), width : '100%', overflow : 'hidden', cursor : 'pointer', diff --git a/app/src/components/VideoContainers/FullScreenView.js b/app/src/components/VideoContainers/FullScreenView.js index cbf2b73..6f99122 100644 --- a/app/src/components/VideoContainers/FullScreenView.js +++ b/app/src/components/VideoContainers/FullScreenView.js @@ -8,7 +8,7 @@ import * as stateActions from '../../actions/stateActions'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; import VideoView from './VideoView'; -const styles = () => +const styles = (theme) => ({ root : { @@ -29,7 +29,7 @@ const styles = () => flexDirection : 'row', justifyContent : 'flex-start', alignItems : 'center', - padding : '0.4vmin' + padding : theme.spacing(1) }, button : { diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index 9e1381d..6fa1994 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -5,7 +5,7 @@ import { withStyles } from '@material-ui/core/styles'; import * as appPropTypes from '../appPropTypes'; import EditableInput from '../Controls/EditableInput'; -const styles = () => +const styles = (theme) => ({ root : { @@ -48,54 +48,42 @@ const styles = () => }, info : { + width : '100%', + height : '100%', + padding : theme.spacing(1), position : 'absolute', zIndex : 10, - top : '0.6vmin', - left : '0.6vmin', - bottom : 0, - right : 0, display : 'flex', flexDirection : 'column', justifyContent : 'space-between' }, media : { - flex : '0 0 auto', - display : 'flex', - flexDirection : 'row' + display : 'flex', + transitionProperty : 'opacity', + transitionDuration : '.15s', + '&.hidden' : + { + opacity : 0, + transitionDuration : '0s' + } }, box : { - padding : '0.4vmin', + padding : theme.spacing(0.5), borderRadius : 2, backgroundColor : 'rgba(0, 0, 0, 0.25)', '& p' : { - userSelect : 'none', - pointerEvents : 'none', - margin : 0, - color : 'rgba(255, 255, 255, 0.7)', - fontSize : 10, - - '&:last-child' : - { - marginBottom : 0 - } + userSelect : 'none', + margin : 0, + color : 'rgba(255, 255, 255, 0.7)', + fontSize : '0.8em' } }, peer : { - flex : '0 0 auto', - display : 'flex', - flexDirection : 'column', - justifyContent : 'flex-end', - position : 'absolute', - bottom : '0.6vmin', - left : 0, - borderRadius : 2, - backgroundColor : 'rgba(0, 0, 0, 0.25)', - padding : '0.5vmin', - alignItems : 'flex-start' + display : 'flex' }, displayNameEdit : { @@ -120,12 +108,7 @@ const styles = () => }, deviceInfo : { - marginTop : '0.4vmin', - display : 'flex', - flexDirection : 'row', - justifyContent : 'flex-start', - alignItems : 'flex-end', - '& span' : + '& span' : { userSelect : 'none', pointerEvents : 'none', @@ -181,59 +164,62 @@ class VideoView extends React.PureComponent return (
    - { advancedMode ? -
    -
    - { audioCodec ? -

    {audioCodec}

    - :null - } + - :null - } +
    { showPeerInfo ?
    - { isMe ? - onChangeDisplayName(newDisplayName)} - /> - : - - {displayName} - - } - - { advancedMode ? -
    - - {peer.device.name} {Math.floor(peer.device.version) || null} +
    + { isMe ? + onChangeDisplayName(newDisplayName)} + /> + : + + {displayName} -
    - :null - } + } + + { advancedMode ? +
    + + {peer.device.name} {Math.floor(peer.device.version) || null} + +
    + :null + } +
    :null } diff --git a/app/src/components/VideoWindow/NewWindow.js b/app/src/components/VideoWindow/NewWindow.js index 2ae370b..b2255f4 100644 --- a/app/src/components/VideoWindow/NewWindow.js +++ b/app/src/components/VideoWindow/NewWindow.js @@ -6,7 +6,7 @@ import FullScreen from '../FullScreen'; import FullScreenIcon from '@material-ui/icons/Fullscreen'; import FullScreenExitIcon from '@material-ui/icons/FullscreenExit'; -const styles = () => +const styles = (theme) => ({ root : { @@ -27,7 +27,7 @@ const styles = () => flexDirection : 'row', justifyContent : 'flex-start', alignItems : 'center', - padding : '0.4vmin' + padding : theme.spacing(1) }, button : { From 9d385da30bb03269adbdbd0251fa05fdfae2e418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 15:00:22 +0200 Subject: [PATCH 40/44] Fixed flipped video on screen sharing. --- app/src/components/Containers/Me.js | 51 +++++++++---------- .../components/VideoContainers/VideoView.js | 10 ++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 8800b05..8d3e465 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -49,7 +49,8 @@ const styles = (theme) => }, fab : { - margin : theme.spacing(1) + margin : theme.spacing(1), + pointerEvents : 'auto' }, viewContainer : { @@ -72,6 +73,7 @@ const styles = (theme) => opacity : 0, transition : 'opacity 0.3s', touchAction : 'none', + pointerEvents : 'none', '&.hover' : { opacity : 1 @@ -93,15 +95,9 @@ const styles = (theme) => const Me = (props) => { const [ hover, setHover ] = useState(false); - const [ webcamHover, setWebcamHover ] = useState(false); - const [ screenHover, setScreenHover ] = useState(false); let touchTimeout = null; - let touchWebcamTimeout = null; - - let touchScreenTimeout = null; - const { roomClient, me, @@ -236,24 +232,24 @@ const Me = (props) => >
    setWebcamHover(true)} - onMouseOut={() => setWebcamHover(false)} + className={classnames(classes.controls, hover ? 'hover' : null)} + onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} onTouchStart={() => { - if (touchWebcamTimeout) - clearTimeout(touchWebcamTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - setWebcamHover(true); + setHover(true); }} onTouchEnd={() => { - if (touchWebcamTimeout) - clearTimeout(touchWebcamTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - touchWebcamTimeout = setTimeout(() => + touchTimeout = setTimeout(() => { - setWebcamHover(false); + setHover(false); }, 2000); }} > @@ -394,25 +390,25 @@ const Me = (props) => >
    setScreenHover(true)} - onMouseOut={() => setScreenHover(false)} + className={classnames(classes.controls, hover ? 'hover' : null)} + onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} onTouchStart={() => { - if (touchScreenTimeout) - clearTimeout(touchScreenTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - setScreenHover(true); + setHover(true); }} onTouchEnd={() => { - if (touchScreenTimeout) - clearTimeout(touchScreenTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - touchScreenTimeout = setTimeout(() => + touchTimeout = setTimeout(() => { - setScreenHover(false); + setHover(false); }, 2000); }} > @@ -421,6 +417,7 @@ const Me = (props) => transitionProperty : 'opacity', transitionDuration : '.15s', backgroundColor : 'var(--peer-video-bg-color)', - '&.is-me' : + '&.isMe' : { transform : 'scaleX(-1)' }, @@ -142,6 +142,7 @@ class VideoView extends React.PureComponent { const { isMe, + isScreen, peer, displayName, showPeerInfo, @@ -229,7 +230,7 @@ class VideoView extends React.PureComponent ref='video' className={classnames(classes.video, { hidden : !videoVisible, - 'is-me' : isMe, + 'isMe' : isMe && !isScreen, loading : videoProfile === 'none', contain : videoContain })} @@ -320,8 +321,9 @@ class VideoView extends React.PureComponent VideoView.propTypes = { - isMe : PropTypes.bool, - peer : PropTypes.oneOfType( + isMe : PropTypes.bool, + isScreen : PropTypes.bool, + peer : PropTypes.oneOfType( [ appPropTypes.Me, appPropTypes.Peer ]), displayName : PropTypes.string, showPeerInfo : PropTypes.bool, From 1c22fd2c5603b287f1c9b8e33f42c1b990b468ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 15:04:41 +0200 Subject: [PATCH 41/44] Fix bug in locked room. --- server/lib/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/Room.js b/server/lib/Room.js index 7a4427a..8c7f8d6 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -165,7 +165,7 @@ class Room extends EventEmitter } else if (this._locked) // Don't allow connections to a locked room { - notification(socket, 'roomLocked'); + this._notification(socket, 'roomLocked'); socket.disconnect(true); return; } From 07ef68d62e8b54e9160346d203c17674ba79f14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 15:18:33 +0200 Subject: [PATCH 42/44] Fix autocorrect setting for display-name. --- app/src/components/VideoContainers/VideoView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index ec1719c..89f4c55 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -201,7 +201,7 @@ class VideoView extends React.PureComponent shouldBlockWhileLoading editProps={{ maxLength : 30, - autoCorrect : false, + autoCorrect : 'off', spellCheck : false }} onChange={({ newDisplayName }) => onChangeDisplayName(newDisplayName)} From 115bcfc62d93e1383ff95286f20ad598737b81bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 25 Jun 2019 22:34:18 +0200 Subject: [PATCH 43/44] Cleaned up Peer container --- app/src/components/Containers/Peer.js | 46 ++++++++++++--------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index edba53d..ee7fd2e 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -102,15 +102,9 @@ const styles = (theme) => const Peer = (props) => { const [ hover, setHover ] = useState(false); - const [ webcamHover, setWebcamHover ] = useState(false); - const [ screenHover, setScreenHover ] = useState(false); let touchTimeout = null; - let touchWebcamTimeout = null; - - let touchScreenTimeout = null; - const { roomClient, advancedMode, @@ -206,24 +200,24 @@ const Peer = (props) => }
    setWebcamHover(true)} - onMouseOut={() => setWebcamHover(false)} + className={classnames(classes.controls, hover ? 'hover' : null)} + onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} onTouchStart={() => { - if (touchWebcamTimeout) - clearTimeout(touchWebcamTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - setWebcamHover(true); + setHover(true); }} onTouchEnd={() => { - if (touchWebcamTimeout) - clearTimeout(touchWebcamTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - touchWebcamTimeout = setTimeout(() => + touchTimeout = setTimeout(() => { - setWebcamHover(false); + setHover(false); }, 2000); }} > @@ -330,25 +324,25 @@ const Peer = (props) => { screenVisible ?
    setScreenHover(true)} - onMouseOut={() => setScreenHover(false)} + className={classnames(classes.controls, hover ? 'hover' : null)} + onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} onTouchStart={() => { - if (touchScreenTimeout) - clearTimeout(touchScreenTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - setScreenHover(true); + setHover(true); }} onTouchEnd={() => { - if (touchScreenTimeout) - clearTimeout(touchScreenTimeout); + if (touchTimeout) + clearTimeout(touchTimeout); - touchScreenTimeout = setTimeout(() => + touchTimeout = setTimeout(() => { - setScreenHover(false); + setHover(false); }, 2000); }} > From a94cd3567689d008783f7490d1f0c2c1fce5d7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Mon, 1 Jul 2019 19:45:09 +0200 Subject: [PATCH 44/44] Prepare for new version --- CHANGELOG.md | 3 ++- README.md | 1 + app/package.json | 2 +- server/package.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375f121..e6b2f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -### 2.1 +### 3.0 * Updated to mediasoup v3 * Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client) - OpenID Connect discovery - Auth code flow * Add spdy http2 support. - Notice it does not supports node 11.x +* Updated to Material UI v4 ### 2.0 * Material UI diff --git a/README.md b/README.md index 6e69fb1..166629e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci * Chat * Screen sharing * File sharing +* Different layouts There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no. diff --git a/app/package.json b/app/package.json index 9db48a7..b85d265 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "multiparty-meeting", - "version": "2.1.0", + "version": "3.0.0", "private": true, "description": "multiparty meeting service", "author": "Håvar Aambø Fosstveit ", diff --git a/server/package.json b/server/package.json index 4269b56..1762e0c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "multiparty-meeting-server", - "version": "2.1.0", + "version": "3.0.0", "private": true, "description": "multiparty meeting server", "author": "Håvar Aambø Fosstveit ",