import Logger from './Logger'; import hark from 'hark'; import { getSignalingUrl } from './urlFactory'; import * as requestActions from './actions/requestActions'; import * as meActions from './actions/meActions'; import * as roomActions from './actions/roomActions'; import * as peerActions from './actions/peerActions'; import * as peerVolumeActions from './actions/peerVolumeActions'; import * as settingsActions from './actions/settingsActions'; import * as chatActions from './actions/chatActions'; import * as fileActions from './actions/fileActions'; import * as lobbyPeerActions from './actions/lobbyPeerActions'; import * as consumerActions from './actions/consumerActions'; import * as producerActions from './actions/producerActions'; import * as notificationActions from './actions/notificationActions'; let WebTorrent; let saveAs; let mediasoupClient; let io; let ScreenShare; let Spotlights; let turnServers, requestTimeout, transportOptions, lastN, mobileLastN; if (process.env.NODE_ENV !== 'test') { ({ turnServers, requestTimeout, transportOptions, lastN, mobileLastN } = window.config); } const logger = new Logger('RoomClient'); const ROOM_OPTIONS = { requestTimeout : requestTimeout, transportOptions : transportOptions, turnServers : turnServers }; const VIDEO_CONSTRAINS = { 'low' : { width : { ideal: 320 }, aspectRatio : 1.334 }, 'medium' : { width : { ideal: 640 }, aspectRatio : 1.334 }, 'high' : { width : { ideal: 1280 }, aspectRatio : 1.334 }, 'veryhigh' : { width : { ideal: 1920 }, aspectRatio : 1.334 }, 'ultra' : { width : { ideal: 3840 }, aspectRatio : 1.334 } }; const VIDEO_ENCODINGS = [ { maxBitrate: 180000, scaleResolutionDownBy: 4 }, { maxBitrate: 360000, scaleResolutionDownBy: 2 }, { maxBitrate: 1500000, scaleResolutionDownBy: 1 } ]; let store; let intl; export default class RoomClient { /** * @param {Object} data * @param {Object} data.store - The Redux store. * @param {Object} data.intl - react-intl object */ static init(data) { store = data.store; intl = data.intl; } constructor( { peerId, accessCode, device, useSimulcast, produce, forceTcp, displayName, muted } = {}) { if (!peerId) throw new Error('Missing peerId'); else if (!device) throw new Error('Missing device'); logger.debug( 'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s", displayName ""]', peerId, device.flag, useSimulcast, produce, forceTcp, displayName); this._signalingUrl = null; // Closed flag. this._closed = false; // Whether we should produce. this._produce = produce; // Wheter we force TCP this._forceTcp = forceTcp; // Use displayName if (displayName) store.dispatch(settingsActions.setDisplayName(displayName)); // Torrent support this._torrentSupport = null; // Whether simulcast should be used. this._useSimulcast = useSimulcast; this._muted = muted; // This device this._device = device; // My peer name. this._peerId = peerId; // Access code this._accessCode = accessCode; // Alert sound this._soundAlert = new Audio('/sounds/notify.mp3'); // Socket.io peer connection this._signalingSocket = null; // The room ID this._roomId = null; // mediasoup-client Device instance. // @type {mediasoupClient.Device} this._mediasoupDevice = null; // Our WebTorrent client this._webTorrent = null; // Max spotlights if (device.bowser.getPlatformType() === 'desktop') this._maxSpotlights = lastN; else this._maxSpotlights = mobileLastN; // Manager of spotlight this._spotlights = null; // Transport for sending. this._sendTransport = null; // Transport for receiving. this._recvTransport = null; // Local mic mediasoup Producer. this._micProducer = null; // Local mic hark this._hark = null; // Local webcam mediasoup Producer. this._webcamProducer = null; // Map of webcam MediaDeviceInfos indexed by deviceId. // @type {Map} this._webcams = {}; this._audioDevices = {}; // mediasoup Consumers. // @type {Map} this._consumers = new Map(); this._screenSharing = null; this._screenSharingProducer = null; this._startKeyListener(); this._startDevicesListener(); } close() { if (this._closed) return; this._closed = true; logger.debug('close()'); this._signalingSocket.close(); // Close mediasoup Transports. if (this._sendTransport) this._sendTransport.close(); if (this._recvTransport) this._recvTransport.close(); store.dispatch(roomActions.setRoomState('closed')); window.location = '/'; } _startKeyListener() { // Add keypress event listner on document document.addEventListener('keypress', (event) => { const key = String.fromCharCode(event.which); 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 { store.dispatch(settingsActions.toggleAdvancedMode()); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.toggleAdvancedMode', defaultMessage : 'Toggled advanced mode' }) })); break; } case '1': // Set democratic view { store.dispatch(roomActions.setDisplayMode('democratic')); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.setDemocraticView', defaultMessage : 'Changed layout to democratic view' }) })); break; } case '2': // Set filmstrip view { store.dispatch(roomActions.setDisplayMode('filmstrip')); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.setFilmStripView', defaultMessage : 'Changed layout to filmstrip view' }) })); break; } case 'm': // Toggle microphone { if (this._micProducer) { if (!this._micProducer.paused) { this.muteMic(); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'devices.microPhoneMute', defaultMessage : 'Muted your microphone' }) })); } else { this.unmuteMic(); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'devices.microPhoneUnMute', defaultMessage : 'Unmuted your microphone' }) })); } } else { this.enableMic(); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'devices.microphoneEnable', defaultMessage : 'Enabled your microphone' }) })); } break; } default: { break; } } } }); } _startDevicesListener() { navigator.mediaDevices.addEventListener('devicechange', async () => { logger.debug('_startDevicesListener() | navigator.mediaDevices.ondevicechange'); await this._updateAudioDevices(); await this._updateWebcams(); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'devices.devicesChanged', defaultMessage : 'Your devices changed, configure your devices in the settings dialog' }) })); }); } login() { const url = `/auth/login?id=${this._peerId}`; window.open(url, 'loginWindow'); } logout() { window.open('/auth/logout', 'logoutWindow'); } receiveLoginChildWindow(data) { logger.debug('receiveFromChildWindow() | [data:"%o"]', data); const { displayName, picture } = data; if (store.getState().room.state === 'connected') { this.changeDisplayName(displayName); this.changePicture(picture); } else { store.dispatch(settingsActions.setDisplayName(displayName)); store.dispatch(meActions.setPicture(picture)); } store.dispatch(meActions.loggedIn(true)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.loggedIn', defaultMessage : 'You are logged in' }) })); } receiveLogoutChildWindow() { logger.debug('receiveLogoutChildWindow()'); store.dispatch(meActions.loggedIn(false)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.loggedOut', defaultMessage : 'You are logged out' }) })); } _soundNotification() { const alertPromise = this._soundAlert.play(); if (alertPromise !== undefined) { alertPromise .then() .catch((error) => { logger.error('_soundAlert.play() | failed: %o', error); }); } } notify(text) { store.dispatch(requestActions.notify({ text: text })); } timeoutCallback(callback) { let called = false; const interval = setTimeout( () => { if (called) return; called = true; callback(new Error('Request timeout.')); }, ROOM_OPTIONS.requestTimeout ); return (...args) => { if (called) return; called = true; clearTimeout(interval); callback(...args); }; } sendRequest(method, data) { return new Promise((resolve, reject) => { if (!this._signalingSocket) { reject('No socket connection.'); } else { this._signalingSocket.emit( 'request', { method, data }, this.timeoutCallback((err, response) => { if (err) { reject(err); } else { resolve(response); } }) ); } }); } async changeDisplayName(displayName) { logger.debug('changeDisplayName() [displayName:"%s"]', displayName); if (!displayName) displayName = 'Guest'; store.dispatch( meActions.setDisplayNameInProgress(true)); try { await this.sendRequest('changeDisplayName', { displayName }); store.dispatch(settingsActions.setDisplayName(displayName)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.changedDisplayName', defaultMessage : 'Your display name changed to {displayName}' }, { displayName }) })); } catch (error) { logger.error('changeDisplayName() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'room.changeDisplayNameError', defaultMessage : 'An error occured while changing your display name' }) })); } store.dispatch( meActions.setDisplayNameInProgress(false)); } async changePicture(picture) { logger.debug('changePicture() [picture: "%s"]', picture); try { await this.sendRequest('changePicture', { picture }); } catch (error) { logger.error('changePicture() | failed: %o', error); } } async sendChatMessage(chatMessage) { logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage); try { store.dispatch( chatActions.addUserMessage(chatMessage.text)); await this.sendRequest('chatMessage', { chatMessage }); } catch (error) { logger.error('sendChatMessage() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'room.chatError', defaultMessage : 'Unable to send chat message' }) })); } } saveFile(file) { file.getBlob((err, blob) => { if (err) { return store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'filesharing.saveFileError', defaultMessage : 'Unable to save file' }) })); } saveAs(blob, file.name); }); } handleDownload(magnetUri) { store.dispatch( fileActions.setFileActive(magnetUri)); const existingTorrent = this._webTorrent.get(magnetUri); if (existingTorrent) { // Never add duplicate torrents, use the existing one instead. return this._handleTorrent(existingTorrent); } this._webTorrent.add(magnetUri, this._handleTorrent); } _handleTorrent(torrent) { // Torrent already done, this can happen if the // same file was sent multiple times. if (torrent.progress === 1) { return store.dispatch( fileActions.setFileDone( torrent.magnetURI, torrent.files )); } let lastMove = 0; torrent.on('download', () => { if (Date.now() - lastMove > 1000) { store.dispatch( fileActions.setFileProgress( torrent.magnetURI, torrent.progress )); lastMove = Date.now(); } }); torrent.on('done', () => { store.dispatch( fileActions.setFileDone( torrent.magnetURI, torrent.files )); }); } async shareFiles(files) { store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'filesharing.startingFileShare', defaultMessage : 'Attempting to share file' }) })); this._webTorrent.seed(files, (torrent) => { store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'filesharing.successfulFileShare', defaultMessage : 'File successfully shared' }) })); store.dispatch(fileActions.addFile( this._peerId, torrent.magnetURI )); this._sendFile(torrent.magnetURI); }); } // { file, name, picture } async _sendFile(magnetUri) { logger.debug('sendFile() [magnetUri: %o]', magnetUri); try { await this.sendRequest('sendFile', { magnetUri }); } catch (error) { logger.error('sendFile() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'filesharing.unableToShare', defaultMessage : 'Unable to share file' }) })); } } async getServerHistory() { logger.debug('getServerHistory()'); try { const { chatHistory, fileHistory, lastNHistory, locked, lobbyPeers, accessCode } = await this.sendRequest('serverHistory'); (chatHistory.length > 0) && store.dispatch( chatActions.addChatHistory(chatHistory)); (fileHistory.length > 0) && store.dispatch( fileActions.addFileHistory(fileHistory)); if (lastNHistory.length > 0) { logger.debug('Got lastNHistory'); // Remove our self from list const index = lastNHistory.indexOf(this._peerId); lastNHistory.splice(index, 1); this._spotlights.addSpeakerList(lastNHistory); } locked ? store.dispatch(roomActions.setRoomLocked()) : store.dispatch(roomActions.setRoomUnLocked()); (lobbyPeers.length > 0) && lobbyPeers.forEach((peer) => { store.dispatch( lobbyPeerActions.addLobbyPeer(peer.peerId)); store.dispatch( lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName)); store.dispatch( lobbyPeerActions.setLobbyPeerPicture(peer.picture)); }); (accessCode != null) && store.dispatch( roomActions.setAccessCode(accessCode)); } catch (error) { logger.error('getServerHistory() | failed: %o', error); } } async muteMic() { logger.debug('muteMic()'); this._micProducer.pause(); try { await this.sendRequest( 'pauseProducer', { producerId: this._micProducer.id }); store.dispatch( producerActions.setProducerPaused(this._micProducer.id)); } catch (error) { logger.error('muteMic() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.microphoneMuteError', defaultMessage : 'Unable to mute your microphone' }) })); } } async unmuteMic() { logger.debug('unmuteMic()'); if (!this._micProducer) { this.enableMic(); } else { this._micProducer.resume(); try { await this.sendRequest( 'resumeProducer', { producerId: this._micProducer.id }); store.dispatch( producerActions.setProducerResumed(this._micProducer.id)); } catch (error) { logger.error('unmuteMic() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.microphoneUnMuteError', defaultMessage : 'Unable to unmute your microphone' }) })); } } } // Updated consumers based on spotlights async updateSpotlights(spotlights) { logger.debug('updateSpotlights()'); try { for (const consumer of this._consumers.values()) { if (consumer.kind === 'video') { if (spotlights.indexOf(consumer.appData.peerId) > -1) { await this._resumeConsumer(consumer); } else { await this._pauseConsumer(consumer); } } } } catch (error) { logger.error('updateSpotlights() failed: %o', error); } } async getAudioTrack() { await navigator.mediaDevices.getUserMedia( { audio : true, video : false }); } async getVideoTrack() { await navigator.mediaDevices.getUserMedia( { audio : false, video : true }); } async changeAudioDevice(deviceId) { logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); store.dispatch( meActions.setAudioInProgress(true)); try { const device = this._audioDevices[deviceId]; if (!device) throw new Error('no audio devices'); logger.debug( 'changeAudioDevice() | new selected webcam [device:%o]', device); if (this._micProducer && this._micProducer.track) this._micProducer.track.stop(); logger.debug('changeAudioDevice() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { audio : { deviceId : { exact: device.deviceId } } }); const track = stream.getAudioTracks()[0]; if (this._micProducer) await this._micProducer.replaceTrack({ track }); if (this._micProducer) this._micProducer.volume = 0; const harkStream = new MediaStream(); harkStream.addTrack(track); if (!harkStream.getAudioTracks()[0]) throw new Error('changeAudioDevice(): given stream has no audio track'); if (this._hark != null) this._hark.stop(); this._hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars 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) // 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 (this._micProducer && volume !== this._micProducer.volume) { this._micProducer.volume = volume; store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); } }); if (this._micProducer && this._micProducer.id) store.dispatch( producerActions.setProducerTrack(this._micProducer.id, track)); store.dispatch(settingsActions.setSelectedAudioDevice(deviceId)); await this._updateAudioDevices(); } catch (error) { logger.error('changeAudioDevice() failed: %o', error); } store.dispatch( meActions.setAudioInProgress(false)); } async changeVideoResolution(resolution) { logger.debug('changeVideoResolution() [resolution: %s]', resolution); store.dispatch( meActions.setWebcamInProgress(true)); try { const deviceId = await this._getWebcamDeviceId(); const device = this._webcams[deviceId]; if (!device) throw new Error('no webcam devices'); this._webcamProducer.track.stop(); logger.debug('changeVideoResolution() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { video : { deviceId : { exact: device.deviceId }, ...VIDEO_CONSTRAINS[resolution] } }); const track = stream.getVideoTracks()[0]; await this._webcamProducer.replaceTrack({ track }); store.dispatch( producerActions.setProducerTrack(this._webcamProducer.id, track)); store.dispatch(settingsActions.setSelectedWebcamDevice(deviceId)); store.dispatch(settingsActions.setVideoResolution(resolution)); await this._updateWebcams(); } catch (error) { logger.error('changeVideoResolution() failed: %o', error); } store.dispatch( meActions.setWebcamInProgress(false)); } async changeWebcam(deviceId) { logger.debug('changeWebcam() [deviceId: %s]', deviceId); store.dispatch( meActions.setWebcamInProgress(true)); try { const device = this._webcams[deviceId]; const resolution = store.getState().settings.resolution; if (!device) throw new Error('no webcam devices'); logger.debug( 'changeWebcam() | new selected webcam [device:%o]', device); this._webcamProducer.track.stop(); logger.debug('changeWebcam() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { video : { deviceId : { exact: device.deviceId }, ...VIDEO_CONSTRAINS[resolution] } }); const track = stream.getVideoTracks()[0]; await this._webcamProducer.replaceTrack({ track }); store.dispatch( producerActions.setProducerTrack(this._webcamProducer.id, track)); store.dispatch(settingsActions.setSelectedWebcamDevice(deviceId)); await this._updateWebcams(); } catch (error) { logger.error('changeWebcam() failed: %o', error); } store.dispatch( meActions.setWebcamInProgress(false)); } setSelectedPeer(peerId) { logger.debug('setSelectedPeer() [peerId:"%s"]', peerId); this._spotlights.setPeerSpotlight(peerId); store.dispatch( roomActions.setSelectedPeer(peerId)); } async promoteLobbyPeer(peerId) { logger.debug('promoteLobbyPeer() [peerId:"%s"]', peerId); store.dispatch( lobbyPeerActions.setLobbyPeerPromotionInProgress(peerId, true)); try { await this.sendRequest('promotePeer', { peerId }); } catch (error) { logger.error('promoteLobbyPeer() failed: %o', error); } store.dispatch( lobbyPeerActions.setLobbyPeerPromotionInProgress(peerId, false)); } // type: mic/webcam/screen // mute: true/false async modifyPeerConsumer(peerId, type, mute) { logger.debug( 'modifyPeerConsumer() [peerId:"%s", type:"%s"]', peerId, type ); if (type === 'mic') store.dispatch( peerActions.setPeerAudioInProgress(peerId, true)); else if (type === 'webcam') store.dispatch( peerActions.setPeerVideoInProgress(peerId, true)); else if (type === 'screen') store.dispatch( peerActions.setPeerScreenInProgress(peerId, true)); try { for (const consumer of this._consumers.values()) { if (consumer.appData.peerId === peerId && consumer.appData.source === type) { if (mute) { await this._pauseConsumer(consumer); } else await this._resumeConsumer(consumer); } } } catch (error) { logger.error('modifyPeerConsumer() failed: %o', error); } if (type === 'mic') store.dispatch( peerActions.setPeerAudioInProgress(peerId, false)); else if (type === 'webcam') store.dispatch( peerActions.setPeerVideoInProgress(peerId, false)); else if (type === 'screen') store.dispatch( peerActions.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( consumerActions.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( consumerActions.setConsumerResumed(consumer.id, 'local')); } catch (error) { logger.error('_resumeConsumer() | failed:%o', error); } } async sendRaiseHandState(state) { logger.debug('sendRaiseHandState: ', state); store.dispatch( meActions.setMyRaiseHandStateInProgress(true)); try { await this.sendRequest('raiseHand', { raiseHandState: state }); store.dispatch( meActions.setMyRaiseHandState(state)); } catch (error) { logger.error('sendRaiseHandState() | failed: %o', error); // We need to refresh the component for it to render changed state store.dispatch(meActions.setMyRaiseHandState(!state)); } store.dispatch( meActions.setMyRaiseHandStateInProgress(false)); } async setMaxSendingSpatialLayer(spatialLayer) { logger.debug('setMaxSendingSpatialLayer() [spatialLayer:%s]', spatialLayer); try { if (this._webcamProducer) await this._webcamProducer.setMaxSpatialLayer(spatialLayer); else if (this._screenSharingProducer) await this._screenSharingProducer.setMaxSpatialLayer(spatialLayer); } catch (error) { logger.error('setMaxSendingSpatialLayer() | failed:"%o"', error); } } async setConsumerPreferredLayers(consumerId, spatialLayer, temporalLayer) { logger.debug( 'setConsumerPreferredLayers() [consumerId:%s, spatialLayer:%s, temporalLayer:%s]', consumerId, spatialLayer, temporalLayer); try { await this.sendRequest( 'setConsumerPreferedLayers', { consumerId, spatialLayer, temporalLayer }); store.dispatch(consumerActions.setConsumerPreferredLayers( consumerId, spatialLayer, temporalLayer)); } catch (error) { logger.error('setConsumerPreferredLayers() | failed:"%o"', error); } } async _loadDynamicImports() { ({ default: WebTorrent } = await import( /* webpackPrefetch: true */ /* webpackChunkName: "webtorrent" */ 'webtorrent' )); ({ default: saveAs } = await import( /* webpackPrefetch: true */ /* webpackChunkName: "file-saver" */ 'file-saver' )); ({ default: ScreenShare } = await import( /* webpackPrefetch: true */ /* webpackChunkName: "screensharing" */ './ScreenShare' )); ({ default: Spotlights } = await import( /* webpackPrefetch: true */ /* webpackChunkName: "spotlights" */ './Spotlights' )); mediasoupClient = await import( /* webpackPrefetch: true */ /* webpackChunkName: "mediasoup" */ 'mediasoup-client' ); ({ default: io } = await import( /* webpackPrefetch: true */ /* webpackChunkName: "socket.io" */ 'socket.io-client' )); } async join({ roomId, joinVideo }) { await this._loadDynamicImports(); this._roomId = roomId; store.dispatch(roomActions.setRoomName(roomId)); this._signalingUrl = getSignalingUrl(this._peerId, roomId); this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; this._webTorrent = this._torrentSupport && new WebTorrent({ tracker : { rtcConfig : { iceServers : ROOM_OPTIONS.turnServers } } }); this._webTorrent.on('error', (error) => { logger.error('Filesharing [error:"%o"]', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id: 'filesharing.error', defaultMessage: 'There was a filesharing error' }) })); }); this._screenSharing = ScreenShare.create(this._device); this._signalingSocket = io(this._signalingUrl); this._spotlights = new Spotlights(this._maxSpotlights, this._signalingSocket); store.dispatch(roomActions.setRoomState('connecting')); this._signalingSocket.on('connect', () => { logger.debug('signaling Peer "connect" event'); }); this._signalingSocket.on('disconnect', (reason) => { logger.warn('signaling Peer "disconnect" event [reason:"%s"]', reason); if (this._closed) return; if (reason === 'io server disconnect') { store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'socket.disconnected', defaultMessage : 'You are disconnected' }) })); this.close(); } store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'socket.reconnecting', defaultMessage : 'You are disconnected, attempting to reconnect' }) })); store.dispatch(roomActions.setRoomState('connecting')); }); this._signalingSocket.on('reconnect_failed', () => { logger.warn('signaling Peer "reconnect_failed" event'); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'socket.disconnected', defaultMessage : 'You are disconnected' }) })); this.close(); }); this._signalingSocket.on('reconnect', (attemptNumber) => { logger.debug('signaling Peer "reconnect" event [attempts:"%s"]', attemptNumber); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'socket.reconnected', defaultMessage : 'You are reconnected' }) })); store.dispatch(roomActions.setRoomState('connected')); }); this._signalingSocket.on('request', async (request, cb) => { logger.debug( 'socket "request" event [method:%s, data:%o]', request.method, request.data); switch (request.method) { 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(consumerActions.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 (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'); 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 (consumer && volume !== consumer.volume) { consumer.volume = volume; store.dispatch(peerVolumeActions.setPeerVolume(peerId, volume)); } }); } break; } default: { logger.error('unknown request.method "%s"', request.method); cb(500, `unknown request.method "${request.method}"`); } } }); this._signalingSocket.on('notification', async (notification) => { logger.debug( 'socket "notification" event [method:%s, data:%o]', notification.method, notification.data); try { switch (notification.method) { case 'enteredLobby': { store.dispatch(roomActions.setInLobby(true)); const { displayName } = store.getState().settings; const { picture } = store.getState().me; await this.sendRequest('changeDisplayName', { displayName }); await this.sendRequest('changePicture', { picture }); break; } case 'signInRequired': { store.dispatch(roomActions.setSignInRequired(true)); break; } case 'roomReady': { store.dispatch(roomActions.toggleJoined()); store.dispatch(roomActions.setInLobby(false)); await this._joinRoom({ joinVideo }); break; } case 'lockRoom': { store.dispatch( roomActions.setRoomLocked()); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.locked', defaultMessage : 'Room is now locked' }) })); break; } case 'unlockRoom': { store.dispatch( roomActions.setRoomUnLocked()); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.unlocked', defaultMessage : 'Room is now unlocked' }) })); break; } case 'parkedPeer': { const { peerId } = notification.data; store.dispatch( lobbyPeerActions.addLobbyPeer(peerId)); store.dispatch( roomActions.setToolbarsVisible(true)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.newLobbyPeer', defaultMessage : 'New participant entered the lobby' }) })); break; } case 'lobby:peerClosed': { const { peerId } = notification.data; store.dispatch( lobbyPeerActions.removeLobbyPeer(peerId)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.lobbyPeerLeft', defaultMessage : 'Participant in lobby left' }) })); break; } case 'lobby:promotedPeer': { const { peerId } = notification.data; store.dispatch( lobbyPeerActions.removeLobbyPeer(peerId)); break; } case 'lobby:changeDisplayName': { const { peerId, displayName } = notification.data; store.dispatch( lobbyPeerActions.setLobbyPeerDisplayName(displayName, peerId)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.lobbyPeerChangedDisplayName', defaultMessage : 'Participant in lobby changed name to {displayName}' }, { displayName }) })); break; } case 'lobby:changePicture': { const { peerId, picture } = notification.data; store.dispatch( lobbyPeerActions.setLobbyPeerPicture(picture, peerId)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.lobbyPeerChangedPicture', defaultMessage : 'Participant in lobby changed picture' }) })); break; } case 'setAccessCode': { const { accessCode } = notification.data; store.dispatch( roomActions.setAccessCode(accessCode)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.setAccessCode', defaultMessage : 'Access code for room updated' }) })); break; } case 'setJoinByAccessCode': { const { joinByAccessCode } = notification.data; store.dispatch( roomActions.setJoinByAccessCode(joinByAccessCode)); if (joinByAccessCode) { store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.accessCodeOn', defaultMessage : 'Access code for room is now activated' }) })); } else { store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.accessCodeOff', defaultMessage : 'Access code for room is now deactivated' }) })); } break; } case 'activeSpeaker': { const { peerId } = notification.data; store.dispatch( roomActions.setRoomActiveSpeaker(peerId)); if (peerId && peerId !== this._peerId) this._spotlights.handleActiveSpeaker(peerId); break; } case 'changeDisplayName': { const { peerId, displayName, oldDisplayName } = notification.data; store.dispatch( peerActions.setPeerDisplayName(displayName, peerId)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.peerChangedDisplayName', defaultMessage : '{oldDisplayName} is now {displayName}' }, { oldDisplayName, displayName }) })); break; } case 'changePicture': { const { peerId, picture } = notification.data; store.dispatch(peerActions.setPeerPicture(peerId, picture)); break; } case 'chatMessage': { const { peerId, chatMessage } = notification.data; store.dispatch( chatActions.addResponseMessage({ ...chatMessage, peerId })); if ( !store.getState().toolarea.toolAreaOpen || (store.getState().toolarea.toolAreaOpen && store.getState().toolarea.currentToolTab !== 'chat') ) // Make sound { store.dispatch( roomActions.setToolbarsVisible(true)); this._soundNotification(); } break; } case 'sendFile': { const { peerId, magnetUri } = notification.data; store.dispatch(fileActions.addFile(peerId, magnetUri)); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.newFile', defaultMessage : 'New file available' }) })); if ( !store.getState().toolarea.toolAreaOpen || (store.getState().toolarea.toolAreaOpen && store.getState().toolarea.currentToolTab !== 'files') ) // Make sound { store.dispatch( roomActions.setToolbarsVisible(true)); this._soundNotification(); } break; } case 'producerScore': { const { producerId, score } = notification.data; store.dispatch( producerActions.setProducerScore(producerId, score)); break; } case 'newPeer': { const { id, displayName, picture, device } = notification.data; store.dispatch( peerActions.addPeer({ id, displayName, picture, device, consumers: [] })); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.newPeer', defaultMessage : '{displayName} joined the room' }, { displayName }) })); break; } case 'peerClosed': { const { peerId } = notification.data; store.dispatch( peerActions.removePeer(peerId)); break; } case 'consumerClosed': { const { consumerId } = notification.data; const consumer = this._consumers.get(consumerId); if (!consumer) break; consumer.close(); if (consumer.hark != null) consumer.hark.stop(); this._consumers.delete(consumerId); const { peerId } = consumer.appData; store.dispatch( consumerActions.removeConsumer(consumerId, peerId)); break; } case 'consumerPaused': { const { consumerId } = notification.data; const consumer = this._consumers.get(consumerId); if (!consumer) break; store.dispatch( consumerActions.setConsumerPaused(consumerId, 'remote')); break; } case 'consumerResumed': { const { consumerId } = notification.data; const consumer = this._consumers.get(consumerId); if (!consumer) break; store.dispatch( consumerActions.setConsumerResumed(consumerId, 'remote')); break; } case 'consumerLayersChanged': { const { consumerId, spatialLayer, temporalLayer } = notification.data; const consumer = this._consumers.get(consumerId); if (!consumer) break; store.dispatch(consumerActions.setConsumerCurrentLayers( consumerId, spatialLayer, temporalLayer)); break; } case 'consumerScore': { const { consumerId, score } = notification.data; store.dispatch( consumerActions.setConsumerScore(consumerId, score)); break; } default: { logger.error( 'unknown notification.method "%s"', notification.method); } } } catch (error) { logger.error('error on socket "notification" event failed:"%o"', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'socket.requestError', defaultMessage : 'Error on server request' }) })); } }); } async _joinRoom({ joinVideo }) { logger.debug('_joinRoom()'); const { displayName, picture } = store.getState().settings; try { this._mediasoupDevice = new mediasoupClient.Device(); const routerRtpCapabilities = await this.sendRequest('getRouterRtpCapabilities'); await this._mediasoupDevice.load({ routerRtpCapabilities }); if (this._produce) { const transportInfo = await this.sendRequest( 'createWebRtcTransport', { forceTcp : this._forceTcp, producing : true, consuming : false }); const { id, iceParameters, iceCandidates, dtlsParameters } = transportInfo; this._sendTransport = this._mediasoupDevice.createSendTransport( { id, iceParameters, iceCandidates, dtlsParameters, iceServers : ROOM_OPTIONS.turnServers }); 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); }); } 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, iceServers : ROOM_OPTIONS.turnServers }); 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(meActions.setMediaCapabilities( { canSendMic : this._mediasoupDevice.canProduce('audio'), canSendWebcam : this._mediasoupDevice.canProduce('video'), canShareScreen : this._mediasoupDevice.canProduce('video') && this._screenSharing.isScreenShareAvailable(), canShareFiles : this._torrentSupport })); const { peers } = await this.sendRequest( 'join', { displayName : displayName, picture : picture, rtpCapabilities : this._mediasoupDevice.rtpCapabilities }); logger.debug('_joinRoom() joined, got peers [peers:"%o"]', peers); for (const peer of peers) { store.dispatch( peerActions.addPeer({ ...peer, consumers: [] })); } this._spotlights.addPeers(peers); this._spotlights.on('spotlights-updated', (spotlights) => { store.dispatch(roomActions.setSpotlights(spotlights)); this.updateSpotlights(spotlights); }); // Don't produce if explicitely requested to not to do it. if (this._produce) { if (this._mediasoupDevice.canProduce('audio')) if (!this._muted) this.enableMic(); if (joinVideo && this._mediasoupDevice.canProduce('video')) this.enableWebcam(); } store.dispatch(roomActions.setRoomState('connected')); // Clean all the existing notifcations. store.dispatch(notificationActions.removeAllNotifications()); this.getServerHistory(); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.joined', defaultMessage : 'You have joined the room' }) })); this._spotlights.start(); } catch (error) { logger.error('_joinRoom() failed:"%o"', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'room.cantJoin', defaultMessage : 'Unable to join the room' }) })); this.close(); } } async lockRoom() { logger.debug('lockRoom()'); try { await this.sendRequest('lockRoom'); store.dispatch( roomActions.setRoomLocked()); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.youLocked', defaultMessage : 'You locked the room' }) })); } catch (error) { store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'room.cantLock', defaultMessage : 'Unable to lock the room' }) })); logger.error('lockRoom() | failed: %o', error); } } async unlockRoom() { logger.debug('unlockRoom()'); try { await this.sendRequest('unlockRoom'); store.dispatch( roomActions.setRoomUnLocked()); store.dispatch(requestActions.notify( { text : intl.formatMessage({ id : 'room.youUnLocked', defaultMessage : 'You unlocked the room' }) })); } catch (error) { store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'room.cantUnLock', defaultMessage : 'Unable to unlock the room' }) })); logger.error('unlockRoom() | failed: %o', error); } } async setAccessCode(code) { logger.debug('setAccessCode()'); try { await this.sendRequest('setAccessCode', { accessCode: code }); store.dispatch( roomActions.setAccessCode(code)); store.dispatch(requestActions.notify( { text : 'Access code saved.' })); } catch (error) { logger.error('setAccessCode() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : 'Unable to set access code.' })); } } async setJoinByAccessCode(value) { logger.debug('setJoinByAccessCode()'); try { await this.sendRequest('setJoinByAccessCode', { joinByAccessCode: value }); store.dispatch( roomActions.setJoinByAccessCode(value)); store.dispatch(requestActions.notify( { text : `You switched Join by access-code to ${value}` })); } catch (error) { logger.error('setAccessCode() | failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : 'Unable to set join by access code.' })); } } async enableMic() { if (this._micProducer) return; if (this._mediasoupDevice && !this._mediasoupDevice.canProduce('audio')) { logger.error('enableMic() | cannot produce audio'); return; } let track; store.dispatch( meActions.setAudioInProgress(true)); try { const deviceId = await this._getAudioDeviceId(); const device = this._audioDevices[deviceId]; if (!device) throw new Error('no audio devices'); logger.debug( 'enableMic() | new selected audio device [device:%o]', device); logger.debug('enableMic() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { audio : { deviceId : { exact: deviceId } } } ); track = stream.getAudioTracks()[0]; this._micProducer = await this._sendTransport.produce( { track, codecOptions : { opusStereo : 1, opusDtx : 1 }, appData : { source: 'mic' } }); store.dispatch(producerActions.addProducer( { 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(settingsActions.setSelectedAudioDevice(deviceId)); await this._updateAudioDevices(); this._micProducer.on('transportclose', () => { this._micProducer = null; }); this._micProducer.on('trackended', () => { store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.microphoneDisconnected', defaultMessage : 'Microphone disconnected' }) })); this.disableMic() .catch(() => {}); }); this._micProducer.volume = 0; const harkStream = new MediaStream(); harkStream.addTrack(track); if (!harkStream.getAudioTracks()[0]) throw new Error('enableMic(): given stream has no audio track'); if (this._hark != null) this._hark.stop(); this._hark = hark(harkStream, { play: false }); // eslint-disable-next-line no-unused-vars 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) // 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 (this._micProducer && volume !== this._micProducer.volume) { this._micProducer.volume = volume; store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); } }); } catch (error) { logger.error('enableMic() failed:%o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.microphoneError', defaultMessage : 'An error occured while accessing your microphone' }) })); if (track) track.stop(); } store.dispatch( meActions.setAudioInProgress(false)); } async disableMic() { logger.debug('disableMic()'); if (!this._micProducer) return; store.dispatch(meActions.setAudioInProgress(true)); this._micProducer.close(); store.dispatch( producerActions.removeProducer(this._micProducer.id)); try { await this.sendRequest( 'closeProducer', { producerId: this._micProducer.id }); } catch (error) { logger.error('disableMic() [error:"%o"]', error); } this._micProducer = null; store.dispatch(meActions.setAudioInProgress(false)); } async enableScreenSharing() { if (this._screenSharingProducer) return; if (!this._mediasoupDevice.canProduce('video')) { logger.error('enableScreenSharing() | cannot produce video'); return; } let track; store.dispatch(meActions.setScreenShareInProgress(true)); try { const available = this._screenSharing.isScreenShareAvailable(); if (!available) throw new Error('screen sharing not available'); logger.debug('enableScreenSharing() | calling getUserMedia()'); const stream = await this._screenSharing.start({ width : 1280, height : 720, frameRate : 3 }); track = stream.getVideoTracks()[0]; 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(producerActions.addProducer( { 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] })); this._screenSharingProducer.on('transportclose', () => { this._screenSharingProducer = null; }); this._screenSharingProducer.on('trackended', () => { store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.screenSharingDisconnected', defaultMessage : 'Screen sharing disconnected' }) })); this.disableScreenSharing() .catch(() => {}); }); logger.debug('enableScreenSharing() succeeded'); } catch (error) { logger.error('enableScreenSharing() failed: %o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.screenSharingError', defaultMessage : 'An error occured while accessing your screen' }) })); if (track) track.stop(); } store.dispatch(meActions.setScreenShareInProgress(false)); } async disableScreenSharing() { logger.debug('disableScreenSharing()'); if (!this._screenSharingProducer) return; store.dispatch(meActions.setScreenShareInProgress(true)); this._screenSharingProducer.close(); store.dispatch( producerActions.removeProducer(this._screenSharingProducer.id)); try { await this.sendRequest( 'closeProducer', { producerId: this._screenSharingProducer.id }); } catch (error) { logger.error('disableScreenSharing() [error:"%o"]', error); } this._screenSharingProducer = null; store.dispatch(meActions.setScreenShareInProgress(false)); } async enableWebcam() { if (this._webcamProducer) return; if (!this._mediasoupDevice.canProduce('video')) { logger.error('enableWebcam() | cannot produce video'); return; } let track; store.dispatch( meActions.setWebcamInProgress(true)); try { const deviceId = await this._getWebcamDeviceId(); const device = this._webcams[deviceId]; const resolution = store.getState().settings.resolution; if (!device) throw new Error('no webcam devices'); logger.debug( '_setWebcamProducer() | new selected webcam [device:%o]', device); logger.debug('_setWebcamProducer() | calling getUserMedia()'); const stream = await navigator.mediaDevices.getUserMedia( { video : { deviceId : { exact: deviceId }, ...VIDEO_CONSTRAINS[resolution] } }); track = stream.getVideoTracks()[0]; 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(producerActions.addProducer( { 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(settingsActions.setSelectedWebcamDevice(deviceId)); await this._updateWebcams(); this._webcamProducer.on('transportclose', () => { this._webcamProducer = null; }); this._webcamProducer.on('trackended', () => { store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.cameraDisconnected', defaultMessage : 'Camera disconnected' }) })); this.disableWebcam() .catch(() => {}); }); logger.debug('_setWebcamProducer() succeeded'); } catch (error) { logger.error('_setWebcamProducer() failed:%o', error); store.dispatch(requestActions.notify( { type : 'error', text : intl.formatMessage({ id : 'devices.cameraError', defaultMessage : 'An error occured while accessing your camera' }) })); if (track) track.stop(); } store.dispatch( meActions.setWebcamInProgress(false)); } async disableWebcam() { logger.debug('disableWebcam()'); if (!this._webcamProducer) return; store.dispatch(meActions.setWebcamInProgress(true)); this._webcamProducer.close(); store.dispatch( producerActions.removeProducer(this._webcamProducer.id)); try { await this.sendRequest( 'closeProducer', { producerId: this._webcamProducer.id }); } catch (error) { logger.error('disableWebcam() [error:"%o"]', error); } this._webcamProducer = null; store.dispatch(meActions.setWebcamInProgress(false)); } async _updateAudioDevices() { logger.debug('_updateAudioDevices()'); // Reset the list. this._audioDevices = {}; try { logger.debug('_updateAudioDevices() | calling enumerateDevices()'); const devices = await navigator.mediaDevices.enumerateDevices(); for (const device of devices) { if (device.kind !== 'audioinput') continue; this._audioDevices[device.deviceId] = device; } store.dispatch( meActions.setAudioDevices(this._audioDevices)); } catch (error) { logger.error('_updateAudioDevices() failed:%o', error); } } async _updateWebcams() { logger.debug('_updateWebcams()'); // Reset the list. this._webcams = {}; try { logger.debug('_updateWebcams() | calling enumerateDevices()'); const devices = await navigator.mediaDevices.enumerateDevices(); for (const device of devices) { if (device.kind !== 'videoinput') continue; this._webcams[device.deviceId] = device; } store.dispatch( meActions.setWebcamDevices(this._webcams)); } catch (error) { logger.error('_updateWebcams() failed:%o', error); } } async _getAudioDeviceId() { logger.debug('_getAudioDeviceId()'); try { logger.debug('_getAudioDeviceId() | calling _updateAudioDeviceId()'); await this._updateAudioDevices(); const { selectedAudioDevice } = store.getState().settings; if (selectedAudioDevice && this._audioDevices[selectedAudioDevice]) return selectedAudioDevice; else { const audioDevices = Object.values(this._audioDevices); return audioDevices[0] ? audioDevices[0].deviceId : null; } } catch (error) { logger.error('_getAudioDeviceId() failed:%o', error); } } async _getWebcamDeviceId() { logger.debug('_getWebcamDeviceId()'); try { logger.debug('_getWebcamDeviceId() | calling _updateWebcams()'); await this._updateWebcams(); const { selectedWebcam } = store.getState().settings; if (selectedWebcam && this._webcams[selectedWebcam]) return selectedWebcam; else { const webcams = Object.values(this._webcams); return webcams[0] ? webcams[0].deviceId : null; } } catch (error) { logger.error('_getWebcamDeviceId() failed:%o', error); } } }