UNPKG

nativescript-connectycube

Version:
1,371 lines (1,318 loc) 127 kB
const { adapter, navigator, MediaStream, MediaStreamTrack, RTCPeerConnection, RTCRtpReceiver, RTCRtpSender, } = require('../cubeDependencies'); const Utils = require('../cubeInternalUtils'); ('use strict'); /* The MIT License (MIT) Copyright (c) 2016 Meetecho Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // List of sessions Janus.sessions = {}; Janus.mobile = Utils.getEnv().reactnative; Janus.isExtensionEnabled = function () { if (Janus.mobile) { return false; } if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { // No need for the extension, getDisplayMedia is supported return true; } if (window.navigator.userAgent.match('Chrome')) { let 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) { // Older versions of Chrome don't support this extension-based approach, so lie return true; } return Janus.extension.isInstalled(); } else { // Firefox and others, no need for the extension (but this doesn't mean it will work) return true; } }; const defaultExtension = { // Screensharing Chrome Extension ID extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj', isInstalled: function () { if (Janus.mobile) { return false; } else { return document.querySelector('#janus-extension-installed') !== null; } }, getScreen: function (callback) { if (Janus.mobile) { callback(); } else { let pending = window.setTimeout(function () { let error = new Error('NavigatorUserMediaError'); error.name = 'The required Chrome extension is not installed: click <a href="#">here</a> to install it. (NOTE: this will need you to refresh the page)'; return callback(error); }, 1000); this.cache[pending] = callback; window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); } }, init: function () { let cache = {}; this.cache = cache; // Wait for events from the Chrome Extension if (!Janus.mobile) { window.addEventListener('message', function (event) { if (event.origin != window.location.origin) return; if (event.data.type == 'janusGotScreen' && cache[event.data.id]) { let callback = cache[event.data.id]; delete cache[event.data.id]; if (event.data.sourceId === '') { // user canceled let error = new Error('NavigatorUserMediaError'); error.name = 'You cancelled the request for permission, giving up...'; callback(error); } else { callback(null, event.data.sourceId); } } else if (event.data.type == 'janusGetScreenPending') { window.clearTimeout(event.data.id); } }); } }, }; Janus.useDefaultDependencies = function (deps) { let f = (deps && deps.fetch) || fetch; let p = (deps && deps.Promise) || Promise; let socketCls = (deps && deps.WebSocket) || WebSocket; return { newWebSocket: function (server, proto) { return new socketCls(server, proto); }, extension: (deps && deps.extension) || defaultExtension, isArray: function (arr) { return Array.isArray(arr); }, webRTCAdapter: (deps && deps.adapter) || adapter, httpAPICall: function (url, options) { let fetchOptions = { method: options.verb, headers: { Accept: 'application/json, text/plain, */*', }, cache: 'no-cache', }; if (options.verb === 'POST') { fetchOptions.headers['Content-Type'] = 'application/json'; } if (typeof options.withCredentials !== 'undefined') { fetchOptions.credentials = options.withCredentials === true ? 'include' : options.withCredentials ? options.withCredentials : 'omit'; } if (options.body) { fetchOptions.body = JSON.stringify(options.body); } let fetching = f(url, fetchOptions).catch(function (error) { return p.reject({ message: 'Probably a network error, is the server down?', error: error, }); }); /* * fetch() does not natively support timeouts. * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first. */ if (options.timeout) { let timeout = new p(function (resolve, reject) { let timerId = setTimeout(function () { clearTimeout(timerId); return reject({ message: 'Request timed out', timeout: options.timeout, }); }, options.timeout); }); fetching = p.race([fetching, timeout]); } fetching .then(function (response) { if (response.ok) { if (typeof options.success === typeof Janus.noop) { return response.json().then( function (parsed) { try { options.success(parsed); } catch (error) { Janus.error('Unhandled httpAPICall success callback error', error); } }, function (error) { return p.reject({ message: 'Failed to parse response body', error: error, response: response, }); } ); } } else { return p.reject({ message: 'API call failed', response: response }); } }) .catch(function (error) { if (typeof options.error === typeof Janus.noop) { options.error(error.message || '<< internal error >>', error); } }); return fetching; }, }; }; Janus.useOldDependencies = function (deps) { let jq = (deps && deps.jQuery) || jQuery; let socketCls = (deps && deps.WebSocket) || WebSocket; return { newWebSocket: function (server, proto) { return new socketCls(server, proto); }, isArray: function (arr) { return jq.isArray(arr); }, extension: (deps && deps.extension) || defaultExtension, webRTCAdapter: (deps && deps.adapter) || adapter, httpAPICall: function (url, options) { let payload = typeof options.body !== 'undefined' ? { contentType: 'application/json', data: JSON.stringify(options.body), } : {}; let credentials = typeof options.withCredentials !== 'undefined' ? { xhrFields: { withCredentials: options.withCredentials } } : {}; return jq.ajax( jq.extend(payload, credentials, { url: url, type: options.verb, cache: false, dataType: 'json', async: options.async, timeout: options.timeout, success: function (result) { if (typeof options.success === typeof Janus.noop) { options.success(result); } }, error: function (xhr, status, err) { if (typeof options.error === typeof Janus.noop) { options.error(status, err); } }, }) ); }, }; }; // Helper function to convert a deprecated media object to a tracks array Janus.mediaToTracks = function (media) { let tracks = []; if (!media) { // Default is bidirectional audio and video, using default devices tracks.push({ type: 'audio', capture: true, recv: true }); tracks.push({ type: 'video', capture: true, recv: true }); } else { if ( !media.keepAudio && media.audio !== false && (typeof media.audio === 'undefined' || media.audio || media.audioSend || media.audioRecv || media.addAudio || media.replaceAudio || media.removeAudio) ) { // We may need an audio track let track = { type: 'audio' }; if (media.removeAudio) { track.remove = true; } else { if (media.addAudio) track.add = true; else if (media.replaceAudio) track.replace = true; // Check if we need to capture an audio device if (media.audioSend !== false) track.capture = media.audio || true; // Check if we need to receive audio if (media.audioRecv !== false) track.recv = true; } // Add an audio track if needed if (track.remove || track.capture || track.recv) tracks.push(track); } if ( !media.keepVideo && media.video !== false && (typeof media.video === 'undefined' || media.video || media.videoSend || media.videoRecv || media.addVideo || media.replaceVideo || media.removeVideo) ) { // We may need a video track let track = { type: 'video' }; if (media.removeVideo) { track.remove = true; } else { if (media.addVideo) track.add = true; else if (media.replaceVideo) track.replace = true; // Check if we need to capture a video device if (media.videoSend !== false) { track.capture = media.video || true; if (['screen', 'window', 'desktop'].includes(track.capture)) { // Change the type to 'screen' track.type = 'screen'; track.capture = { video: {} }; // Check if there's constraints if (media.screenshareFrameRate) track.capture.frameRate = media.screenshareFrameRate; if (media.screenshareHeight) track.capture.height = media.screenshareHeight; if (media.screenshareWidth) track.capture.width = media.screenshareWidth; } } // Check if we need to receive video if (media.videoRecv !== false) track.recv = true; } // Add a video track if needed if (track.remove || track.capture || track.recv) tracks.push(track); } if (media.data) { // We need a data channel tracks.push({ type: 'data' }); } } // Done return tracks; }; // Helper function to convert a track object to a set of constraints Janus.trackConstraints = function (track) { let constraints = {}; if (!track || !track.capture) return constraints; if (track.type === 'audio') { // Just put the capture part in the constraints constraints.audio = track.capture; } else if (track.type === 'video') { // Check if one of the keywords was passed if ((track.simulcast || track.svc) && track.capture === true) track.capture = 'hires'; if (track.capture === true || typeof track.capture === 'object') { // Use the provided capture object as video constraint constraints.video = track.capture; } else { let width = 0; let height = 0; if (track.capture === 'lowres') { // Small resolution, 4:3 width = 320; height = 240; } else if (track.capture === 'lowres-16:9') { // Small resolution, 16:9 width = 320; height = 180; } else if (track.capture === 'hires' || track.capture === 'hires-16:9' || track.capture === 'hdres') { // High(HD) resolution is only 16:9 width = 1280; height = 720; } else if (track.capture === 'fhdres') { // Full HD resolution is only 16:9 width = 1920; height = 1080; } else if (track.capture === '4kres') { // 4K resolution is only 16:9 width = 3840; height = 2160; } else if (track.capture === 'stdres') { // Normal resolution, 4:3 width = 640; height = 480; } else if (track.capture === 'stdres-16:9') { // Normal resolution, 16:9 width = 640; height = 360; } else { Janus.log('Default video setting is stdres 4:3'); width = 640; height = 480; } constraints.video = { width: { ideal: width }, height: { ideal: height }, }; } } else if (track.type === 'screen') { // Use the provided capture object as video constraint constraints.video = track.capture; } return constraints; }; Janus.noop = function () { }; Janus.dataChanDefaultLabel = 'JanusDataChannel'; // Note: in the future we may want to change this, e.g., as was // attempted in https://github.com/meetecho/janus-gateway/issues/1670 Janus.endOfCandidates = null; // Stop all tracks from a given stream Janus.stopAllTracks = function (stream) { try { // Try a MediaStreamTrack.stop() for each track let tracks = stream.getTracks(); for (let mst of tracks) { Janus.log(mst); if (mst && mst.dontStop !== true) { mst.stop(); } } } catch (e) { // Do nothing if this fails } }; // Initialization Janus.init = function (options) { options = options || {}; options.callback = typeof options.callback == 'function' ? options.callback : Janus.noop; if (Janus.initDone) { // Already initialized options.callback(); } else { if (typeof console.log == 'undefined') { console.log = function () { }; } // Console logging (all debugging disabled by default) Janus.trace = Janus.noop; Janus.debug = Janus.noop; Janus.vdebug = Janus.noop; Janus.log = Janus.noop; Janus.warn = Janus.noop; Janus.error = Janus.noop; if (options.debug === true || options.debug === 'all') { // Enable all debugging levels Janus.trace = console.trace.bind(console); Janus.debug = console.debug.bind(console); Janus.vdebug = console.debug.bind(console); Janus.log = console.log.bind(console); Janus.warn = console.warn.bind(console); Janus.error = console.error.bind(console); } else if (Array.isArray(options.debug)) { for (let d of options.debug) { switch (d) { case 'trace': Janus.trace = console.trace.bind(console); break; case 'debug': Janus.debug = console.debug.bind(console); break; case 'vdebug': Janus.vdebug = console.debug.bind(console); break; case 'log': Janus.log = console.log.bind(console); break; case 'warn': Janus.warn = console.warn.bind(console); break; case 'error': Janus.error = console.error.bind(console); break; default: console.error( "Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')" ); break; } } } Janus.log('Initializing library'); let usedDependencies = options.dependencies || Janus.useDefaultDependencies(); Janus.isArray = usedDependencies.isArray; Janus.webRTCAdapter = usedDependencies.webRTCAdapter; Janus.httpAPICall = usedDependencies.httpAPICall; Janus.newWebSocket = usedDependencies.newWebSocket; Janus.extension = usedDependencies.extension; Janus.extension.init(); // Helper method to enumerate devices Janus.listDevices = function (callback, config) { callback = typeof callback == 'function' ? callback : Janus.noop; if (!config) config = { audio: true, video: true }; if (Janus.isGetUserMediaAvailable()) { navigator.mediaDevices .getUserMedia(config) .then(function (stream) { navigator.mediaDevices.enumerateDevices().then(function (devices) { Janus.debug(devices); callback(devices); // Get rid of the now useless stream Janus.stopAllTracks(stream); }); }) .catch(function (err) { Janus.error(err); callback([]); }); } else { Janus.warn('navigator.mediaDevices unavailable'); callback([]); } }; // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js) // Janus.attachMediaStream = function(element, stream) { // try { // element.srcObject = stream; // } catch (e) { // try { // element.src = URL.createObjectURL(stream); // } catch (e) { // Janus.error("Error attaching stream to element", e); // } // } // }; // Janus.reattachMediaStream = function(to, from) { // try { // to.srcObject = from.srcObject; // } catch (e) { // try { // to.src = from.src; // } catch (e) { // Janus.error("Error reattaching stream to element", e); // } // } // }; // Detect tab close: make sure we don't loose existing onbeforeunload handlers // (note: for iOS we need to subscribe to a different event, 'pagehide', see // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe) // let iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; // let eventName = iOS ? 'pagehide' : 'beforeunload'; // let oldOBF = window["on" + eventName]; // window.addEventListener(eventName, function() { // Janus.log("Closing window"); // for(let s in Janus.sessions) { // if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) { // Janus.log("Destroying session " + s); // Janus.sessions[s].destroy({unload: true, notifyDestroyed: false}); // } // } // if(oldOBF && typeof oldOBF == "function") { // oldOBF(); // } // }); // If this is a Safari Technology Preview, check if VP8 is supported Janus.safariVp8 = false; if (Janus.webRTCAdapter.browserDetails.browser === 'safari' && Janus.webRTCAdapter.browserDetails.version >= 605) { // Let's see if RTCRtpSender.getCapabilities() is there if ( RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities('video') && RTCRtpSender.getCapabilities('video').codecs && RTCRtpSender.getCapabilities('video').codecs.length ) { for (let codec of RTCRtpSender.getCapabilities('video').codecs) { if (codec && codec.mimeType && codec.mimeType.toLowerCase() === 'video/vp8') { Janus.safariVp8 = true; break; } } if (Janus.safariVp8) { Janus.log('This version of Safari supports VP8'); } else { Janus.warn( "This version of Safari does NOT support VP8: if you're using a Technology Preview, " + "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu" ); } } else { // We do it in a very ugly way, as there's no alternative... // We create a PeerConnection to see if VP8 is in an offer let testpc = new RTCPeerConnection({}); testpc.createOffer({ offerToReceiveVideo: true }).then(function (offer) { Janus.safariVp8 = offer.sdp.indexOf('VP8') !== -1; if (Janus.safariVp8) { Janus.log('This version of Safari supports VP8'); } else { Janus.warn( "This version of Safari does NOT support VP8: if you're using a Technology Preview, " + "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu" ); } testpc.close(); testpc = null; }); } } Janus.initDone = true; options.callback(); } }; // Helper method to check whether WebRTC is supported by this browser Janus.isWebrtcSupported = function () { return !!RTCPeerConnection; }; // Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP) Janus.isGetUserMediaAvailable = function () { return navigator.mediaDevices && navigator.mediaDevices.getUserMedia; }; // Helper method to create random identifiers (e.g., transaction) Janus.randomString = function (len) { let charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let randomString = ''; for (let i = 0; i < len; i++) { let randomPoz = Math.floor(Math.random() * charSet.length); randomString += charSet.charAt(randomPoz); } return randomString; }; function Janus(gatewayCallbacks) { gatewayCallbacks = gatewayCallbacks || {}; gatewayCallbacks.success = typeof gatewayCallbacks.success == 'function' ? gatewayCallbacks.success : Janus.noop; gatewayCallbacks.error = typeof gatewayCallbacks.error == 'function' ? gatewayCallbacks.error : Janus.noop; gatewayCallbacks.destroyed = typeof gatewayCallbacks.destroyed == 'function' ? gatewayCallbacks.destroyed : Janus.noop; if (!Janus.initDone) { gatewayCallbacks.error('Library not initialized'); return {}; } if (!Janus.isWebrtcSupported()) { gatewayCallbacks.error('WebRTC not supported by this browser'); return {}; } Janus.log('Library initialized: ' + Janus.initDone); if (!gatewayCallbacks.server) { gatewayCallbacks.error('Invalid server url'); return {}; } let websockets = false; let ws = null; let wsHandlers = {}; let wsKeepaliveTimeoutId = null; let servers = null; let serversIndex = 0; let server = gatewayCallbacks.server; if (Janus.isArray(server)) { Janus.log('Multiple servers provided (' + server.length + '), will use the first that works'); server = null; servers = gatewayCallbacks.server; Janus.debug(servers); } else { if (server.indexOf('ws') === 0) { websockets = true; Janus.log('Using WebSockets to contact Janus: ' + server); } else { websockets = false; Janus.log('Using REST API to contact Janus: ' + server); } } let iceServers = gatewayCallbacks.iceServers || [{ urls: 'stun:stun.l.google.com:19302' }]; let iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; let bundlePolicy = gatewayCallbacks.bundlePolicy; // Whether we should enable the withCredentials flag for XHR requests let withCredentials = false; if (typeof gatewayCallbacks.withCredentials !== 'undefined' && gatewayCallbacks.withCredentials !== null) withCredentials = gatewayCallbacks.withCredentials === true; // Optional max events let maxev = 10; if (typeof gatewayCallbacks.max_poll_events !== 'undefined' && gatewayCallbacks.max_poll_events !== null) maxev = gatewayCallbacks.max_poll_events; if (maxev < 1) maxev = 1; // Token to use (only if the token based authentication mechanism is enabled) let token = null; if (typeof gatewayCallbacks.token !== 'undefined' && gatewayCallbacks.token !== null) token = gatewayCallbacks.token; // API secret to use (only if the shared API secret is enabled) let apisecret = null; if (typeof gatewayCallbacks.apisecret !== 'undefined' && gatewayCallbacks.apisecret !== null) apisecret = gatewayCallbacks.apisecret; // Whether we should destroy this session when onbeforeunload is called this.destroyOnUnload = true; if (typeof gatewayCallbacks.destroyOnUnload !== 'undefined' && gatewayCallbacks.destroyOnUnload !== null) this.destroyOnUnload = gatewayCallbacks.destroyOnUnload === true; // Some timeout-related values let keepAlivePeriod = 25000; if (typeof gatewayCallbacks.keepAlivePeriod !== 'undefined' && gatewayCallbacks.keepAlivePeriod !== null) keepAlivePeriod = gatewayCallbacks.keepAlivePeriod; if (isNaN(keepAlivePeriod)) keepAlivePeriod = 25000; let longPollTimeout = 60000; if (typeof gatewayCallbacks.longPollTimeout !== 'undefined' && gatewayCallbacks.longPollTimeout !== null) longPollTimeout = gatewayCallbacks.longPollTimeout; if (isNaN(longPollTimeout)) longPollTimeout = 60000; // overrides for default maxBitrate values for simulcasting function getMaxBitrates(simulcastMaxBitrates) { let maxBitrates = { high: 900000, medium: 300000, low: 100000, }; if (typeof simulcastMaxBitrates !== 'undefined' && simulcastMaxBitrates !== null) { if (simulcastMaxBitrates.high) maxBitrates.high = simulcastMaxBitrates.high; if (simulcastMaxBitrates.medium) maxBitrates.medium = simulcastMaxBitrates.medium; if (simulcastMaxBitrates.low) maxBitrates.low = simulcastMaxBitrates.low; } return maxBitrates; } let connected = false; let sessionId = null; let pluginHandles = {}; let that = this; let retries = 0; let transactions = {}; createSession(gatewayCallbacks); // Public methods this.getServer = function () { return server; }; this.isConnected = function () { return connected; }; this.reconnect = function (callbacks) { callbacks = callbacks || {}; callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop; callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop; callbacks['reconnect'] = true; createSession(callbacks); }; this.getSessionId = function () { return sessionId; }; this.getInfo = function (callbacks) { getInfo(callbacks); }; this.destroy = function (callbacks) { destroySession(callbacks); }; this.attach = function (callbacks) { createHandle(callbacks); }; function eventHandler() { if (sessionId == null) return; Janus.debug('Long poll...'); if (!connected) { Janus.warn('Is the server down? (connected=false)'); return; } let longpoll = server + '/' + sessionId + '?rid=' + new Date().getTime(); if (maxev) longpoll = longpoll + '&maxev=' + maxev; if (token) longpoll = longpoll + '&token=' + encodeURIComponent(token); if (apisecret) longpoll = longpoll + '&apisecret=' + encodeURIComponent(apisecret); Janus.httpAPICall(longpoll, { verb: 'GET', withCredentials: withCredentials, success: handleEvent, timeout: longPollTimeout, error: function (textStatus, errorThrown) { Janus.error(textStatus + ':', errorThrown); retries++; if (retries > 3) { // Did we just lose the server? :-( connected = false; gatewayCallbacks.error('Lost connection to the server (is it down?)'); return; } eventHandler(); }, }); } // Private event handler: this will trigger plugin callbacks, if set function handleEvent(json, skipTimeout) { retries = 0; if (!websockets && typeof sessionId !== 'undefined' && sessionId !== null && skipTimeout !== true) eventHandler(); if (!websockets && Janus.isArray(json)) { // We got an array: it means we passed a maxev > 1, iterate on all objects for (let i = 0; i < json.length; i++) { handleEvent(json[i], true); } return; } if (json['janus'] === 'keepalive') { // Nothing happened Janus.vdebug('Got a keepalive on session ' + sessionId); return; } else if (json['janus'] === 'server_info') { // Just info on the Janus instance Janus.debug('Got info on the Janus instance'); Janus.debug(json); const transaction = json['transaction']; if (transaction) { const reportSuccess = transactions[transaction]; if (reportSuccess) reportSuccess(json); delete transactions[transaction]; } return; } else if (json['janus'] === 'ack') { // Just an ack, we can probably ignore Janus.debug('Got an ack on session ' + sessionId); Janus.debug(json); const transaction = json['transaction']; if (transaction) { const reportSuccess = transactions[transaction]; if (reportSuccess) reportSuccess(json); delete transactions[transaction]; } return; } else if (json['janus'] === 'success') { // Success! Janus.debug('Got a success on session ' + sessionId); Janus.debug(json); const transaction = json['transaction']; if (transaction) { const reportSuccess = transactions[transaction]; if (reportSuccess) reportSuccess(json); delete transactions[transaction]; } return; } else if (json['janus'] === 'trickle') { // We got a trickle candidate from Janus const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.debug('This handle is not attached to this session'); return; } let candidate = json['candidate']; Janus.debug('Got a trickled candidate on session ' + sessionId); Janus.debug(candidate); let config = pluginHandle.webrtcStuff; if (config.pc && config.remoteSdp) { // Add candidate right now Janus.debug('Adding remote candidate:', candidate); if (!candidate || candidate.completed === true) { // end-of-candidates config.pc.addIceCandidate(Janus.endOfCandidates); } else { // New candidate config.pc.addIceCandidate(candidate); } } else { // We didn't do setRemoteDescription (trickle got here before the offer?) Janus.debug("We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate"); if (!config.candidates) config.candidates = []; config.candidates.push(candidate); Janus.debug(config.candidates); } } else if (json['janus'] === 'webrtcup') { // The PeerConnection with the server is up! Notify this Janus.debug('Got a webrtcup event on session ' + sessionId); Janus.debug(json); const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.debug('This handle is not attached to this session'); return; } pluginHandle.webrtcState(true); return; } else if (json['janus'] === 'hangup') { // A plugin asked the core to hangup a PeerConnection on one of our handles Janus.debug('Got a hangup event on session ' + sessionId); Janus.debug(json); const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.debug('This handle is not attached to this session'); return; } pluginHandle.webrtcState(false, json['reason']); pluginHandle.hangup(); } else if (json['janus'] === 'detached') { // A plugin asked the core to detach one of our handles Janus.debug('Got a detached event on session ' + sessionId); Janus.debug(json); const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { // Don't warn here because destroyHandle causes this situation. return; } pluginHandle.ondetached(); pluginHandle.detach(); } else if (json['janus'] === 'media') { // Media started/stopped flowing Janus.debug('Got a media event on session ' + sessionId); Janus.debug(json); const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.debug('This handle is not attached to this session'); return; } pluginHandle.mediaState(json['type'], json['receiving'], json['mid']); } else if (json['janus'] === 'slowlink') { Janus.debug('Got a slowlink event on session ' + sessionId); Janus.debug(json); // Trouble uplink or downlink const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.debug('This handle is not attached to this session'); return; } pluginHandle.slowLink(json['uplink'], json['lost'], json['mid']); } else if (json['janus'] === 'error') { // Oops, something wrong happened Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME Janus.debug(json); let transaction = json['transaction']; if (transaction) { let reportSuccess = transactions[transaction]; if (reportSuccess) { reportSuccess(json); } delete transactions[transaction]; } return; } else if (json['janus'] === 'event') { Janus.debug('Got a plugin event on session ' + sessionId); Janus.debug(json); const sender = json['sender']; if (!sender) { Janus.warn('Missing sender...'); return; } let plugindata = json['plugindata']; if (!plugindata) { Janus.warn('Missing plugindata...'); return; } Janus.debug(' -- Event is coming from ' + sender + ' (' + plugindata['plugin'] + ')'); let data = plugindata['data']; Janus.debug(data); const pluginHandle = pluginHandles[sender]; if (!pluginHandle) { Janus.warn('This handle is not attached to this session'); return; } let jsep = json['jsep']; if (jsep) { Janus.debug('Handling SDP as well...'); Janus.debug(jsep); } let callback = pluginHandle.onmessage; if (callback) { Janus.debug('Notifying application...'); // Send to callback specified when attaching plugin handle callback(data, jsep); } else { // Send to generic callback (?) Janus.debug('No provided notification callback'); } } else if (json['janus'] === 'timeout') { Janus.error('Timeout on session ' + sessionId); Janus.debug(json); if (websockets) { ws.close(3504, 'Gateway timeout'); } return; } else { Janus.warn("Unknown message/event '" + json['janus'] + "' on session " + sessionId); Janus.debug(json); } } // Private helper to send keep-alive messages on WebSockets function keepAlive() { if (!server || !websockets || !connected) return; wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod); let request = { janus: 'keepalive', session_id: sessionId, transaction: Janus.randomString(12), }; if (token) request['token'] = token; if (apisecret) request['apisecret'] = apisecret; ws.send(JSON.stringify(request)); } // Private method to create a session function createSession(callbacks) { let transaction = Janus.randomString(12); let request = { janus: 'create', transaction: transaction }; if (callbacks['reconnect']) { // We're reconnecting, claim the session connected = false; request['janus'] = 'claim'; request['session_id'] = sessionId; // If we were using websockets, ignore the old connection if (ws) { ws.onopen = null; ws.onerror = null; ws.onclose = null; if (wsKeepaliveTimeoutId) { clearTimeout(wsKeepaliveTimeoutId); wsKeepaliveTimeoutId = null; } } } if (token) request['token'] = token; if (apisecret) request['apisecret'] = apisecret; if (!server && Janus.isArray(servers)) { // We still need to find a working server from the list we were given server = servers[serversIndex]; if (server.indexOf('ws') === 0) { websockets = true; Janus.log('Server #' + (serversIndex + 1) + ': trying WebSockets to contact Janus (' + server + ')'); } else { websockets = false; Janus.log('Server #' + (serversIndex + 1) + ': trying REST API to contact Janus (' + server + ')'); } } if (websockets) { ws = Janus.newWebSocket(server, 'janus-protocol'); wsHandlers = { error: function () { Janus.error('Error connecting to the Janus WebSockets server... ' + server); if (Janus.isArray(servers) && !callbacks['reconnect']) { serversIndex++; if (serversIndex === servers.length) { // We tried all the servers the user gave us and they all failed callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?'); return; } // Let's try the next server server = null; setTimeout(function () { createSession(callbacks); }, 200); return; } callbacks.error('Error connecting to the Janus WebSockets server: Is the server down?'); }, open: function () { // We need to be notified about the success transactions[transaction] = function (json) { Janus.debug(json); if (json['janus'] !== 'success') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME callbacks.error(json['error'].reason); return; } wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod); connected = true; sessionId = json['session_id'] ? json['session_id'] : json.data['id']; if (callbacks['reconnect']) { Janus.log('Claimed session: ' + sessionId); } else { Janus.log('Created session: ' + sessionId); } Janus.sessions[sessionId] = that; callbacks.success(); }; ws.send(JSON.stringify(request)); }, message: function (event) { handleEvent(JSON.parse(event.data)); }, close: function () { if (!server || !connected) { return; } connected = false; // FIXME What if this is called when the page is closed? gatewayCallbacks.error('Lost connection to the server (is it down?)'); }, }; for (let eventName in wsHandlers) { ws.addEventListener(eventName, wsHandlers[eventName]); } return; } Janus.httpAPICall(server, { verb: 'POST', withCredentials: withCredentials, body: request, success: function (json) { Janus.debug(json); if (json['janus'] !== 'success') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME callbacks.error(json['error'].reason); return; } connected = true; sessionId = json['session_id'] ? json['session_id'] : json.data['id']; if (callbacks['reconnect']) { Janus.log('Claimed session: ' + sessionId); } else { Janus.log('Created session: ' + sessionId); } Janus.sessions[sessionId] = that; eventHandler(); callbacks.success(); }, error: function (textStatus, errorThrown) { Janus.error(textStatus + ':', errorThrown); // FIXME if (Janus.isArray(servers) && !callbacks['reconnect']) { serversIndex++; if (serversIndex === servers.length) { // We tried all the servers the user gave us and they all failed callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?'); return; } // Let's try the next server server = null; setTimeout(function () { createSession(callbacks); }, 200); return; } if (errorThrown === '') callbacks.error(textStatus + ': Is the server down?'); else if (errorThrown && errorThrown.error) callbacks.error(textStatus + ': ' + errorThrown.error.message); else callbacks.error(textStatus + ': ' + errorThrown); }, }); } // Private method to get info on the server function getInfo(callbacks) { callbacks = callbacks || {}; // FIXME This method triggers a success even when we fail callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop; callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop; Janus.log('Getting info on Janus instance'); if (!connected) { Janus.warn('Is the server down? (connected=false)'); callbacks.error('Is the server down? (connected=false)'); return; } // We just need to send an "info" request let transaction = Janus.randomString(12); let request = { janus: 'info', transaction: transaction }; if (token) request['token'] = token; if (apisecret) request['apisecret'] = apisecret; if (websockets) { transactions[transaction] = function (json) { Janus.log('Server info:'); Janus.debug(json); if (json['janus'] !== 'server_info') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME } callbacks.success(json); }; ws.send(JSON.stringify(request)); return; } Janus.httpAPICall(server, { verb: 'POST', withCredentials: withCredentials, body: request, success: function (json) { Janus.log('Server info:'); Janus.debug(json); if (json['janus'] !== 'server_info') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME } callbacks.success(json); }, error: function (textStatus, errorThrown) { Janus.error(textStatus + ':', errorThrown); // FIXME if (errorThrown === '') callbacks.error(textStatus + ': Is the server down?'); else callbacks.error(textStatus + ': ' + errorThrown); }, }); } // Private method to destroy a session function destroySession(callbacks) { callbacks = callbacks || {}; // FIXME This method triggers a success even when we fail callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop; callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop; let unload = callbacks.unload === true; let notifyDestroyed = true; if (typeof callbacks.notifyDestroyed !== 'undefined' && callbacks.notifyDestroyed !== null) notifyDestroyed = callbacks.notifyDestroyed === true; let cleanupHandles = callbacks.cleanupHandles === true; Janus.log('Destroying session ' + sessionId + ' (unload=' + unload + ')'); if (!sessionId) { Janus.warn('No session to destroy'); callbacks.success(); if (notifyDestroyed) gatewayCallbacks.destroyed(); return; } if (cleanupHandles) { for (let handleId in pluginHandles) destroyHandle(handleId, { noRequest: true }); } if (!connected) { Janus.warn('Is the server down? (connected=false)'); sessionId = null; callbacks.success(); return; } // No need to destroy all handles first, Janus will do that itself let request = { janus: 'destroy', transaction: Janus.randomString(12) }; if (token) request['token'] = token; if (apisecret) request['apisecret'] = apisecret; if (unload) { // We're unloading the page: use sendBeacon for HTTP instead, // or just close the WebSocket connection if we're using that if (websockets) { ws.onclose = null; ws.close(); ws = null; } else { navigator.sendBeacon(server + '/' + sessionId, JSON.stringify(request)); } Janus.log('Destroyed session:'); sessionId = null; connected = false; callbacks.success(); if (notifyDestroyed) gatewayCallbacks.destroyed(); return; } if (websockets) { request['session_id'] = sessionId; let unbindWebSocket = function () { for (let eventName in wsHandlers) { ws.removeEventListener(eventName, wsHandlers[eventName]); } ws.removeEventListener('message', onUnbindMessage); ws.removeEventListener('error', onUnbindError); if (wsKeepaliveTimeoutId) { clearTimeout(wsKeepaliveTimeoutId); } ws.close(); }; let onUnbindMessage = function (event) { let data = JSON.parse(event.data); if (data.session_id == request.session_id && data.transaction == request.transaction) { unbindWebSocket(); callbacks.success(); if (notifyDestroyed) gatewayCallbacks.destroyed(); } }; let onUnbindError = function () { unbindWebSocket(); callbacks.error('Failed to destroy the server: Is the server down?'); if (notifyDestroyed) gatewayCallbacks.destroyed(); }; ws.addEventListener('message', onUnbindMessage); ws.addEventListener('error', onUnbindError); if (ws.readyState === 1) { ws.send(JSON.stringify(request)); } else { onUnbindError(); } return; } Janus.httpAPICall(server + '/' + sessionId, { verb: 'POST', withCredentials: withCredentials, body: request, success: function (json) { Janus.log('Destroyed session:'); Janus.debug(json); sessionId = null; connected = false; if (json['janus'] !== 'success') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME } callbacks.success(); if (notifyDestroyed) gatewayCallbacks.destroyed(); }, error: function (textStatus, errorThrown) { Janus.error(textStatus + ':', errorThrown); // FIXME // Reset everything anyway sessionId = null; connected = false; callbacks.success(); if (notifyDestroyed) gatewayCallbacks.destroyed(); }, }); } // Private method to create a plugin handle function createHandle(callbacks) { callbacks = callbacks || {}; callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop; callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop; callbacks.dataChannelOptions = callbacks.dataChannelOptions || { ordered: true, }; callbacks.consentDialog = typeof callbacks.consentDialog == 'function' ? callbacks.consentDialog : Janus.noop; callbacks.iceState = typeof callbacks.iceState == 'function' ? callbacks.iceState : Janus.noop; callbacks.mediaState = typeof callbacks.mediaState == 'function' ? callbacks.mediaState : Janus.noop; callbacks.webrtcState = typeof callbacks.webrtcState == 'function' ? callbacks.webrtcState : Janus.noop; callbacks.slowLink = typeof callbacks.slowLink == 'function' ? callbacks.slowLink : Janus.noop; callbacks.onmessage = typeof callbacks.onmessage == 'function' ? callbacks.onmessage : Janus.noop; callbacks.onlocaltrack = typeof callbacks.onlocaltrack == 'function' ? callbacks.onlocaltrack : Janus.noop; callbacks.onremotetrack = typeof callbacks.onremotetrack == 'function' ? callbacks.onremotetrack : Janus.noop; callbacks.ondata = typeof callbacks.ondata == 'function' ? callbacks.ondata : Janus.noop; callbacks.ondataopen = typeof callbacks.ondataopen == 'function' ? callbacks.ondataopen : Janus.noop; callbacks.oncleanup = typeof callbacks.oncleanup == 'function' ? callbacks.oncleanup : Janus.noop; callbacks.ondetached = typeof callbacks.ondetached == 'function' ? callbacks.ondetached : Janus.noop; if (!connected) { Janus.warn('Is the server down? (connected=false)'); callbacks.error('Is the server down? (connected=false)'); return; } let plugin = callbacks.plugin; if (!plugin) { Janus.error('Invalid plugin'); callbacks.error('Invalid plugin'); return; } let opaqueId = callbacks.opaqueId; let loopIndex = callbacks.loopIndex; let handleToken = callbacks.token ? callbacks.token : token; let transaction = Janus.randomString(12); let request = { janus: 'attach', plugin: plugin, opaque_id: opaqueId, loop_index: loopIndex, transaction: transaction, }; if (handleToken) request['token'] = handleToken; if (apisecret) request['apisecret'] = apisecret; if (websockets) { transactions[transaction] = function (json) { Janus.debug(json); if (json['janus'] !== 'success') { Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME callbacks.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); return; } let handleId = json.data['id']; Janus.log('Created handle: ' + handleId);