// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import adapter from 'webrtc-adapter'; import WebrtcClient from './webrtc_client.jsx'; const transationLength = 12; export default class WebrtcSession { static randomString(len) { const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let randomString = ''; for (let i = 0; i < len; i++) { const randomPoz = Math.floor(Math.random() * charSet.length); randomString += charSet.substring(randomPoz, randomPoz + 1); } return randomString; } static getLocalMedia(constraints, element, callback) { const media = constraints || {audio: true, video: true}; navigator.mediaDevices.getUserMedia(media). then((stream) => { if (element) { element.srcObject = stream; } if (callback && typeof callback === 'function') { callback(null, stream); } }). catch((error) => { callback(error); }); } static stopMediaStream(stream) { const tracks = stream.getTracks(); tracks.forEach((track) => { track.stop(); }); } constructor(opts) { // super(); const options = opts || {}; this.getServer = this.getServer.bind(this); this.isConnected = this.isConnected.bind(this); this.getSessionId = this.getSessionId.bind(this); this.handleEvent = this.handleEvent.bind(this); this.keepAlive = this.keepAlive.bind(this); this.createSession = this.createSession.bind(this); this.attach = this.attach.bind(this); this.sendMessage = this.sendMessage.bind(this); this.sendTrickleCandidate = this.sendTrickleCandidate.bind(this); this.sendData = this.sendData.bind(this); this.sendDtmf = this.sendDtmf.bind(this); this.destroy = this.destroy.bind(this); this.destroyHandle = this.destroyHandle.bind(this); this.streamsDone = this.streamsDone.bind(this); this.prepareWebrtc = this.prepareWebrtc.bind(this); this.prepareWebrtcPeer = this.prepareWebrtcPeer.bind(this); this.createOffer = this.createOffer.bind(this); this.createAnswer = this.createAnswer.bind(this); this.sendSDP = this.sendSDP.bind(this); this.getVolume = this.getVolume.bind(this); this.isMuted = this.isMuted.bind(this); this.mute = this.mute.bind(this); this.getBitrate = this.getBitrate.bind(this); this.webrtcError = this.webrtcError.bind(this); this.cleanupWebrtc = this.cleanupWebrtc.bind(this); this.isAudioSendEnabled = this.isAudioSendEnabled.bind(this); this.isAudioRecvEnabled = this.isAudioRecvEnabled.bind(this); this.isVideoSendEnabled = this.isVideoSendEnabled.bind(this); this.isVideoRecvEnabled = this.isVideoRecvEnabled.bind(this); this.isDataEnabled = this.isDataEnabled.bind(this); this.isTrickleEnabled = this.isTrickleEnabled.bind(this); this.unbindWebSocket = this.unbindWebSocket.bind(this); this.websockets = false; this.ws = null; this.wsHandlers = {}; this.wsKeepaliveTimeoutId = null; this.servers = null; this.server = null; this.serversIndex = 0; this.connected = false; this.sessionId = null; this.pluginHandles = {}; this.retries = 0; this.transactions = {}; this.client = new WebrtcClient(); this.client.init({debug: options.debug}); this.gatewayCallbacks = options || {}; this.gatewayCallbacks.success = (typeof options.success == 'function') ? options.success : this.client.noop; this.gatewayCallbacks.error = (typeof options.error == 'function') ? options.error : this.client.noop; this.gatewayCallbacks.destroyed = (typeof options.destroyed == 'function') ? options.destroyed : this.client.noop; if (!this.client.initDone) { this.gatewayCallbacks.error('webrtc_client.not_initialize', 'Library not initialized'); return {}; } if (!this.client.isWebrtcSupported()) { this.gatewayCallbacks.error('webrtc_client.browser.not_supported', 'WebRTC not supported by this browser'); return {}; } this.client.log('Library initialized: ' + this.client.initDone); if (!options.server) { this.gatewayCallbacks.error('webrtc_client.invalid_gateway', 'Invalid gateway url'); return {}; } if (Array.isArray(options.server)) { this.client.log('Multiple servers provided (' + options.server.length + '), will use the first that works'); for (let i = 0; i < options.server; i++) { const server = options.server[i]; if (server.indexOf('ws') !== 0) { this.gatewayCallbacks.error('webrtc_client.must_be_websocket', 'every server provided must be a websocket'); return {}; } } this.servers = options.server; this.client.debug(this.servers); } else if (options.server.indexOf('ws') === 0) { this.websockets = true; this.servers = [options.server]; this.client.log('Using WebSockets to contact Janus: ' + options.server); } else { this.gatewayCallbacks.error('webrtc_client.invalid_websocket', 'This library must connect to a websocket'); return {}; } this.iceServers = options.iceServers; if (!this.iceServers || this.iceServers.length === 0) { this.iceServers = [{url: 'stun:stun.l.google.com:19302'}]; } // Optional max events this.maxev = null; if (options.max_poll_events) { this.maxev = options.max_poll_events; } if (this.maxev < 1) { this.maxev = 1; } // Token to use (only if the token based authentication mechanism is enabled) this.token = null; if (options.token) { this.token = options.token; } // API secret to use (only if the shared API secret is enabled) this.apisecret = null; if (options.apisecret) { this.apisecret = options.apisecret; } // Whether we should destroy this session when onbeforeunload is called this.destroyOnUnload = options.destroyOnUnload !== false; this.createSession(); return this; } getServer() { return this.server; } isConnected() { return this.connected; } getSessionId() { return this.sessionId; } handleEvent(json) { this.retries = 0; this.client.debug('Got event on session ' + this.sessionId); this.client.debug(json); const transaction = json.transaction; const sender = json.sender; const plugindata = json.plugindata; const jsep = json.jsep; switch (json.janus) { case 'keepalive': // Nothing happened break; case 'ack': case 'success': // Success or just an ack, we can probably ignore if (transaction) { const reportSuccess = this.transactions[transaction]; if (reportSuccess) { reportSuccess(json); } Reflect.deleteProperty(this.transactions, transaction); } break; case 'webrtcup': // The PeerConnection with the gateway is up! Notify this if (sender) { const pluginHandle = this.pluginHandles[sender]; if (pluginHandle) { pluginHandle.webrtcState(true); } else { this.client.warn('This handle is not attached to this session'); } } else { this.client.warn('Missing sender...'); } break; case 'hangup': // A plugin asked the core to hangup a PeerConnection on one of our handles if (sender) { const pluginHandle = this.pluginHandles[sender]; if (pluginHandle) { pluginHandle.webrtcState(false); pluginHandle.hangup(); } else { this.client.warn('This handle is not attached to this session'); } } else { this.client.warn('Missing sender...'); } break; case 'detached': // A plugin asked the core to detach one of our handles if (sender) { const pluginHandle = this.pluginHandles[sender]; if (pluginHandle) { pluginHandle.ondetached(); pluginHandle.detach(); } else { this.client.warn('This handle is not attached to this session'); } } else { this.client.warn('Missing sender...'); } break; case 'media': // Media started/stopped flowing if (sender) { const pluginHandle = this.pluginHandles[sender]; if (pluginHandle) { pluginHandle.mediaState(json.type, json.receiving); } else { this.client.warn('This handle is not attached to this session'); } } else { this.client.warn('Missing sender...'); } break; case 'error': // Oops, something wrong happened this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason); if (transaction) { const reportSuccess = this.transactions[transaction]; if (reportSuccess) { reportSuccess(json); } Reflect.deleteProperty(this.transactions, transaction); } break; case 'event': if (sender) { if (plugindata) { this.client.debug(` -- Event is coming from ${sender} ( ${plugindata.plugin} )`); const data = plugindata.data; this.client.debug(data); const pluginHandle = this.pluginHandles[sender]; if (pluginHandle) { pluginHandle.mediaState(json.type, json.receiving); if (jsep) { this.client.debug('Handling SDP as well...'); this.client.debug(jsep); } const callback = pluginHandle.onmessage; if (callback) { this.client.debug('Notifying application...'); // Send to callback specified when attaching plugin handle callback(data, jsep); } else { // Send to generic callback (?) this.client.debug('No provided notification callback'); } } else { this.client.warn('This handle is not attached to this session'); } } else { this.client.warn('Missing plugindata...'); } } else { this.client.warn('Missing sender...'); } break; default: this.client.warn(`Unknown message "${json.janus}"`); break; } } keepAlive() { if (this.server === null || !this.websockets || !this.connected) { return; } this.wsKeepaliveTimeoutId = setTimeout(this.keepAlive, 30000); const request = { janus: 'keepalive', session_id: this.sessionId, transaction: WebrtcSession.randomString(transationLength) }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } this.ws.send(JSON.stringify(request)); } createSession() { const transaction = WebrtcSession.randomString(transationLength); const request = { janus: 'create', transaction }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } if (this.server === null && Array.isArray(this.servers)) { // We still need to find a working server from the list we were given this.server = this.servers[this.serversIndex]; if (this.server.indexOf('ws') === 0) { this.websockets = true; this.client.log('Server #' + (this.serversIndex + 1) + ': trying WebSockets to contact Janus (' + this.server + ')'); } } if (this.websockets) { this.ws = new WebSocket(this.server, 'janus-protocol'); this.wsHandlers = { error: () => { this.client.error('Error connecting to the Janus WebSockets server... ' + this.server); if (Array.isArray(this.servers)) { this.serversIndex++; if (this.serversIndex === this.servers.length) { // We tried all the servers the user gave us and they all failed this.gatewayCallbacks.error('webrtc_client.cannot_connect_servers', 'Error connecting to any of the provided Janus servers: Is the gateway down?'); return; } // Let's try the next server this.server = null; setTimeout(() => { this.createSession(); }, 200); return; } this.gatewayCallbacks.error('webrtc_client.cannot_connect_server', 'Error connecting to the Janus WebSockets server: Is the gateway down?'); }, open: () => { // We need to be notified about the success this.transactions[transaction] = (json) => { this.client.debug(json); if (json.janus !== 'success') { this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason); // FIXME this.gatewayCallbacks.error(json.error.reason); return; } this.wsKeepaliveTimeoutId = setTimeout(this.keepAlive, 30000); this.connected = true; this.sessionId = json.data.id; this.client.log('Created session: ' + this.sessionId); this.client.sessions[this.sessionId] = this; this.gatewayCallbacks.success(); }; this.ws.send(JSON.stringify(request)); }, message: (event) => { this.handleEvent(JSON.parse(event.data)); }, close: () => { if (!this.connected) { return; } this.connected = false; // FIXME What if this is called when the page is closed? this.gatewayCallbacks.error('Lost connection to the gateway (is it down?)'); } }; for (var eventName in this.wsHandlers) { if (this.wsHandlers.hasOwnProperty(eventName)) { this.ws.addEventListener(eventName, this.wsHandlers[eventName]); } } } } attach(cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; callbacks.consentDialog = (typeof cbs.consentDialog == 'function') ? cbs.consentDialog : this.client.noop; callbacks.mediaState = (typeof cbs.mediaState == 'function') ? cbs.mediaState : this.client.noop; callbacks.webrtcState = (typeof cbs.webrtcState == 'function') ? cbs.webrtcState : this.client.noop; callbacks.onmessage = (typeof cbs.onmessage == 'function') ? cbs.onmessage : this.client.noop; callbacks.onlocalstream = (typeof cbs.onlocalstream == 'function') ? cbs.onlocalstream : this.client.noop; callbacks.onremotestream = (typeof cbs.onremotestream == 'function') ? cbs.onremotestream : this.client.noop; callbacks.ondata = (typeof cbs.ondata == 'function') ? cbs.ondata : this.client.noop; callbacks.ondataopen = (typeof cbs.ondataopen == 'function') ? cbs.ondataopen : this.client.noop; callbacks.oncleanup = (typeof cbs.oncleanup == 'function') ? cbs.oncleanup : this.client.noop; callbacks.ondetached = (typeof cbs.ondetached == 'function') ? cbs.ondetached : this.client.noop; if (!this.connected) { this.client.warn('Is the gateway down? (connected=false)'); callbacks.error('Is the gateway down? (connected=false)'); return; } const plugin = callbacks.plugin; if (!plugin) { this.client.error('Invalid plugin'); callbacks.error('Invalid plugin'); return; } const transaction = WebrtcSession.randomString(transationLength); const request = { janus: 'attach', plugin, transaction }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } if (this.websockets) { this.transactions[transaction] = (json) => { this.client.debug(json); if (json.janus !== 'success') { const error = `Ooops: ${json.error.code} ${json.error.reason}`; this.client.error(error); callbacks.error(error); return; } const handleId = json.data.id; this.client.log('Created handle: ' + handleId); const pluginHandle = { session: this, plugin, id: handleId, webrtcStuff: { started: false, myStream: null, streamExternal: false, remoteStream: null, mySdp: null, pc: null, dataChannel: null, dtmfSender: null, trickle: true, iceDone: false, sdpSent: false, volume: { value: null, timer: null }, bitrate: { value: null, bsnow: null, bsbefore: null, tsnow: null, tsbefore: null, timer: null } }, getId: () => { return handleId; }, getPlugin: () => { return plugin; }, getVolume: () => { return this.getVolume(handleId); }, isAudioMuted: () => { return this.isMuted(handleId, false); }, muteAudio: () => { return this.mute(handleId, false, true); }, unmuteAudio: () => { return this.mute(handleId, false, false); }, isVideoMuted: () => { return this.isMuted(handleId, true); }, muteVideo: () => { return this.mute(handleId, true, true); }, unmuteVideo: () => { return this.mute(handleId, true, false); }, getBitrate: () => { return this.getBitrate(handleId); }, send: (cb) => { this.sendMessage(handleId, cb); }, data: (cb) => { this.sendData(handleId, cb); }, dtmf: (cb) => { this.sendDtmf(handleId, cb); }, consentDialog: callbacks.consentDialog, mediaState: callbacks.mediaState, webrtcState: callbacks.webrtcState, onmessage: callbacks.onmessage, createOffer: (cb) => { this.prepareWebrtc(handleId, cb); }, createAnswer: (cb) => { this.prepareWebrtc(handleId, cb); }, handleRemoteJsep: (cb) => { this.prepareWebrtcPeer(handleId, cb); }, onlocalstream: callbacks.onlocalstream, onremotestream: callbacks.onremotestream, ondata: callbacks.ondata, ondataopen: callbacks.ondataopen, oncleanup: callbacks.oncleanup, ondetached: callbacks.ondetached, hangup: (sendRequest) => { this.cleanupWebrtc(handleId, sendRequest === true); }, detach: (cb) => { this.destroyHandle(handleId, cb); } }; this.pluginHandles[handleId] = pluginHandle; callbacks.success(pluginHandle); }; request.session_id = this.sessionId; this.ws.send(JSON.stringify(request)); } } sendMessage(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; if (!this.connected) { const error = 'Is the gateway down? (connected=false)'; this.client.warn(error); callbacks.error(error); return; } const message = callbacks.message; const jsep = callbacks.jsep; const transaction = WebrtcSession.randomString(transationLength); const request = { janus: 'message', body: message, transaction }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } if (jsep) { request.jsep = jsep; } this.client.debug('Sending message to plugin (handle=' + handleId + '):'); this.client.debug(request); if (this.websockets) { request.session_id = this.sessionId; request.handle_id = handleId; this.transactions[transaction] = (json) => { this.client.debug('Message sent!'); this.client.debug(json); if (json.janus === 'success') { // We got a success, must have been a synchronous transaction const plugindata = json.plugindata; if (!plugindata) { this.client.warn('Request succeeded, but missing plugindata...'); callbacks.success(); return; } this.client.log('Synchronous transaction successful (' + plugindata.plugin + ')'); const data = plugindata.data; this.client.debug(data); callbacks.success(data); return; } else if (json.janus !== 'ack') { // Not a success and not an ack, must be an error if (json.error) { this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason); callbacks.error(json.error.code + ' ' + json.error.reason); } else { this.client.error('Unknown error'); callbacks.error('Unknown error'); } return; } // If we got here, the plugin decided to handle the request asynchronously callbacks.success(); }; this.ws.send(JSON.stringify(request)); } } sendTrickleCandidate(handleId, candidate) { if (!this.connected) { this.client.warn('Is the gateway down? (connected=false)'); return; } var request = { janus: 'trickle', candidate, transaction: WebrtcSession.randomString(transationLength) }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } this.client.debug('Sending trickle candidate (handle=' + handleId + '):'); this.client.debug(request); if (this.websockets) { request.session_id = this.sessionId; request.handle_id = handleId; this.ws.send(JSON.stringify(request)); } } sendData(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; const text = callbacks.text; if (!text) { this.client.warn('Invalid text'); callbacks.error('Invalid text'); return; } this.client.log('Sending string on data channel: ' + text); config.dataChannel.send(text); callbacks.success(); } sendDtmf(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; if (!config.dtmfSender) { // Create the DTMF sender, if possible if (config.myStream) { const tracks = config.myStream.getAudioTracks(); if (tracks && tracks.length > 0) { const localAudioTrack = tracks[0]; config.dtmfSender = config.pc.createDTMFSender(localAudioTrack); this.client.log('Created DTMF Sender'); config.dtmfSender.ontonechange = (tone) => { this.client.debug('Sent DTMF tone: ' + tone.tone); }; } } if (!config.dtmfSender) { this.client.warn('Invalid DTMF configuration'); callbacks.error('Invalid DTMF configuration'); return; } } const dtmf = callbacks.dtmf; if (!dtmf) { this.client.warn('Invalid DTMF parameters'); callbacks.error('Invalid DTMF parameters'); return; } const tones = dtmf.tones; if (!tones) { this.client.warn('Invalid DTMF string'); callbacks.error('Invalid DTMF string'); return; } let duration = dtmf.duration; if (!duration) { duration = 500; // We choose 500ms as the default duration for a tone } let gap = dtmf.gap; if (!gap) { gap = 50; // We choose 50ms as the default gap between tones } this.client.debug('Sending DTMF string ' + tones + ' (duration ' + duration + 'ms, gap ' + gap + 'ms'); config.dtmfSender.insertDTMF(tones, duration, gap); } destroy(sync) { const syncRequest = (sync === true); this.client.log('Destroying session ' + this.sessionId); if (!this.connected) { this.client.warn('Is the gateway down? (connected=false)'); return; } if (!this.sessionId) { this.client.warn('No session to destroy'); this.gatewayCallbacks.destroyed(); return; } Reflect.deleteProperty(this.client.sessions, this.sessionId); // Destroy all handles first for (const ph in this.pluginHandles) { if (this.pluginHandles.hasOwnProperty(ph)) { const phv = this.pluginHandles[ph]; this.client.log('Destroying handle ' + phv.id + ' (' + phv.plugin + ')'); this.destroyHandle(phv.id, null, syncRequest); } } // Ok, go on var request = {janus: 'destroy', transaction: WebrtcSession.randomString(transationLength)}; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } if (this.websockets) { request.session_id = this.sessionId; let onUnbindMessage = null; let onUnbindError = null; onUnbindMessage = (event) => { var data = JSON.parse(event.data); if (data.session_id === request.session_id && data.transaction === request.transaction) { this.unbindWebSocket(onUnbindMessage, onUnbindError); this.gatewayCallbacks.destroyed(); } }; onUnbindError = () => { this.unbindWebSocket(onUnbindMessage, onUnbindError); this.gatewayCallbacks.destroyed(); }; this.ws.addEventListener('message', onUnbindMessage); this.ws.addEventListener('error', onUnbindError); this.ws.send(JSON.stringify(request)); } } disconnect() { this.connected = false; this.ws.close(); } destroyHandle(handleId, cbs, sync) { const syncRequest = (sync === true); this.client.log('Destroying handle ' + handleId + ' (sync=' + syncRequest + ')'); const callbacks = cbs || {}; callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : this.client.noop; callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : this.client.noop; this.cleanupWebrtc(handleId); if (!this.connected) { this.client.warn('Is the gateway down? (connected=false)'); return; } const request = { janus: 'detach', transaction: WebrtcSession.randomString(transationLength) }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } if (this.websockets) { request.session_id = this.sessionId; request.handle_id = handleId; this.ws.send(JSON.stringify(request)); Reflect.deleteProperty(this.pluginHandles, handleId); callbacks.success(); } } streamsDone(handleId, jsep, media, callbacks, stream) { const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; this.client.debug('streamsDone:', stream); config.myStream = stream; const pcConfig = {iceServers: this.iceServers}; const pcConstraints = { optional: [{DtlsSrtpKeyAgreement: true}] }; this.client.log('Creating PeerConnection'); this.client.debug(pcConstraints); config.pc = new window.RTCPeerConnection(pcConfig, pcConstraints); this.client.debug(config.pc); if (config.pc.getStats) { // FIXME config.volume.value = 0; config.bitrate.value = '0 kbits/sec'; } this.client.log('Preparing local SDP and gathering candidates (trickle=' + config.trickle + ')'); config.pc.onicecandidate = (event) => { if (!event.candidate || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { this.client.log('End of candidates.'); config.iceDone = true; if (config.trickle === true) { // Notify end of candidates this.sendTrickleCandidate(handleId, {completed: true}); } else { // No trickle, time to send the complete SDP (including all candidates) this.sendSDP(handleId, callbacks); } } else { // JSON.stringify doesn't work on some WebRTC objects anymore // See https://code.google.com/p/chromium/issues/detail?id=467366 const candidate = { candidate: event.candidate.candidate, sdpMid: event.candidate.sdpMid, sdpMLineIndex: event.candidate.sdpMLineIndex }; if (config.trickle === true) { // Send candidate this.sendTrickleCandidate(handleId, candidate); } } }; if (stream) { this.client.log('Adding local stream'); config.pc.addStream(stream); pluginHandle.onlocalstream(stream); } config.pc.onaddstream = (remoteStream) => { this.client.log('Handling Remote Stream'); this.client.debug(remoteStream); config.remoteStream = remoteStream; pluginHandle.onremotestream(remoteStream.stream); }; // Any data channel to create? if (this.isDataEnabled(media)) { this.client.log('Creating data channel'); const onDataChannelMessage = (event) => { this.client.log('Received message on data channel: ' + event.data); pluginHandle.ondata(event.data); // FIXME }; const onDataChannelStateChange = () => { const dcState = config.dataChannel ? config.dataChannel.readyState : 'null'; this.client.log('State change on data channel: ' + dcState); if (dcState === 'open') { pluginHandle.ondataopen(); // FIXME } }; const onDataChannelError = (error) => { this.client.error('Got error on data channel:', error); // TODO }; // Until we implement the proxying of open requests within the this.client core, we open a channel ourselves whatever the case config.dataChannel = config.pc.createDataChannel('this.clientDataChannel', {ordered: false}); // FIXME Add options (ordered, maxRetransmits, etc.) config.dataChannel.onmessage = onDataChannelMessage; config.dataChannel.onopen = onDataChannelStateChange; config.dataChannel.onclose = onDataChannelStateChange; config.dataChannel.onerror = onDataChannelError; } // Create offer/answer now DO I WANT THIS?? if (jsep) { config.pc.setRemoteDescription( new RTCSessionDescription(jsep), () => { this.client.log('Remote description accepted!'); this.createAnswer(handleId, media, callbacks); }, callbacks.error); } else { this.createOffer(handleId, media, callbacks); } } prepareWebrtc(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.webrtcError; const jsep = callbacks.jsep; const media = callbacks.media; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; // Are we updating a session? if (config.pc) { this.client.log('Updating existing media session'); // Create offer/answer now if (jsep) { config.pc.setRemoteDescription( new window.RTCSessionDescription(jsep), () => { this.client.log('Remote description accepted!'); this.createAnswer(handleId, media, callbacks); }, callbacks.error); } else { this.createOffer(handleId, media, callbacks); } return; } // Was a MediaStream object passed, or do we need to take care of that? if (callbacks.stream) { const stream = callbacks.stream; this.client.log('MediaStream provided by the application'); this.client.debug(stream); // Skip the getUserMedia part config.streamExternal = true; this.streamsDone(handleId, jsep, media, callbacks, stream); return; } config.trickle = this.isTrickleEnabled(callbacks.trickle); if (this.isAudioSendEnabled(media) || this.isVideoSendEnabled(media)) { let constraints = {mandatory: {}, optional: []}; pluginHandle.consentDialog(true); let audioSupport = this.isAudioSendEnabled(media); if (audioSupport === true && media) { if (typeof media.audio === 'object') { audioSupport = media.audio; } } let videoSupport = this.isVideoSendEnabled(media); if (videoSupport === true && media) { if (media.video && media.video !== 'screen' && media.video !== 'window') { let width = 0; let height = 0; let maxHeight = 0; if (media.video === 'lowres') { // Small resolution, 4:3 height = 240; maxHeight = 240; width = 320; } else if (media.video === 'lowres-16:9') { // Small resolution, 16:9 height = 180; maxHeight = 180; width = 320; } else if (media.video === 'hires' || media.video === 'hires-16:9') { // High resolution is only 16:9 height = 720; maxHeight = 720; width = 1280; if (navigator.mozGetUserMedia) { const firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); if (firefoxVer < 38) { // Unless this is and old Firefox, which doesn't support it this.client.warn(media.video + ' unsupported, falling back to stdres (old Firefox)'); height = 480; maxHeight = 480; width = 640; } } } else if (media.video === 'stdres') { // Normal resolution, 4:3 height = 480; maxHeight = 480; width = 640; } else if (media.video === 'stdres-16:9') { // Normal resolution, 16:9 height = 360; maxHeight = 360; width = 640; } else { this.client.log('Default video setting (' + media.video + ') is stdres 4:3'); height = 480; maxHeight = 480; width = 640; } this.client.log('Adding media constraint ' + media.video); if (navigator.mozGetUserMedia) { const firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); if (firefoxVer < 38) { videoSupport = { require: ['height', 'width'], height: {max: maxHeight, min: height}, width: {max: width, min: width} }; } else { // http://stackoverflow.com/questions/28282385/webrtc-firefox-constraints/28911694#28911694 // https://github.com/meetecho/janus-gateway/pull/246 videoSupport = { height: {ideal: height}, width: {ideal: width} }; } } else { videoSupport = { mandatory: { maxHeight, minHeight: height, maxWidth: width, minWidth: width }, optional: [] }; } if (typeof media.video === 'object') { videoSupport = media.video; } this.client.debug(videoSupport); } else if (media.video === 'screen' || media.video === 'window') { // Not a webcam, but screen capture if (window.location.protocol !== 'https:') { // Screen sharing mandates HTTPS this.client.warn('Screen sharing only works on HTTPS, try the https:// version of this page'); pluginHandle.consentDialog(false); callbacks.error('Screen sharing only works on HTTPS, try the https:// version of this page'); return; } // We're going to try and use the extension for Chrome 34+, the old approach // for older versions of Chrome, or the experimental support in Firefox 33+ const cache = {}; const self = this; function callbackUserMedia(error, stream) { pluginHandle.consentDialog(false); if (error) { callbacks.error({code: error.code, name: error.name, message: error.message}); } else { self.streamsDone(handleId, jsep, media, callbacks, stream); } } function getScreenMedia(constraint, gsmCallback) { this.client.log('Adding media constraint (screen capture)'); this.client.debug(constraint); navigator.mediaDevices.getUserMedia(constraint). then((stream) => { gsmCallback(null, stream); }). catch((error) => { pluginHandle.consentDialog(false); gsmCallback(error); }); } if (window.navigator.userAgent.match('Chrome')) { const chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); let maxver = 33; if (window.navigator.userAgent.match('Linux')) { maxver = 35; // 'known' crash in chrome 34 and 35 on linux } if (chromever >= 26 && chromever <= maxver) { // Chrome 26->33 requires some awkward chrome://flags manipulation constraints = { video: { mandatory: { googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3, chromeMediaSource: 'screen' } }, audio: this.isAudioSendEnabled(media) }; getScreenMedia(constraints, callbackUserMedia); } else { // Chrome 34+ requires an extension var pending = window.setTimeout( () => { const error = new Error('NavigatorUserMediaError'); error.name = 'The required Chrome extension is not installed: click here to install it. (NOTE: this will need you to refresh the page)'; pluginHandle.consentDialog(false); return callbacks.error(error); }, 1000); cache[pending] = [callbackUserMedia, null]; window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); } } else if (window.navigator.userAgent.match('Firefox')) { const ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); if (ffver >= 33) { // Firefox 33+ has experimental support for screen sharing constraints = { video: { mozMediaSource: media.video, mediaSource: media.video }, audio: this.isAudioSendEnabled(media) }; getScreenMedia(constraints, (err, stream) => { callbackUserMedia(err, stream); // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 if (!err) { let lastTime = stream.currentTime; const polly = window.setInterval(() => { if (!stream) { window.clearInterval(polly); } if (stream.currentTime === lastTime) { window.clearInterval(polly); if (stream.onended) { stream.onended(); } } lastTime = stream.currentTime; }, 500); } }); } else { const error = new Error('NavigatorUserMediaError'); error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'; pluginHandle.consentDialog(false); callbacks.error(error); return; } } // Wait for events from the Chrome Extension window.addEventListener('message', (event) => { if (event.origin !== window.location.origin) { return; } if (event.data.type === 'mattermostGotScreen' && cache[event.data.id]) { const data = cache[event.data.id]; const callback = data[0]; Reflect.deleteProperty(cache, event.data.id); if (event.data.sourceId === '') { // user canceled const error = new Error('NavigatorUserMediaError'); error.name = 'You cancelled the request for permission, giving up...'; pluginHandle.consentDialog(false); callbacks.error(error); } else { constraints = { audio: this.isAudioSendEnabled(media), video: { mandatory: { chromeMediaSource: 'desktop', maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 }, optional: [ {googLeakyBucket: true}, {googTemporalLayeredScreencast: true} ] } }; constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId; getScreenMedia(constraints, callback); } } else if (event.data.type === 'mattermostGetScreenPending') { window.clearTimeout(event.data.id); } }); return; } } // If we got here, we're not screensharing if (!media || media.video !== 'screen') { // Check whether all media sources are actually available or not navigator.mediaDevices.enumerateDevices().then((devices) => { const audioExist = devices.some((device) => { return device.kind === 'audioinput'; }); const videoExist = devices.some((device) => { return device.kind === 'videoinput'; }); // Check whether a missing device is really a problem const audioSend = this.isAudioSendEnabled(media); const videoSend = this.isVideoSendEnabled(media); if (audioSend || videoSend) { // We need to send either audio or video const haveAudioDevice = audioSend ? audioExist : false; const haveVideoDevice = videoSend ? videoExist : false; if (!haveAudioDevice && !haveVideoDevice) { // FIXME Should we really give up, or just assume recvonly for both? pluginHandle.consentDialog(false); callbacks.error('No capture device found'); return false; } } navigator.mediaDevices.getUserMedia({ audio: audioExist ? audioSupport : false, video: videoExist ? videoSupport : false }). then((stream) => { pluginHandle.consentDialog(false); this.streamsDone(handleId, jsep, media, callbacks, stream); }). catch((error) => { pluginHandle.consentDialog(false); callbacks.error({ code: error.code, name: error.name, message: error.message }); }); return true; }). catch((error) => { pluginHandle.consentDialog(false); callbacks.error('enumerateDevices error', error); }); } } else { // No need to do a getUserMedia, create offer/answer right away this.streamsDone(handleId, jsep, media, callbacks); } } prepareWebrtcPeer(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.webrtcError; const jsep = callbacks.jsep; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; if (jsep) { if (config.pc === null) { this.client.warn('Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep'); callbacks.error('No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep'); return; } config.pc.setRemoteDescription( new window.RTCSessionDescription(jsep), () => { this.client.log('Remote description accepted!'); callbacks.success(); }, callbacks.error); } else { callbacks.error('Invalid JSEP'); } } createOffer(handleId, media, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; this.client.log('Creating offer (iceDone=' + config.iceDone + ')'); // https://code.google.com/p/webrtc/issues/detail?id=3508 let mediaConstraints = null; const browser = adapter.browserDetails.browser; if (browser === 'firefox' || browser === 'edge') { mediaConstraints = { offerToReceiveAudio: this.isAudioRecvEnabled(media), offerToReceiveVideo: this.isVideoRecvEnabled(media) }; } else { mediaConstraints = { mandatory: { OfferToReceiveAudio: this.isAudioRecvEnabled(media), OfferToReceiveVideo: this.isVideoRecvEnabled(media) } }; } this.client.debug(mediaConstraints); config.pc.createOffer( (offer) => { this.client.debug(offer); if (!config.mySdp) { this.client.log('Setting local description'); config.mySdp = offer.sdp; config.pc.setLocalDescription(offer); } if (!config.iceDone && !config.trickle) { // Don't do anything until we have all candidates this.client.log('Waiting for all candidates...'); return; } if (config.sdpSent) { this.client.log('Offer already sent, not sending it again'); return; } this.client.log('Offer ready'); this.client.debug(callbacks); config.sdpSent = true; // JSON.stringify doesn't work on some WebRTC objects anymore // See https://code.google.com/p/chromium/issues/detail?id=467366 const jsep = { type: offer.type, sdp: offer.sdp }; callbacks.success(jsep); }, callbacks.error, mediaConstraints); } createAnswer(handleId, media, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); callbacks.error('Invalid handle'); return; } const config = pluginHandle.webrtcStuff; this.client.log('Creating answer (iceDone=' + config.iceDone + ')'); let mediaConstraints = null; const browser = adapter.browserDetails.browser; if (browser === 'firefox' || browser === 'edge') { mediaConstraints = { offerToReceiveAudio: this.isAudioRecvEnabled(media), offerToReceiveVideo: this.isVideoRecvEnabled(media) }; } else { mediaConstraints = { mandatory: { OfferToReceiveAudio: this.isAudioRecvEnabled(media), OfferToReceiveVideo: this.isVideoRecvEnabled(media) } }; } this.client.debug(mediaConstraints); config.pc.createAnswer( (answer) => { this.client.debug(answer); if (!config.mySdp) { this.client.log('Setting local description'); config.mySdp = answer.sdp; config.pc.setLocalDescription(answer); } if (!config.iceDone && !config.trickle) { // Don't do anything until we have all candidates this.client.log('Waiting for all candidates...'); return; } if (config.sdpSent) { // FIXME badly this.client.log('Answer already sent, not sending it again'); return; } config.sdpSent = true; // JSON.stringify doesn't work on some WebRTC objects anymore // See https://code.google.com/p/chromium/issues/detail?id=467366 const jsep = { type: answer.type, sdp: answer.sdp }; callbacks.success(jsep); }, callbacks.error, mediaConstraints); } sendSDP(handleId, cbs) { const callbacks = cbs || {}; callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop; callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop; const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle, not sending anything'); return; } const config = pluginHandle.webrtcStuff; this.client.log('Sending offer/answer SDP...'); if (!config.mySdp) { this.client.warn('Local SDP instance is invalid, not sending anything...'); return; } config.mySdp = { type: config.pc.localDescription.type, sdp: config.pc.localDescription.sdp }; if (config.sdpSent) { this.client.log('Offer/Answer SDP already sent, not sending it again'); return; } if (config.trickle === false) { config.mySdp.trickle = false; } this.client.debug(callbacks); config.sdpSent = true; callbacks.success(config.mySdp); } getVolume(handleId) { const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); return 0; } const config = pluginHandle.webrtcStuff; const browser = adapter.browserDetails.browser; // Start getting the volume, if getStats is supported if (config.pc.getStats && browser === 'chrome') { // FIXME if (!config.remoteStream) { this.client.warn('Remote stream unavailable'); return 0; } // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html if (!config.volume.timer) { this.client.log('Starting volume monitor'); config.volume.timer = setInterval(() => { config.pc.getStats((stats) => { const results = stats.result(); for (let i = 0; i < results.length; i++) { const res = results[i]; if (res.type === 'ssrc' && res.stat('audioOutputLevel')) { config.volume.value = res.stat('audioOutputLevel'); } } }); }, 200); return 0; // We don't have a volume to return yet } return config.volume.value; } this.client.log('Getting the remote volume unsupported by browser'); return 0; } isMuted(handleId, video) { const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); return true; } const config = pluginHandle.webrtcStuff; if (!config.pc) { this.client.warn('Invalid PeerConnection'); return true; } if (!config.myStream) { this.client.warn('Invalid local MediaStream'); return true; } if (video) { // Check video track if (!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { this.client.warn('No video track'); return true; } return !config.myStream.getVideoTracks()[0].enabled; } // Check audio track if (!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { this.client.warn('No audio track'); return true; } return !config.myStream.getAudioTracks()[0].enabled; } mute(handleId, video, mute) { const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); return false; } const config = pluginHandle.webrtcStuff; if (!config.pc) { this.client.warn('Invalid PeerConnection'); return false; } if (!config.myStream) { this.client.warn('Invalid local MediaStream'); return false; } if (video) { // Mute/unmute video track if (!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { this.client.warn('No video track'); return false; } config.myStream.getVideoTracks()[0].enabled = mute; return true; } // Mute/unmute audio track if (!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { this.client.warn('No audio track'); return false; } config.myStream.getAudioTracks()[0].enabled = mute; return true; } getBitrate(handleId) { const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle || !pluginHandle.webrtcStuff) { this.client.warn('Invalid handle'); return 'Invalid handle'; } const config = pluginHandle.webrtcStuff; if (!config.pc) { return 'Invalid PeerConnection'; } // Start getting the bitrate, if getStats is supported const browser = adapter.browserDetails.browser; if (config.pc.getStats && browser === 'chrome') { // Do it the Chrome way if (!config.remoteStream) { this.client.warn('Remote stream unavailable'); return 'Remote stream unavailable'; } // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html if (!config.bitrate.timer) { this.client.log('Starting bitrate timer (Chrome)'); config.bitrate.timer = setInterval(() => { config.pc.getStats((stats) => { const results = stats.result(); for (let i = 0; i < results.length; i++) { const res = results[i]; if (res.type === 'ssrc' && res.stat('googFrameHeightReceived')) { config.bitrate.bsnow = res.stat('bytesReceived'); config.bitrate.tsnow = res.timestamp; if (config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) { // Skip this round config.bitrate.bsbefore = config.bitrate.bsnow; config.bitrate.tsbefore = config.bitrate.tsnow; } else { // Calculate bitrate var bitRate = Math.round(((config.bitrate.bsnow - config.bitrate.bsbefore) * 8) / (config.bitrate.tsnow - config.bitrate.tsbefore)); config.bitrate.value = bitRate + ' kbits/sec'; //~ this.client.log('Estimated bitrate is ' + config.bitrate.value); config.bitrate.bsbefore = config.bitrate.bsnow; config.bitrate.tsbefore = config.bitrate.tsnow; } } } }); }, 1000); return '0 kbits/sec'; // We don't have a bitrate value yet } return config.bitrate.value; } else if (config.pc.getStats && browser === 'firefox') { // Do it the Firefox way if (!config.remoteStream || !config.remoteStream.stream) { this.client.warn('Remote stream unavailable'); return 'Remote stream unavailable'; } const videoTracks = config.remoteStream.stream.getVideoTracks(); if (!videoTracks || videoTracks.length < 1) { this.client.warn('No video track'); return 'No video track'; } // https://github.com/muaz-khan/getStats/blob/master/getStats.js if (!config.bitrate.timer) { this.client.log('Starting bitrate timer (Firefox)'); config.bitrate.timer = setInterval(() => { // We need a helper callback function cb(res) { if (!res || res.inbound_rtp_video_1 == null || res.inbound_rtp_video_1 == null) { config.bitrate.value = 'Missing inbound_rtp_video_1'; return; } config.bitrate.bsnow = res.inbound_rtp_video_1.bytesReceived; config.bitrate.tsnow = res.inbound_rtp_video_1.timestamp; if (config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) { // Skip this round config.bitrate.bsbefore = config.bitrate.bsnow; config.bitrate.tsbefore = config.bitrate.tsnow; } else { // Calculate bitrate var bitRate = Math.round(((config.bitrate.bsnow - config.bitrate.bsbefore) * 8) / (config.bitrate.tsnow - config.bitrate.tsbefore)); config.bitrate.value = bitRate + ' kbits/sec'; config.bitrate.bsbefore = config.bitrate.bsnow; config.bitrate.tsbefore = config.bitrate.tsnow; } } // Actually get the stats config.pc.getStats(videoTracks[0], (stats) => { cb(stats); }, cb); }, 1000); return '0 kbits/sec'; // We don't have a bitrate value yet } return config.bitrate.value; } this.client.warn('Getting the video bitrate unsupported by browser'); return 'Feature unsupported by browser'; } webrtcError(error) { this.client.error('WebRTC error:', error); } cleanupWebrtc(handleId, hangupRequest) { this.client.log('Cleaning WebRTC stuff'); const pluginHandle = this.pluginHandles[handleId]; if (!pluginHandle) { // Nothing to clean return; } const config = pluginHandle.webrtcStuff; if (config) { if (hangupRequest === true) { // Send a hangup request (we don't really care about the response) const request = { janus: 'hangup', transaction: WebrtcSession.randomString(transationLength) }; if (this.token) { request.token = this.token; } if (this.apisecret) { request.apisecret = this.apisecret; } this.client.debug('Sending hangup request (handle=' + handleId + '):'); this.client.debug(request); if (this.websockets) { request.session_id = this.sessionId; request.handle_id = handleId; this.ws.send(JSON.stringify(request)); } } // Cleanup stack config.remoteStream = null; if (config.volume.timer) { clearInterval(config.volume.timer); } config.volume.value = null; if (config.bitrate.timer) { clearInterval(config.bitrate.timer); } config.bitrate.timer = null; config.bitrate.bsnow = null; config.bitrate.bsbefore = null; config.bitrate.tsnow = null; config.bitrate.tsbefore = null; config.bitrate.value = null; try { // Try a MediaStream.stop() first if (!config.streamExternal && config.myStream) { this.client.log('Stopping local stream'); config.myStream.stop(); } } catch (e) { // Do nothing if this fails } try { // Try a MediaStreamTrack.stop() for each track as well if (!config.streamExternal && config.myStream) { this.client.log('Stopping local stream tracks'); WebrtcSession.stopMediaStream(config.myStream); } } catch (e) { // Do nothing if this fails } config.streamExternal = false; config.myStream = null; // Close PeerConnection try { config.pc.close(); } catch (e) { // Do nothing } config.pc = null; config.mySdp = null; config.iceDone = false; config.sdpSent = false; config.dataChannel = null; config.dtmfSender = null; } pluginHandle.oncleanup(); } isAudioSendEnabled(media) { this.client.debug('isAudioSendEnabled:', media); if (!media) { return true; // Default } if (media.audio === false) { return false; // Generic audio has precedence } if (!media.audioSend) { return true; // Default } return (media.audioSend === true); } isAudioRecvEnabled(media) { this.client.debug('isAudioRecvEnabled:', media); if (!media) { return true; // Default } if (media.audio === false) { return false; // Generic audio has precedence } if (!media.audioRecv) { return true; // Default } return (media.audioRecv === true); } isVideoSendEnabled(media) { this.client.debug('isVideoSendEnabled:', media); const browser = adapter.browserDetails.browser; if (browser === 'edge') { this.client.warn("Edge doesn't support compatible video yet"); return false; } if (!media) { return true; // Default } if (media.video === false) { return false; // Generic video has precedence } if (!media.videoSend) { return true; // Default } return (media.videoSend === true); } isVideoRecvEnabled(media) { this.client.debug('isVideoRecvEnabled:', media); const browser = adapter.browserDetails.browser; if (browser === 'edge') { this.client.warn("Edge doesn't support compatible video yet"); return false; } if (!media) { return true; // Default } if (media.video === false) { return false; // Generic video has precedence } if (!media.videoRecv) { return true; // Default } return (media.videoRecv === true); } isDataEnabled(media) { this.client.debug('isDataEnabled:', media); const browser = adapter.browserDetails.browser; if (browser === 'edge') { this.client.warn("Edge doesn't support data channels yet"); return false; } if (!media) { return false; // Default } return (media.data === true); } isTrickleEnabled(trickle) { this.client.debug('isTrickleEnabled:', trickle); if (!trickle) { return true; // Default is true } return (trickle === true); } unbindWebSocket = (onUnbindMessage, onUnbindError) => { for (var eventName in this.wsHandlers) { if (this.wsHandlers.hasOwnProperty(eventName)) { this.ws.removeEventListener(eventName, this.wsHandlers[eventName]); } } this.ws.removeEventListener('message', onUnbindMessage); this.ws.removeEventListener('error', onUnbindError); if (this.wsKeepaliveTimeoutId) { clearTimeout(this.wsKeepaliveTimeoutId); } }; }