diff options
Diffstat (limited to 'dom/media/PeerConnection.js')
-rw-r--r-- | dom/media/PeerConnection.js | 1680 |
1 files changed, 1680 insertions, 0 deletions
diff --git a/dom/media/PeerConnection.js b/dom/media/PeerConnection.js new file mode 100644 index 000000000..98b8debbe --- /dev/null +++ b/dom/media/PeerConnection.js @@ -0,0 +1,1680 @@ +/* jshint moz:true, browser:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PeerConnectionIdp", + "resource://gre/modules/media/PeerConnectionIdp.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport", + "resource://gre/modules/media/RTCStatsReport.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); + +const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1"; +const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1"; +const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1"; +const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1"; +const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1"; +const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1"; +const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1"; +const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1"; +const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1"; +const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1"; +const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1"; + +const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}"); +const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}"); +const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}"); +const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}"); +const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}"); +const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}"); +const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}"); +const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}"); +const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}"); +const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}"); +const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}"); + +// Global list of PeerConnection objects, so they can be cleaned up when +// a page is torn down. (Maps inner window ID to an array of PC objects). +function GlobalPCList() { + this._list = {}; + this._networkdown = false; // XXX Need to query current state somehow + this._lifecycleobservers = {}; + this._nextId = 1; + Services.obs.addObserver(this, "inner-window-destroyed", true); + Services.obs.addObserver(this, "profile-change-net-teardown", true); + Services.obs.addObserver(this, "network:offline-about-to-go-offline", true); + Services.obs.addObserver(this, "network:offline-status-changed", true); + Services.obs.addObserver(this, "gmp-plugin-crash", true); + Services.obs.addObserver(this, "PeerConnection:response:allow", true); + Services.obs.addObserver(this, "PeerConnection:response:deny", true); + if (Cc["@mozilla.org/childprocessmessagemanager;1"]) { + let mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + mm.addMessageListener("gmp-plugin-crash", this); + } +} +GlobalPCList.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIMessageListener, + Ci.nsISupportsWeakReference, + Ci.IPeerConnectionManager]), + classID: PC_MANAGER_CID, + _xpcom_factory: { + createInstance: function(outer, iid) { + if (outer) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return _globalPCList.QueryInterface(iid); + } + }, + + notifyLifecycleObservers: function(pc, type) { + for (var key of Object.keys(this._lifecycleobservers)) { + this._lifecycleobservers[key](pc, pc._winID, type); + } + }, + + addPC: function(pc) { + let winID = pc._winID; + if (this._list[winID]) { + this._list[winID].push(Cu.getWeakReference(pc)); + } else { + this._list[winID] = [Cu.getWeakReference(pc)]; + } + pc._globalPCListId = this._nextId++; + this.removeNullRefs(winID); + }, + + findPC: function(globalPCListId) { + for (let winId in this._list) { + if (this._list.hasOwnProperty(winId)) { + for (let pcref of this._list[winId]) { + let pc = pcref.get(); + if (pc && pc._globalPCListId == globalPCListId) { + return pc; + } + } + } + } + }, + + removeNullRefs: function(winID) { + if (this._list[winID] === undefined) { + return; + } + this._list[winID] = this._list[winID].filter( + function (e,i,a) { return e.get() !== null; }); + + if (this._list[winID].length === 0) { + delete this._list[winID]; + } + }, + + hasActivePeerConnection: function(winID) { + this.removeNullRefs(winID); + return this._list[winID] ? true : false; + }, + + handleGMPCrash: function(data) { + let broadcastPluginCrash = function(list, winID, pluginID, pluginName) { + if (list.hasOwnProperty(winID)) { + list[winID].forEach(function(pcref) { + let pc = pcref.get(); + if (pc) { + pc._pc.pluginCrash(pluginID, pluginName); + } + }); + } + }; + + // a plugin crashed; if it's associated with any of our PCs, fire an + // event to the DOM window + for (let winId in this._list) { + broadcastPluginCrash(this._list, winId, data.pluginID, data.pluginName); + } + }, + + receiveMessage: function(message) { + if (message.name == "gmp-plugin-crash") { + this.handleGMPCrash(message.data); + } + }, + + observe: function(subject, topic, data) { + let cleanupPcRef = function(pcref) { + let pc = pcref.get(); + if (pc) { + pc._pc.close(); + delete pc._observer; + pc._pc = null; + } + }; + + let cleanupWinId = function(list, winID) { + if (list.hasOwnProperty(winID)) { + list[winID].forEach(cleanupPcRef); + delete list[winID]; + } + }; + + if (topic == "inner-window-destroyed") { + let winID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + cleanupWinId(this._list, winID); + + if (this._lifecycleobservers.hasOwnProperty(winID)) { + delete this._lifecycleobservers[winID]; + } + } else if (topic == "profile-change-net-teardown" || + topic == "network:offline-about-to-go-offline") { + // Delete all peerconnections on shutdown - mostly synchronously (we + // need them to be done deleting transports and streams before we + // return)! All socket operations must be queued to STS thread + // before we return to here. + // Also kill them if "Work Offline" is selected - more can be created + // while offline, but attempts to connect them should fail. + for (let winId in this._list) { + cleanupWinId(this._list, winId); + } + this._networkdown = true; + } + else if (topic == "network:offline-status-changed") { + if (data == "offline") { + // this._list shold be empty here + this._networkdown = true; + } else if (data == "online") { + this._networkdown = false; + } + } else if (topic == "gmp-plugin-crash") { + if (subject instanceof Ci.nsIWritablePropertyBag2) { + let pluginID = subject.getPropertyAsUint32("pluginID"); + let pluginName = subject.getPropertyAsAString("pluginName"); + let data = { pluginID, pluginName }; + this.handleGMPCrash(data); + } + } else if (topic == "PeerConnection:response:allow" || + topic == "PeerConnection:response:deny") { + var pc = this.findPC(data); + if (pc) { + if (topic == "PeerConnection:response:allow") { + pc._settlePermission.allow(); + } else { + let err = new pc._win.DOMException("The request is not allowed by " + + "the user agent or the platform in the current context.", + "NotAllowedError"); + pc._settlePermission.deny(err); + } + } + } + }, + + _registerPeerConnectionLifecycleCallback: function(winID, cb) { + this._lifecycleobservers[winID] = cb; + }, +}; +var _globalPCList = new GlobalPCList(); + +function RTCIceCandidate() { + this.candidate = this.sdpMid = this.sdpMLineIndex = null; +} +RTCIceCandidate.prototype = { + classDescription: "RTCIceCandidate", + classID: PC_ICE_CID, + contractID: PC_ICE_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer]), + + init: function(win) { this._win = win; }, + + __init: function(dict) { + this.candidate = dict.candidate; + this.sdpMid = dict.sdpMid; + this.sdpMLineIndex = ("sdpMLineIndex" in dict)? dict.sdpMLineIndex : null; + } +}; + +function RTCSessionDescription() { + this.type = this.sdp = null; +} +RTCSessionDescription.prototype = { + classDescription: "RTCSessionDescription", + classID: PC_SESSION_CID, + contractID: PC_SESSION_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer]), + + init: function(win) { this._win = win; }, + + __init: function(dict) { + this.type = dict.type; + this.sdp = dict.sdp; + } +}; + +function RTCStatsReport(win, dict) { + this._win = win; + this._pcid = dict.pcid; + this._report = convertToRTCStatsReport(dict); +} +RTCStatsReport.prototype = { + classDescription: "RTCStatsReport", + classID: PC_STATS_CID, + contractID: PC_STATS_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + setInternal: function(aKey, aObj) { + return this.__DOM_IMPL__.__set(aKey, aObj); + }, + + // TODO: Remove legacy API eventually + // + // Since maplike is recent, we still also make the stats available as legacy + // enumerable read-only properties directly on our content-facing object. + // Must be called after our webidl sandwich is made. + + makeStatsPublic: function(warnNullable) { + let legacyProps = {}; + for (let key in this._report) { + let value = Cu.cloneInto(this._report[key], this._win); + this.setInternal(key, value); + + legacyProps[key] = { + enumerable: true, configurable: false, + get: Cu.exportFunction(function() { + if (warnNullable.warn) { + warnNullable.warn(); + warnNullable.warn = null; + } + return value; + }, this.__DOM_IMPL__.wrappedJSObject) + }; + } + Object.defineProperties(this.__DOM_IMPL__.wrappedJSObject, legacyProps); + }, + + get mozPcid() { return this._pcid; } +}; + +function RTCPeerConnection() { + this._senders = []; + this._receivers = []; + + this._pc = null; + this._observer = null; + this._closed = false; + + this._onCreateOfferSuccess = null; + this._onCreateOfferFailure = null; + this._onCreateAnswerSuccess = null; + this._onCreateAnswerFailure = null; + this._onGetStatsSuccess = null; + this._onGetStatsFailure = null; + this._onReplaceTrackSender = null; + this._onReplaceTrackWithTrack = null; + this._onReplaceTrackSuccess = null; + this._onReplaceTrackFailure = null; + + this._localType = null; + this._remoteType = null; + // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9 + // canTrickle == null means unknown; when a remote description is received it + // is set to true or false based on the presence of the "trickle" ice-option + this._canTrickle = null; + + // States + this._iceGatheringState = this._iceConnectionState = "new"; +} +RTCPeerConnection.prototype = { + classDescription: "RTCPeerConnection", + classID: PC_CID, + contractID: PC_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer]), + init: function(win) { this._win = win; }, + + __init: function(rtcConfig) { + this._winID = this._win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + // TODO: Update this code once we support pc.setConfiguration, to track + // setting from content independently from pref (Bug 1181768). + if (rtcConfig.iceTransportPolicy == "all" && + Services.prefs.getBoolPref("media.peerconnection.ice.relay_only")) { + rtcConfig.iceTransportPolicy = "relay"; + } + this._config = Object.assign({}, rtcConfig); + + if (!rtcConfig.iceServers || + !Services.prefs.getBoolPref("media.peerconnection.use_document_iceservers")) { + try { + rtcConfig.iceServers = + JSON.parse(Services.prefs.getCharPref("media.peerconnection.default_iceservers") || "[]"); + } catch (e) { + this.logWarning( + "Ignoring invalid media.peerconnection.default_iceservers in about:config"); + rtcConfig.iceServers = []; + } + try { + this._mustValidateRTCConfiguration(rtcConfig, + "Ignoring invalid media.peerconnection.default_iceservers in about:config"); + } catch (e) { + this.logWarning(e.message); + rtcConfig.iceServers = []; + } + } else { + // This gets executed in the typical case when iceServers + // are passed in through the web page. + this._mustValidateRTCConfiguration(rtcConfig, + "RTCPeerConnection constructor passed invalid RTCConfiguration"); + } + var principal = Cu.getWebIDLCallerPrincipal(); + this._isChrome = Services.scriptSecurityManager.isSystemPrincipal(principal); + + if (_globalPCList._networkdown) { + throw new this._win.DOMException( + "Can't create RTCPeerConnections when the network is down", + "InvalidStateError"); + } + + this.makeGetterSetterEH("ontrack"); + this.makeLegacyGetterSetterEH("onaddstream", "Use peerConnection.ontrack instead."); + this.makeLegacyGetterSetterEH("onaddtrack", "Use peerConnection.ontrack instead."); + this.makeGetterSetterEH("onicecandidate"); + this.makeGetterSetterEH("onnegotiationneeded"); + this.makeGetterSetterEH("onsignalingstatechange"); + this.makeGetterSetterEH("onremovestream"); + this.makeGetterSetterEH("ondatachannel"); + this.makeGetterSetterEH("oniceconnectionstatechange"); + this.makeGetterSetterEH("onidentityresult"); + this.makeGetterSetterEH("onpeeridentity"); + this.makeGetterSetterEH("onidpassertionerror"); + this.makeGetterSetterEH("onidpvalidationerror"); + + this._pc = new this._win.PeerConnectionImpl(); + this._operationsChain = this._win.Promise.resolve(); + + this.__DOM_IMPL__._innerObject = this; + this._observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__); + + var location = "" + this._win.location; + + // Warn just once per PeerConnection about deprecated getStats usage. + this._warnDeprecatedStatsAccessNullable = { warn: () => + this.logWarning("non-maplike pc.getStats access is deprecated! " + + "See http://w3c.github.io/webrtc-pc/#example for usage.") }; + + // Add a reference to the PeerConnection to global list (before init). + _globalPCList.addPC(this); + + this._impl.initialize(this._observer, this._win, rtcConfig, + Services.tm.currentThread); + this._initCertificate(rtcConfig.certificates); + this._initIdp(); + _globalPCList.notifyLifecycleObservers(this, "initialized"); + }, + + get _impl() { + if (!this._pc) { + throw new this._win.DOMException( + "RTCPeerConnection is gone (did you enter Offline mode?)", + "InvalidStateError"); + } + return this._pc; + }, + + getConfiguration: function() { + return this._config; + }, + + _initCertificate: function(certificates) { + let certPromise; + if (certificates && certificates.length > 0) { + if (certificates.length > 1) { + throw new this._win.DOMException( + "RTCPeerConnection does not currently support multiple certificates", + "NotSupportedError"); + } + let cert = certificates.find(c => c.expires > Date.now()); + if (!cert) { + throw new this._win.DOMException( + "Unable to create RTCPeerConnection with an expired certificate", + "InvalidParameterError"); + } + certPromise = Promise.resolve(cert); + } else { + certPromise = this._win.RTCPeerConnection.generateCertificate({ + name: "ECDSA", namedCurve: "P-256" + }); + } + this._certificateReady = certPromise + .then(cert => this._impl.certificate = cert); + }, + + _initIdp: function() { + this._peerIdentity = new this._win.Promise((resolve, reject) => { + this._resolvePeerIdentity = resolve; + this._rejectPeerIdentity = reject; + }); + this._lastIdentityValidation = this._win.Promise.resolve(); + + let prefName = "media.peerconnection.identity.timeout"; + let idpTimeout = Services.prefs.getIntPref(prefName); + this._localIdp = new PeerConnectionIdp(this._win, idpTimeout); + this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout); + }, + + // Add a function to the internal operations chain. + + _chain: function(func) { + this._checkClosed(); // out here DOMException line-numbers work. + let p = this._operationsChain.then(() => { + // Don't _checkClosed() inside the chain, because it throws, and spec + // behavior as of this writing is to NOT reject outstanding promises on + // close. This is what happens most of the time anyways, as the c++ code + // stops calling us once closed, hanging the chain. However, c++ may + // already have queued tasks on us, so if we're one of those then sit back. + if (!this._closed) { + return func(); + } + }); + // don't propagate errors in the operations chain (this is a fork of p). + this._operationsChain = p.catch(() => {}); + return p; + }, + + // This wrapper helps implement legacy callbacks in a manner that produces + // correct line-numbers in errors, provided that methods validate their inputs + // before putting themselves on the pc's operations chain. + // + // It also serves as guard against settling promises past close(). + + _legacyCatchAndCloseGuard: function(onSuccess, onError, func) { + if (!onSuccess) { + return func().then(v => (this._closed ? new Promise(() => {}) : v), + e => (this._closed ? new Promise(() => {}) : Promise.reject(e))); + } + try { + return func().then(this._wrapLegacyCallback(onSuccess), + this._wrapLegacyCallback(onError)); + } catch (e) { + this._wrapLegacyCallback(onError)(e); + return this._win.Promise.resolve(); // avoid webidl TypeError + } + }, + + _wrapLegacyCallback: function(func) { + return result => { + try { + func && func(result); + } catch (e) { + this.logErrorAndCallOnError(e); + } + }; + }, + + /** + * An RTCConfiguration may look like this: + * + * { "iceServers": [ { urls: "stun:stun.example.org", }, + * { url: "stun:stun.example.org", }, // deprecated version + * { urls: ["turn:turn1.x.org", "turn:turn2.x.org"], + * username:"jib", credential:"mypass"} ] } + * + * This function normalizes the structure of the input for rtcConfig.iceServers for us, + * so we test well-formed stun/turn urls before passing along to C++. + * msg - Error message to detail which array-entry failed, if any. + */ + _mustValidateRTCConfiguration: function(rtcConfig, msg) { + + // Normalize iceServers input + rtcConfig.iceServers.forEach(server => { + if (typeof server.urls === "string") { + server.urls = [server.urls]; + } else if (!server.urls && server.url) { + // TODO: Remove support for legacy iceServer.url eventually (Bug 1116766) + server.urls = [server.url]; + this.logWarning("RTCIceServer.url is deprecated! Use urls instead."); + } + }); + + let ios = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService); + + let nicerNewURI = uriStr => { + try { + return ios.newURI(uriStr, null, null); + } catch (e if (e.result == Cr.NS_ERROR_MALFORMED_URI)) { + throw new this._win.DOMException(msg + " - malformed URI: " + uriStr, + "SyntaxError"); + } + }; + + rtcConfig.iceServers.forEach(server => { + if (!server.urls) { + throw new this._win.DOMException(msg + " - missing urls", "InvalidAccessError"); + } + server.urls.forEach(urlStr => { + let url = nicerNewURI(urlStr); + if (url.scheme in { turn:1, turns:1 }) { + if (server.username == undefined) { + throw new this._win.DOMException(msg + " - missing username: " + urlStr, + "InvalidAccessError"); + } + if (server.credential == undefined) { + throw new this._win.DOMException(msg + " - missing credential: " + urlStr, + "InvalidAccessError"); + } + if (server.credentialType != "password") { + this.logWarning("RTCConfiguration TURN credentialType \""+ + server.credentialType + + "\" is not yet implemented. Treating as password."+ + " https://bugzil.la/1247616"); + } + } + else if (!(url.scheme in { stun:1, stuns:1 })) { + throw new this._win.DOMException(msg + " - improper scheme: " + url.scheme, + "SyntaxError"); + } + if (url.scheme in { stuns:1, turns:1 }) { + this.logWarning(url.scheme.toUpperCase() + " is not yet supported."); + } + }); + }); + }, + + // Ideally, this should be of the form _checkState(state), + // where the state is taken from an enumeration containing + // the valid peer connection states defined in the WebRTC + // spec. See Bug 831756. + _checkClosed: function() { + if (this._closed) { + throw new this._win.DOMException("Peer connection is closed", + "InvalidStateError"); + } + }, + + dispatchEvent: function(event) { + // PC can close while events are firing if there is an async dispatch + // in c++ land. But let through "closed" signaling and ice connection events. + if (!this._closed || this._inClose) { + this.__DOM_IMPL__.dispatchEvent(event); + } + }, + + // Log error message to web console and window.onerror, if present. + logErrorAndCallOnError: function(e) { + this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.exceptionFlag); + + // Safely call onerror directly if present (necessary for testing) + try { + if (typeof this._win.onerror === "function") { + this._win.onerror(e.message, e.fileName, e.lineNumber); + } + } catch(e) { + // If onerror itself throws, service it. + try { + this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.errorFlag); + } catch(e) {} + } + }, + + logError: function(msg) { + this.logStackMsg(msg, Ci.nsIScriptError.errorFlag); + }, + + logWarning: function(msg) { + this.logStackMsg(msg, Ci.nsIScriptError.warningFlag); + }, + + logStackMsg: function(msg, flag) { + let err = this._win.Error(); + this.logMsg(msg, err.fileName, err.lineNumber, flag); + }, + + logMsg: function(msg, file, line, flag) { + let scriptErrorClass = Cc["@mozilla.org/scripterror;1"]; + let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError); + scriptError.initWithWindowID(msg, file, null, line, 0, flag, + "content javascript", this._winID); + let console = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + console.logMessage(scriptError); + }, + + getEH: function(type) { + return this.__DOM_IMPL__.getEventHandler(type); + }, + + setEH: function(type, handler) { + this.__DOM_IMPL__.setEventHandler(type, handler); + }, + + makeGetterSetterEH: function(name) { + Object.defineProperty(this, name, + { + get:function() { return this.getEH(name); }, + set:function(h) { return this.setEH(name, h); } + }); + }, + + makeLegacyGetterSetterEH: function(name, msg) { + Object.defineProperty(this, name, + { + get:function() { return this.getEH(name); }, + set:function(h) { + this.logWarning(name + " is deprecated! " + msg); + return this.setEH(name, h); + } + }); + }, + + _addIdentityAssertion: function(sdpPromise, origin) { + if (!this._localIdp.enabled) { + return sdpPromise; + } + return Promise.all([ + this._certificateReady + .then(() => this._localIdp.getIdentityAssertion(this._impl.fingerprint, + origin)), + sdpPromise + ]).then(([,sdp]) => this._localIdp.addIdentityAttribute(sdp)); + }, + + createOffer: function(optionsOrOnSuccess, onError, options) { + // This entry-point handles both new and legacy call sig. Decipher which one + let onSuccess; + if (typeof optionsOrOnSuccess == "function") { + onSuccess = optionsOrOnSuccess; + } else { + options = optionsOrOnSuccess; + } + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + // TODO: Remove error on constraint-like RTCOptions next cycle (1197021). + // Note that webidl bindings make o.mandatory implicit but not o.optional. + function convertLegacyOptions(o) { + // Detect (mandatory OR optional) AND no other top-level members. + let lcy = ((o.mandatory && Object.keys(o.mandatory).length) || o.optional) && + Object.keys(o).length == (o.mandatory? 1 : 0) + (o.optional? 1 : 0); + if (!lcy) { + return false; + } + let old = o.mandatory || {}; + if (o.mandatory) { + delete o.mandatory; + } + if (o.optional) { + o.optional.forEach(one => { + // The old spec had optional as an array of objects w/1 attribute each. + // Assumes our JS-webidl bindings only populate passed-in properties. + let key = Object.keys(one)[0]; + if (key && old[key] === undefined) { + old[key] = one[key]; + } + }); + delete o.optional; + } + o.offerToReceiveAudio = old.OfferToReceiveAudio; + o.offerToReceiveVideo = old.OfferToReceiveVideo; + o.mozDontOfferDataChannel = old.MozDontOfferDataChannel; + o.mozBundleOnly = old.MozBundleOnly; + Object.keys(o).forEach(k => { + if (o[k] === undefined) { + delete o[k]; + } + }); + return true; + } + + if (options && convertLegacyOptions(options)) { + this.logError( + "Mandatory/optional in createOffer options no longer works! Use " + + JSON.stringify(options) + " instead (note the case difference)!"); + options = {}; + } + + let origin = Cu.getWebIDLCallerPrincipal().origin; + return this._chain(() => { + let p = Promise.all([this.getPermission(), this._certificateReady]) + .then(() => new this._win.Promise((resolve, reject) => { + this._onCreateOfferSuccess = resolve; + this._onCreateOfferFailure = reject; + this._impl.createOffer(options); + })); + p = this._addIdentityAssertion(p, origin); + return p.then( + sdp => new this._win.RTCSessionDescription({ type: "offer", sdp: sdp })); + }); + }); + }, + + createAnswer: function(optionsOrOnSuccess, onError) { + // This entry-point handles both new and legacy call sig. Decipher which one + let onSuccess, options; + if (typeof optionsOrOnSuccess == "function") { + onSuccess = optionsOrOnSuccess; + } else { + options = optionsOrOnSuccess; + } + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + let origin = Cu.getWebIDLCallerPrincipal().origin; + return this._chain(() => { + let p = Promise.all([this.getPermission(), this._certificateReady]) + .then(() => new this._win.Promise((resolve, reject) => { + // We give up line-numbers in errors by doing this here, but do all + // state-checks inside the chain, to support the legacy feature that + // callers don't have to wait for setRemoteDescription to finish. + if (!this.remoteDescription) { + throw new this._win.DOMException("setRemoteDescription not called", + "InvalidStateError"); + } + if (this.remoteDescription.type != "offer") { + throw new this._win.DOMException("No outstanding offer", + "InvalidStateError"); + } + this._onCreateAnswerSuccess = resolve; + this._onCreateAnswerFailure = reject; + this._impl.createAnswer(); + })); + p = this._addIdentityAssertion(p, origin); + return p.then(sdp => { + return new this._win.RTCSessionDescription({ type: "answer", sdp: sdp }); + }); + }); + }); + }, + + getPermission: function() { + if (this._havePermission) { + return this._havePermission; + } + if (this._isChrome || + AppConstants.MOZ_B2G || + Services.prefs.getBoolPref("media.navigator.permission.disabled")) { + return this._havePermission = Promise.resolve(); + } + return this._havePermission = new Promise((resolve, reject) => { + this._settlePermission = { allow: resolve, deny: reject }; + let outerId = this._win.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + + let chrome = new CreateOfferRequest(outerId, this._winID, + this._globalPCListId, false); + let request = this._win.CreateOfferRequest._create(this._win, chrome); + Services.obs.notifyObservers(request, "PeerConnection:request", null); + }); + }, + + setLocalDescription: function(desc, onSuccess, onError) { + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + this._localType = desc.type; + + let type; + switch (desc.type) { + case "offer": + type = Ci.IPeerConnection.kActionOffer; + break; + case "answer": + type = Ci.IPeerConnection.kActionAnswer; + break; + case "pranswer": + throw new this._win.DOMException("pranswer not yet implemented", + "NotSupportedError"); + case "rollback": + type = Ci.IPeerConnection.kActionRollback; + break; + default: + throw new this._win.DOMException( + "Invalid type " + desc.type + " provided to setLocalDescription", + "InvalidParameterError"); + } + + if (desc.type !== "rollback" && !desc.sdp) { + throw new this._win.DOMException( + "Empty or null SDP provided to setLocalDescription", + "InvalidParameterError"); + } + + return this._chain(() => this.getPermission() + .then(() => new this._win.Promise((resolve, reject) => { + this._onSetLocalDescriptionSuccess = resolve; + this._onSetLocalDescriptionFailure = reject; + this._impl.setLocalDescription(type, desc.sdp); + }))); + }); + }, + + _validateIdentity: function(sdp, origin) { + let expectedIdentity; + + // Only run a single identity verification at a time. We have to do this to + // avoid problems with the fact that identity validation doesn't block the + // resolution of setRemoteDescription(). + let validation = this._lastIdentityValidation + .then(() => this._remoteIdp.verifyIdentityFromSDP(sdp, origin)) + .then(msg => { + expectedIdentity = this._impl.peerIdentity; + // If this pc has an identity already, then the identity in sdp must match + if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) { + this.close(); + throw new this._win.DOMException( + "Peer Identity mismatch, expected: " + expectedIdentity, + "IncompatibleSessionDescriptionError"); + } + if (msg) { + // Set new identity and generate an event. + this._impl.peerIdentity = msg.identity; + this._resolvePeerIdentity(Cu.cloneInto({ + idp: this._remoteIdp.provider, + name: msg.identity + }, this._win)); + } + }) + .catch(e => { + this._rejectPeerIdentity(e); + // If we don't expect a specific peer identity, failure to get a valid + // peer identity is not a terminal state, so replace the promise to + // allow another attempt. + if (!this._impl.peerIdentity) { + this._peerIdentity = new this._win.Promise((resolve, reject) => { + this._resolvePeerIdentity = resolve; + this._rejectPeerIdentity = reject; + }); + } + throw e; + }); + this._lastIdentityValidation = validation.catch(() => {}); + + // Only wait for IdP validation if we need identity matching + return expectedIdentity ? validation : this._win.Promise.resolve(); + }, + + setRemoteDescription: function(desc, onSuccess, onError) { + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + this._remoteType = desc.type; + + let type; + switch (desc.type) { + case "offer": + type = Ci.IPeerConnection.kActionOffer; + break; + case "answer": + type = Ci.IPeerConnection.kActionAnswer; + break; + case "pranswer": + throw new this._win.DOMException("pranswer not yet implemented", + "NotSupportedError"); + case "rollback": + type = Ci.IPeerConnection.kActionRollback; + break; + default: + throw new this._win.DOMException( + "Invalid type " + desc.type + " provided to setRemoteDescription", + "InvalidParameterError"); + } + + if (!desc.sdp && desc.type !== "rollback") { + throw new this._win.DOMException( + "Empty or null SDP provided to setRemoteDescription", + "InvalidParameterError"); + } + + // Get caller's origin before hitting the promise chain + let origin = Cu.getWebIDLCallerPrincipal().origin; + + return this._chain(() => { + let setRem = this.getPermission() + .then(() => new this._win.Promise((resolve, reject) => { + this._onSetRemoteDescriptionSuccess = resolve; + this._onSetRemoteDescriptionFailure = reject; + this._impl.setRemoteDescription(type, desc.sdp); + })).then(() => { this._updateCanTrickle(); }); + + if (desc.type === "rollback") { + return setRem; + } + + // Do setRemoteDescription and identity validation in parallel + let validId = this._validateIdentity(desc.sdp, origin); + return this._win.Promise.all([setRem, validId]) + .then(() => {}); // must return undefined + }); + }); + }, + + setIdentityProvider: function(provider, protocol, username) { + this._checkClosed(); + this._localIdp.setIdentityProvider(provider, protocol, username); + }, + + getIdentityAssertion: function() { + let origin = Cu.getWebIDLCallerPrincipal().origin; + return this._chain( + () => this._certificateReady.then( + () => this._localIdp.getIdentityAssertion(this._impl.fingerprint, origin) + ) + ); + }, + + get canTrickleIceCandidates() { + return this._canTrickle; + }, + + _updateCanTrickle: function() { + let containsTrickle = section => { + let lines = section.toLowerCase().split(/(?:\r\n?|\n)/); + return lines.some(line => { + let prefix = "a=ice-options:"; + if (line.substring(0, prefix.length) !== prefix) { + return false; + } + let tokens = line.substring(prefix.length).split(" "); + return tokens.some(x => x === "trickle"); + }); + }; + + let desc = null; + try { + // The getter for remoteDescription can throw if the pc is closed. + desc = this.remoteDescription; + } catch (e) {} + if (!desc) { + this._canTrickle = null; + return; + } + + let sections = desc.sdp.split(/(?:\r\n?|\n)m=/); + let topSection = sections.shift(); + this._canTrickle = + containsTrickle(topSection) || sections.every(containsTrickle); + }, + + + addIceCandidate: function(c, onSuccess, onError) { + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + if (!c.candidate && !c.sdpMLineIndex) { + throw new this._win.DOMException("Invalid candidate passed to addIceCandidate!", + "InvalidParameterError"); + } + return this._chain(() => new this._win.Promise((resolve, reject) => { + this._onAddIceCandidateSuccess = resolve; + this._onAddIceCandidateError = reject; + this._impl.addIceCandidate(c.candidate, c.sdpMid || "", c.sdpMLineIndex); + })); + }); + }, + + addStream: function(stream) { + stream.getTracks().forEach(track => this.addTrack(track, stream)); + }, + + getStreamById: function(id) { + throw new this._win.DOMException("getStreamById not yet implemented", + "NotSupportedError"); + }, + + addTrack: function(track, stream) { + if (stream.currentTime === undefined) { + throw new this._win.DOMException("invalid stream.", "InvalidParameterError"); + } + this._checkClosed(); + this._senders.forEach(sender => { + if (sender.track == track) { + throw new this._win.DOMException("already added.", + "InvalidParameterError"); + } + }); + this._impl.addTrack(track, stream); + let sender = this._win.RTCRtpSender._create(this._win, + new RTCRtpSender(this, track, + stream)); + this._senders.push(sender); + return sender; + }, + + removeTrack: function(sender) { + this._checkClosed(); + var i = this._senders.indexOf(sender); + if (i >= 0) { + this._senders.splice(i, 1); + this._impl.removeTrack(sender.track); // fires negotiation needed + } + }, + + _insertDTMF: function(sender, tones, duration, interToneGap) { + return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap); + }, + + _getDTMFToneBuffer: function(sender) { + return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__); + }, + + _replaceTrack: function(sender, withTrack) { + // TODO: Do a (sender._stream.getTracks().indexOf(track) < 0) check + // on both track args someday. + // + // The proposed API will be that both tracks must already be in the same + // stream. However, since our MediaStreams currently are limited to one + // track per type, we allow replacement with an outside track not already + // in the same stream. + // + // Since a track may be replaced more than once, the track being replaced + // may not be in the stream either, so we check neither arg right now. + + return new this._win.Promise((resolve, reject) => { + this._onReplaceTrackSender = sender; + this._onReplaceTrackWithTrack = withTrack; + this._onReplaceTrackSuccess = resolve; + this._onReplaceTrackFailure = reject; + this._impl.replaceTrack(sender.track, withTrack); + }); + }, + + _setParameters: function(sender, parameters) { + if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) { + return; + } + // validate parameters input + var encodings = parameters.encodings || []; + + encodings.reduce((uniqueRids, encoding) => { + if (encoding.scaleResolutionDownBy < 1.0) { + throw new this._win.RangeError("scaleResolutionDownBy must be >= 1.0"); + } + if (!encoding.rid && encodings.length > 1) { + throw new this._win.DOMException("Missing rid", "TypeError"); + } + if (uniqueRids[encoding.rid]) { + throw new this._win.DOMException("Duplicate rid", "TypeError"); + } + uniqueRids[encoding.rid] = true; + return uniqueRids; + }, {}); + + this._impl.setParameters(sender.track, parameters); + }, + + _getParameters: function(sender) { + if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) { + return; + } + return this._impl.getParameters(sender.track); + }, + + close: function() { + if (this._closed) { + return; + } + this._closed = true; + this._inClose = true; + this.changeIceConnectionState("closed"); + this._localIdp.close(); + this._remoteIdp.close(); + this._impl.close(); + this._inClose = false; + }, + + getLocalStreams: function() { + this._checkClosed(); + return this._impl.getLocalStreams(); + }, + + getRemoteStreams: function() { + this._checkClosed(); + return this._impl.getRemoteStreams(); + }, + + getSenders: function() { + return this._senders; + }, + + getReceivers: function() { + return this._receivers; + }, + + mozSelectSsrc: function(receiver, ssrcIndex) { + this._impl.selectSsrc(receiver.track, ssrcIndex); + }, + + get localDescription() { + this._checkClosed(); + let sdp = this._impl.localDescription; + if (sdp.length == 0) { + return null; + } + + return new this._win.RTCSessionDescription({ type: this._localType, + sdp: sdp }); + }, + + get remoteDescription() { + this._checkClosed(); + let sdp = this._impl.remoteDescription; + if (sdp.length == 0) { + return null; + } + return new this._win.RTCSessionDescription({ type: this._remoteType, + sdp: sdp }); + }, + + get peerIdentity() { return this._peerIdentity; }, + get idpLoginUrl() { return this._localIdp.idpLoginUrl; }, + get id() { return this._impl.id; }, + set id(s) { this._impl.id = s; }, + get iceGatheringState() { return this._iceGatheringState; }, + get iceConnectionState() { return this._iceConnectionState; }, + + get signalingState() { + // checking for our local pc closed indication + // before invoking the pc methods. + if (this._closed) { + return "closed"; + } + return { + "SignalingInvalid": "", + "SignalingStable": "stable", + "SignalingHaveLocalOffer": "have-local-offer", + "SignalingHaveRemoteOffer": "have-remote-offer", + "SignalingHaveLocalPranswer": "have-local-pranswer", + "SignalingHaveRemotePranswer": "have-remote-pranswer", + "SignalingClosed": "closed" + }[this._impl.signalingState]; + }, + + changeIceGatheringState: function(state) { + this._iceGatheringState = state; + _globalPCList.notifyLifecycleObservers(this, "icegatheringstatechange"); + }, + + changeIceConnectionState: function(state) { + this._iceConnectionState = state; + _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange"); + this.dispatchEvent(new this._win.Event("iceconnectionstatechange")); + }, + + getStats: function(selector, onSuccess, onError) { + return this._legacyCatchAndCloseGuard(onSuccess, onError, () => { + return this._chain(() => new this._win.Promise((resolve, reject) => { + this._onGetStatsSuccess = resolve; + this._onGetStatsFailure = reject; + this._impl.getStats(selector); + })); + }); + }, + + createDataChannel: function(label, dict) { + this._checkClosed(); + if (dict == undefined) { + dict = {}; + } + if (dict.maxRetransmitNum != undefined) { + dict.maxRetransmits = dict.maxRetransmitNum; + this.logWarning("Deprecated RTCDataChannelInit dictionary entry maxRetransmitNum used!"); + } + if (dict.outOfOrderAllowed != undefined) { + dict.ordered = !dict.outOfOrderAllowed; // the meaning is swapped with + // the name change + this.logWarning("Deprecated RTCDataChannelInit dictionary entry outOfOrderAllowed used!"); + } + + if (dict.preset != undefined) { + dict.negotiated = dict.preset; + this.logWarning("Deprecated RTCDataChannelInit dictionary entry preset used!"); + } + if (dict.stream != undefined) { + dict.id = dict.stream; + this.logWarning("Deprecated RTCDataChannelInit dictionary entry stream used!"); + } + + if (dict.maxRetransmitTime !== null && dict.maxRetransmits !== null) { + throw new this._win.DOMException( + "Both maxRetransmitTime and maxRetransmits cannot be provided", + "InvalidParameterError"); + } + let protocol; + if (dict.protocol == undefined) { + protocol = ""; + } else { + protocol = dict.protocol; + } + + // Must determine the type where we still know if entries are undefined. + let type; + if (dict.maxRetransmitTime != undefined) { + type = Ci.IPeerConnection.kDataChannelPartialReliableTimed; + } else if (dict.maxRetransmits != undefined) { + type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit; + } else { + type = Ci.IPeerConnection.kDataChannelReliable; + } + + // Synchronous since it doesn't block. + let channel = this._impl.createDataChannel( + label, protocol, type, !dict.ordered, dict.maxRetransmitTime, + dict.maxRetransmits, dict.negotiated ? true : false, + dict.id != undefined ? dict.id : 0xFFFF + ); + return channel; + } +}; + +// This is a separate object because we don't want to expose it to DOM. +function PeerConnectionObserver() { + this._dompc = null; +} +PeerConnectionObserver.prototype = { + classDescription: "PeerConnectionObserver", + classID: PC_OBS_CID, + contractID: PC_OBS_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer]), + init: function(win) { this._win = win; }, + + __init: function(dompc) { + this._dompc = dompc._innerObject; + }, + + newError: function(message, code) { + // These strings must match those defined in the WebRTC spec. + const reasonName = [ + "", + "InternalError", + "InvalidCandidateError", + "InvalidParameterError", + "InvalidStateError", + "InvalidSessionDescriptionError", + "IncompatibleSessionDescriptionError", + "InternalError", + "IncompatibleMediaStreamTrackError", + "InternalError" + ]; + let name = reasonName[Math.min(code, reasonName.length - 1)]; + return new this._dompc._win.DOMException(message, name); + }, + + dispatchEvent: function(event) { + this._dompc.dispatchEvent(event); + }, + + onCreateOfferSuccess: function(sdp) { + this._dompc._onCreateOfferSuccess(sdp); + }, + + onCreateOfferError: function(code, message) { + this._dompc._onCreateOfferFailure(this.newError(message, code)); + }, + + onCreateAnswerSuccess: function(sdp) { + this._dompc._onCreateAnswerSuccess(sdp); + }, + + onCreateAnswerError: function(code, message) { + this._dompc._onCreateAnswerFailure(this.newError(message, code)); + }, + + onSetLocalDescriptionSuccess: function() { + this._dompc._onSetLocalDescriptionSuccess(); + }, + + onSetRemoteDescriptionSuccess: function() { + this._dompc._onSetRemoteDescriptionSuccess(); + }, + + onSetLocalDescriptionError: function(code, message) { + this._localType = null; + this._dompc._onSetLocalDescriptionFailure(this.newError(message, code)); + }, + + onSetRemoteDescriptionError: function(code, message) { + this._remoteType = null; + this._dompc._onSetRemoteDescriptionFailure(this.newError(message, code)); + }, + + onAddIceCandidateSuccess: function() { + this._dompc._onAddIceCandidateSuccess(); + }, + + onAddIceCandidateError: function(code, message) { + this._dompc._onAddIceCandidateError(this.newError(message, code)); + }, + + onIceCandidate: function(level, mid, candidate) { + if (candidate == "") { + this.foundIceCandidate(null); + } else { + this.foundIceCandidate(new this._dompc._win.RTCIceCandidate( + { + candidate: candidate, + sdpMid: mid, + sdpMLineIndex: level + } + )); + } + }, + + onNegotiationNeeded: function() { + this.dispatchEvent(new this._win.Event("negotiationneeded")); + }, + + + // This method is primarily responsible for updating iceConnectionState. + // This state is defined in the WebRTC specification as follows: + // + // iceConnectionState: + // ------------------- + // new The ICE Agent is gathering addresses and/or waiting for + // remote candidates to be supplied. + // + // checking The ICE Agent has received remote candidates on at least + // one component, and is checking candidate pairs but has not + // yet found a connection. In addition to checking, it may + // also still be gathering. + // + // connected The ICE Agent has found a usable connection for all + // components but is still checking other candidate pairs to + // see if there is a better connection. It may also still be + // gathering. + // + // completed The ICE Agent has finished gathering and checking and found + // a connection for all components. Open issue: it is not + // clear how the non controlling ICE side knows it is in the + // state. + // + // failed The ICE Agent is finished checking all candidate pairs and + // failed to find a connection for at least one component. + // Connections may have been found for some components. + // + // disconnected Liveness checks have failed for one or more components. + // This is more aggressive than failed, and may trigger + // intermittently (and resolve itself without action) on a + // flaky network. + // + // closed The ICE Agent has shut down and is no longer responding to + // STUN requests. + + handleIceConnectionStateChange: function(iceConnectionState) { + let pc = this._dompc; + if (pc.iceConnectionState === 'new') { + var checking_histogram = Services.telemetry.getHistogramById("WEBRTC_ICE_CHECKING_RATE"); + if (iceConnectionState === 'checking') { + checking_histogram.add(true); + } else if (iceConnectionState === 'failed') { + checking_histogram.add(false); + } + } else if (pc.iceConnectionState === 'checking') { + var success_histogram = Services.telemetry.getHistogramById("WEBRTC_ICE_SUCCESS_RATE"); + if (iceConnectionState === 'completed' || + iceConnectionState === 'connected') { + success_histogram.add(true); + } else if (iceConnectionState === 'failed') { + success_histogram.add(false); + } + } + + if (iceConnectionState === 'failed') { + pc.logError("ICE failed, see about:webrtc for more details"); + } + + pc.changeIceConnectionState(iceConnectionState); + }, + + // This method is responsible for updating iceGatheringState. This + // state is defined in the WebRTC specification as follows: + // + // iceGatheringState: + // ------------------ + // new The object was just created, and no networking has occurred + // yet. + // + // gathering The ICE engine is in the process of gathering candidates for + // this RTCPeerConnection. + // + // complete The ICE engine has completed gathering. Events such as adding + // a new interface or a new TURN server will cause the state to + // go back to gathering. + // + handleIceGatheringStateChange: function(gatheringState) { + this._dompc.changeIceGatheringState(gatheringState); + }, + + onStateChange: function(state) { + switch (state) { + case "SignalingState": + this.dispatchEvent(new this._win.Event("signalingstatechange")); + break; + + case "IceConnectionState": + this.handleIceConnectionStateChange(this._dompc._pc.iceConnectionState); + break; + + case "IceGatheringState": + this.handleIceGatheringStateChange(this._dompc._pc.iceGatheringState); + break; + + case "SdpState": + // No-op + break; + + case "ReadyState": + // No-op + break; + + case "SipccState": + // No-op + break; + + default: + this._dompc.logWarning("Unhandled state type: " + state); + break; + } + }, + + onGetStatsSuccess: function(dict) { + let pc = this._dompc; + let chromeobj = new RTCStatsReport(pc._win, dict); + let webidlobj = pc._win.RTCStatsReport._create(pc._win, chromeobj); + chromeobj.makeStatsPublic(pc._warnDeprecatedStatsAccessNullable); + pc._onGetStatsSuccess(webidlobj); + }, + + onGetStatsError: function(code, message) { + this._dompc._onGetStatsFailure(this.newError(message, code)); + }, + + onAddStream: function(stream) { + let ev = new this._dompc._win.MediaStreamEvent("addstream", + { stream: stream }); + this.dispatchEvent(ev); + }, + + onRemoveStream: function(stream) { + this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream", + { stream: stream })); + }, + + onAddTrack: function(track, streams) { + let pc = this._dompc; + let receiver = pc._win.RTCRtpReceiver._create(pc._win, + new RTCRtpReceiver(this, + track)); + pc._receivers.push(receiver); + let ev = new pc._win.RTCTrackEvent("track", + { receiver: receiver, + track: track, + streams: streams }); + this.dispatchEvent(ev); + + // Fire legacy event as well for a little bit. + ev = new pc._win.MediaStreamTrackEvent("addtrack", { track: track }); + this.dispatchEvent(ev); + }, + + onRemoveTrack: function(track) { + let pc = this._dompc; + let i = pc._receivers.findIndex(receiver => receiver.track == track); + if (i >= 0) { + pc._receivers.splice(i, 1); + } + }, + + onReplaceTrackSuccess: function() { + var pc = this._dompc; + pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack; + pc._onReplaceTrackWithTrack = null; + pc._onReplaceTrackSender = null; + pc._onReplaceTrackSuccess(); + }, + + onReplaceTrackError: function(code, message) { + var pc = this._dompc; + pc._onReplaceTrackWithTrack = null; + pc._onReplaceTrackSender = null; + pc._onReplaceTrackFailure(this.newError(message, code)); + }, + + foundIceCandidate: function(cand) { + this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate", + { candidate: cand } )); + }, + + notifyDataChannel: function(channel) { + this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel", + { channel: channel })); + }, + + onDTMFToneChange: function(trackId, tone) { + var pc = this._dompc; + var sender = pc._senders.find(sender => sender.track.id == trackId) + sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange", + { tone: tone })); + } +}; + +function RTCPeerConnectionStatic() { +} +RTCPeerConnectionStatic.prototype = { + classDescription: "RTCPeerConnectionStatic", + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer]), + + classID: PC_STATIC_CID, + contractID: PC_STATIC_CONTRACT, + + init: function(win) { + this._winID = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + }, + + registerPeerConnectionLifecycleCallback: function(cb) { + _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb); + }, +}; + +function RTCDTMFSender(sender) { + this._sender = sender; +} +RTCDTMFSender.prototype = { + classDescription: "RTCDTMFSender", + classID: PC_DTMF_SENDER_CID, + contractID: PC_DTMF_SENDER_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + get toneBuffer() { + return this._sender._pc._getDTMFToneBuffer(this._sender); + }, + + get ontonechange() { + return this.__DOM_IMPL__.getEventHandler("ontonechange"); + }, + + set ontonechange(handler) { + this.__DOM_IMPL__.setEventHandler("ontonechange", handler); + }, + + insertDTMF: function(tones, duration, interToneGap) { + this._sender._pc._checkClosed(); + + if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) { + throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped", + "InvalidStateError"); + } + + duration = Math.max(40, Math.min(duration, 6000)); + if (interToneGap < 30) interToneGap = 30; + + tones = tones.toUpperCase(); + + if (tones.match(/[^0-9A-D#*,]/)) { + throw new this._sender._pc._win.DOMException("Invalid DTMF characters", + "InvalidCharacterError"); + } + + this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap); + }, +}; + +function RTCRtpSender(pc, track, stream) { + this._pc = pc; + this.track = track; + this._stream = stream; + this.dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this)); +} +RTCRtpSender.prototype = { + classDescription: "RTCRtpSender", + classID: PC_SENDER_CID, + contractID: PC_SENDER_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + replaceTrack: function(withTrack) { + return this._pc._chain(() => this._pc._replaceTrack(this, withTrack)); + }, + + setParameters: function(parameters) { + return this._pc._win.Promise.resolve() + .then(() => this._pc._setParameters(this, parameters)); + }, + + getParameters: function() { + return this._pc._getParameters(this); + } +}; + +function RTCRtpReceiver(pc, track) { + this._pc = pc; + this.track = track; +} +RTCRtpReceiver.prototype = { + classDescription: "RTCRtpReceiver", + classID: PC_RECEIVER_CID, + contractID: PC_RECEIVER_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), +}; + +function CreateOfferRequest(windowID, innerWindowID, callID, isSecure) { + this.windowID = windowID; + this.innerWindowID = innerWindowID; + this.callID = callID; + this.isSecure = isSecure; +} +CreateOfferRequest.prototype = { + classDescription: "CreateOfferRequest", + classID: PC_COREQUEST_CID, + contractID: PC_COREQUEST_CONTRACT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory( + [GlobalPCList, + RTCDTMFSender, + RTCIceCandidate, + RTCSessionDescription, + RTCPeerConnection, + RTCPeerConnectionStatic, + RTCRtpReceiver, + RTCRtpSender, + RTCStatsReport, + PeerConnectionObserver, + CreateOfferRequest] +); |