/* 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 = {}; // Screensharing Chrome Extension ID Janus.extensionId = "hapfgfdkleiggjjpfpenajgdnfckjpaj"; Janus.isExtensionEnabled = function() { if(window.navigator.userAgent.match('Chrome')) { var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); var 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 (document.getElementById('janus-extension-installed') !== null); } else { // Firefox of others, no need for the extension (but this doesn't mean it will work) return true; } }; Janus.noop = function() {}; // Initialization Janus.init = function(options) { options = options || {}; options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; if(Janus.initDone === true) { // Already initialized options.callback(); } else { if(typeof console == "undefined" || 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(var i in options.debug) { var d = options.debug[i]; 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"); // Helper method to enumerate devices Janus.listDevices = function(callback) { callback = (typeof callback == "function") ? callback : Janus.noop; if(navigator.mediaDevices) { navigator.mediaDevices.getUserMedia({ audio: true, video: true }) .then(function(stream) { navigator.mediaDevices.enumerateDevices().then(function(devices) { Janus.debug(devices); callback(devices); // Get rid of the now useless stream try { stream.stop(); } catch(e) {} try { var tracks = stream.getTracks(); for(var i in tracks) { var mst = tracks[i]; if(mst !== null && mst !== undefined) mst.stop(); } } catch(e) {} }); }) .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) { if(adapter.browserDetails.browser === 'chrome') { var chromever = adapter.browserDetails.version; if(chromever >= 43) { element.srcObject = stream; } else if(typeof element.src !== 'undefined') { element.src = URL.createObjectURL(stream); } else { Janus.error("Error attaching stream to element"); } } else if(adapter.browserDetails.browser === 'safari' || window.navigator.userAgent.match(/iPad/i) || window.navigator.userAgent.match(/iPhone/i)) { element.src = URL.createObjectURL(stream); } else { element.srcObject = stream; } }; Janus.reattachMediaStream = function(to, from) { if(adapter.browserDetails.browser === 'chrome') { var chromever = adapter.browserDetails.version; if(chromever >= 43) { to.srcObject = from.srcObject; } else if(typeof to.src !== 'undefined') { to.src = from.src; } } else if(adapter.browserDetails.browser === 'safari' || window.navigator.userAgent.match(/iPad/i) || window.navigator.userAgent.match(/iPhone/i)) { to.src = from.src; } else { to.srcObject = from.srcObject; } }; // Prepare a helper method to send AJAX requests in a syntax similar to jQuery (at least for what we care) Janus.ajax = function(params) { // Check params if(params === null || params === undefined) return; params.success = (typeof params.success == "function") ? params.success : Janus.noop; params.error = (typeof params.error == "function") ? params.error : Janus.noop; // Make sure there's an URL if(params.url === null || params.url === undefined) { Janus.error('Missing url', params.url); params.error(null, -1, 'Missing url'); return; } // Validate async params.async = (params.async === null || params.async === undefined) ? true : (params.async === true); Janus.log(params); // IE doesn't even know what WebRTC is, so no polyfill needed var XHR = new XMLHttpRequest(); XHR.open(params.type, params.url, params.async); if(params.contentType !== null && params.contentType !== undefined) XHR.setRequestHeader('Content-type', params.contentType); if(params.withCredentials !== null && params.withCredentials !== undefined) XHR.withCredentials = params.withCredentials; if(params.async) { XHR.onreadystatechange = function () { if(XHR.readyState != 4) return; if(XHR.status !== 200) { // Got an error? params.error(XHR, XHR.status !== 0 ? XHR.status : 'error', ""); return; } // Got payload try { params.success(JSON.parse(XHR.responseText)); } catch(e) { params.error(XHR, XHR.status, 'Could not parse response, error: ' + e + ', text: ' + XHR.responseText); } }; } try { XHR.send(params.data); if(!params.async) { if(XHR.status !== 200) { // Got an error? params.error(XHR, XHR.status !== 0 ? XHR.status : 'error', ""); return; } // Got payload try { params.success(JSON.parse(XHR.responseText)); } catch(e) { params.error(XHR, XHR.status, 'Could not parse response, error: ' + e + ', text: ' + XHR.responseText); } } } catch(e) { // Something broke up params.error(XHR, 'error', ''); }; }; // Detect tab close: make sure we don't loose existing onbeforeunload handlers var oldOBF = window.onbeforeunload; window.onbeforeunload = function() { Janus.log("Closing window"); for(var s in Janus.sessions) { if(Janus.sessions[s] !== null && Janus.sessions[s] !== undefined && Janus.sessions[s].destroyOnUnload) { Janus.log("Destroying session " + s); Janus.sessions[s].destroy({asyncRequest: false}); } } if(oldOBF && typeof oldOBF == "function") oldOBF(); } Janus.initDone = true; options.callback(); } }; // Helper method to check whether WebRTC is supported by this browser Janus.isWebrtcSupported = function() { return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null && navigator.getUserMedia !== undefined && navigator.getUserMedia !== null; }; // Helper method to create random identifiers (e.g., transaction) Janus.randomString = function(len) { var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; var randomString = ''; for (var i = 0; i < len; i++) { var randomPoz = Math.floor(Math.random() * charSet.length); randomString += charSet.substring(randomPoz,randomPoz+1); } return randomString; } // Janus session object function Janus(gatewayCallbacks) { if(Janus.initDone === undefined) { gatewayCallbacks.error("Library not initialized"); return {}; } if(!Janus.isWebrtcSupported()) { gatewayCallbacks.error("WebRTC not supported by this browser"); return {}; } Janus.log("Library initialized: " + Janus.initDone); 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(gatewayCallbacks.server === null || gatewayCallbacks.server === undefined) { gatewayCallbacks.error("Invalid gateway url"); return {}; } var websockets = false; var ws = null; var wsHandlers = {}; var wsKeepaliveTimeoutId = null; var servers = null, serversIndex = 0; var server = gatewayCallbacks.server; if(Array.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); } } var iceServers = gatewayCallbacks.iceServers; if(iceServers === undefined || iceServers === null) iceServers = [{urls: "stun:stun.l.google.com:19302"}]; var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; // Whether IPv6 candidates should be gathered var ipv6Support = gatewayCallbacks.ipv6; if(ipv6Support === undefined || ipv6Support === null) ipv6Support = false; // Whether we should enable the withCredentials flag for XHR requests var withCredentials = false; if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null) withCredentials = gatewayCallbacks.withCredentials === true; // Optional max events var maxev = null; if(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) var token = null; if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null) token = gatewayCallbacks.token; // API secret to use (only if the shared API secret is enabled) var apisecret = null; if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null) apisecret = gatewayCallbacks.apisecret; // Whether we should destroy this session when onbeforeunload is called this.destroyOnUnload = true; if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null) this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true); var connected = false; var sessionId = null; var pluginHandles = {}; var that = this; var retries = 0; var transactions = {}; createSession(gatewayCallbacks); // Public methods this.getServer = function() { return server; }; this.isConnected = function() { return connected; }; this.getSessionId = function() { return sessionId; }; this.destroy = function(callbacks) { destroySession(callbacks, true); }; this.attach = function(callbacks) { createHandle(callbacks); }; function eventHandler() { if(sessionId == null) return; Janus.debug('Long poll...'); if(!connected) { Janus.warn("Is the gateway down? (connected=false)"); return; } var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime(); if(maxev !== undefined && maxev !== null) longpoll = longpoll + "&maxev=" + maxev; if(token !== null && token !== undefined) longpoll = longpoll + "&token=" + token; if(apisecret !== null && apisecret !== undefined) longpoll = longpoll + "&apisecret=" + apisecret; Janus.ajax({ type: 'GET', url: longpoll, withCredentials: withCredentials, cache: false, timeout: 60000, // FIXME success: handleEvent, error: function(XMLHttpRequest, textStatus, errorThrown) { Janus.error(textStatus + ": " + errorThrown); retries++; if(retries > 3) { // Did we just lose the gateway? :-( connected = false; gatewayCallbacks.error("Lost connection to the gateway (is it down?)"); return; } eventHandler(); }, dataType: "json" }); } // Private event handler: this will trigger plugin callbacks, if set function handleEvent(json) { retries = 0; if(!websockets && sessionId !== undefined && sessionId !== null) setTimeout(eventHandler, 200); if(!websockets && Array.isArray(json)) { // We got an array: it means we passed a maxev > 1, iterate on all objects for(var i=0; i 0) { var local_audio_track = tracks[0]; config.dtmfSender = config.pc.createDTMFSender(local_audio_track); Janus.log("Created DTMF Sender"); config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; } } if(config.dtmfSender === null || config.dtmfSender === undefined) { Janus.warn("Invalid DTMF configuration"); callbacks.error("Invalid DTMF configuration"); return; } } var dtmf = callbacks.dtmf; if(dtmf === null || dtmf === undefined) { Janus.warn("Invalid DTMF parameters"); callbacks.error("Invalid DTMF parameters"); return; } var tones = dtmf.tones; if(tones === null || tones === undefined) { Janus.warn("Invalid DTMF string"); callbacks.error("Invalid DTMF string"); return; } var duration = dtmf.duration; if(duration === null || duration === undefined) duration = 500; // We choose 500ms as the default duration for a tone var gap = dtmf.gap; if(gap === null || gap === undefined) gap = 50; // We choose 50ms as the default gap between tones Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); config.dtmfSender.insertDTMF(tones, duration, gap); } // Private method to destroy a plugin handle function destroyHandle(handleId, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; var asyncRequest = true; if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null) asyncRequest = (callbacks.asyncRequest === true); Janus.log("Destroying handle " + handleId + " (sync=" + asyncRequest + ")"); cleanupWebrtc(handleId); if (pluginHandles[handleId].detached) { // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here delete pluginHandles[handleId]; callbacks.success(); return; } if(!connected) { Janus.warn("Is the gateway down? (connected=false)"); callbacks.error("Is the gateway down? (connected=false)"); return; } var request = { "janus": "detach", "transaction": Janus.randomString(12) }; if(token !== null && token !== undefined) request["token"] = token; if(apisecret !== null && apisecret !== undefined) request["apisecret"] = apisecret; if(websockets) { request["session_id"] = sessionId; request["handle_id"] = handleId; ws.send(JSON.stringify(request)); delete pluginHandles[handleId]; callbacks.success(); return; } Janus.ajax({ type: 'POST', url: server + "/" + sessionId + "/" + handleId, async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work withCredentials: withCredentials, cache: false, contentType: "application/json", data: JSON.stringify(request), success: function(json) { Janus.log("Destroyed handle:"); Janus.debug(json); if(json["janus"] !== "success") { Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME } delete pluginHandles[handleId]; callbacks.success(); }, error: function(XMLHttpRequest, textStatus, errorThrown) { Janus.error(textStatus + ": " + errorThrown); // FIXME // We cleanup anyway delete pluginHandles[handleId]; callbacks.success(); }, dataType: "json" }); } // WebRTC stuff function streamsDone(handleId, jsep, media, callbacks, stream) { var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); callbacks.error("Invalid handle"); return; } var config = pluginHandle.webrtcStuff; Janus.debug("streamsDone:", stream); config.myStream = stream; var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy}; //~ var pc_constraints = {'mandatory': {'MozDontOfferDataChannel':true}}; var pc_constraints = { "optional": [{"DtlsSrtpKeyAgreement": true}] }; if(ipv6Support === true) { // FIXME This is only supported in Chrome right now // For support in Firefox track this: https://bugzilla.mozilla.org/show_bug.cgi?id=797262 pc_constraints.optional.push({"googIPv6":true}); } if(adapter.browserDetails.browser === "edge") { // This is Edge, enable BUNDLE explicitly pc_config.bundlePolicy = "max-bundle"; } Janus.log("Creating PeerConnection"); Janus.debug(pc_constraints); config.pc = new RTCPeerConnection(pc_config, pc_constraints); Janus.debug(config.pc); if(config.pc.getStats) { // FIXME config.volume.value = 0; config.bitrate.value = "0 kbits/sec"; } Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")"); config.pc.oniceconnectionstatechange = function(e) { if(config.pc) pluginHandle.iceState(config.pc.iceConnectionState); }; config.pc.onicecandidate = function(event) { if (event.candidate == null || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { Janus.log("End of candidates."); config.iceDone = true; if(config.trickle === true) { // Notify end of candidates sendTrickleCandidate(handleId, {"completed": true}); } else { // No trickle, time to send the complete SDP (including all candidates) 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 var candidate = { "candidate": event.candidate.candidate, "sdpMid": event.candidate.sdpMid, "sdpMLineIndex": event.candidate.sdpMLineIndex }; if(config.trickle === true) { // Send candidate sendTrickleCandidate(handleId, candidate); } } }; if(stream !== null && stream !== undefined) { Janus.log('Adding local stream'); config.pc.addStream(stream); pluginHandle.onlocalstream(stream); } config.pc.ontrack = function(remoteStream) { Janus.log("Handling Remote Stream"); Janus.debug(remoteStream); config.remoteStream = remoteStream; pluginHandle.onremotestream(remoteStream.streams[0]); }; // Any data channel to create? if(isDataEnabled(media)) { Janus.log("Creating data channel"); var onDataChannelMessage = function(event) { Janus.log('Received message on data channel: ' + event.data); pluginHandle.ondata(event.data); // FIXME } var onDataChannelStateChange = function() { var dcState = config.dataChannel !== null ? config.dataChannel.readyState : "null"; Janus.log('State change on data channel: ' + dcState); if(dcState === 'open') { pluginHandle.ondataopen(); // FIXME } } var onDataChannelError = function(error) { Janus.error('Got error on data channel:', error); // TODO } // Until we implement the proxying of open requests within the Janus core, we open a channel ourselves whatever the case config.dataChannel = config.pc.createDataChannel("JanusDataChannel", {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 if(jsep === null || jsep === undefined) { createOffer(handleId, media, callbacks); } else { if(adapter.browserDetails.browser === "edge") { // This is Edge, add an a=end-of-candidates at the end jsep.sdp += "a=end-of-candidates\r\n"; } config.pc.setRemoteDescription( new RTCSessionDescription(jsep), function() { Janus.log("Remote description accepted!"); createAnswer(handleId, media, callbacks); }, callbacks.error); } } function prepareWebrtc(handleId, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; var jsep = callbacks.jsep; var media = callbacks.media; var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); callbacks.error("Invalid handle"); return; } var config = pluginHandle.webrtcStuff; // Are we updating a session? if(config.pc !== undefined && config.pc !== null) { Janus.log("Updating existing media session"); // Create offer/answer now if(jsep === null || jsep === undefined) { createOffer(handleId, media, callbacks); } else { if(adapter.browserDetails.browser === "edge") { // This is Edge, add an a=end-of-candidates at the end jsep.sdp += "a=end-of-candidates\r\n"; } config.pc.setRemoteDescription( new RTCSessionDescription(jsep), function() { Janus.log("Remote description accepted!"); createAnswer(handleId, media, callbacks); }, callbacks.error); } return; } // Was a MediaStream object passed, or do we need to take care of that? if(callbacks.stream !== null && callbacks.stream !== undefined) { var stream = callbacks.stream; Janus.log("MediaStream provided by the application"); Janus.debug(stream); // Skip the getUserMedia part config.streamExternal = true; streamsDone(handleId, jsep, media, callbacks, stream); return; } config.trickle = isTrickleEnabled(callbacks.trickle); if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) { var constraints = { mandatory: {}, optional: []}; pluginHandle.consentDialog(true); var audioSupport = isAudioSendEnabled(media); if(audioSupport === true && media != undefined && media != null) { if(typeof media.audio === 'object') { audioSupport = media.audio; } } var videoSupport = isVideoSendEnabled(media); if(videoSupport === true && media != undefined && media != null) { if(media.video && media.video != 'screen' && media.video != 'window') { var width = 0; var height = 0, 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) { var firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); if(firefoxVer < 38) { // Unless this is and old Firefox, which doesn't support it Janus.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 { Janus.log("Default video setting (" + media.video + ") is stdres 4:3"); height = 480; maxHeight = 480; width = 640; } Janus.log("Adding media constraint " + media.video); if(navigator.mozGetUserMedia) { var 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': maxHeight, 'minHeight': height, 'maxWidth': width, 'minWidth': width }, 'optional': [] }; } if(typeof media.video === 'object') { videoSupport = media.video; } Janus.debug(videoSupport); } else if(media.video === 'screen' || media.video === 'window') { if (!media.screenshareFrameRate) { media.screenshareFrameRate = 3; } // Not a webcam, but screen capture if(window.location.protocol !== 'https:') { // Screen sharing mandates HTTPS Janus.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+ var cache = {}; function callbackUserMedia (error, stream) { pluginHandle.consentDialog(false); if(error) { callbacks.error({code: error.code, name: error.name, message: error.message}); } else { streamsDone(handleId, jsep, media, callbacks, stream); } }; function getScreenMedia(constraints, gsmCallback) { Janus.log("Adding media constraint (screen capture)"); Janus.debug(constraints); navigator.mediaDevices.getUserMedia(constraints) .then(function(stream) { gsmCallback(null, stream); }) .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); }); }; if(adapter.browserDetails.browser === 'chrome') { var chromever = adapter.browserDetails.version; var 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, minFrameRate: media.screenshareFrameRate, maxFrameRate: media.screenshareFrameRate, chromeMediaSource: 'screen' } }, audio: isAudioSendEnabled(media) }; getScreenMedia(constraints, callbackUserMedia); } else { // Chrome 34+ requires an extension var pending = window.setTimeout( function () { 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')) { var 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: isAudioSendEnabled(media) }; getScreenMedia(constraints, function (err, stream) { callbackUserMedia(err, stream); // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 if (!err) { var lastTime = stream.currentTime; var polly = window.setInterval(function () { if(!stream) window.clearInterval(polly); if(stream.currentTime == lastTime) { window.clearInterval(polly); if(stream.onended) { stream.onended(); } } lastTime = stream.currentTime; }, 500); } }); } else { var 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', function (event) { if(event.origin != window.location.origin) return; if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { var data = cache[event.data.id]; var callback = data[0]; delete cache[event.data.id]; if (event.data.sourceId === '') { // user canceled var error = new Error('NavigatorUserMediaError'); error.name = 'You cancelled the request for permission, giving up...'; pluginHandle.consentDialog(false); callbacks.error(error); } else { constraints = { audio: isAudioSendEnabled(media), video: { mandatory: { chromeMediaSource: 'desktop', maxWidth: window.screen.width, maxHeight: window.screen.height, minFrameRate: media.screenshareFrameRate, maxFrameRate: media.screenshareFrameRate, }, optional: [ {googLeakyBucket: true}, {googTemporalLayeredScreencast: true} ] } }; constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId; getScreenMedia(constraints, callback); } } else if (event.data.type == 'janusGetScreenPending') { window.clearTimeout(event.data.id); } }); return; } } // If we got here, we're not screensharing if(media === null || media === undefined || media.video !== 'screen') { // Check whether all media sources are actually available or not navigator.mediaDevices.enumerateDevices().then(function(devices) { var audioExist = devices.some(function(device) { return device.kind === 'audioinput'; }), videoExist = devices.some(function(device) { return device.kind === 'videoinput'; }); // Check whether a missing device is really a problem var audioSend = isAudioSendEnabled(media); var videoSend = isVideoSendEnabled(media); var needAudioDevice = isAudioSendRequired(media); var needVideoDevice = isVideoSendRequired(media); if(audioSend || videoSend || needAudioDevice || needVideoDevice) { // We need to send either audio or video var haveAudioDevice = audioSend ? audioExist : false; var 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; } else if(!haveAudioDevice && needAudioDevice) { pluginHandle.consentDialog(false); callbacks.error('Audio capture is required, but no capture device found'); return false; } else if(!haveVideoDevice && needVideoDevice) { pluginHandle.consentDialog(false); callbacks.error('Video capture is required, but no capture device found'); return false; } } navigator.mediaDevices.getUserMedia({ audio: audioExist ? audioSupport : false, video: videoExist ? videoSupport : false }) .then(function(stream) { pluginHandle.consentDialog(false); streamsDone(handleId, jsep, media, callbacks, stream); }) .catch(function(error) { pluginHandle.consentDialog(false); callbacks.error({code: error.code, name: error.name, message: error.message}); }); }) .catch(function(error) { pluginHandle.consentDialog(false); callbacks.error('enumerateDevices error', error); }); } } else { // No need to do a getUserMedia, create offer/answer right away streamsDone(handleId, jsep, media, callbacks); } } function prepareWebrtcPeer(handleId, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; var jsep = callbacks.jsep; var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); callbacks.error("Invalid handle"); return; } var config = pluginHandle.webrtcStuff; if(jsep !== undefined && jsep !== null) { if(config.pc === null) { Janus.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; } if(adapter.browserDetails.browser === "edge") { // This is Edge, add an a=end-of-candidates at the end jsep.sdp += "a=end-of-candidates\r\n"; } config.pc.setRemoteDescription( new RTCSessionDescription(jsep), function() { Janus.log("Remote description accepted!"); callbacks.success(); }, callbacks.error); } else { callbacks.error("Invalid JSEP"); } } function createOffer(handleId, media, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); callbacks.error("Invalid handle"); return; } var config = pluginHandle.webrtcStuff; Janus.log("Creating offer (iceDone=" + config.iceDone + ")"); // https://code.google.com/p/webrtc/issues/detail?id=3508 var mediaConstraints = null; if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") { mediaConstraints = { 'offerToReceiveAudio':isAudioRecvEnabled(media), 'offerToReceiveVideo':isVideoRecvEnabled(media) }; } else { mediaConstraints = { 'mandatory': { 'OfferToReceiveAudio':isAudioRecvEnabled(media), 'OfferToReceiveVideo':isVideoRecvEnabled(media) } }; } Janus.debug(mediaConstraints); config.pc.createOffer( function(offer) { Janus.debug(offer); if(config.mySdp === null || config.mySdp === undefined) { Janus.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 Janus.log("Waiting for all candidates..."); return; } if(config.sdpSent) { Janus.log("Offer already sent, not sending it again"); return; } Janus.log("Offer ready"); Janus.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 var jsep = { "type": offer.type, "sdp": offer.sdp }; callbacks.success(jsep); }, callbacks.error, mediaConstraints); } function createAnswer(handleId, media, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); callbacks.error("Invalid handle"); return; } var config = pluginHandle.webrtcStuff; Janus.log("Creating answer (iceDone=" + config.iceDone + ")"); var mediaConstraints = null; if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") { mediaConstraints = { 'offerToReceiveAudio':isAudioRecvEnabled(media), 'offerToReceiveVideo':isVideoRecvEnabled(media) }; } else { mediaConstraints = { 'mandatory': { 'OfferToReceiveAudio':isAudioRecvEnabled(media), 'OfferToReceiveVideo':isVideoRecvEnabled(media) } }; } Janus.debug(mediaConstraints); config.pc.createAnswer( function(answer) { Janus.debug(answer); if(config.mySdp === null || config.mySdp === undefined) { Janus.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 Janus.log("Waiting for all candidates..."); return; } if(config.sdpSent) { // FIXME badly Janus.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 var jsep = { "type": answer.type, "sdp": answer.sdp }; callbacks.success(jsep); }, callbacks.error, mediaConstraints); } function sendSDP(handleId, callbacks) { callbacks = callbacks || {}; callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle, not sending anything"); return; } var config = pluginHandle.webrtcStuff; Janus.log("Sending offer/answer SDP..."); if(config.mySdp === null || config.mySdp === undefined) { Janus.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) { Janus.log("Offer/Answer SDP already sent, not sending it again"); return; } if(config.trickle === false) config.mySdp["trickle"] = false; Janus.debug(callbacks); config.sdpSent = true; callbacks.success(config.mySdp); } function getVolume(handleId) { var pluginHandle = pluginHandles[handleId]; if(pluginHandle === null || pluginHandle === undefined || pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { Janus.warn("Invalid handle"); return 0; } var config = pluginHandle.webrtcStuff; // Start getting the volume, if getStats is supported if(config.pc.getStats && adapter.browserDetails.browser == "chrome") { // FIXME if(config.remoteStream === null || config.remoteStream === undefined) { Janus.warn("Remote stream unavailable"); return 0; } // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html if(config.volume.timer === null || config.volume.timer === undefined) { Janus.log("Starting volume monitor"); config.volume.timer = setInterval(function() { config.pc.getStats(function(stats) { var results = stats.result(); for(var i=0; i