mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
5c1742c0d9
This change RTCPeerConnection.addTrack() to allow any MediaStream as argument. The MediaStream is in the end used as an identifier of how the tracks will be grouped together on the receiving side of the peer connection. MozReview-Commit-ID: 9wDPOmMHYDc --HG-- extra : rebase_source : 5fd59853c2d207cbcdaa1e4a767b3c4de20a1beb extra : histedit_source : 2b88e899a329df07a46c1f12e449956d59645cf7
1648 lines
57 KiB
JavaScript
1648 lines
57 KiB
JavaScript
/* 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_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}");
|
|
|
|
// 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 == "network:app-offline-status-changed") {
|
|
// App changed offline status. The subject contains the appId for which
|
|
// we need to check the status
|
|
let appId = subject.QueryInterface(Ci.nsIAppOfflineInfo).appId;
|
|
let ios = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
|
|
for (let winId in this._list) {
|
|
if (appId != this._list[winId]._appId) {
|
|
continue;
|
|
}
|
|
if (ios.isAppOffline(appId)) {
|
|
cleanupWinId(this._list, winId);
|
|
}
|
|
}
|
|
} 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");
|
|
}
|
|
// Save the appId
|
|
var principal = Cu.getWebIDLCallerPrincipal();
|
|
this._appId = principal.appId;
|
|
this._isChrome = Services.scriptSecurityManager.isSystemPrincipal(principal);
|
|
|
|
// Get the offline status for this appId
|
|
let appOffline = false;
|
|
if (this._appId != Ci.nsIScriptSecurityManager.NO_APP_ID &&
|
|
this._appId != Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
|
|
let ios = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
|
|
appOffline = ios.isAppOffline(this._appId);
|
|
}
|
|
|
|
if (_globalPCList._networkdown || appOffline) {
|
|
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;
|
|
this._isLoop = location.startsWith("about:loop") ||
|
|
location.startsWith("https://hello.firefox.com/");
|
|
|
|
// 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.
|
|
|
|
_legacyCatch: function(onSuccess, onError, func) {
|
|
if (!onSuccess) {
|
|
return func();
|
|
}
|
|
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) {
|
|
throw new this._win.DOMException(msg + " - missing username: " + urlStr,
|
|
"InvalidAccessError");
|
|
}
|
|
if (!server.credential) {
|
|
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._legacyCatch(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._legacyCatch(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._legacyCatch(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._legacyCatch(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._legacyCatch(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));
|
|
},
|
|
|
|
removeStream: function(stream) {
|
|
// Bug 844295: Not implementing this functionality.
|
|
throw new this._win.DOMException("removeStream not yet implemented",
|
|
"NotSupportedError");
|
|
},
|
|
|
|
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
|
|
}
|
|
},
|
|
|
|
_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._legacyCatch(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(pc._isLoop ?
|
|
"LOOP_ICE_SUCCESS_RATE" : "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 }));
|
|
}
|
|
};
|
|
|
|
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 RTCRtpSender(pc, track, stream) {
|
|
this._pc = pc;
|
|
this.track = track;
|
|
this._stream = stream;
|
|
}
|
|
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,
|
|
RTCIceCandidate,
|
|
RTCSessionDescription,
|
|
RTCPeerConnection,
|
|
RTCPeerConnectionStatic,
|
|
RTCRtpReceiver,
|
|
RTCRtpSender,
|
|
RTCStatsReport,
|
|
PeerConnectionObserver,
|
|
CreateOfferRequest]
|
|
);
|