diff options
Diffstat (limited to 'dom/media/PeerConnection.sys.mjs')
-rw-r--r-- | dom/media/PeerConnection.sys.mjs | 2022 |
1 files changed, 2022 insertions, 0 deletions
diff --git a/dom/media/PeerConnection.sys.mjs b/dom/media/PeerConnection.sys.mjs new file mode 100644 index 0000000000..adb9cc3ba3 --- /dev/null +++ b/dom/media/PeerConnection.sys.mjs @@ -0,0 +1,2022 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + PeerConnectionIdp: "resource://gre/modules/media/PeerConnectionIdp.sys.mjs", +}); + +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_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;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_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}"); +const PC_COREQUEST_CID = Components.ID( + "{74b2122d-65a8-4824-aa9e-3d664cb75dc2}" +); + +function logMsg(msg, file, line, flag, winID) { + let scriptErrorClass = Cc["@mozilla.org/scripterror;1"]; + let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError); + scriptError.initWithWindowID( + `WebRTC: ${msg}`, + file, + null, + line, + 0, + flag, + "content javascript", + winID + ); + Services.console.logMessage(scriptError); +} + +let setupPrototype = (_class, dict) => { + _class.prototype.classDescription = _class.name; + Object.assign(_class.prototype, dict); +}; + +// 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). +export class GlobalPCList { + constructor() { + 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 (Services.cpmm) { + Services.cpmm.addMessageListener("gmp-plugin-crash", this); + } + } + + notifyLifecycleObservers(pc, type) { + for (var key of Object.keys(this._lifecycleobservers)) { + this._lifecycleobservers[key](pc, pc._winID, type); + } + } + + addPC(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(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; + } + } + } + } + return null; + } + + removeNullRefs(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]; + } + } + + handleGMPCrash(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({ name, data }) { + if (name == "gmp-plugin-crash") { + this.handleGMPCrash(data); + } + } + + observe(subject, topic, data) { + let cleanupPcRef = function (pcref) { + let pc = pcref.get(); + if (pc) { + pc._suppressEvents = true; + pc.close(); + } + }; + + 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" + ) { + // As Necko doesn't prevent us from accessing the network we still need to + // monitor the network offline/online state here. See bug 1326483 + this._networkdown = true; + } else if (topic == "network:offline-status-changed") { + if (data == "offline") { + 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(winID, cb) { + this._lifecycleobservers[winID] = cb; + } +} + +setupPrototype(GlobalPCList, { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + classID: PC_MANAGER_CID, +}); + +var _globalPCList = new GlobalPCList(); + +export class RTCIceCandidate { + init(win) { + this._win = win; + } + + __init(dict) { + if (dict.sdpMid == null && dict.sdpMLineIndex == null) { + throw new this._win.TypeError( + "Either sdpMid or sdpMLineIndex must be specified" + ); + } + Object.assign(this, dict); + } +} + +setupPrototype(RTCIceCandidate, { + classID: PC_ICE_CID, + contractID: PC_ICE_CONTRACT, + QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), +}); + +export class RTCSessionDescription { + init(win) { + this._win = win; + this._winID = this._win.windowGlobalChild.innerWindowId; + } + + __init({ type, sdp }) { + if (!type) { + throw new this._win.TypeError( + "Missing required 'type' member of RTCSessionDescriptionInit" + ); + } + Object.assign(this, { _type: type, _sdp: sdp }); + } + + get type() { + return this._type; + } + set type(type) { + this.warn(); + this._type = type; + } + + get sdp() { + return this._sdp; + } + set sdp(sdp) { + this.warn(); + this._sdp = sdp; + } + + warn() { + if (!this._warned) { + // Warn once per RTCSessionDescription about deprecated writable usage. + this.logWarning( + "RTCSessionDescription's members are readonly! " + + "Writing to them is deprecated and will break soon!" + ); + this._warned = true; + } + } + + logWarning(msg) { + let err = this._win.Error(); + logMsg( + msg, + err.fileName, + err.lineNumber, + Ci.nsIScriptError.warningFlag, + this._winID + ); + } +} + +setupPrototype(RTCSessionDescription, { + classID: PC_SESSION_CID, + contractID: PC_SESSION_CONTRACT, + QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), +}); + +// Records PC related telemetry +class PeerConnectionTelemetry { + // ICE connection state enters connected or completed. + recordConnected() { + Services.telemetry.scalarAdd("webrtc.peerconnection.connected", 1); + this.recordConnected = () => {}; + } + // DataChannel is created + _recordDataChannelCreated() { + Services.telemetry.scalarAdd( + "webrtc.peerconnection.datachannel_created", + 1 + ); + this._recordDataChannelCreated = () => {}; + } + // DataChannel initialized with maxRetransmitTime + _recordMaxRetransmitTime(maxRetransmitTime) { + if (maxRetransmitTime === undefined) { + return false; + } + Services.telemetry.scalarAdd( + "webrtc.peerconnection.datachannel_max_retx_used", + 1 + ); + this._recordMaxRetransmitTime = () => true; + return true; + } + // DataChannel initialized with maxPacketLifeTime + _recordMaxPacketLifeTime(maxPacketLifeTime) { + if (maxPacketLifeTime === undefined) { + return false; + } + Services.telemetry.scalarAdd( + "webrtc.peerconnection.datachannel_max_life_used", + 1 + ); + this._recordMaxPacketLifeTime = () => true; + return true; + } + // DataChannel initialized + recordDataChannelInit(maxRetransmitTime, maxPacketLifeTime) { + const retxUsed = this._recordMaxRetransmitTime(maxRetransmitTime); + if (this._recordMaxPacketLifeTime(maxPacketLifeTime) && retxUsed) { + Services.telemetry.scalarAdd( + "webrtc.peerconnection.datachannel_max_retx_and_life_used", + 1 + ); + this.recordDataChannelInit = () => {}; + } + this._recordDataChannelCreated(); + } +} + +export class RTCPeerConnection { + constructor() { + this._pc = null; + this._closed = false; + + // 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; + + // So we can record telemetry on state transitions + this._iceConnectionState = "new"; + + this._hasStunServer = this._hasTurnServer = false; + this._iceGatheredRelayCandidates = false; + // Records telemetry + this._pcTelemetry = new PeerConnectionTelemetry(); + } + + init(win) { + this._win = win; + } + + // Pref-based overrides; will _not_ be reflected in getConfiguration + _applyPrefsToConfig(rtcConfig) { + if ( + rtcConfig.iceTransportPolicy == "all" && + Services.prefs.getBoolPref("media.peerconnection.ice.relay_only") + ) { + rtcConfig.iceTransportPolicy = "relay"; + } + + 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._validateIceServers( + rtcConfig.iceServers, + "Ignoring invalid media.peerconnection.default_iceservers in about:config" + ); + } catch (e) { + this.logWarning(e.message); + rtcConfig.iceServers = []; + } + } + } + + _validateConfig(rtcConfig) { + if ("sdpSemantics" in rtcConfig) { + if (rtcConfig.sdpSemantics == "plan-b") { + this.logWarning( + `Outdated and non-standard {sdpSemantics: "plan-b"} is not ` + + `supported! WebRTC may be unreliable. Please update code to ` + + `follow standard "unified-plan".` + ); + } + // Don't let it show up in getConfiguration. + delete rtcConfig.sdpSemantics; + } + + if (this._config) { + // certificates must match + if (rtcConfig.certificates.length != this._config.certificates.length) { + throw new this._win.DOMException( + "Cannot change certificates with setConfiguration (length differs)", + "InvalidModificationError" + ); + } + for (let i = 0; i < rtcConfig.certificates.length; i++) { + if (rtcConfig.certificates[i] != this._config.certificates[i]) { + throw new this._win.DOMException( + `Cannot change certificates with setConfiguration ` + + `(cert at index ${i} differs)`, + "InvalidModificationError" + ); + } + } + + // bundlePolicy must match + if (rtcConfig.bundlePolicy != this._config.bundlePolicy) { + throw new this._win.DOMException( + "Cannot change bundlePolicy with setConfiguration", + "InvalidModificationError" + ); + } + + // peerIdentity must match + if ( + rtcConfig.peerIdentity && + rtcConfig.peerIdentity != this._config.peerIdentity + ) { + throw new this._win.DOMException( + "Cannot change peerIdentity with setConfiguration", + "InvalidModificationError" + ); + } + + // TODO (bug 1339203): rtcpMuxPolicy must match + // TODO (bug 1529398): iceCandidatePoolSize must match if sLD has ever + // been called. + } + + // This gets executed in the typical case when iceServers + // are passed in through the web page. + this._validateIceServers( + rtcConfig.iceServers, + "RTCPeerConnection constructor passed invalid RTCConfiguration" + ); + } + + _checkIfIceRestartRequired(rtcConfig) { + if (this._config) { + if (rtcConfig.iceTransportPolicy != this._config.iceTransportPolicy) { + this._pc.restartIceNoRenegotiationNeeded(); + return; + } + if ( + JSON.stringify(this._config.iceServers) != + JSON.stringify(rtcConfig.iceServers) + ) { + this._pc.restartIceNoRenegotiationNeeded(); + } + } + } + + __init(rtcConfig) { + this._winID = this._win.windowGlobalChild.innerWindowId; + let certificates = rtcConfig.certificates || []; + + if (certificates.some(c => c.expires <= Date.now())) { + throw new this._win.DOMException( + "Unable to create RTCPeerConnection with an expired certificate", + "InvalidAccessError" + ); + } + + // TODO(bug 1531875): Check origin of certs + + // TODO(bug 1176518): Remove this code once we support multiple certs + let certificate; + if (certificates.length == 1) { + certificate = certificates[0]; + } else if (certificates.length) { + throw new this._win.DOMException( + "RTCPeerConnection does not currently support multiple certificates", + "NotSupportedError" + ); + } + + this._documentPrincipal = Cu.getWebIDLCallerPrincipal(); + + 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("ondatachannel"); + this.makeGetterSetterEH("oniceconnectionstatechange"); + this.makeGetterSetterEH("onicegatheringstatechange"); + this.makeGetterSetterEH("onconnectionstatechange"); + this.makeGetterSetterEH("onidentityresult"); + this.makeGetterSetterEH("onpeeridentity"); + this.makeGetterSetterEH("onidpassertionerror"); + this.makeGetterSetterEH("onidpvalidationerror"); + + this._pc = new this._win.PeerConnectionImpl(); + + this.__DOM_IMPL__._innerObject = this; + const observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__); + + // Add a reference to the PeerConnection to global list (before init). + _globalPCList.addPC(this); + + this._pc.initialize(observer, this._win); + + this.setConfiguration(rtcConfig); + + this._certificateReady = this._initCertificate(certificate); + this._initIdp(); + _globalPCList.notifyLifecycleObservers(this, "initialized"); + } + + getConfiguration() { + const config = Object.assign({}, this._config); + delete config.sdpSemantics; + return config; + } + + setConfiguration(rtcConfig) { + this._checkClosed(); + this._validateConfig(rtcConfig); + this._checkIfIceRestartRequired(rtcConfig); + + // Allow prefs to tweak these settings before passing to c++, but hide all + // of that from JS. + const configWithPrefTweaks = Object.assign({}, rtcConfig); + this._applyPrefsToConfig(configWithPrefTweaks); + this._pc.setConfiguration(configWithPrefTweaks); + + this._config = Object.assign({}, rtcConfig); + } + + async _initCertificate(certificate) { + if (!certificate) { + certificate = await this._win.RTCPeerConnection.generateCertificate({ + name: "ECDSA", + namedCurve: "P-256", + }); + } + this._pc.certificate = certificate; + } + + _resetPeerIdentityPromise() { + this._peerIdentity = new this._win.Promise((resolve, reject) => { + this._resolvePeerIdentity = resolve; + this._rejectPeerIdentity = reject; + }); + } + + _initIdp() { + this._resetPeerIdentityPromise(); + this._lastIdentityValidation = this._win.Promise.resolve(); + + let prefName = "media.peerconnection.identity.timeout"; + let idpTimeout = Services.prefs.getIntPref(prefName); + this._localIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout); + this._remoteIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout); + } + + // Add a function to the internal operations chain. + + _chain(operation) { + return this._pc.chain(operation); + } + + // It's basically impossible to use async directly in JSImplemented code, + // because the implicit promise must be wrapped to the right type for content. + // + // The _async wrapper takes care of this. The _legacy wrapper implements + // 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. + // + // These wrappers also serve as guards against settling promises past close(). + + _async(func) { + return this._win.Promise.resolve(this._closeWrapper(func)); + } + + _legacy(...args) { + return this._win.Promise.resolve(this._legacyCloseWrapper(...args)); + } + + _auto(onSucc, onErr, func) { + return typeof onSucc == "function" + ? this._legacy(onSucc, onErr, func) + : this._async(func); + } + + async _closeWrapper(func) { + let closed = this._closed; + try { + let result = await func(); + if (!closed && this._closed) { + await new Promise(() => {}); + } + return result; + } catch (e) { + if (!closed && this._closed) { + await new Promise(() => {}); + } + throw e; + } + } + + async _legacyCloseWrapper(onSucc, onErr, func) { + let wrapCallback = cb => result => { + try { + cb && cb(result); + } catch (e) { + this.logErrorAndCallOnError(e); + } + }; + + try { + wrapCallback(onSucc)(await func()); + } catch (e) { + wrapCallback(onErr)(e); + } + } + + // This implements the fairly common "Queue a task" logic + async _queueTaskWithClosedCheck(func) { + const pc = this; + return new this._win.Promise((resolve, reject) => { + Services.tm.dispatchToMainThread({ + run() { + try { + if (!pc._closed) { + func(); + resolve(); + } + } catch (e) { + reject(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. + */ + _validateIceServers(iceServers, msg) { + // Normalize iceServers input + 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 nicerNewURI = uriStr => { + try { + return Services.io.newURI(uriStr); + } catch (e) { + if (e.result == Cr.NS_ERROR_MALFORMED_URI) { + throw new this._win.DOMException( + `${msg} - malformed URI: ${uriStr}`, + "SyntaxError" + ); + } + throw e; + } + }; + + var stunServers = 0; + + iceServers.forEach(({ urls, username, credential, credentialType }) => { + if (!urls) { + // TODO: Remove once url is deprecated (Bug 1369563) + throw new this._win.TypeError( + "Missing required 'urls' member of RTCIceServer" + ); + } + if (!urls.length) { + throw new this._win.DOMException( + `${msg} - urls is empty`, + "SyntaxError" + ); + } + urls + .map(url => nicerNewURI(url)) + .forEach(({ scheme, spec, query }) => { + if (scheme in { turn: 1, turns: 1 }) { + if (username == undefined) { + throw new this._win.DOMException( + `${msg} - missing username: ${spec}`, + "InvalidAccessError" + ); + } + if (username.length > 512) { + throw new this._win.DOMException( + `${msg} - username longer then 512 bytes: ${username}`, + "InvalidAccessError" + ); + } + if (credential == undefined) { + throw new this._win.DOMException( + `${msg} - missing credential: ${spec}`, + "InvalidAccessError" + ); + } + if (credentialType != "password") { + this.logWarning( + `RTCConfiguration TURN credentialType \"${credentialType}\"` + + " is not yet implemented. Treating as password." + + " https://bugzil.la/1247616" + ); + } + this._hasTurnServer = true; + // If this is not a TURN TCP/TLS server, it is also a STUN server + const parameters = query.split("&"); + if (!parameters.includes("transport=tcp")) { + this._hasStunServer = true; + } + stunServers += 1; + } else if (scheme in { stun: 1, stuns: 1 }) { + this._hasStunServer = true; + stunServers += 1; + } else { + throw new this._win.DOMException( + `${msg} - improper scheme: ${scheme}`, + "SyntaxError" + ); + } + if (scheme in { stuns: 1 }) { + this.logWarning(scheme.toUpperCase() + " is not yet supported."); + } + if (stunServers >= 5) { + this.logError( + "Using five or more STUN/TURN servers causes problems" + ); + } else if (stunServers > 2) { + this.logWarning( + "Using more than two STUN/TURN servers slows down discovery" + ); + } + }); + }); + } + + // 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() { + if (this._closed) { + throw new this._win.DOMException( + "Peer connection is closed", + "InvalidStateError" + ); + } + } + + dispatchEvent(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._suppressEvents) { + this.__DOM_IMPL__.dispatchEvent(event); + } + } + + // Log error message to web console and window.onerror, if present. + logErrorAndCallOnError(e) { + this.logMsg( + e.message, + e.fileName, + e.lineNumber, + Ci.nsIScriptError.errorFlag + ); + + // 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(msg) { + this.logStackMsg(msg, Ci.nsIScriptError.errorFlag); + } + + logWarning(msg) { + this.logStackMsg(msg, Ci.nsIScriptError.warningFlag); + } + + logStackMsg(msg, flag) { + let err = this._win.Error(); + this.logMsg(msg, err.fileName, err.lineNumber, flag); + } + + logMsg(msg, file, line, flag) { + return logMsg(msg, file, line, flag, this._winID); + } + + getEH(type) { + return this.__DOM_IMPL__.getEventHandler(type); + } + + setEH(type, handler) { + this.__DOM_IMPL__.setEventHandler(type, handler); + } + + makeGetterSetterEH(name) { + Object.defineProperty(this, name, { + get() { + return this.getEH(name); + }, + set(h) { + this.setEH(name, h); + }, + }); + } + + makeLegacyGetterSetterEH(name, msg) { + Object.defineProperty(this, name, { + get() { + return this.getEH(name); + }, + set(h) { + this.logWarning(name + " is deprecated! " + msg); + this.setEH(name, h); + }, + }); + } + + createOffer(optionsOrOnSucc, onErr, options) { + let onSuccess = null; + if (typeof optionsOrOnSucc == "function") { + onSuccess = optionsOrOnSucc; + } else { + options = optionsOrOnSucc; + } + // This entry-point handles both new and legacy call sig. Decipher which one + if (onSuccess) { + return this._legacy(onSuccess, onErr, () => this._createOffer(options)); + } + return this._async(() => this._createOffer(options)); + } + + // Ensures that we have at least one transceiver of |kind| that is + // configured to receive. It will create one if necessary. + _ensureOfferToReceive(kind) { + let hasRecv = this.getTransceivers().some( + transceiver => + transceiver.getKind() == kind && + (transceiver.direction == "sendrecv" || + transceiver.direction == "recvonly") && + !transceiver.stopped + ); + + if (!hasRecv) { + this._addTransceiverNoEvents(kind, { direction: "recvonly" }); + } + } + + // Handles offerToReceiveAudio/Video + _ensureTransceiversForOfferToReceive(options) { + if (options.offerToReceiveAudio) { + this._ensureOfferToReceive("audio"); + } + + if (options.offerToReceiveVideo) { + this._ensureOfferToReceive("video"); + } + + this.getTransceivers() + .filter(transceiver => { + return ( + (options.offerToReceiveVideo === false && + transceiver.receiver.track.kind == "video") || + (options.offerToReceiveAudio === false && + transceiver.receiver.track.kind == "audio") + ); + }) + .forEach(transceiver => { + if (transceiver.direction == "sendrecv") { + transceiver.setDirectionInternal("sendonly"); + } else if (transceiver.direction == "recvonly") { + transceiver.setDirectionInternal("inactive"); + } + }); + } + + _createOffer(options) { + this._checkClosed(); + this._ensureTransceiversForOfferToReceive(options); + return this._chain(() => this._createAnOffer(options)); + } + + async _createAnOffer(options = {}) { + switch (this.signalingState) { + case "stable": + case "have-local-offer": + break; + default: + throw new this._win.DOMException( + `Cannot create offer in ${this.signalingState}`, + "InvalidStateError" + ); + } + let haveAssertion; + if (this._localIdp.enabled) { + haveAssertion = this._getIdentityAssertion(); + } + await this._getPermission(); + await this._certificateReady; + let sdp = await new Promise((resolve, reject) => { + this._onCreateOfferSuccess = resolve; + this._onCreateOfferFailure = reject; + this._pc.createOffer(options); + }); + if (haveAssertion) { + await haveAssertion; + sdp = this._localIdp.addIdentityAttribute(sdp); + } + return Cu.cloneInto({ type: "offer", sdp }, this._win); + } + + createAnswer(optionsOrOnSucc, onErr) { + // This entry-point handles both new and legacy call sig. Decipher which one + if (typeof optionsOrOnSucc == "function") { + return this._legacy(optionsOrOnSucc, onErr, () => this._createAnswer({})); + } + return this._async(() => this._createAnswer(optionsOrOnSucc)); + } + + _createAnswer(options) { + this._checkClosed(); + return this._chain(() => this._createAnAnswer()); + } + + async _createAnAnswer() { + if (this.signalingState != "have-remote-offer") { + throw new this._win.DOMException( + `Cannot create answer in ${this.signalingState}`, + "InvalidStateError" + ); + } + let haveAssertion; + if (this._localIdp.enabled) { + haveAssertion = this._getIdentityAssertion(); + } + await this._getPermission(); + await this._certificateReady; + let sdp = await new Promise((resolve, reject) => { + this._onCreateAnswerSuccess = resolve; + this._onCreateAnswerFailure = reject; + this._pc.createAnswer(); + }); + if (haveAssertion) { + await haveAssertion; + sdp = this._localIdp.addIdentityAttribute(sdp); + } + return Cu.cloneInto({ type: "answer", sdp }, this._win); + } + + async _getPermission() { + if (!this._havePermission) { + const privileged = + this._documentPrincipal.isSystemPrincipal || + Services.prefs.getBoolPref("media.navigator.permission.disabled"); + + if (privileged) { + this._havePermission = Promise.resolve(); + } else { + this._havePermission = new Promise((resolve, reject) => { + this._settlePermission = { allow: resolve, deny: reject }; + let outerId = this._win.docShell.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"); + }); + } + } + return this._havePermission; + } + + _sanityCheckSdp(sdp) { + // The fippo butter finger filter AKA non-ASCII chars + // Note: SDP allows non-ASCII character in the subject (who cares?) + // eslint-disable-next-line no-control-regex + let pos = sdp.search(/[^\u0000-\u007f]/); + if (pos != -1) { + throw new this._win.DOMException( + "SDP contains non ASCII characters at position " + pos, + "InvalidParameterError" + ); + } + } + + setLocalDescription(desc, onSucc, onErr) { + return this._auto(onSucc, onErr, () => this._setLocalDescription(desc)); + } + + _setLocalDescription({ type, sdp }) { + if (type == "pranswer") { + throw new this._win.DOMException( + "pranswer not yet implemented", + "NotSupportedError" + ); + } + this._checkClosed(); + return this._chain(async () => { + // Avoid Promise.all ahead of synchronous part of spec algorithm, since it + // defers. NOTE: The spec says to return an already-rejected promise in + // some cases, which is difficult to achieve in practice from JS (would + // require avoiding await and then() entirely), but we want to come as + // close as we reasonably can. + const p = this._getPermission(); + if (!type) { + switch (this.signalingState) { + case "stable": + case "have-local-offer": + case "have-remote-pranswer": + type = "offer"; + break; + default: + type = "answer"; + break; + } + } + if (!sdp) { + if (type == "offer") { + await this._createAnOffer(); + } else if (type == "answer") { + await this._createAnAnswer(); + } + } else { + this._sanityCheckSdp(sdp); + } + + try { + await new Promise((resolve, reject) => { + this._onSetDescriptionSuccess = resolve; + this._onSetDescriptionFailure = reject; + this._pc.setLocalDescription(this._actions[type], sdp); + }); + await p; + } catch (e) { + this._pc.onSetDescriptionError(); + throw e; + } + await this._pc.onSetDescriptionSuccess(type, false); + }); + } + + async _validateIdentity(sdp, origin) { + // 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(). + const validate = async () => { + // Access this._pc synchronously in case pc is closed later + const identity = this._pc.peerIdentity; + await this._lastIdentityValidation; + const msg = await this._remoteIdp.verifyIdentityFromSDP(sdp, origin); + // If this pc has an identity already, then the identity in sdp must match + if (identity && (!msg || msg.identity !== identity)) { + throw new this._win.DOMException( + "Peer Identity mismatch, expected: " + identity, + "OperationError" + ); + } + if (this._closed) { + return; + } + if (msg) { + // Set new identity and generate an event. + this._pc.peerIdentity = msg.identity; + this._resolvePeerIdentity( + Cu.cloneInto( + { + idp: this._remoteIdp.provider, + name: msg.identity, + }, + this._win + ) + ); + } + }; + + const haveValidation = validate(); + + // Always eat errors on this chain + this._lastIdentityValidation = haveValidation.catch(() => {}); + + // If validation fails, we have some work to do. Fork it so it cannot + // interfere with the validation chain itself, even if the catch function + // throws. + haveValidation.catch(e => { + if (this._closed) { + return; + } + 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._pc.peerIdentity) { + this._resetPeerIdentityPromise(); + } + }); + + if (this._closed) { + return; + } + // Only wait for IdP validation if we need identity matching + if (this._pc.peerIdentity) { + await haveValidation; + } + } + + setRemoteDescription(desc, onSucc, onErr) { + return this._auto(onSucc, onErr, () => this._setRemoteDescription(desc)); + } + + _setRemoteDescription({ type, sdp }) { + if (!type) { + throw new this._win.TypeError( + "Missing required 'type' member of RTCSessionDescriptionInit" + ); + } + if (type == "pranswer") { + throw new this._win.DOMException( + "pranswer not yet implemented", + "NotSupportedError" + ); + } + this._checkClosed(); + return this._chain(async () => { + try { + if (type == "offer" && this.signalingState == "have-local-offer") { + await new Promise((resolve, reject) => { + this._onSetDescriptionSuccess = resolve; + this._onSetDescriptionFailure = reject; + this._pc.setLocalDescription( + Ci.IPeerConnection.kActionRollback, + "" + ); + }); + await this._pc.onSetDescriptionSuccess("rollback", false); + this._updateCanTrickle(); + } + + if (this._closed) { + return; + } + + this._sanityCheckSdp(sdp); + + const p = this._getPermission(); + + const haveSetRemote = new Promise((resolve, reject) => { + this._onSetDescriptionSuccess = resolve; + this._onSetDescriptionFailure = reject; + this._pc.setRemoteDescription(this._actions[type], sdp); + }); + + if (type != "rollback") { + // Do setRemoteDescription and identity validation in parallel + await this._validateIdentity(sdp); + } + await p; + await haveSetRemote; + } catch (e) { + this._pc.onSetDescriptionError(); + throw e; + } + + await this._pc.onSetDescriptionSuccess(type, true); + this._updateCanTrickle(); + }); + } + + setIdentityProvider(provider, { protocol, usernameHint, peerIdentity } = {}) { + this._checkClosed(); + peerIdentity = peerIdentity || this._pc.peerIdentity; + this._localIdp.setIdentityProvider( + provider, + protocol, + usernameHint, + peerIdentity + ); + } + + async _getIdentityAssertion() { + await this._certificateReady; + return this._localIdp.getIdentityAssertion( + this._pc.fingerprint, + this._documentPrincipal.origin + ); + } + + getIdentityAssertion() { + this._checkClosed(); + return this._win.Promise.resolve( + this._chain(() => this._getIdentityAssertion()) + ); + } + + get canTrickleIceCandidates() { + return this._canTrickle; + } + + _updateCanTrickle() { + 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(cand, onSucc, onErr) { + if ( + cand.candidate != "" && + cand.sdpMid == null && + cand.sdpMLineIndex == null + ) { + throw new this._win.TypeError( + "Cannot add a candidate without specifying either sdpMid or sdpMLineIndex" + ); + } + return this._auto(onSucc, onErr, () => this._addIceCandidate(cand)); + } + + async _addIceCandidate({ + candidate, + sdpMid, + sdpMLineIndex, + usernameFragment, + }) { + this._checkClosed(); + return this._chain(async () => { + if ( + !this._pc.pendingRemoteDescription.length && + !this._pc.currentRemoteDescription.length + ) { + throw new this._win.DOMException( + "No remoteDescription.", + "InvalidStateError" + ); + } + return new Promise((resolve, reject) => { + this._onAddIceCandidateSuccess = resolve; + this._onAddIceCandidateError = reject; + this._pc.addIceCandidate( + candidate, + sdpMid || "", + usernameFragment || "", + sdpMLineIndex + ); + }); + }); + } + + restartIce() { + this._pc.restartIce(); + } + + addStream(stream) { + stream.getTracks().forEach(track => this.addTrack(track, stream)); + } + + addTrack(track, ...streams) { + this._checkClosed(); + + if ( + this.getTransceivers().some( + transceiver => transceiver.sender.track == track + ) + ) { + throw new this._win.DOMException( + "This track is already set on a sender.", + "InvalidAccessError" + ); + } + + let transceiver = this.getTransceivers().find(transceiver => { + return ( + transceiver.sender.track == null && + transceiver.getKind() == track.kind && + !transceiver.stopped && + !transceiver.hasBeenUsedToSend() + ); + }); + + if (transceiver) { + transceiver.sender.setTrack(track); + transceiver.sender.setStreamsImpl(...streams); + if (transceiver.direction == "recvonly") { + transceiver.setDirectionInternal("sendrecv"); + } else if (transceiver.direction == "inactive") { + transceiver.setDirectionInternal("sendonly"); + } + } else { + transceiver = this._addTransceiverNoEvents( + track, + { + streams, + direction: "sendrecv", + }, + true + ); + } + + this.updateNegotiationNeeded(); + return transceiver.sender; + } + + removeTrack(sender) { + this._checkClosed(); + + if (!this._pc.createdSender(sender)) { + throw new this._win.DOMException( + "This sender was not created by this PeerConnection", + "InvalidAccessError" + ); + } + + let transceiver = this.getTransceivers().find( + transceiver => !transceiver.stopped && transceiver.sender == sender + ); + + // If the transceiver was removed due to rollback, let it slide. + if (!transceiver || !sender.track) { + return; + } + + sender.setTrack(null); + if (transceiver.direction == "sendrecv") { + transceiver.setDirectionInternal("recvonly"); + } else if (transceiver.direction == "sendonly") { + transceiver.setDirectionInternal("inactive"); + } + + this.updateNegotiationNeeded(); + } + + _addTransceiverNoEvents(sendTrackOrKind, init, addTrackMagic) { + let sendTrack = null; + let kind; + if (typeof sendTrackOrKind == "string") { + kind = sendTrackOrKind; + switch (kind) { + case "audio": + case "video": + break; + default: + throw new this._win.TypeError("Invalid media kind"); + } + } else { + sendTrack = sendTrackOrKind; + kind = sendTrack.kind; + } + + try { + return this._pc.addTransceiver(init, kind, sendTrack, addTrackMagic); + } catch (e) { + // Exceptions thrown by c++ code do not propagate. In most cases, that's + // fine because we're using Promises, which can be copied. But this is + // not promise-based, so we have to do this sketchy stuff. + const holder = new StructuredCloneHolder( + "", + "", + new ClonedErrorHolder(e) + ); + throw holder.deserialize(this._win); + } + } + + addTransceiver(sendTrackOrKind, init) { + this._checkClosed(); + let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init); + this.updateNegotiationNeeded(); + return transceiver; + } + + updateNegotiationNeeded() { + this._pc.updateNegotiationNeeded(); + } + + close() { + if (this._closed) { + return; + } + this._closed = true; + this.changeIceConnectionState("closed"); + if (this._localIdp) { + this._localIdp.close(); + } + if (this._remoteIdp) { + this._remoteIdp.close(); + } + this._pc.close(); + this._suppressEvents = true; + } + + getLocalStreams() { + this._checkClosed(); + let localStreams = new Set(); + this.getTransceivers().forEach(transceiver => { + transceiver.sender.getStreams().forEach(stream => { + localStreams.add(stream); + }); + }); + return [...localStreams.values()]; + } + + getRemoteStreams() { + this._checkClosed(); + return this._pc.getRemoteStreams(); + } + + getSenders() { + return this.getTransceivers() + .filter(transceiver => !transceiver.stopped) + .map(transceiver => transceiver.sender); + } + + getReceivers() { + return this.getTransceivers() + .filter(transceiver => !transceiver.stopped) + .map(transceiver => transceiver.receiver); + } + + mozSetPacketCallback(callback) { + this._onPacket = callback; + } + + mozEnablePacketDump(level, type, sending) { + this._pc.enablePacketDump(level, type, sending); + } + + mozDisablePacketDump(level, type, sending) { + this._pc.disablePacketDump(level, type, sending); + } + + getTransceivers() { + return this._pc.getTransceivers(); + } + + get localDescription() { + return this.pendingLocalDescription || this.currentLocalDescription; + } + + get currentLocalDescription() { + this._checkClosed(); + const sdp = this._pc.currentLocalDescription; + if (!sdp.length) { + return null; + } + const type = this._pc.currentOfferer ? "offer" : "answer"; + return new this._win.RTCSessionDescription({ type, sdp }); + } + + get pendingLocalDescription() { + this._checkClosed(); + const sdp = this._pc.pendingLocalDescription; + if (!sdp.length) { + return null; + } + const type = this._pc.pendingOfferer ? "offer" : "answer"; + return new this._win.RTCSessionDescription({ type, sdp }); + } + + get remoteDescription() { + return this.pendingRemoteDescription || this.currentRemoteDescription; + } + + get currentRemoteDescription() { + this._checkClosed(); + const sdp = this._pc.currentRemoteDescription; + if (!sdp.length) { + return null; + } + const type = this._pc.currentOfferer ? "answer" : "offer"; + return new this._win.RTCSessionDescription({ type, sdp }); + } + + get pendingRemoteDescription() { + this._checkClosed(); + const sdp = this._pc.pendingRemoteDescription; + if (!sdp.length) { + return null; + } + const type = this._pc.pendingOfferer ? "answer" : "offer"; + return new this._win.RTCSessionDescription({ type, sdp }); + } + + get peerIdentity() { + return this._peerIdentity; + } + get idpLoginUrl() { + return this._localIdp.idpLoginUrl; + } + get id() { + return this._pc.id; + } + set id(s) { + this._pc.id = s; + } + get iceGatheringState() { + return this._pc.iceGatheringState; + } + get iceConnectionState() { + return this._iceConnectionState; + } + get connectionState() { + return this._pc.connectionState; + } + + get signalingState() { + // checking for our local pc closed indication + // before invoking the pc methods. + if (this._closed) { + return "closed"; + } + return this._pc.signalingState; + } + + handleIceGatheringStateChange() { + _globalPCList.notifyLifecycleObservers(this, "icegatheringstatechange"); + this.dispatchEvent(new this._win.Event("icegatheringstatechange")); + if (this.iceGatheringState === "complete") { + this.dispatchEvent( + new this._win.RTCPeerConnectionIceEvent("icecandidate", { + candidate: null, + }) + ); + } + } + + changeIceConnectionState(state) { + if (state != this._iceConnectionState) { + this._iceConnectionState = state; + _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange"); + if (!this._closed) { + this.dispatchEvent(new this._win.Event("iceconnectionstatechange")); + } + } + } + + getStats(selector, onSucc, onErr) { + if (selector !== null) { + let matchingSenders = this.getSenders().filter(s => s.track === selector); + let matchingReceivers = this.getReceivers().filter( + r => r.track === selector + ); + + if (matchingSenders.length + matchingReceivers.length != 1) { + throw new this._win.DOMException( + "track must be associated with a unique sender or receiver, but " + + " is associated with " + + matchingSenders.length + + " senders and " + + matchingReceivers.length + + " receivers.", + "InvalidAccessError" + ); + } + } + + return this._auto(onSucc, onErr, () => this._pc.getStats(selector)); + } + + get sctp() { + return this._pc.sctp; + } + + createDataChannel( + label, + { + maxRetransmits, + ordered, + negotiated, + id = null, + maxRetransmitTime, + maxPacketLifeTime, + protocol, + } = {} + ) { + this._checkClosed(); + this._pcTelemetry.recordDataChannelInit( + maxRetransmitTime, + maxPacketLifeTime + ); + + if (maxPacketLifeTime === undefined) { + maxPacketLifeTime = maxRetransmitTime; + } + + if (maxRetransmitTime !== undefined) { + this.logWarning( + "Use maxPacketLifeTime instead of deprecated maxRetransmitTime which will stop working soon in createDataChannel!" + ); + } + + if (protocol.length > 32767) { + // At least 65536/2 UTF-16 characters. UTF-8 might be too long. + // Spec says to check how long |protocol| and |label| are in _bytes_. This + // is a little ambiguous. For now, examine the length of the utf-8 encoding. + const byteCounter = new TextEncoder(); + + if (byteCounter.encode(protocol).length > 65535) { + throw new this._win.TypeError( + "protocol cannot be longer than 65535 bytes" + ); + } + } + + if (label.length > 32767) { + const byteCounter = new TextEncoder(); + if (byteCounter.encode(label).length > 65535) { + throw new this._win.TypeError( + "label cannot be longer than 65535 bytes" + ); + } + } + + if (!negotiated) { + id = null; + } else if (id === null) { + throw new this._win.TypeError("id is required when negotiated is true"); + } + if (maxPacketLifeTime !== undefined && maxRetransmits !== undefined) { + throw new this._win.TypeError( + "Both maxPacketLifeTime and maxRetransmits cannot be provided" + ); + } + if (id == 65535) { + throw new this._win.TypeError("id cannot be 65535"); + } + // Must determine the type where we still know if entries are undefined. + let type; + if (maxPacketLifeTime !== undefined) { + type = Ci.IPeerConnection.kDataChannelPartialReliableTimed; + } else if (maxRetransmits !== undefined) { + type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit; + } else { + type = Ci.IPeerConnection.kDataChannelReliable; + } + // Synchronous since it doesn't block. + let dataChannel; + try { + dataChannel = this._pc.createDataChannel( + label, + protocol, + type, + ordered, + maxPacketLifeTime, + maxRetransmits, + negotiated, + id + ); + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + + const msg = + id === null ? "No available id could be generated" : "Id is in use"; + throw new this._win.DOMException(msg, "OperationError"); + } + + // Spec says to only do this if this is the first DataChannel created, + // but the c++ code that does the "is negotiation needed" checking will + // only ever return true on the first one. + this.updateNegotiationNeeded(); + + return dataChannel; + } +} + +setupPrototype(RTCPeerConnection, { + classID: PC_CID, + contractID: PC_CONTRACT, + QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), + _actions: { + offer: Ci.IPeerConnection.kActionOffer, + answer: Ci.IPeerConnection.kActionAnswer, + pranswer: Ci.IPeerConnection.kActionPRAnswer, + rollback: Ci.IPeerConnection.kActionRollback, + }, +}); + +// This is a separate class because we don't want to expose it to DOM. + +export class PeerConnectionObserver { + init(win) { + this._win = win; + } + + __init(dompc) { + this._dompc = dompc._innerObject; + } + + newError({ message, name }) { + return new this._dompc._win.DOMException(message, name); + } + + dispatchEvent(event) { + this._dompc.dispatchEvent(event); + } + + onCreateOfferSuccess(sdp) { + this._dompc._onCreateOfferSuccess(sdp); + } + + onCreateOfferError(error) { + this._dompc._onCreateOfferFailure(this.newError(error)); + } + + onCreateAnswerSuccess(sdp) { + this._dompc._onCreateAnswerSuccess(sdp); + } + + onCreateAnswerError(error) { + this._dompc._onCreateAnswerFailure(this.newError(error)); + } + + onSetDescriptionSuccess() { + this._dompc._onSetDescriptionSuccess(); + } + + onSetDescriptionError(error) { + this._dompc._onSetDescriptionFailure(this.newError(error)); + } + + onAddIceCandidateSuccess() { + this._dompc._onAddIceCandidateSuccess(); + } + + onAddIceCandidateError(error) { + this._dompc._onAddIceCandidateError(this.newError(error)); + } + + onIceCandidate(sdpMLineIndex, sdpMid, candidate, usernameFragment) { + let win = this._dompc._win; + if (candidate || sdpMid || usernameFragment) { + if (candidate.includes(" typ relay ")) { + this._dompc._iceGatheredRelayCandidates = true; + } + candidate = new win.RTCIceCandidate({ + candidate, + sdpMid, + sdpMLineIndex, + usernameFragment, + }); + } + this.dispatchEvent( + new win.RTCPeerConnectionIceEvent("icecandidate", { candidate }) + ); + } + + // This method is primarily responsible for updating iceConnectionState. + // This state is defined in the WebRTC specification as follows: + // + // iceConnectionState: + // ------------------- + // new Any of the RTCIceTransports are in the new state and none + // of them are in the checking, failed or disconnected state. + // + // checking Any of the RTCIceTransports are in the checking state and + // none of them are in the failed or disconnected state. + // + // connected All RTCIceTransports are in the connected, completed or + // closed state and at least one of them is in the connected + // state. + // + // completed All RTCIceTransports are in the completed or closed state + // and at least one of them is in the completed state. + // + // failed Any of the RTCIceTransports are in the failed state. + // + // disconnected Any of the RTCIceTransports are in the disconnected state + // and none of them are in the failed state. + // + // closed All of the RTCIceTransports are in the closed state. + + handleIceConnectionStateChange(iceConnectionState) { + let pc = this._dompc; + if (pc.iceConnectionState === iceConnectionState) { + return; + } + + if (iceConnectionState === "failed") { + if (!pc._hasStunServer) { + pc.logError( + "ICE failed, add a STUN server and see about:webrtc for more details" + ); + } else if (!pc._hasTurnServer) { + pc.logError( + "ICE failed, add a TURN server and see about:webrtc for more details" + ); + } else if (pc._hasTurnServer && !pc._iceGatheredRelayCandidates) { + pc.logError( + "ICE failed, your TURN server appears to be broken, see about:webrtc for more details" + ); + } else { + pc.logError("ICE failed, see about:webrtc for more details"); + } + } + + pc.changeIceConnectionState(iceConnectionState); + } + + onStateChange(state) { + if (!this._dompc) { + return; + } + + if (state == "SignalingState") { + this.dispatchEvent(new this._win.Event("signalingstatechange")); + return; + } + + if (!this._dompc._pc) { + return; + } + + switch (state) { + case "IceConnectionState": + let connState = this._dompc._pc.iceConnectionState; + this._dompc._queueTaskWithClosedCheck(() => { + this.handleIceConnectionStateChange(connState); + }); + break; + + case "IceGatheringState": + this._dompc.handleIceGatheringStateChange(); + break; + + case "ConnectionState": + _globalPCList.notifyLifecycleObservers(this, "connectionstatechange"); + this.dispatchEvent(new this._win.Event("connectionstatechange")); + break; + + default: + this._dompc.logWarning("Unhandled state type: " + state); + break; + } + } + + onTransceiverNeeded(kind, transceiverImpl) { + this._dompc._onTransceiverNeeded(kind, transceiverImpl); + } + + notifyDataChannel(channel) { + this.dispatchEvent( + new this._dompc._win.RTCDataChannelEvent("datachannel", { channel }) + ); + } + + fireTrackEvent(receiver, streams) { + const pc = this._dompc; + const transceiver = pc.getTransceivers().find(t => t.receiver == receiver); + if (!transceiver) { + return; + } + const track = receiver.track; + this.dispatchEvent( + new this._win.RTCTrackEvent("track", { + transceiver, + receiver, + track, + streams, + }) + ); + // Fire legacy event as well for a little bit. + this.dispatchEvent( + new this._win.MediaStreamTrackEvent("addtrack", { track }) + ); + } + + fireStreamEvent(stream) { + const ev = new this._win.MediaStreamEvent("addstream", { stream }); + this.dispatchEvent(ev); + } + + fireNegotiationNeededEvent() { + this.dispatchEvent(new this._win.Event("negotiationneeded")); + } + + onPacket(level, type, sending, packet) { + var pc = this._dompc; + if (pc._onPacket) { + pc._onPacket(level, type, sending, packet); + } + } +} + +setupPrototype(PeerConnectionObserver, { + classID: PC_OBS_CID, + contractID: PC_OBS_CONTRACT, + QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), +}); + +export class RTCPeerConnectionStatic { + init(win) { + this._winID = win.windowGlobalChild.innerWindowId; + } + + registerPeerConnectionLifecycleCallback(cb) { + _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb); + } +} + +setupPrototype(RTCPeerConnectionStatic, { + classID: PC_STATIC_CID, + contractID: PC_STATIC_CONTRACT, + QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), +}); + +export class CreateOfferRequest { + constructor(windowID, innerWindowID, callID, isSecure) { + Object.assign(this, { windowID, innerWindowID, callID, isSecure }); + } +} + +setupPrototype(CreateOfferRequest, { + classID: PC_COREQUEST_CID, + contractID: PC_COREQUEST_CONTRACT, + QueryInterface: ChromeUtils.generateQI([]), +}); |