From d19d2d83e57845313540a3e5009826427fd94ad1 Mon Sep 17 00:00:00 2001 From: Jonas Smedegaard Date: Wed, 24 May 2017 13:27:52 +0200 Subject: Add janus.js. --- js/lib/janus.js | 2244 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2244 insertions(+) create mode 100644 js/lib/janus.js diff --git a/js/lib/janus.js b/js/lib/janus.js new file mode 100644 index 0000000..b8bc4f3 --- /dev/null +++ b/js/lib/janus.js @@ -0,0 +1,2244 @@ +/* + 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 ($('#janus-extension-installed').length > 0); + } 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; + } + }; + // 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; +} + + +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 : jQuery.noop; + gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : jQuery.noop; + gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : jQuery.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($.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); }; + 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; + $.ajax({ + type: 'GET', + url: longpoll, + xhrFields: { + 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 && $.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 : jQuery.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop; + Janus.warn(callbacks); + var asyncRequest = true; + if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null) + asyncRequest = (callbacks.asyncRequest === true); + Janus.log("Destroying handle " + handleId + " (async=" + 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; + } + $.ajax({ + type: 'POST', + url: server + "/" + sessionId + "/" + handleId, + async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work + xhrFields: { + 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 : jQuery.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 : jQuery.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 : jQuery.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.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 : jQuery.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.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 : jQuery.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.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