From 5068a68f36e4876c124cc9c14f48ef1fb6aa3914 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Wed, 31 Oct 2018 15:03:11 +0100 Subject: [PATCH 01/21] Start of release branch for 1.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..09cf6e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +### RC1 1.0 +* First stable release? From 42fc038842ce58ace8b8145b5ca71c934b509f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 31 Oct 2018 18:17:06 +0100 Subject: [PATCH 02/21] Update README.md --- README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3e00c7b..cd32c15 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,14 @@ A WebRTC meeting service using [mediasoup](https://mediasoup.org) as its backend. -Try it online at https://akademia.no. You can add /roomname to the URL for specifying a room. +Try it online at https://letsmeet.no. You can add /roomname to the URL for specifying a room. +## Features +* Audio/Video +* Chat +* Screen sharing +* File sharing +* Different video layouts ## Installation @@ -22,15 +28,6 @@ $ cp server/config.example.js server/config.js * Copy `app/config.example.js` to `app/config.js` : -In addition, the server requires a screen to be installed for the server -to be able to seed shared torrent files. This is because the headless -Electron instance used by WebTorrent expects one. - -See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for -more information about this. - -* Copy `config.example.js` as `config.js` and customize it for your scenario: - ```bash $ cp app/config.example.js app/config.js ``` @@ -72,7 +69,7 @@ $ node server.js ## Deploy it in a server -* Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and dobbel check location path settings: +* Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and check location path settings: ```bash $ cp multiparty-meeting.service /etc/systemd/system/ $ edit /etc/systemd/system/multiparty-meeting.service @@ -97,7 +94,6 @@ $ systemctl enable multiparty-meeting * 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) * If you want your service running at standard ports 80/443 you should: - * Make a redirect from HTTP port 80 to HTTPS (with Apache/NGINX) * Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443) From abc6f4a7c45504c3c6aed22d8a32b1932e319c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 31 Oct 2018 18:21:25 +0100 Subject: [PATCH 03/21] Update config.example.js --- server/config.example.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/server/config.example.js b/server/config.example.js index 9615767..0c3e2ea 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -59,20 +59,22 @@ module.exports = useinbandfec : 1 } }, - { - kind : 'video', - name : 'VP8', - clockRate : 90000 - } // { - // kind : 'video', - // name : 'H264', - // clockRate : 90000, - // parameters : - // { - // 'packetization-mode' : 1 - // } + // kind : 'video', + // name : 'VP8', + // clockRate : 90000 // } + { + kind : 'video', + name : 'H264', + clockRate : 90000, + parameters : + { + 'packetization-mode' : 1, + 'profile-level-id' : '42e01f', + 'level-asymmetry-allowed' : 1 + } + } ], // mediasoup per Peer max sending bitrate (in bps). maxBitrate : 500000 From 377209b904e31f89532b0e6972760ac6012d4230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 31 Oct 2018 18:22:25 +0100 Subject: [PATCH 04/21] Update config.example.js --- server/config.example.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/config.example.js b/server/config.example.js index 0c3e2ea..f9400f3 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -16,15 +16,6 @@ module.exports = }, // Listening port for https server. listeningPort : 3443, - turnServers : [ - { - urls : [ - 'turn:example.com:443?transport=tcp' - ], - username : 'example', - credential : 'example' - } - ], mediasoup : { // mediasoup Server settings. From 8350fcf00667a38617b9327440e40a79ac772853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 12 Nov 2018 11:22:20 +0100 Subject: [PATCH 05/21] Change default https listening port to 443 --- server/config.example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/config.example.js b/server/config.example.js index 9615767..1f92b40 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -15,7 +15,7 @@ module.exports = key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem` }, // Listening port for https server. - listeningPort : 3443, + listeningPort : 443, turnServers : [ { urls : [ From 9d44674b05dd57a65d21457e7add8a575323fffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 12 Nov 2018 11:23:00 +0100 Subject: [PATCH 06/21] Add port listener config for http server --- server/config.example.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/config.example.js b/server/config.example.js index 1f92b40..e73681b 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -16,6 +16,10 @@ module.exports = }, // Listening port for https server. listeningPort : 443, + // Any http request is redirected to https. + // Listening port for http server. + listeningRedirectPort : 80, + // STUN/TURN turnServers : [ { urls : [ From 4d0020be9eb398994e277107def648f96f42271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 13 Nov 2018 09:38:28 +0100 Subject: [PATCH 07/21] Enable compression on Express server --- server/package.json | 1 + server/server.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/server/package.json b/server/package.json index fd346eb..a42eed7 100644 --- a/server/package.json +++ b/server/package.json @@ -9,6 +9,7 @@ "dependencies": { "base-64": "^0.1.0", "colors": "^1.1.2", + "compression": "^1.7.3", "debug": "^4.1.0", "express": "^4.16.3", "mediasoup": "^2.1.0", diff --git a/server/server.js b/server/server.js index 4b56dbf..8daa1f0 100755 --- a/server/server.js +++ b/server/server.js @@ -9,6 +9,7 @@ const fs = require('fs'); const https = require('https'); const http = require('http'); const express = require('express'); +const compression = require('compression'); const url = require('url'); const Logger = require('./lib/Logger'); const Room = require('./lib/Room'); @@ -39,6 +40,8 @@ const tls = const app = express(); +app.use(compression()); + const dataporten = new Dataporten.Setup(config.oauth2); app.all('*', (req, res, next) => From 874464627aa201f01bfa9be8008a7c7dbff04303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Tue, 13 Nov 2018 12:39:05 +0100 Subject: [PATCH 08/21] systemd update: Server.js moved under server dir --- multiparty-meeting.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multiparty-meeting.service b/multiparty-meeting.service index 39fd30d..8a6a6f7 100644 --- a/multiparty-meeting.service +++ b/multiparty-meeting.service @@ -3,13 +3,13 @@ Description=multiparty-meeting is a audio / video meeting service running in the After=network.target [Service] -ExecStart=/usr/local/src/multiparty-meeting/server.js +ExecStart=/usr/local/src/multiparty-meeting/server/server.js Restart=always User=nobody Group=nogroup Environment=PATH=/usr/bin:/usr/local/bin Environment=NODE_ENV=production -WorkingDirectory=/usr/local/src/multiparty-meeting +WorkingDirectory=/usr/local/src/multiparty-meeting/server [Install] WantedBy=multi-user.target From 0efba32b31de8aceed1371c14d1e5ddbbe58ac32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Tue, 13 Nov 2018 12:40:18 +0100 Subject: [PATCH 09/21] systemd: Allow unpriv user listen on ports < 1024 --- multiparty-meeting.service | 1 + 1 file changed, 1 insertion(+) diff --git a/multiparty-meeting.service b/multiparty-meeting.service index 8a6a6f7..e988f6d 100644 --- a/multiparty-meeting.service +++ b/multiparty-meeting.service @@ -10,6 +10,7 @@ Group=nogroup Environment=PATH=/usr/bin:/usr/local/bin Environment=NODE_ENV=production WorkingDirectory=/usr/local/src/multiparty-meeting/server +AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target From d358170be92bab3a8564e3922f41b49f46a4d3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 13 Nov 2018 15:24:28 +0100 Subject: [PATCH 10/21] Cleanups to notifications and code --- app/lib/RoomClient.js | 122 ++++++++++++++++++++++++------------------ server/lib/Room.js | 5 +- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 5524293..9536e74 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -154,6 +154,21 @@ export default class RoomClient this._loginWindow.close(); } + _soundNotification() + { + const alertPromise = this._soundAlert.play(); + + if (alertPromise !== undefined) + { + alertPromise + .then() + .catch((error) => + { + logger.error('_soundAlert.play() | failed: %o', error); + }); + } + } + notify(text) { this._dispatch(requestActions.notify({ text: text })); @@ -223,13 +238,13 @@ export default class RoomClient this._dispatch(stateActions.setDisplayName(displayName)); - this.notify('Display name changed'); + this.notify(`Your display name changed to ${displayName}.`); } catch (error) { logger.error('changeDisplayName() | failed: %o', error); - this.notify(`Could not change display name: ${error}`); + this.notify('An error occured while changing your display name.'); // We need to refresh the component for it to render the previous // displayName again. @@ -263,7 +278,7 @@ export default class RoomClient { logger.error('sendChatMessage() | failed: %o', error); - this.notify(`Could not send chat: ${error}`); + this.notify('An error occured while sending chat message.'); } } @@ -279,7 +294,7 @@ export default class RoomClient { logger.error('sendFile() | failed: %o', error); - this.notify('An error occurred while sharing a file'); + this.notify('An error occurred while sharing file.'); } } @@ -325,7 +340,7 @@ export default class RoomClient { logger.error('getServerHistory() | failed: %o', error); - this.notify(`Could not get chat history: ${error}`); + this.notify('An error occured while getting server history.'); } } @@ -965,7 +980,7 @@ export default class RoomClient { logger.error('sendRaiseHandState() | failed: %o', error); - this.notify(`Could not change raise hand state: ${error}`); + this.notify(`An error occured while ${state ? 'raising' : 'lowering'} hand.`); // We need to refresh the component for it to render changed state this._dispatch(stateActions.setMyRaiseHandState(!state)); @@ -1014,7 +1029,7 @@ export default class RoomClient { logger.warn('signaling Peer "disconnect" event'); - this.notify('WebSocket disconnected'); + this.notify('You are disconnected.'); // Leave Room. try { this._room.remoteClose({ cause: 'signaling disconnected' }); } @@ -1054,7 +1069,7 @@ export default class RoomClient this._signalingSocket.on('display-name-changed', (data) => { // eslint-disable-next-line no-shadow - const { peerName, displayName, oldDisplayName } = data; + const { peerName, displayName } = data; // NOTE: Hack, we shouldn't do this, but this is just a demo. const peer = this._room.getPeerByName(peerName); @@ -1066,12 +1081,14 @@ export default class RoomClient return; } + const oldDisplayName = peer.appData.displayName; + peer.appData.displayName = displayName; this._dispatch( stateActions.setPeerDisplayName(displayName, peerName)); - this.notify(`${oldDisplayName} is now ${displayName}`); + this.notify(`${oldDisplayName} changed their display name to ${displayName}.`); }); this._signalingSocket.on('profile-picture-changed', (data) => @@ -1092,7 +1109,7 @@ export default class RoomClient this._dispatch(stateActions.setPicture(data.picture)); this._dispatch(stateActions.loggedIn()); - this.notify(`Authenticated successfully: ${data}`); + this.notify('You are logged in.'); this.closeLoginWindow(); }); @@ -1103,6 +1120,18 @@ export default class RoomClient logger.debug('Got raiseHandState from "%s"', peerName); + // NOTE: Hack, we shouldn't do this, but this is just a demo. + const peer = this._room.getPeerByName(peerName); + + if (!peer) + { + logger.error('peer not found'); + + return; + } + + this.notify(`${peer.appData.displayName} ${raiseHandState ? 'raised' : 'lowered'} their hand.`); + this._dispatch( stateActions.setPeerRaiseHandState(peerName, raiseHandState)); }); @@ -1120,43 +1149,33 @@ export default class RoomClient (this._getState().toolarea.toolAreaOpen && this._getState().toolarea.currentToolTab !== 'chat')) // Make sound { - const alertPromise = this._soundAlert.play(); - - if (alertPromise !== undefined) - { - alertPromise - .then() - .catch((error) => - { - logger.error('_soundAlert.play() | failed: %o', error); - }); - } + this._soundNotification(); } }); this._signalingSocket.on('file-receive', (data) => { - const payload = data.file; + const { peerName, file } = data; - this._dispatch(stateActions.addFile(payload)); + // NOTE: Hack, we shouldn't do this, but this is just a demo. + const peer = this._room.getPeerByName(peerName); - this.notify(`${payload.name} shared a file`); + if (!peer) + { + logger.error('peer not found'); + + return; + } + + this._dispatch(stateActions.addFile(file)); + + this.notify(`${peer.appData.displayName} shared a file.`); if (!this._getState().toolarea.toolAreaOpen || (this._getState().toolarea.toolAreaOpen && this._getState().toolarea.currentToolTab !== 'files')) // Make sound { - const alertPromise = this._soundAlert.play(); - - if (alertPromise !== undefined) - { - alertPromise - .then() - .catch((error) => - { - logger.error('_soundAlert.play() | failed: %o', error); - }); - } + this._soundNotification(); } }); } @@ -1210,17 +1229,7 @@ export default class RoomClient logger.debug( 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); - const alertPromise = this._soundAlert.play(); - - if (alertPromise !== undefined) - { - alertPromise - .then() - .catch((error) => - { - logger.error('_soundAlert.play() | failed: %o', error); - }); - } + this._soundNotification(); this._handlePeer(peer); }); @@ -1284,7 +1293,7 @@ export default class RoomClient this.getServerHistory(); - this.notify('You are in the room'); + this.notify('You have joined the room.'); this._spotlights.on('spotlights-updated', (spotlights) => { @@ -1305,7 +1314,7 @@ export default class RoomClient { logger.error('_joinRoom() failed:%o', error); - this.notify(`Could not join the room: ${error.toString()}`); + this.notify('An error occured while joining the room.'); this.close(); } @@ -1419,7 +1428,7 @@ export default class RoomClient { logger.error('_setMicProducer() failed:%o', error); - this.notify(`Mic producer failed: ${error.name}:${error.message}`); + this.notify('An error occured while accessing your microphone.'); if (producer) producer.close(); @@ -1525,7 +1534,14 @@ export default class RoomClient { logger.error('_setScreenShareProducer() failed:%o', error); - this.notify(`Screen share producer failed: ${error.name}:${error.message}`); + 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.'); + } if (producer) producer.close(); @@ -1623,7 +1639,7 @@ export default class RoomClient { logger.error('_setWebcamProducer() failed:%o', error); - this.notify(`Webcam producer failed: ${error.name}:${error.message}`); + this.notify('An error occured while accessing your camera.'); if (producer) producer.close(); @@ -1741,7 +1757,7 @@ export default class RoomClient if (notify) { - this.notify(`${displayName} joined the room`); + this.notify(`${displayName} joined the room.`); } for (const consumer of peer.consumers) @@ -1759,7 +1775,7 @@ export default class RoomClient if (this._room.joined) { - this.notify(`${peer.appData.displayName} left the room`); + this.notify(`${displayName} left the room.`); } }); diff --git a/server/lib/Room.js b/server/lib/Room.js index 28fafb5..f306ccb 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -321,7 +321,8 @@ class Room extends EventEmitter signalingPeer.socket.broadcast.to(this._roomId).emit( 'file-receive', { - file : fileData + peerName : signalingPeer.peerName, + file : fileData } ); }); @@ -334,7 +335,7 @@ class Room extends EventEmitter const { raiseHandState } = request; const { mediaPeer } = signalingPeer; - mediaPeer.appData.raiseHandState = request.raiseHandState; + mediaPeer.appData.raiseHandState = raiseHandState; // Spread to others signalingPeer.socket.broadcast.to(this._roomId).emit( 'raisehand-message', From 6d9ebacee3da2f0608bec81275570cafa821e913 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Tue, 13 Nov 2018 15:41:53 +0100 Subject: [PATCH 11/21] removed setCanChangeWebcam becaus not needed and this fixes the bug with unable to select camara if only 1 webcam is connected --- app/lib/RoomClient.js | 2 -- app/lib/components/Settings.jsx | 11 ++--------- app/lib/components/appPropTypes.js | 1 - app/lib/redux/STATE.md | 1 - app/lib/redux/stateActions.js | 8 -------- 5 files changed, 2 insertions(+), 21 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 9536e74..086a35e 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -1730,8 +1730,6 @@ export default class RoomClient else if (!this._webcams.has(currentWebcamId)) this._webcam.device = array[0]; - this._dispatch( - stateActions.setCanChangeWebcam(len >= 2)); if (len >= 1) this._dispatch( stateActions.setWebcamDevices(this._webcams)); diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index d0d3572..b27a20f 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -22,12 +22,6 @@ const Settings = ({ }) => { let webcams; - let webcamText; - - if (me.canChangeWebcam) - webcamText = 'Select camera'; - else - webcamText = 'Unable to select camera'; if (me.webcamDevices) webcams = Array.from(me.webcamDevices.values()); @@ -51,13 +45,12 @@ const Settings = ({
handleChangeWebcam(webcam.value)} - placeholder={webcamText} + placeholder={'Select camera'} /> - + }; }; -export const setCanChangeWebcam = (flag) => -{ - return { - type : 'SET_CAN_CHANGE_WEBCAM', - payload : flag - }; -}; - export const setWebcamDevices = (devices) => { return { From 63bae0cb77b01e762d80eb4a7af6ad47e68a1994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Tue, 13 Nov 2018 16:07:31 +0100 Subject: [PATCH 12/21] Added support for keyboard shortcuts --- app/lib/RoomClient.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 9536e74..78e7504 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -115,6 +115,8 @@ export default class RoomClient this._screenSharingProducer = null; + this._startKeyListener(); + this._join({ displayName, device }); } @@ -137,6 +139,33 @@ export default class RoomClient this._dispatch(stateActions.setRoomState('closed')); } + _startKeyListener() + { + // Add keypress event listner on document + document.addEventListener('keypress', (event) => + { + const key = String.fromCharCode(event.keyCode); + + const source = event.target; + + const exclude = [ 'input', 'textarea' ]; + + if (exclude.indexOf(source.tagName.toLowerCase()) === -1) + { + logger.debug('keyPress() [key:"%s"]', key); + + switch (key) + { + case 'a': // Activate advanced mode + { + this._dispatch(stateActions.toggleAdvancedMode()); + this.notify('Toggled advanced mode.'); + } + } + } + }); + } + login() { const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; From ce5ef1e18e0a67ab2ec778289dea8159c99a98b2 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Wed, 14 Nov 2018 12:03:01 +0100 Subject: [PATCH 13/21] fix: no hark after change audio device --- app/lib/RoomClient.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 086a35e..d0beadc 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -552,8 +552,38 @@ export default class RoomClient const track = stream.getAudioTracks()[0]; + const oldTrack = this._micProducer.track; + const newTrack = await this._micProducer.replaceTrack(track); + const harkStream = new MediaStream; + + harkStream.addTrack(newTrack); + if (!harkStream.getAudioTracks()[0]) + throw new Error('changeAudioDevice(): given stream has no audio track'); + if (this._micProducer.hark != null) this._micProducer.hark.stop(); + this._micProducer.hark = hark(harkStream, { play: false }); + + // eslint-disable-next-line no-unused-vars + 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) + // 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; + + if (volume !== this._micProducer.volume) + { + this._micProducer.volume = volume; + this._dispatch(stateActions.setProducerVolume(this._micProducer.id, volume)); + } + }); + track.stop(); this._dispatch( From c2ff420ea428895e705df8e537f4d521e477d54a Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Wed, 14 Nov 2018 12:04:37 +0100 Subject: [PATCH 14/21] Remove logging for audio level changes --- app/lib/store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/store.js b/app/lib/store.js index cdff97d..1208e8c 100644 --- a/app/lib/store.js +++ b/app/lib/store.js @@ -18,6 +18,7 @@ if (process.env.NODE_ENV === 'development') { const reduxLogger = createLogger( { + predicate : (getState, action) => action.type !== 'SET_PRODUCER_VOLUME', duration : true, timestamp : false, level : 'log', @@ -43,4 +44,4 @@ export const store = createStore( reducers, undefined, enhancer -); \ No newline at end of file +); From 5a8ba8afd4a130b2b53401104449e03135d76ed0 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Wed, 14 Nov 2018 12:33:51 +0100 Subject: [PATCH 15/21] Fix:lint warning --- app/lib/RoomClient.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index ba2a927..9bf973c 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -581,8 +581,6 @@ export default class RoomClient const track = stream.getAudioTracks()[0]; - const oldTrack = this._micProducer.track; - const newTrack = await this._micProducer.replaceTrack(track); const harkStream = new MediaStream; From 854d1e583b8d54e23c34164271af0fabe4814bae Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Wed, 14 Nov 2018 12:45:22 +0100 Subject: [PATCH 16/21] Fix tooltip ToolAreaButton --- app/lib/components/ToolArea/ToolAreaButton.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx index 43139a7..a0a3dd3 100644 --- a/app/lib/components/ToolArea/ToolAreaButton.jsx +++ b/app/lib/components/ToolArea/ToolAreaButton.jsx @@ -29,7 +29,6 @@ class ToolAreaButton extends React.Component })} data-tip='Toggle tool area' data-type='dark' - data-for='globaltip' onClick={() => toggleToolArea()} /> From e695909911b3587cb1b01d8c935a67dfbbae8c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 14 Nov 2018 13:23:22 +0100 Subject: [PATCH 17/21] Added support for moving video to new window. Fixed various bugs. --- app/lib/RoomClient.js | 24 ++++- app/lib/components/Chat/Chat.jsx | 2 +- app/lib/components/FullScreenView.jsx | 1 - app/lib/components/FullView.jsx | 7 +- app/lib/components/Peer.jsx | 27 ++++- app/lib/components/Room.jsx | 3 + app/lib/components/ScreenView.jsx | 4 +- .../components/VideoWindow/VideoWindow.jsx | 98 +++++++++++++++++++ app/lib/redux/reducers/room.js | 12 +++ app/lib/redux/stateActions.js | 8 ++ app/package.json | 1 + .../images/icon_new_window_black.svg | 8 ++ app/stylus/components/Peer.styl | 5 + 13 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 app/lib/components/VideoWindow/VideoWindow.jsx create mode 100644 app/resources/images/icon_new_window_black.svg diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 4700f52..b709fbc 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -16,7 +16,7 @@ import { const logger = new Logger('RoomClient'); -const ROOM_OPTIONS = +let ROOM_OPTIONS = { requestTimeout : requestTimeout, transportOptions : transportOptions, @@ -71,6 +71,9 @@ export default class RoomClient // Socket.io peer connection this._signalingSocket = io(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 = roomId; @@ -160,6 +163,21 @@ export default class RoomClient { this._dispatch(stateActions.toggleAdvancedMode()); this.notify('Toggled advanced mode.'); + break; + } + + case '1': // Set democratic view + { + this._dispatch(stateActions.setDisplayMode('democratic')); + this.notify('Changed layout to democratic view.'); + break; + } + + case '2': // Set filmstrip view + { + this._dispatch(stateActions.setDisplayMode('filmstrip')); + this.notify('Changed layout to filmstrip view.'); + break; } } } @@ -213,9 +231,9 @@ export default class RoomClient if (called) return; called = true; - callback(new Error('Callback timeout')); + callback(new Error('Request timeout.')); }, - 5000 + ROOM_OPTIONS.requestTimeout ); return (...args) => diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx index 33d3d53..20f330c 100644 --- a/app/lib/components/Chat/Chat.jsx +++ b/app/lib/components/Chat/Chat.jsx @@ -58,7 +58,7 @@ Chat.propTypes = Chat.defaultProps = { senderPlaceHolder : 'Type a message...', - autofocus : true, + autofocus : false, displayName : null }; diff --git a/app/lib/components/FullScreenView.jsx b/app/lib/components/FullScreenView.jsx index d47ffda..0b3d2cb 100644 --- a/app/lib/components/FullScreenView.jsx +++ b/app/lib/components/FullScreenView.jsx @@ -56,7 +56,6 @@ const FullScreenView = (props) => videoTrack={consumer ? consumer.track : null} videoVisible={consumerVisible} videoProfile={consumerProfile} - toggleFullscreen={() => toggleConsumerFullscreen(consumer)} />
); diff --git a/app/lib/components/FullView.jsx b/app/lib/components/FullView.jsx index 96b1f0f..ee64aa6 100644 --- a/app/lib/components/FullView.jsx +++ b/app/lib/components/FullView.jsx @@ -84,8 +84,7 @@ export default class FullView extends React.Component FullView.propTypes = { - videoTrack : PropTypes.any, - videoVisible : PropTypes.bool, - videoProfile : PropTypes.string, - toggleFullscreen : PropTypes.func.isRequired + videoTrack : PropTypes.any, + videoVisible : PropTypes.bool, + videoProfile : PropTypes.string }; diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index fc11ce3..ee0861f 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -39,6 +39,7 @@ class Peer extends Component onMuteMic, onUnmuteMic, toggleConsumerFullscreen, + toggleConsumerWindow, style } = this.props; @@ -126,6 +127,15 @@ class Peer extends Component }} /> +
+ { + e.stopPropagation(); + toggleConsumerWindow(webcamConsumer); + }} + /> +
@@ -155,6 +165,15 @@ class Peer extends Component visible : this.state.controlsVisible })} > +
+ { + e.stopPropagation(); + toggleConsumerWindow(screenConsumer); + }} + /> +
@@ -190,7 +209,8 @@ Peer.propTypes = onUnmuteMic : PropTypes.func.isRequired, streamDimensions : PropTypes.object, style : PropTypes.object, - toggleConsumerFullscreen : PropTypes.func.isRequired + toggleConsumerFullscreen : PropTypes.func.isRequired, + toggleConsumerWindow : PropTypes.func.isRequired }; const mapStateToProps = (state, { name }) => @@ -228,6 +248,11 @@ const mapDispatchToProps = (dispatch) => { if (consumer) dispatch(stateActions.toggleConsumerFullscreen(consumer.id)); + }, + toggleConsumerWindow : (consumer) => + { + if (consumer) + dispatch(stateActions.toggleConsumerWindow(consumer.id)); } }; }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 64c10af..653b377 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -16,6 +16,7 @@ import Notifications from './Notifications'; import ToolAreaButton from './ToolArea/ToolAreaButton'; import ToolArea from './ToolArea/ToolArea'; import FullScreenView from './FullScreenView'; +import VideoWindow from './VideoWindow/VideoWindow'; import Draggable from 'react-draggable'; import { idle } from '../utils'; import Sidebar from './Sidebar'; @@ -88,6 +89,8 @@ class Room extends React.Component + +
diff --git a/app/lib/components/ScreenView.jsx b/app/lib/components/ScreenView.jsx index b95be2b..719c406 100644 --- a/app/lib/components/ScreenView.jsx +++ b/app/lib/components/ScreenView.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import Spinner from 'react-spinner'; -export default class PeerView extends React.Component +export default class ScreenView extends React.Component { constructor(props) { @@ -157,7 +157,7 @@ export default class PeerView extends React.Component } } -PeerView.propTypes = +ScreenView.propTypes = { isMe : PropTypes.bool, advancedMode : PropTypes.bool, diff --git a/app/lib/components/VideoWindow/VideoWindow.jsx b/app/lib/components/VideoWindow/VideoWindow.jsx new file mode 100644 index 0000000..c03fbdf --- /dev/null +++ b/app/lib/components/VideoWindow/VideoWindow.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import NewWindow from 'react-new-window'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import * as appPropTypes from '../appPropTypes'; +import * as stateActions from '../../redux/stateActions'; +import FullView from '../FullView'; + +const VideoWindow = (props) => +{ + const { + advancedMode, + consumer, + toggleConsumerWindow, + toolbarsVisible + } = props; + + if (!consumer) + return null; + + const consumerVisible = ( + Boolean(consumer) && + !consumer.locallyPaused && + !consumer.remotelyPaused + ); + + let consumerProfile; + + if (consumer) + consumerProfile = consumer.profile; + + return ( + +
+ {consumerVisible && !consumer.supported ? +
+

incompatible video

+
+ :null + } + +
+
+ { + e.stopPropagation(); + toggleConsumerWindow(); + }} + /> +
+ + +
+ + ); +}; + +VideoWindow.propTypes = +{ + advancedMode : PropTypes.bool, + consumer : appPropTypes.Consumer, + toggleConsumerWindow : PropTypes.func.isRequired, + toolbarsVisible : PropTypes.bool +}; + +const mapStateToProps = (state) => +{ + return { + consumer : state.consumers[state.room.windowConsumer], + toolbarsVisible : state.room.toolbarsVisible + }; +}; + +const mapDispatchToProps = (dispatch) => +{ + return { + toggleConsumerWindow : () => + { + dispatch(stateActions.toggleConsumerWindow(null)); + } + }; +}; + +const VideoWindowContainer = connect( + mapStateToProps, + mapDispatchToProps +)(VideoWindow); + +export default VideoWindowContainer; diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index 6a956fd..89d2b21 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -6,6 +6,7 @@ const initialState = showSettings : false, advancedMode : false, fullScreenConsumer : null, // ConsumerID + windowConsumer : null, // ConsumerID toolbarsVisible : true, mode : 'democratic', selectedPeerName : null, @@ -62,6 +63,17 @@ const room = (state = initialState, action) => return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId }; } + case 'TOGGLE_WINDOW_CONSUMER': + { + const { consumerId } = action.payload; + const currentConsumer = state.windowConsumer; + + if (currentConsumer === consumerId) + return { ...state, windowConsumer: null }; + else + return { ...state, windowConsumer: consumerId }; + } + case 'SET_TOOLBARS_VISIBLE': { const { toolbarsVisible } = action.payload; diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 2a7d30c..3ced6b6 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -389,6 +389,14 @@ export const toggleConsumerFullscreen = (consumerId) => }; }; +export const toggleConsumerWindow = (consumerId) => +{ + return { + type : 'TOGGLE_WINDOW_CONSUMER', + payload : { consumerId } + }; +}; + export const setToolbarsVisible = (toolbarsVisible) => ({ type : 'SET_TOOLBARS_VISIBLE', payload : { toolbarsVisible } diff --git a/app/package.json b/app/package.json index ff04fbd..ebb8fe1 100644 --- a/app/package.json +++ b/app/package.json @@ -28,6 +28,7 @@ "react-dom": "^16.5.2", "react-draggable": "^3.0.5", "react-dropdown": "^1.5.0", + "react-new-window": "0.0.9", "react-redux": "^5.0.7", "react-spinner": "^0.2.7", "react-tooltip": "^3.9.0", diff --git a/app/resources/images/icon_new_window_black.svg b/app/resources/images/icon_new_window_black.svg new file mode 100644 index 0000000..a0915e8 --- /dev/null +++ b/app/resources/images/icon_new_window_black.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 1f1d493..9e38042 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -189,6 +189,11 @@ background-image: url('/resources/images/icon_fullscreen_black.svg'); background-color: rgba(#fff, 0.7); } + + &.newwindow { + background-image: url('/resources/images/icon_new_window_black.svg'); + background-color: rgba(#fff, 0.7); + } } } } From e6b46a983aaa401a725577571cfb8eaa521ffe7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 14 Nov 2018 13:54:39 +0100 Subject: [PATCH 18/21] Fix keylistener --- app/lib/RoomClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index b17dfc3..fa283d7 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -147,7 +147,7 @@ export default class RoomClient // Add keypress event listner on document document.addEventListener('keypress', (event) => { - const key = String.fromCharCode(event.keyCode); + const key = String.fromCharCode(event.which); const source = event.target; From 3590b0b05fdb3f17f2c088199ae90d1f3e9e4ded Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Thu, 15 Nov 2018 11:31:10 +0100 Subject: [PATCH 19/21] Fixes toolArea close, add close button Fix: click somewhere to close toolArea Add: close button to toolArea --- app/lib/components/ToolArea/ToolArea.jsx | 17 +++++++-- .../components/ToolArea/ToolAreaButton.jsx | 2 +- app/resources/images/arrow_right.svg | 1 + app/stylus/components/ToolArea.styl | 38 ++++++++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 app/resources/images/arrow_right.svg diff --git a/app/lib/components/ToolArea/ToolArea.jsx b/app/lib/components/ToolArea/ToolArea.jsx index 82f9725..925bd31 100644 --- a/app/lib/components/ToolArea/ToolArea.jsx +++ b/app/lib/components/ToolArea/ToolArea.jsx @@ -23,7 +23,8 @@ class ToolArea extends React.Component toolAreaOpen, unreadMessages, unreadFiles, - toggleToolArea + toggleToolArea, + closeToolArea } = this.props; const VisibleTab = { @@ -43,11 +44,17 @@ class ToolArea extends React.Component />
+
({ @@ -101,7 +109,8 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = { setToolTab : stateActions.setToolTab, - toggleToolArea : stateActions.toggleToolArea + toggleToolArea : stateActions.toggleToolArea, + closeToolArea : stateActions.closeToolArea }; const ToolAreaContainer = connect( diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx index a0a3dd3..c33047c 100644 --- a/app/lib/components/ToolArea/ToolAreaButton.jsx +++ b/app/lib/components/ToolArea/ToolAreaButton.jsx @@ -27,7 +27,7 @@ class ToolAreaButton extends React.Component className={classnames('button toolarea-button', { on : toolAreaOpen })} - data-tip='Toggle tool area' + data-tip='Open tool box' data-type='dark' onClick={() => toggleToolArea()} /> diff --git a/app/resources/images/arrow_right.svg b/app/resources/images/arrow_right.svg new file mode 100644 index 0000000..a0e5f30 --- /dev/null +++ b/app/resources/images/arrow_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl index a2a8fd8..7fb18d2 100644 --- a/app/stylus/components/ToolArea.styl +++ b/app/stylus/components/ToolArea.styl @@ -5,7 +5,6 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); display: none; &.open { @@ -14,7 +13,8 @@ +desktop() { &.open { - display: none; + background: rgba(0, 0, 0, 0.3); + display: block; } } } @@ -33,6 +33,36 @@ .toolarea-shade.open { display: block; } + > .button { + background-position: center; + background-size: 100%; + background-repeat: no-repeat; + background-color: rgba(#aef); + cursor: pointer; + border-radius: 15%; + padding: 1px; + + +desktop() { + height: 36px; + width: 18px; + } + + +mobile() { + height: 32px; + width: 16px; + } + &.toolarea-close-button { + background-image: url('/resources/images/arrow_right.svg'); + position: absolute; + top: 50%; + left: -22px; + display: none; + &.on { + display: block; + } + } + } + } @media (min-width: 600px) { @@ -128,6 +158,10 @@ background-image: url('/resources/images/icon_tool_area_black.svg'); } } + + &.toolarea-close-button { + background-image: url('/resources/images/arrow_right.svg'); + } } > .badge { From ca54a49dd3dc2972267bfe511bb7d03d1bc1ee94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 15 Nov 2018 13:13:39 +0100 Subject: [PATCH 20/21] Added support for setting separate window in fullscreen --- app/lib/components/FullScreen.js | 107 +++++++ app/lib/components/FullScreenView.jsx | 2 +- app/lib/components/Sidebar.jsx | 32 +- app/lib/components/VideoWindow/NewWindow.jsx | 286 ++++++++++++++++++ .../components/VideoWindow/VideoWindow.jsx | 48 +-- app/package.json | 1 - app/stylus/components/FullScreenView.styl | 7 +- 7 files changed, 430 insertions(+), 53 deletions(-) create mode 100644 app/lib/components/FullScreen.js create mode 100644 app/lib/components/VideoWindow/NewWindow.jsx diff --git a/app/lib/components/FullScreen.js b/app/lib/components/FullScreen.js new file mode 100644 index 0000000..f3a0600 --- /dev/null +++ b/app/lib/components/FullScreen.js @@ -0,0 +1,107 @@ +const key = { + fullscreenEnabled : 0, + fullscreenElement : 1, + requestFullscreen : 2, + exitFullscreen : 3, + fullscreenchange : 4, + fullscreenerror : 5 +}; + +const webkit = [ + 'webkitFullscreenEnabled', + 'webkitFullscreenElement', + 'webkitRequestFullscreen', + 'webkitExitFullscreen', + 'webkitfullscreenchange', + 'webkitfullscreenerror' +]; + +const moz = [ + 'mozFullScreenEnabled', + 'mozFullScreenElement', + 'mozRequestFullScreen', + 'mozCancelFullScreen', + 'mozfullscreenchange', + 'mozfullscreenerror' +]; + +const ms = [ + 'msFullscreenEnabled', + 'msFullscreenElement', + 'msRequestFullscreen', + 'msExitFullscreen', + 'MSFullscreenChange', + 'MSFullscreenError' +]; + +export default class FullScreen +{ + constructor(document) + { + this.document = document; + this.vendor = ( + ('fullscreenEnabled' in this.document && Object.keys(key)) || + (webkit[0] in this.document && webkit) || + (moz[0] in this.document && moz) || + (ms[0] in this.document && ms) || + [] + ); + } + + requestFullscreen(element) + { + element[this.vendor[key.requestFullscreen]](); + } + + requestFullscreenFunction(element) + { + element[this.vendor[key.requestFullscreen]]; + } + + addEventListener(type, handler) + { + this.document.addEventListener(this.vendor[key[type]], handler); + } + + removeEventListener(type, handler) + { + this.document.removeEventListener(this.vendor[key[type]], handler); + } + + get exitFullscreen() + { + return this.document[this.vendor[key.exitFullscreen]].bind(this.document); + } + + get fullscreenEnabled() + { + return Boolean(this.document[this.vendor[key.fullscreenEnabled]]); + } + set fullscreenEnabled(val) {} + + get fullscreenElement() + { + return this.document[this.vendor[key.fullscreenElement]]; + } + set fullscreenElement(val) {} + + get onfullscreenchange() + { + return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()]; + } + + set onfullscreenchange(handler) + { + this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler; + } + + get onfullscreenerror() + { + return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()]; + } + + set onfullscreenerror(handler) + { + this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler; + } +} diff --git a/app/lib/components/FullScreenView.jsx b/app/lib/components/FullScreenView.jsx index 0b3d2cb..f01ce7a 100644 --- a/app/lib/components/FullScreenView.jsx +++ b/app/lib/components/FullScreenView.jsx @@ -40,7 +40,7 @@ const FullScreenView = (props) =>
diff --git a/app/lib/components/Sidebar.jsx b/app/lib/components/Sidebar.jsx index 7f03abb..fc9f9ae 100644 --- a/app/lib/components/Sidebar.jsx +++ b/app/lib/components/Sidebar.jsx @@ -4,46 +4,52 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import * as appPropTypes from './appPropTypes'; import * as requestActions from '../redux/requestActions'; -import fscreen from 'fscreen'; +import FullScreen from './FullScreen'; class Sidebar extends Component { - state = { - fullscreen : false - }; + constructor(props) + { + super(props); + + this.fullscreen = new FullScreen(document); + this.state = { + fullscreen : false + }; + } handleToggleFullscreen = () => { - if (fscreen.fullscreenElement) + if (this.fullscreen.fullscreenElement) { - fscreen.exitFullscreen(); + this.fullscreen.exitFullscreen(); } else { - fscreen.requestFullscreen(document.documentElement); + this.fullscreen.requestFullscreen(document.documentElement); } }; handleFullscreenChange = () => { this.setState({ - fullscreen : fscreen.fullscreenElement !== null + fullscreen : this.fullscreen.fullscreenElement !== null }); }; componentDidMount() { - if (fscreen.fullscreenEnabled) + if (this.fullscreen.fullscreenEnabled) { - fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); + this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); } } componentWillUnmount() { - if (fscreen.fullscreenEnabled) + if (this.fullscreen.fullscreenEnabled) { - fscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); + this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); } } @@ -85,7 +91,7 @@ class Sidebar extends Component })} data-component='Sidebar' > - {fscreen.fullscreenEnabled && ( + {this.fullscreen.fullscreenEnabled && (
+ { + if (this.fullscreen.fullscreenElement) + { + this.fullscreen.exitFullscreen(); + } + else + { + this.fullscreen.requestFullscreen(this.window.document.documentElement); + } + }; + + handleFullscreenChange = () => + { + this.setState({ + fullscreen : this.fullscreen.fullscreenElement !== null + }); + }; + + constructor(props) + { + super(props); + + this.container = document.createElement('div'); + this.window = null; + this.windowCheckerInterval = null; + this.released = false; + this.fullscreen = null; + + this.state = { + mounted : false, + fullscreen : false + }; + } + + render() + { + if (!this.state.mounted) + return null; + + return ReactDOM.createPortal([ +
+
+ {this.fullscreen.fullscreenEnabled && ( +
+ )} +
+ {this.props.children} +
+ ], this.container); + } + + componentDidMount() + { + this.openChild(); + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ mounted: true }); + + this.fullscreen = new FullScreen(this.window.document); + + if (this.fullscreen.fullscreenEnabled) + { + this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); + } + } + + openChild() + { + const { + url, + title, + name, + features, + onBlock, + center + } = this.props; + + if (center === 'parent') + { + features.left = + (window.top.outerWidth / 2) + window.top.screenX - (features.width / 2); + features.top = + (window.top.outerHeight / 2) + window.top.screenY - (features.height / 2); + } + else if (center === 'screen') + { + const screenLeft = + window.screenLeft !== undefined ? window.screenLeft : screen.left; + const screenTop = + window.screenTop !== undefined ? window.screenTop : screen.top; + + const width = window.innerWidth + ? window.innerWidth + : document.documentElement.clientWidth + ? document.documentElement.clientWidth + : screen.width; + const height = window.innerHeight + ? window.innerHeight + : document.documentElement.clientHeight + ? document.documentElement.clientHeight + : screen.height; + + features.left = (width / 2) - (features.width / 2) + screenLeft; + features.top = (height / 2) - (features.height / 2) + screenTop; + } + + this.window = window.open(url, name, toWindowFeatures(features)); + + this.windowCheckerInterval = setInterval(() => + { + if (!this.window || this.window.closed) + { + this.release(); + } + }, 50); + + if (this.window) + { + this.window.document.title = title; + this.window.document.body.appendChild(this.container); + + if (this.props.copyStyles) + { + setTimeout(() => copyStyles(document, this.window.document), 0); + } + + this.window.addEventListener('beforeunload', () => this.release()); + } + else if (typeof onBlock === 'function') + { + onBlock(null); + } + } + + componentWillUnmount() + { + if (this.window) + { + if (this.fullscreen.fullscreenEnabled) + { + this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); + } + + this.window.close(); + } + } + + release() + { + if (this.released) + { + return; + } + + this.released = true; + + clearInterval(this.windowCheckerInterval); + + const { onUnload } = this.props; + + if (typeof onUnload === 'function') + { + onUnload(null); + } + } +} + +NewWindow.propTypes = { + children : PropTypes.node, + url : PropTypes.string, + name : PropTypes.string, + title : PropTypes.string, + features : PropTypes.object, + onUnload : PropTypes.func, + onBlock : PropTypes.func, + center : PropTypes.oneOf([ 'parent', 'screen' ]), + copyStyles : PropTypes.bool +}; + +function copyStyles(source, target) +{ + Array.from(source.styleSheets).forEach((styleSheet) => + { + let rules; + + try + { + rules = styleSheet.cssRules; + } + catch (err) {} + + if (rules) + { + const newStyleEl = source.createElement('style'); + + Array.from(styleSheet.cssRules).forEach((cssRule) => + { + const { cssText, type } = cssRule; + + let returnText = cssText; + + if ([ 3, 5 ].includes(type)) + { + returnText = cssText + .split('url(') + .map((line) => + { + if (line[1] === '/') + { + return `${line.slice(0, 1)}${ + window.location.origin + }${line.slice(1)}`; + } + + return line; + }) + .join('url('); + } + + newStyleEl.appendChild(source.createTextNode(returnText)); + }); + + target.head.appendChild(newStyleEl); + } + else if (styleSheet.href) + { + const newLinkEl = source.createElement('link'); + + newLinkEl.rel = 'stylesheet'; + newLinkEl.href = styleSheet.href; + target.head.appendChild(newLinkEl); + } + }); +} + +function toWindowFeatures(obj) +{ + return Object.keys(obj) + .reduce((features, name) => + { + const value = obj[name]; + + if (typeof value === 'boolean') + { + features.push(`${name}=${value ? 'yes' : 'no'}`); + } + else + { + features.push(`${name}=${value}`); + } + + return features; + }, []) + .join(','); +} + +export default NewWindow; diff --git a/app/lib/components/VideoWindow/VideoWindow.jsx b/app/lib/components/VideoWindow/VideoWindow.jsx index c03fbdf..c5a4a56 100644 --- a/app/lib/components/VideoWindow/VideoWindow.jsx +++ b/app/lib/components/VideoWindow/VideoWindow.jsx @@ -1,8 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import NewWindow from 'react-new-window'; +import NewWindow from './NewWindow'; import PropTypes from 'prop-types'; -import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; import * as stateActions from '../../redux/stateActions'; import FullView from '../FullView'; @@ -12,8 +11,7 @@ const VideoWindow = (props) => const { advancedMode, consumer, - toggleConsumerWindow, - toolbarsVisible + toggleConsumerWindow } = props; if (!consumer) @@ -32,34 +30,12 @@ const VideoWindow = (props) => return ( -
- {consumerVisible && !consumer.supported ? -
-

incompatible video

-
- :null - } - -
-
- { - e.stopPropagation(); - toggleConsumerWindow(); - }} - /> -
- - -
+ ); }; @@ -68,15 +44,13 @@ VideoWindow.propTypes = { advancedMode : PropTypes.bool, consumer : appPropTypes.Consumer, - toggleConsumerWindow : PropTypes.func.isRequired, - toolbarsVisible : PropTypes.bool + toggleConsumerWindow : PropTypes.func.isRequired }; const mapStateToProps = (state) => { return { - consumer : state.consumers[state.room.windowConsumer], - toolbarsVisible : state.room.toolbarsVisible + consumer : state.consumers[state.room.windowConsumer] }; }; @@ -85,7 +59,7 @@ const mapDispatchToProps = (dispatch) => return { toggleConsumerWindow : () => { - dispatch(stateActions.toggleConsumerWindow(null)); + dispatch(stateActions.toggleConsumerWindow()); } }; }; diff --git a/app/package.json b/app/package.json index ebb8fe1..21db4b7 100644 --- a/app/package.json +++ b/app/package.json @@ -14,7 +14,6 @@ "domready": "^1.0.8", "drag-drop": "^4.2.0", "file-saver": "^1.3.8", - "fscreen": "^1.0.2", "hark": "^1.2.2", "js-cookie": "^2.2.0", "magnet-uri": "^5.2.3", diff --git a/app/stylus/components/FullScreenView.styl b/app/stylus/components/FullScreenView.styl index fe6458d..f1f4c29 100644 --- a/app/stylus/components/FullScreenView.styl +++ b/app/stylus/components/FullScreenView.styl @@ -44,10 +44,15 @@ height: 5vmin; } - &.fullscreen { + &.exitfullscreen { background-image: url('/resources/images/icon_fullscreen_exit_black.svg'); background-color: rgba(#fff, 0.7); } + + &.fullscreen { + background-image: url('/resources/images/icon_fullscreen_black.svg'); + background-color: rgba(#fff, 0.7); + } } } From 802a869f8c5f3c22c2c90b28a66ad3dbbf428cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 15 Nov 2018 15:16:57 +0100 Subject: [PATCH 21/21] Update dependencies --- app/package.json | 3 +-- server/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/package.json b/app/package.json index 21db4b7..ab2510a 100644 --- a/app/package.json +++ b/app/package.json @@ -18,7 +18,7 @@ "js-cookie": "^2.2.0", "magnet-uri": "^5.2.3", "marked": "^0.5.1", - "mediasoup-client": "^2.1.1", + "mediasoup-client": "^2.3.2", "prop-types": "^15.6.2", "random-string": "^0.2.0", "react": "^16.5.2", @@ -27,7 +27,6 @@ "react-dom": "^16.5.2", "react-draggable": "^3.0.5", "react-dropdown": "^1.5.0", - "react-new-window": "0.0.9", "react-redux": "^5.0.7", "react-spinner": "^0.2.7", "react-tooltip": "^3.9.0", diff --git a/server/package.json b/server/package.json index a42eed7..098bd70 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "compression": "^1.7.3", "debug": "^4.1.0", "express": "^4.16.3", - "mediasoup": "^2.1.0", + "mediasoup": "^2.3.3", "passport-dataporten": "^1.3.0", "socket.io": "^2.1.1" },