diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index fe0998c..5524293 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -3,6 +3,7 @@ import * as mediasoupClient from 'mediasoup-client'; import Logger from './Logger'; import hark from 'hark'; import ScreenShare from './ScreenShare'; +import Spotlights from './Spotlights'; import { getSignalingUrl } from './urlFactory'; import * as cookiesManager from './cookiesManager'; import * as requestActions from './redux/requestActions'; @@ -19,7 +20,8 @@ const ROOM_OPTIONS = { requestTimeout : requestTimeout, transportOptions : transportOptions, - turnServers : turnServers + turnServers : turnServers, + maxSpotlights : 4 }; const VIDEO_CONSTRAINS = @@ -63,6 +65,9 @@ export default class RoomClient // My peer name. this._peerName = peerName; + // Alert sound + this._soundAlert = new Audio('/resources/sounds/notify.mp3'); + // Socket.io peer connection this._signalingSocket = io(signalingUrl); @@ -70,6 +75,12 @@ export default class RoomClient this._room = new mediasoupClient.Room(ROOM_OPTIONS); this._room.roomId = roomId; + // Max spotlights + this._maxSpotlights = ROOM_OPTIONS.maxSpotlights; + + // Manager of spotlight + this._spotlights = new Spotlights(this._maxSpotlights, this._room); + // Transport for sending. this._sendTransport = null; @@ -280,7 +291,8 @@ export default class RoomClient { const { chatHistory, - fileHistory + fileHistory, + lastN } = await this.sendRequest('server-history'); if (chatHistory.length > 0) @@ -296,6 +308,18 @@ export default class RoomClient this._dispatch(stateActions.addFileHistory(fileHistory)); } + + if (lastN.length > 0) + { + logger.debug('Got lastN'); + + // Remove our self from list + const index = lastN.indexOf(this._peerName); + + lastN.splice(index, 1); + + this._spotlights.addSpeakerList(lastN); + } } catch (error) { @@ -319,6 +343,43 @@ export default class RoomClient this._micProducer.resume(); } + // Updated consumers based on spotlights + async updateSpotlights(spotlights) + { + logger.debug('updateSpotlights()'); + + try + { + for (const peer of this._room.peers) + { + if (spotlights.indexOf(peer.name) > -1) // Resume video for speaker + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'video' || !consumer.supported) + continue; + + await consumer.resume(); + } + } + else // Pause video for everybody else + { + for (const consumer of peer.consumers) + { + if (consumer.kind !== 'video') + continue; + + await consumer.pause('not-speaker'); + } + } + } + } + catch (error) + { + logger.error('updateSpotlights() failed: %o', error); + } + } + installExtension() { logger.debug('installExtension()'); @@ -395,7 +456,6 @@ export default class RoomClient try { - await this._updateWebcams(); await this._setWebcamProducer(); } catch (error) @@ -484,6 +544,8 @@ export default class RoomClient this._dispatch( stateActions.setProducerTrack(this._micProducer.id, newTrack)); + cookiesManager.setAudioDevice({ audioDeviceId: deviceId }); + await this._updateAudioDevices(); } catch (error) @@ -538,6 +600,8 @@ export default class RoomClient this._dispatch( stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); + cookiesManager.setVideoDevice({ videoDeviceId: deviceId }); + await this._updateWebcams(); } catch (error) @@ -611,6 +675,16 @@ export default class RoomClient stateActions.setWebcamInProgress(false)); } + setSelectedPeer(peerName) + { + logger.debug('setSelectedPeer() [peerName:"%s"]', peerName); + + this._spotlights.setPeerSpotlight(peerName); + + this._dispatch( + stateActions.setSelectedPeer(peerName)); + } + async mutePeerAudio(peerName) { logger.debug('mutePeerAudio() [peerName:"%s"]', peerName); @@ -972,6 +1046,9 @@ export default class RoomClient this._dispatch( stateActions.setRoomActiveSpeaker(peerName)); + + if (peerName && peerName !== this._peerName) + this._spotlights.handleActiveSpeaker(peerName); }); this._signalingSocket.on('display-name-changed', (data) => @@ -1038,6 +1115,23 @@ export default class RoomClient this._dispatch( stateActions.addResponseMessage({ ...chatMessage, peerName })); + + if (!this._getState().toolarea.toolAreaOpen || + (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._signalingSocket.on('file-receive', (data) => @@ -1047,6 +1141,23 @@ export default class RoomClient this._dispatch(stateActions.addFile(payload)); this.notify(`${payload.name} 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); + }); + } + } }); } @@ -1099,6 +1210,18 @@ 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._handlePeer(peer); }); @@ -1139,31 +1262,20 @@ export default class RoomClient })); // Don't produce if explicitely requested to not to do it. - if (!this._produce) - return; + if (this._produce) + { + if (this._room.canSend('audio')) + await this._setMicProducer(); - // NOTE: Don't depend on this Promise to continue (so we don't do return). - Promise.resolve() - // Add our mic. - .then(() => - { - if (!this._room.canSend('audio')) - return; - - this._setMicProducer() - .catch(() => {}); - }) // Add our webcam (unless the cookie says no). - .then(() => + if (this._room.canSend('video')) { - if (!this._room.canSend('video')) - return; - const devicesCookie = cookiesManager.getDevices(); if (!devicesCookie || devicesCookie.webcamEnabled) - this.enableWebcam(); - }); + await this.enableWebcam(); + } + } this._dispatch(stateActions.setRoomState('connected')); @@ -1171,15 +1283,23 @@ export default class RoomClient this._dispatch(stateActions.removeAllNotifications()); this.getServerHistory(); - + this.notify('You are in the room'); + this._spotlights.on('spotlights-updated', (spotlights) => + { + this._dispatch(stateActions.setSpotlights(spotlights)); + this.updateSpotlights(spotlights); + }); + const peers = this._room.peers; for (const peer of peers) { this._handlePeer(peer, { notify: false }); } + + this._spotlights.start(); } catch (error) { @@ -1203,10 +1323,6 @@ export default class RoomClient try { - logger.debug('_setMicProducer() | calling _updateAudioDevices()'); - - await this._updateAudioDevices(); - logger.debug('_setMicProducer() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); @@ -1233,6 +1349,10 @@ export default class RoomClient codec : producer.rtpParameters.codecs[0].name })); + logger.debug('_setMicProducer() | calling _updateAudioDevices()'); + + await this._updateAudioDevices(); + producer.on('close', (originator) => { logger.debug( @@ -1268,9 +1388,12 @@ export default class RoomClient logger.debug('mic Producer "unhandled" event'); }); - if (!stream.getAudioTracks()[0]) + const harkStream = new MediaStream; + + harkStream.addTrack(producer.track); + if (!harkStream.getAudioTracks()[0]) throw new Error('_setMicProducer(): given stream has no audio track'); - producer.hark = hark(stream, { play: false }); + producer.hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars producer.hark.on('volume_change', (dBs, threshold) => @@ -1423,18 +1546,12 @@ export default class RoomClient try { - const { device } = this._webcam; - - if (!device) - throw new Error('no webcam devices'); - logger.debug('_setWebcamProducer() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { video : { - deviceId : { exact: device.deviceId }, ...VIDEO_CONSTRAINS } }); @@ -1456,14 +1573,15 @@ export default class RoomClient { id : producer.id, source : 'webcam', - deviceLabel : device.label, - type : this._getWebcamType(device), locallyPaused : producer.locallyPaused, remotelyPaused : producer.remotelyPaused, track : producer.track, codec : producer.rtpParameters.codecs[0].name })); + logger.debug('_setWebcamProducer() | calling _updateWebcams()'); + await this._updateWebcams(); + producer.on('close', (originator) => { logger.debug( @@ -1549,9 +1667,6 @@ export default class RoomClient else if (!this._audioDevices.has(currentAudioDeviceId)) this._audioDevice.device = array[0]; - this._dispatch( - stateActions.setCanChangeWebcam(this._webcams.size >= 2)); - this._dispatch( stateActions.setCanChangeAudioDevice(len >= 2)); if (len >= 1) @@ -1599,9 +1714,6 @@ export default class RoomClient else if (!this._webcams.has(currentWebcamId)) this._webcam.device = array[0]; - this._dispatch( - stateActions.setCanChangeWebcam(this._webcams.size >= 2)); - this._dispatch( stateActions.setCanChangeWebcam(len >= 2)); if (len >= 1) @@ -1614,22 +1726,6 @@ export default class RoomClient } } - _getWebcamType(device) - { - if (/(back|rear)/i.test(device.label)) - { - logger.debug('_getWebcamType() | it seems to be a back camera'); - - return 'back'; - } - else - { - logger.debug('_getWebcamType() | it seems to be a front camera'); - - return 'front'; - } - } - _handlePeer(peer, { notify = true } = {}) { const displayName = peer.appData.displayName; @@ -1772,9 +1868,13 @@ export default class RoomClient // Receive the consumer (if we can). if (consumer.supported) { - // Pause it if video and we are in audio-only mode. - if (consumer.kind === 'video' && this._getState().me.audioOnly) - consumer.pause('audio-only-mode'); + 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) => diff --git a/app/lib/Spotlights.js b/app/lib/Spotlights.js new file mode 100644 index 0000000..8b91e99 --- /dev/null +++ b/app/lib/Spotlights.js @@ -0,0 +1,184 @@ +import { EventEmitter } from 'events'; +import Logger from './Logger'; + +const logger = new Logger('Spotlight'); + +export default class Spotlights extends EventEmitter +{ + constructor(maxSpotlights, room) + { + super(); + + this._room = room; + this._maxSpotlights = maxSpotlights; + this._peerList = []; + this._selectedSpotlights = []; + this._currentSpotlights = []; + this._started = false; + } + + start() + { + const peers = this._room.peers; + + for (const peer of peers) + { + this._handlePeer(peer); + } + + this._handleRoom(); + + this._started = true; + this._spotlightsUpdated(); + } + + peerInSpotlights(peerName) + { + if (this._started) + { + return this._currentSpotlights.indexOf(peerName) !== -1; + } + else + { + return false; + } + } + + setPeerSpotlight(peerName) + { + logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName); + + const index = this._selectedSpotlights.indexOf(peerName); + + if (index !== -1) + { + this._selectedSpotlights = []; + } + else + { + this._selectedSpotlights = [ peerName ]; + } + + /* + if (index === -1) // We don't have this peer in the list, adding + { + this._selectedSpotlights.push(peerName); + } + else // We have this peer, remove + { + this._selectedSpotlights.splice(index, 1); + } + */ + + if (this._started) + this._spotlightsUpdated(); + } + + _handleRoom() + { + this._room.on('newpeer', (peer) => + { + logger.debug( + 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); + this._handlePeer(peer); + }); + } + + addSpeakerList(speakerList) + { + this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ]; + + if (this._started) + this._spotlightsUpdated(); + } + + _handlePeer(peer) + { + logger.debug('_handlePeer() [peerName:"%s"]', peer.name); + + 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); + + if (index > -1) + { + this._peerList.splice(index, 1); + this._peerList = [ peerName ].concat(this._peerList); + + this._spotlightsUpdated(); + } + } + + _spotlightsUpdated() + { + let spotlights; + + if (this._selectedSpotlights.length > 0) + { + spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ]; + } + else + { + spotlights = this._peerList; + } + + if ( + !this._arraysEqual( + this._currentSpotlights, spotlights.slice(0, this._maxSpotlights) + ) + ) + { + logger.debug('_spotlightsUpdated() | spotlights updated, emitting'); + + this._currentSpotlights = spotlights.slice(0, this._maxSpotlights); + this.emit('spotlights-updated', this._currentSpotlights); + } + else + logger.debug('_spotlightsUpdated() | spotlights not updated'); + } + + _arraysEqual(arr1, arr2) + { + if (arr1.length !== arr2.length) + return false; + + for (let i = arr1.length; i--;) + { + if (arr1[i] !== arr2[i]) + return false; + } + + return true; + } +} diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx index 898bf75..33d3d53 100644 --- a/app/lib/components/Chat/Chat.jsx +++ b/app/lib/components/Chat/Chat.jsx @@ -34,6 +34,11 @@ class Chat extends Component autoFocus={autofocus} autoComplete='off' /> + ); diff --git a/app/lib/components/Chat/MessageList.jsx b/app/lib/components/Chat/MessageList.jsx index e80e17b..43a9671 100644 --- a/app/lib/components/Chat/MessageList.jsx +++ b/app/lib/components/Chat/MessageList.jsx @@ -30,7 +30,7 @@ class MessageList extends Component return (
No one has said anything yet...
+No one has shared files yet...
++{hiddenPeersCount}
participant
+ {(hiddenPeersCount === 1) ? null : 's'}
+
this video is paused
+