summaryrefslogtreecommitdiffstats
path: root/dom/media/PeerConnection.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/PeerConnection.js')
-rw-r--r--dom/media/PeerConnection.js1680
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]
+);