PeerConnection.sys.mjs (59635B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 ChromeUtils.defineESModuleGetters(lazy, { 7 PeerConnectionIdp: "resource://gre/modules/media/PeerConnectionIdp.sys.mjs", 8 }); 9 10 const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1"; 11 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1"; 12 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1"; 13 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1"; 14 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1"; 15 const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1"; 16 17 const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}"); 18 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}"); 19 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}"); 20 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}"); 21 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}"); 22 const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}"); 23 const PC_COREQUEST_CID = Components.ID( 24 "{74b2122d-65a8-4824-aa9e-3d664cb75dc2}" 25 ); 26 27 function logWebRTCMsg(msg, file, line, flag, win) { 28 let scriptErrorClass = Cc["@mozilla.org/scripterror;1"]; 29 let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError); 30 scriptError.initWithWindowID( 31 `WebRTC: ${msg}`, 32 file, 33 line, 34 0, 35 flag, 36 "content javascript", 37 win.windowGlobalChild.innerWindowId 38 ); 39 Services.console.logMessage(scriptError); 40 if ( 41 Services.prefs.getBoolPref("media.peerconnection.treat_warnings_as_errors") 42 ) { 43 throw new win.TypeError(msg); 44 } 45 } 46 47 let setupPrototype = (_class, dict) => { 48 _class.prototype.classDescription = _class.name; 49 Object.assign(_class.prototype, dict); 50 }; 51 52 // Global list of PeerConnection objects, so they can be cleaned up when 53 // a page is torn down. (Maps inner window ID to an array of PC objects). 54 export class GlobalPCList { 55 constructor() { 56 this._list = {}; 57 this._networkdown = false; // XXX Need to query current state somehow 58 this._lifecycleobservers = {}; 59 this._nextId = 1; 60 Services.obs.addObserver(this, "inner-window-destroyed"); 61 Services.obs.addObserver(this, "profile-change-net-teardown"); 62 Services.obs.addObserver(this, "network:offline-about-to-go-offline"); 63 Services.obs.addObserver(this, "network:offline-status-changed"); 64 Services.obs.addObserver(this, "gmp-plugin-crash"); 65 Services.obs.addObserver(this, "PeerConnection:response:allow"); 66 Services.obs.addObserver(this, "PeerConnection:response:deny"); 67 if (Services.cpmm) { 68 Services.cpmm.addMessageListener("gmp-plugin-crash", this); 69 } 70 } 71 72 notifyLifecycleObservers(pc, type) { 73 for (var key of Object.keys(this._lifecycleobservers)) { 74 this._lifecycleobservers[key](pc, pc._winID, type); 75 } 76 } 77 78 addPC(pc) { 79 let winID = pc._winID; 80 if (this._list[winID]) { 81 this._list[winID].push(Cu.getWeakReference(pc)); 82 } else { 83 this._list[winID] = [Cu.getWeakReference(pc)]; 84 } 85 pc._globalPCListId = this._nextId++; 86 this.removeNullRefs(winID); 87 } 88 89 findPC(globalPCListId) { 90 for (let winId in this._list) { 91 if (this._list.hasOwnProperty(winId)) { 92 for (let pcref of this._list[winId]) { 93 let pc = pcref.get(); 94 if (pc && pc._globalPCListId == globalPCListId) { 95 return pc; 96 } 97 } 98 } 99 } 100 return null; 101 } 102 103 removeNullRefs(winID) { 104 if (this._list[winID] === undefined) { 105 return; 106 } 107 this._list[winID] = this._list[winID].filter(function (e) { 108 return e.get() !== null; 109 }); 110 111 if (this._list[winID].length === 0) { 112 delete this._list[winID]; 113 } 114 } 115 116 handleGMPCrash(data) { 117 let broadcastPluginCrash = function (list, winID, pluginID, pluginName) { 118 if (list.hasOwnProperty(winID)) { 119 list[winID].forEach(function (pcref) { 120 let pc = pcref.get(); 121 if (pc) { 122 pc._pc.pluginCrash(pluginID, pluginName); 123 } 124 }); 125 } 126 }; 127 128 // a plugin crashed; if it's associated with any of our PCs, fire an 129 // event to the DOM window 130 for (let winId in this._list) { 131 broadcastPluginCrash(this._list, winId, data.pluginID, data.pluginName); 132 } 133 } 134 135 receiveMessage({ name, data }) { 136 if (name == "gmp-plugin-crash") { 137 this.handleGMPCrash(data); 138 } 139 } 140 141 observe(subject, topic, data) { 142 let cleanupPcRef = function (pcref) { 143 let pc = pcref.get(); 144 if (pc) { 145 pc._suppressEvents = true; 146 pc.close(); 147 } 148 }; 149 150 let cleanupWinId = function (list, winID) { 151 if (list.hasOwnProperty(winID)) { 152 list[winID].forEach(cleanupPcRef); 153 delete list[winID]; 154 } 155 }; 156 157 if (topic == "inner-window-destroyed") { 158 let winID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 159 cleanupWinId(this._list, winID); 160 161 if (this._lifecycleobservers.hasOwnProperty(winID)) { 162 delete this._lifecycleobservers[winID]; 163 } 164 } else if ( 165 topic == "profile-change-net-teardown" || 166 topic == "network:offline-about-to-go-offline" 167 ) { 168 // As Necko doesn't prevent us from accessing the network we still need to 169 // monitor the network offline/online state here. See bug 1326483 170 this._networkdown = true; 171 } else if (topic == "network:offline-status-changed") { 172 if (data == "offline") { 173 this._networkdown = true; 174 } else if (data == "online") { 175 this._networkdown = false; 176 } 177 } else if (topic == "gmp-plugin-crash") { 178 if (subject instanceof Ci.nsIWritablePropertyBag2) { 179 let pluginID = subject.getPropertyAsUint32("pluginID"); 180 let pluginName = subject.getPropertyAsAString("pluginName"); 181 let data = { pluginID, pluginName }; 182 this.handleGMPCrash(data); 183 } 184 } else if ( 185 topic == "PeerConnection:response:allow" || 186 topic == "PeerConnection:response:deny" 187 ) { 188 var pc = this.findPC(data); 189 if (pc) { 190 if (topic == "PeerConnection:response:allow") { 191 pc._settlePermission.allow(); 192 } else { 193 let err = new pc._win.DOMException( 194 "The request is not allowed by " + 195 "the user agent or the platform in the current context.", 196 "NotAllowedError" 197 ); 198 pc._settlePermission.deny(err); 199 } 200 } 201 } 202 } 203 204 _registerPeerConnectionLifecycleCallback(winID, cb) { 205 this._lifecycleobservers[winID] = cb; 206 } 207 } 208 209 setupPrototype(GlobalPCList, { 210 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 211 classID: PC_MANAGER_CID, 212 }); 213 214 var _globalPCList = new GlobalPCList(); 215 216 // Parses grammar in RFC5245 section 15 and ICE TCP from RFC6544 section 4.5. 217 function parseCandidate(line) { 218 const match = line.match( 219 /^(a=)?candidate:([A-Za-z0-9+\/]{1,32}) (\d+) (UDP|TCP) (\d+) ([A-Za-z0-9.:-]+) (\d+) typ (host|srflx|prflx|relay)(?: raddr ([A-Za-z0-9.:-]+) rport (\d+))?(.*)$/i 220 ); 221 if (!match) { 222 return null; 223 } 224 const candidate = { 225 foundation: match[2], 226 componentId: parseInt(match[3], 10), 227 transport: match[4], 228 priority: parseInt(match[5], 10), 229 address: match[6], 230 port: parseInt(match[7], 10), 231 type: match[8], 232 relatedAddress: match[9], 233 relatedPort: match[10], 234 }; 235 if (candidate.componentId < 1 || candidate.componentId > 256) { 236 return null; 237 } 238 if (candidate.priority < 0 || candidate.priority > 4294967295) { 239 return null; 240 } 241 if (candidate.port < 0 || candidate.port > 65535) { 242 return null; 243 } 244 candidate.component = { 1: "rtp", 2: "rtcp" }[candidate.componentId] || null; 245 candidate.protocol = 246 { udp: "udp", tcp: "tcp" }[candidate.transport.toLowerCase()] || null; 247 248 const tcpTypeMatch = match[11].match(/tcptype (\S+)/i); 249 if (tcpTypeMatch) { 250 candidate.tcpType = tcpTypeMatch[1]; 251 if ( 252 candidate.protocol != "tcp" || 253 !["active", "passive", "so"].includes(candidate.tcpType) 254 ) { 255 return null; 256 } 257 } 258 return candidate; 259 } 260 261 export class RTCIceCandidate { 262 init(win) { 263 this._win = win; 264 } 265 266 __init(dict) { 267 if (dict.sdpMid == null && dict.sdpMLineIndex == null) { 268 throw new this._win.TypeError( 269 "Either sdpMid or sdpMLineIndex must be specified" 270 ); 271 } 272 Object.assign(this, dict); 273 const candidate = parseCandidate(this.candidate); 274 if (!candidate) { 275 return; 276 } 277 Object.assign(this, candidate); 278 } 279 280 toJSON() { 281 return { 282 candidate: this.candidate, 283 sdpMid: this.sdpMid, 284 sdpMLineIndex: this.sdpMLineIndex, 285 usernameFragment: this.usernameFragment, 286 }; 287 } 288 } 289 290 setupPrototype(RTCIceCandidate, { 291 classID: PC_ICE_CID, 292 contractID: PC_ICE_CONTRACT, 293 QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), 294 }); 295 296 export class RTCSessionDescription { 297 init(win) { 298 this._win = win; 299 this._winID = this._win.windowGlobalChild.innerWindowId; 300 this._legacyPref = Services.prefs.getBoolPref( 301 "media.peerconnection.description.legacy.enabled" 302 ); 303 } 304 305 __init({ type, sdp }) { 306 Object.assign(this, { _type: type, _sdp: sdp }); 307 } 308 309 get type() { 310 return this._type; 311 } 312 set type(type) { 313 if (!this._legacyPref) { 314 // TODO: this throws even in sloppy mode. Remove in bug 1883992 315 throw new this._win.TypeError("setting getter-only property type"); 316 } 317 this.warn(); 318 this._type = type; 319 } 320 321 get sdp() { 322 return this._sdp; 323 } 324 set sdp(sdp) { 325 if (!this._legacyPref) { 326 // TODO: this throws even in sloppy mode. Remove in bug 1883992 327 throw new this._win.TypeError("setting getter-only property sdp"); 328 } 329 this.warn(); 330 this._sdp = sdp; 331 } 332 333 warn() { 334 if (!this._warned) { 335 // Warn once per RTCSessionDescription about deprecated writable usage. 336 if (this._legacyPref) { 337 this.logMsg( 338 "RTCSessionDescription's members are readonly! " + 339 "Writing to them is deprecated and will break soon!", 340 Ci.nsIScriptError.warningFlag 341 ); 342 } else { 343 this.logMsg( 344 "RTCSessionDescription's members are readonly! " + 345 "Writing to them no longer works!", 346 Ci.nsIScriptError.errorFlag 347 ); 348 } 349 this._warned = true; 350 } 351 } 352 353 logMsg(msg, flag) { 354 let err = this._win.Error(); 355 logWebRTCMsg(msg, err.fileName, err.lineNumber, flag, this._win); 356 } 357 } 358 359 setupPrototype(RTCSessionDescription, { 360 classID: PC_SESSION_CID, 361 contractID: PC_SESSION_CONTRACT, 362 QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), 363 }); 364 365 export class RTCPeerConnection { 366 constructor() { 367 this._pc = null; 368 this._closed = false; 369 this._pendingLocalDescription = null; 370 this._pendingRemoteDescription = null; 371 this._currentLocalDescription = null; 372 this._currentRemoteDescription = null; 373 this._legacyPref = Services.prefs.getBoolPref( 374 "media.peerconnection.description.legacy.enabled" 375 ); 376 377 // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9 378 // canTrickle == null means unknown; when a remote description is received it 379 // is set to true or false based on the presence of the "trickle" ice-option 380 this._canTrickle = null; 381 382 // So we can record telemetry on state transitions 383 this._iceConnectionState = "new"; 384 385 this._hasStunServer = this._hasTurnServer = false; 386 this._iceGatheredRelayCandidates = false; 387 } 388 389 init(win) { 390 this._win = win; 391 } 392 393 // Pref-based overrides; will _not_ be reflected in getConfiguration 394 _applyPrefsToConfig(rtcConfig) { 395 if ( 396 rtcConfig.iceTransportPolicy == "all" && 397 Services.prefs.getBoolPref("media.peerconnection.ice.relay_only") 398 ) { 399 rtcConfig.iceTransportPolicy = "relay"; 400 } 401 402 if ( 403 !rtcConfig.iceServers || 404 !Services.prefs.getBoolPref( 405 "media.peerconnection.use_document_iceservers" 406 ) 407 ) { 408 try { 409 rtcConfig.iceServers = JSON.parse( 410 Services.prefs.getCharPref( 411 "media.peerconnection.default_iceservers" 412 ) || "[]" 413 ); 414 } catch (e) { 415 this.logWarning( 416 "Ignoring invalid media.peerconnection.default_iceservers in about:config" 417 ); 418 rtcConfig.iceServers = []; 419 } 420 try { 421 this._validateIceServers( 422 rtcConfig.iceServers, 423 "Ignoring invalid media.peerconnection.default_iceservers in about:config" 424 ); 425 } catch (e) { 426 this.logWarning(e.message); 427 rtcConfig.iceServers = []; 428 } 429 } 430 } 431 432 _validateConfig(rtcConfig) { 433 if ("sdpSemantics" in rtcConfig) { 434 if (rtcConfig.sdpSemantics == "plan-b") { 435 this.logWarning( 436 `Outdated and non-standard {sdpSemantics: "plan-b"} is not ` + 437 `supported! WebRTC may be unreliable. Please update code to ` + 438 `follow standard "unified-plan".` 439 ); 440 } 441 // Don't let it show up in getConfiguration. 442 delete rtcConfig.sdpSemantics; 443 } 444 445 if (this._config) { 446 // certificates must match 447 if (rtcConfig.certificates.length != this._config.certificates.length) { 448 throw new this._win.DOMException( 449 "Cannot change certificates with setConfiguration (length differs)", 450 "InvalidModificationError" 451 ); 452 } 453 for (let i = 0; i < rtcConfig.certificates.length; i++) { 454 if (rtcConfig.certificates[i] != this._config.certificates[i]) { 455 throw new this._win.DOMException( 456 `Cannot change certificates with setConfiguration ` + 457 `(cert at index ${i} differs)`, 458 "InvalidModificationError" 459 ); 460 } 461 } 462 463 // bundlePolicy must match 464 if (rtcConfig.bundlePolicy != this._config.bundlePolicy) { 465 throw new this._win.DOMException( 466 "Cannot change bundlePolicy with setConfiguration", 467 "InvalidModificationError" 468 ); 469 } 470 471 // peerIdentity must match 472 if ( 473 rtcConfig.peerIdentity && 474 rtcConfig.peerIdentity != this._config.peerIdentity 475 ) { 476 throw new this._win.DOMException( 477 "Cannot change peerIdentity with setConfiguration", 478 "InvalidModificationError" 479 ); 480 } 481 482 // TODO (bug 1339203): rtcpMuxPolicy must match 483 // TODO (bug 1529398): iceCandidatePoolSize must match if sLD has ever 484 // been called. 485 } 486 487 // This gets executed in the typical case when iceServers 488 // are passed in through the web page. 489 this._validateIceServers( 490 rtcConfig.iceServers, 491 "RTCPeerConnection constructor passed invalid RTCConfiguration" 492 ); 493 } 494 495 _checkIfIceRestartRequired(rtcConfig) { 496 if (this._config) { 497 if (rtcConfig.iceTransportPolicy != this._config.iceTransportPolicy) { 498 this._pc.restartIceNoRenegotiationNeeded(); 499 return; 500 } 501 if ( 502 JSON.stringify(this._config.iceServers) != 503 JSON.stringify(rtcConfig.iceServers) 504 ) { 505 this._pc.restartIceNoRenegotiationNeeded(); 506 } 507 } 508 } 509 510 __init(rtcConfig) { 511 this._winID = this._win.windowGlobalChild.innerWindowId; 512 let certificates = rtcConfig.certificates || []; 513 514 if (certificates.some(c => c.expires <= Date.now())) { 515 throw new this._win.DOMException( 516 "Unable to create RTCPeerConnection with an expired certificate", 517 "InvalidAccessError" 518 ); 519 } 520 521 // TODO(bug 1531875): Check origin of certs 522 523 // TODO(bug 1176518): Remove this code once we support multiple certs 524 let certificate; 525 if (certificates.length == 1) { 526 certificate = certificates[0]; 527 } else if (certificates.length) { 528 throw new this._win.DOMException( 529 "RTCPeerConnection does not currently support multiple certificates", 530 "NotSupportedError" 531 ); 532 } 533 534 this._documentPrincipal = Cu.getWebIDLCallerPrincipal(); 535 536 if (_globalPCList._networkdown) { 537 throw new this._win.DOMException( 538 "Can't create RTCPeerConnections when the network is down", 539 "InvalidStateError" 540 ); 541 } 542 543 this.makeGetterSetterEH("ontrack"); 544 this.makeLegacyGetterSetterEH( 545 "onaddstream", 546 "Use peerConnection.ontrack instead." 547 ); 548 this.makeLegacyGetterSetterEH( 549 "onaddtrack", 550 "Use peerConnection.ontrack instead." 551 ); 552 this.makeGetterSetterEH("onicecandidate"); 553 this.makeGetterSetterEH("onnegotiationneeded"); 554 this.makeGetterSetterEH("onsignalingstatechange"); 555 this.makeGetterSetterEH("ondatachannel"); 556 this.makeGetterSetterEH("oniceconnectionstatechange"); 557 this.makeGetterSetterEH("onicegatheringstatechange"); 558 this.makeGetterSetterEH("onconnectionstatechange"); 559 this.makeGetterSetterEH("onidentityresult"); 560 this.makeGetterSetterEH("onpeeridentity"); 561 this.makeGetterSetterEH("onidpassertionerror"); 562 this.makeGetterSetterEH("onidpvalidationerror"); 563 564 this._pc = new this._win.PeerConnectionImpl(); 565 566 this.__DOM_IMPL__._innerObject = this; 567 const observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__); 568 569 // Add a reference to the PeerConnection to global list (before init). 570 _globalPCList.addPC(this); 571 572 this._pc.initialize(observer, this._win); 573 574 this.setConfiguration(rtcConfig); 575 576 this._certificateReady = this._initCertificate(certificate); 577 this._initIdp(); 578 _globalPCList.notifyLifecycleObservers(this, "initialized"); 579 } 580 581 getConfiguration() { 582 const config = Object.assign({}, this._config); 583 delete config.sdpSemantics; 584 return config; 585 } 586 587 setConfiguration(rtcConfig) { 588 this._checkClosed(); 589 this._validateConfig(rtcConfig); 590 this._checkIfIceRestartRequired(rtcConfig); 591 592 // Allow prefs to tweak these settings before passing to c++, but hide all 593 // of that from JS. 594 const configWithPrefTweaks = Object.assign({}, rtcConfig); 595 this._applyPrefsToConfig(configWithPrefTweaks); 596 this._pc.setConfiguration(configWithPrefTweaks); 597 598 this._config = Object.assign({}, rtcConfig); 599 } 600 601 async _initCertificate(certificate) { 602 if (!certificate) { 603 certificate = await this._win.RTCPeerConnection.generateCertificate({ 604 name: "ECDSA", 605 namedCurve: "P-256", 606 }); 607 } 608 this._pc.certificate = certificate; 609 } 610 611 _resetPeerIdentityPromise() { 612 this._peerIdentity = new this._win.Promise((resolve, reject) => { 613 this._resolvePeerIdentity = resolve; 614 this._rejectPeerIdentity = reject; 615 }); 616 } 617 618 _initIdp() { 619 this._resetPeerIdentityPromise(); 620 this._lastIdentityValidation = this._win.Promise.resolve(); 621 622 let prefName = "media.peerconnection.identity.timeout"; 623 let idpTimeout = Services.prefs.getIntPref(prefName); 624 this._localIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout); 625 this._remoteIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout); 626 } 627 628 // Add a function to the internal operations chain. 629 630 _chain(operation) { 631 return this._pc.chain(operation); 632 } 633 634 // It's basically impossible to use async directly in JSImplemented code, 635 // because the implicit promise must be wrapped to the right type for content. 636 // 637 // The _async wrapper takes care of this. The _legacy wrapper implements 638 // legacy callbacks in a manner that produces correct line-numbers in errors, 639 // provided that methods validate their inputs before putting themselves on 640 // the pc's operations chain. 641 // 642 // These wrappers also serve as guards against settling promises past close(). 643 644 _async(func) { 645 return this._win.Promise.resolve(this._closeWrapper(func)); 646 } 647 648 _legacy(...args) { 649 return this._win.Promise.resolve(this._legacyCloseWrapper(...args)); 650 } 651 652 _auto(onSucc, onErr, func) { 653 return typeof onSucc == "function" 654 ? this._legacy(onSucc, onErr, func) 655 : this._async(func); 656 } 657 658 async _closeWrapper(func) { 659 let closed = this._closed; 660 try { 661 let result = await func(); 662 if (!closed && this._closed) { 663 await new Promise(() => {}); 664 } 665 return result; 666 } catch (e) { 667 if (!closed && this._closed) { 668 await new Promise(() => {}); 669 } 670 throw e; 671 } 672 } 673 674 async _legacyCloseWrapper(onSucc, onErr, func) { 675 let wrapCallback = cb => result => { 676 try { 677 cb && cb(result); 678 } catch (e) { 679 this.logErrorAndCallOnError(e); 680 } 681 }; 682 683 try { 684 wrapCallback(onSucc)(await func()); 685 } catch (e) { 686 wrapCallback(onErr)(e); 687 } 688 } 689 690 // This implements the fairly common "Queue a task" logic 691 async _queueTaskWithClosedCheck(func) { 692 const pc = this; 693 return new this._win.Promise((resolve, reject) => { 694 Services.tm.dispatchToMainThread({ 695 run() { 696 try { 697 if (!pc._closed) { 698 func(); 699 resolve(); 700 } 701 } catch (e) { 702 reject(e); 703 } 704 }, 705 }); 706 }); 707 } 708 709 /** 710 * An RTCConfiguration may look like this: 711 * 712 * { "iceServers": [ { urls: "stun:stun.example.org", }, 713 * { url: "stun:stun.example.org", }, // deprecated version 714 * { urls: ["turn:turn1.x.org", "turn:turn2.x.org"], 715 * username:"jib", credential:"mypass"} ] } 716 * 717 * This function normalizes the structure of the input for rtcConfig.iceServers for us, 718 * so we test well-formed stun/turn urls before passing along to C++. 719 * msg - Error message to detail which array-entry failed, if any. 720 */ 721 _validateIceServers(iceServers, msg) { 722 // Normalize iceServers input 723 iceServers.forEach(server => { 724 if (typeof server.urls === "string") { 725 server.urls = [server.urls]; 726 } else if (!server.urls && server.url) { 727 // TODO: Remove support for legacy iceServer.url eventually (Bug 1116766) 728 server.urls = [server.url]; 729 this.logWarning("RTCIceServer.url is deprecated! Use urls instead."); 730 } 731 }); 732 733 let nicerNewURI = uriStr => { 734 try { 735 return Services.io.newURI(uriStr); 736 } catch (e) { 737 if (e.result == Cr.NS_ERROR_MALFORMED_URI) { 738 throw new this._win.DOMException( 739 `${msg} - malformed URI: ${uriStr}`, 740 "SyntaxError" 741 ); 742 } 743 throw e; 744 } 745 }; 746 747 let stunServers = 0; 748 749 iceServers.forEach(({ urls, username, credential, credentialType }) => { 750 if (!urls) { 751 // TODO: Remove once url is deprecated (Bug 1369563) 752 throw new this._win.TypeError( 753 "Missing required 'urls' member of RTCIceServer" 754 ); 755 } 756 if (!urls.length) { 757 throw new this._win.DOMException( 758 `${msg} - urls is empty`, 759 "SyntaxError" 760 ); 761 } 762 urls 763 .map(url => nicerNewURI(url)) 764 .forEach(({ scheme, spec, query }) => { 765 if (scheme in { turn: 1, turns: 1 }) { 766 if (username == undefined) { 767 throw new this._win.DOMException( 768 `${msg} - missing username: ${spec}`, 769 "InvalidAccessError" 770 ); 771 } 772 if (username.length > 512) { 773 throw new this._win.DOMException( 774 `${msg} - username longer then 512 bytes: ${username}`, 775 "InvalidAccessError" 776 ); 777 } 778 if (credential == undefined) { 779 throw new this._win.DOMException( 780 `${msg} - missing credential: ${spec}`, 781 "InvalidAccessError" 782 ); 783 } 784 if (credentialType != "password") { 785 this.logWarning( 786 `RTCConfiguration TURN credentialType \"${credentialType}\"` + 787 " is not yet implemented. Treating as password." + 788 " https://bugzil.la/1247616" 789 ); 790 } 791 this._hasTurnServer = true; 792 // If this is not a TURN TCP/TLS server, it is also a STUN server 793 const parameters = query.split("&"); 794 if (!parameters.includes("transport=tcp")) { 795 this._hasStunServer = true; 796 } 797 stunServers += 1; 798 } else if (scheme in { stun: 1, stuns: 1 }) { 799 this._hasStunServer = true; 800 stunServers += 1; 801 } else { 802 throw new this._win.DOMException( 803 `${msg} - improper scheme: ${scheme}`, 804 "SyntaxError" 805 ); 806 } 807 if (scheme in { stuns: 1 }) { 808 this.logWarning(scheme.toUpperCase() + " is not yet supported."); 809 } 810 if (stunServers >= 5) { 811 this.logError( 812 "Using five or more STUN/TURN servers slows down discovery" 813 ); 814 } 815 }); 816 }); 817 } 818 819 // Ideally, this should be of the form _checkState(state), 820 // where the state is taken from an enumeration containing 821 // the valid peer connection states defined in the WebRTC 822 // spec. See Bug 831756. 823 _checkClosed() { 824 if (this._closed) { 825 throw new this._win.DOMException( 826 "Peer connection is closed", 827 "InvalidStateError" 828 ); 829 } 830 } 831 832 dispatchEvent(event) { 833 // PC can close while events are firing if there is an async dispatch 834 // in c++ land. But let through "closed" signaling and ice connection events. 835 if (!this._suppressEvents) { 836 this.__DOM_IMPL__.dispatchEvent(event); 837 } 838 } 839 840 // Log error message to web console and window.onerror, if present. 841 logErrorAndCallOnError(e) { 842 this.logMsg( 843 e.message, 844 e.fileName, 845 e.lineNumber, 846 Ci.nsIScriptError.errorFlag 847 ); 848 849 // Safely call onerror directly if present (necessary for testing) 850 try { 851 if (typeof this._win.onerror === "function") { 852 this._win.onerror(e.message, e.fileName, e.lineNumber); 853 } 854 } catch (e) { 855 // If onerror itself throws, service it. 856 try { 857 this.logMsg( 858 e.message, 859 e.fileName, 860 e.lineNumber, 861 Ci.nsIScriptError.errorFlag 862 ); 863 } catch (e) {} 864 } 865 } 866 867 logError(msg) { 868 this.logStackMsg(msg, Ci.nsIScriptError.errorFlag); 869 } 870 871 logWarning(msg) { 872 this.logStackMsg(msg, Ci.nsIScriptError.warningFlag); 873 } 874 875 logStackMsg(msg, flag) { 876 let err = this._win.Error(); 877 this.logMsg(msg, err.fileName, err.lineNumber, flag); 878 } 879 880 logMsg(msg, file, line, flag) { 881 return logWebRTCMsg(msg, file, line, flag, this._win); 882 } 883 884 getEH(type) { 885 return this.__DOM_IMPL__.getEventHandler(type); 886 } 887 888 setEH(type, handler) { 889 this.__DOM_IMPL__.setEventHandler(type, handler); 890 } 891 892 makeGetterSetterEH(name) { 893 Object.defineProperty(this, name, { 894 get() { 895 return this.getEH(name); 896 }, 897 set(h) { 898 this.setEH(name, h); 899 }, 900 }); 901 } 902 903 makeLegacyGetterSetterEH(name, msg) { 904 Object.defineProperty(this, name, { 905 get() { 906 return this.getEH(name); 907 }, 908 set(h) { 909 this.logWarning(name + " is deprecated! " + msg); 910 this.setEH(name, h); 911 }, 912 }); 913 } 914 915 createOffer(optionsOrOnSucc, onErr, options) { 916 let onSuccess = null; 917 if (typeof optionsOrOnSucc == "function") { 918 onSuccess = optionsOrOnSucc; 919 } else { 920 options = optionsOrOnSucc; 921 } 922 // This entry-point handles both new and legacy call sig. Decipher which one 923 if (onSuccess) { 924 return this._legacy(onSuccess, onErr, () => this._createOffer(options)); 925 } 926 return this._async(() => this._createOffer(options)); 927 } 928 929 // Ensures that we have at least one transceiver of |kind| that is 930 // configured to receive. It will create one if necessary. 931 _ensureOfferToReceive(kind) { 932 let hasRecv = this.getTransceivers().some( 933 transceiver => 934 transceiver.getKind() == kind && 935 (transceiver.direction == "sendrecv" || 936 transceiver.direction == "recvonly") && 937 !transceiver.stopped 938 ); 939 940 if (!hasRecv) { 941 this._addTransceiverNoEvents(kind, { direction: "recvonly" }); 942 } 943 } 944 945 // Handles offerToReceiveAudio/Video 946 _ensureTransceiversForOfferToReceive(options) { 947 if (options.offerToReceiveAudio) { 948 this._ensureOfferToReceive("audio"); 949 } 950 951 if (options.offerToReceiveVideo) { 952 this._ensureOfferToReceive("video"); 953 } 954 955 this.getTransceivers() 956 .filter(transceiver => { 957 return ( 958 (options.offerToReceiveVideo === false && 959 transceiver.receiver.track.kind == "video") || 960 (options.offerToReceiveAudio === false && 961 transceiver.receiver.track.kind == "audio") 962 ); 963 }) 964 .forEach(transceiver => { 965 if (transceiver.direction == "sendrecv") { 966 transceiver.setDirectionInternal("sendonly"); 967 } else if (transceiver.direction == "recvonly") { 968 transceiver.setDirectionInternal("inactive"); 969 } 970 }); 971 } 972 973 _maybeDuplicateFingerprints(sdp) { 974 if (this._pc.duplicateFingerprintQuirk) { 975 // hack to put a=fingerprint under all m= lines 976 const lines = sdp.split("\n"); 977 const fingerprint = lines.find(line => line.startsWith("a=fingerprint")); 978 if (fingerprint) { 979 for (let i = 0; i < lines.length; i++) { 980 if (lines[i].startsWith("m=")) { 981 let j = i + 1; 982 while (j < lines.length && lines[j].startsWith("c=")) { 983 j++; 984 } 985 lines.splice(j, 0, fingerprint); 986 } 987 } 988 sdp = lines.join("\n"); 989 } 990 } 991 return sdp; 992 } 993 994 _createOffer(options) { 995 this._checkClosed(); 996 this._ensureTransceiversForOfferToReceive(options); 997 return this._chain(async () => 998 Cu.cloneInto(await this._createAnOffer(options), this._win) 999 ); 1000 } 1001 1002 async _createAnOffer(options = {}) { 1003 switch (this.signalingState) { 1004 case "stable": 1005 case "have-local-offer": 1006 break; 1007 default: 1008 throw new this._win.DOMException( 1009 `Cannot create offer in ${this.signalingState}`, 1010 "InvalidStateError" 1011 ); 1012 } 1013 let haveAssertion; 1014 if (this._localIdp.enabled) { 1015 haveAssertion = this._getIdentityAssertion(); 1016 } 1017 await this._getPermission(); 1018 await this._certificateReady; 1019 let sdp = await new Promise((resolve, reject) => { 1020 this._onCreateOfferSuccess = resolve; 1021 this._onCreateOfferFailure = reject; 1022 this._pc.createOffer(options); 1023 }); 1024 sdp = this._maybeDuplicateFingerprints(sdp); 1025 if (haveAssertion) { 1026 await haveAssertion; 1027 sdp = this._localIdp.addIdentityAttribute(sdp); 1028 } 1029 return { type: "offer", sdp }; 1030 } 1031 1032 createAnswer(optionsOrOnSucc, onErr) { 1033 // This entry-point handles both new and legacy call sig. Decipher which one 1034 if (typeof optionsOrOnSucc == "function") { 1035 return this._legacy(optionsOrOnSucc, onErr, () => this._createAnswer({})); 1036 } 1037 return this._async(() => this._createAnswer(optionsOrOnSucc)); 1038 } 1039 1040 _createAnswer() { 1041 this._checkClosed(); 1042 return this._chain(async () => 1043 Cu.cloneInto(await this._createAnAnswer(), this._win) 1044 ); 1045 } 1046 1047 async _createAnAnswer() { 1048 if (this.signalingState != "have-remote-offer") { 1049 throw new this._win.DOMException( 1050 `Cannot create answer in ${this.signalingState}`, 1051 "InvalidStateError" 1052 ); 1053 } 1054 let haveAssertion; 1055 if (this._localIdp.enabled) { 1056 haveAssertion = this._getIdentityAssertion(); 1057 } 1058 await this._getPermission(); 1059 await this._certificateReady; 1060 let sdp = await new Promise((resolve, reject) => { 1061 this._onCreateAnswerSuccess = resolve; 1062 this._onCreateAnswerFailure = reject; 1063 this._pc.createAnswer(); 1064 }); 1065 sdp = this._maybeDuplicateFingerprints(sdp); 1066 if (haveAssertion) { 1067 await haveAssertion; 1068 sdp = this._localIdp.addIdentityAttribute(sdp); 1069 } 1070 return { type: "answer", sdp }; 1071 } 1072 1073 async _getPermission() { 1074 if (!this._havePermission) { 1075 const privileged = 1076 this._documentPrincipal.isSystemPrincipal || 1077 Services.prefs.getBoolPref("media.navigator.permission.disabled"); 1078 1079 if (privileged) { 1080 this._havePermission = Promise.resolve(); 1081 } else { 1082 this._havePermission = new Promise((resolve, reject) => { 1083 this._settlePermission = { allow: resolve, deny: reject }; 1084 let outerId = this._win.docShell.outerWindowID; 1085 1086 let chrome = new CreateOfferRequest( 1087 outerId, 1088 this._winID, 1089 this._globalPCListId, 1090 false 1091 ); 1092 let request = this._win.CreateOfferRequest._create(this._win, chrome); 1093 Services.obs.notifyObservers(request, "PeerConnection:request"); 1094 }); 1095 } 1096 } 1097 return this._havePermission; 1098 } 1099 1100 _sanityCheckSdp(sdp) { 1101 // The fippo butter finger filter AKA non-ASCII chars 1102 // Note: SDP allows non-ASCII character in the subject (who cares?) 1103 // eslint-disable-next-line no-control-regex 1104 let pos = sdp.search(/[^\u0000-\u007f]/); 1105 if (pos != -1) { 1106 throw new this._win.DOMException( 1107 "SDP contains non ASCII characters at position " + pos, 1108 "InvalidParameterError" 1109 ); 1110 } 1111 } 1112 1113 setLocalDescription(desc, onSucc, onErr) { 1114 return this._auto(onSucc, onErr, () => this._setLocalDescription(desc)); 1115 } 1116 1117 _setLocalDescription({ type, sdp }) { 1118 if (type == "pranswer") { 1119 throw new this._win.DOMException( 1120 "pranswer not yet implemented", 1121 "NotSupportedError" 1122 ); 1123 } 1124 this._checkClosed(); 1125 return this._chain(async () => { 1126 // Avoid Promise.all ahead of synchronous part of spec algorithm, since it 1127 // defers. NOTE: The spec says to return an already-rejected promise in 1128 // some cases, which is difficult to achieve in practice from JS (would 1129 // require avoiding await and then() entirely), but we want to come as 1130 // close as we reasonably can. 1131 const p = this._getPermission(); 1132 if (!type) { 1133 switch (this.signalingState) { 1134 case "stable": 1135 case "have-local-offer": 1136 case "have-remote-pranswer": 1137 type = "offer"; 1138 break; 1139 default: 1140 type = "answer"; 1141 break; 1142 } 1143 } 1144 if (!sdp) { 1145 if (type == "offer") { 1146 sdp = (await this._createAnOffer()).sdp; 1147 } else if (type == "answer") { 1148 sdp = (await this._createAnAnswer()).sdp; 1149 } 1150 } else { 1151 this._sanityCheckSdp(sdp); 1152 } 1153 1154 try { 1155 await new Promise((resolve, reject) => { 1156 this._onSetDescriptionSuccess = resolve; 1157 this._onSetDescriptionFailure = reject; 1158 this._pc.setLocalDescription(this._actions[type], sdp); 1159 }); 1160 await p; 1161 } catch (e) { 1162 this._pc.onSetDescriptionError(); 1163 throw e; 1164 } 1165 await this._pc.onSetDescriptionSuccess(type, false); 1166 }); 1167 } 1168 1169 async _validateIdentity(sdp, origin) { 1170 // Only run a single identity verification at a time. We have to do this to 1171 // avoid problems with the fact that identity validation doesn't block the 1172 // resolution of setRemoteDescription(). 1173 const validate = async () => { 1174 // Access this._pc synchronously in case pc is closed later 1175 const identity = this._pc.peerIdentity; 1176 await this._lastIdentityValidation; 1177 const msg = await this._remoteIdp.verifyIdentityFromSDP(sdp, origin); 1178 // If this pc has an identity already, then the identity in sdp must match 1179 if (identity && (!msg || msg.identity !== identity)) { 1180 throw new this._win.DOMException( 1181 "Peer Identity mismatch, expected: " + identity, 1182 "OperationError" 1183 ); 1184 } 1185 if (this._closed) { 1186 return; 1187 } 1188 if (msg) { 1189 // Set new identity and generate an event. 1190 this._pc.peerIdentity = msg.identity; 1191 this._resolvePeerIdentity( 1192 Cu.cloneInto( 1193 { 1194 idp: this._remoteIdp.provider, 1195 name: msg.identity, 1196 }, 1197 this._win 1198 ) 1199 ); 1200 } 1201 }; 1202 1203 const haveValidation = validate(); 1204 1205 // Always eat errors on this chain 1206 this._lastIdentityValidation = haveValidation.catch(() => {}); 1207 1208 // If validation fails, we have some work to do. Fork it so it cannot 1209 // interfere with the validation chain itself, even if the catch function 1210 // throws. 1211 haveValidation.catch(e => { 1212 if (this._closed) { 1213 return; 1214 } 1215 this._rejectPeerIdentity(e); 1216 1217 // If we don't expect a specific peer identity, failure to get a valid 1218 // peer identity is not a terminal state, so replace the promise to 1219 // allow another attempt. 1220 if (!this._pc.peerIdentity) { 1221 this._resetPeerIdentityPromise(); 1222 } 1223 }); 1224 1225 if (this._closed) { 1226 return; 1227 } 1228 // Only wait for IdP validation if we need identity matching 1229 if (this._pc.peerIdentity) { 1230 await haveValidation; 1231 } 1232 } 1233 1234 setRemoteDescription(desc, onSucc, onErr) { 1235 return this._auto(onSucc, onErr, () => this._setRemoteDescription(desc)); 1236 } 1237 1238 _setRemoteDescription({ type, sdp }) { 1239 if (type == "pranswer") { 1240 throw new this._win.DOMException( 1241 "pranswer not yet implemented", 1242 "NotSupportedError" 1243 ); 1244 } 1245 this._checkClosed(); 1246 return this._chain(async () => { 1247 try { 1248 if (type == "offer" && this.signalingState == "have-local-offer") { 1249 await new Promise((resolve, reject) => { 1250 this._onSetDescriptionSuccess = resolve; 1251 this._onSetDescriptionFailure = reject; 1252 this._pc.setLocalDescription( 1253 Ci.IPeerConnection.kActionRollback, 1254 "" 1255 ); 1256 }); 1257 await this._pc.onSetDescriptionSuccess("rollback", false); 1258 this._updateCanTrickle(); 1259 } 1260 1261 if (this._closed) { 1262 return; 1263 } 1264 1265 this._sanityCheckSdp(sdp); 1266 1267 const p = this._getPermission(); 1268 1269 const haveSetRemote = new Promise((resolve, reject) => { 1270 this._onSetDescriptionSuccess = resolve; 1271 this._onSetDescriptionFailure = reject; 1272 this._pc.setRemoteDescription(this._actions[type], sdp); 1273 }); 1274 1275 if (type != "rollback") { 1276 // Do setRemoteDescription and identity validation in parallel 1277 await this._validateIdentity(sdp); 1278 } 1279 await p; 1280 await haveSetRemote; 1281 } catch (e) { 1282 this._pc.onSetDescriptionError(); 1283 throw e; 1284 } 1285 1286 await this._pc.onSetDescriptionSuccess(type, true); 1287 this._updateCanTrickle(); 1288 }); 1289 } 1290 1291 setIdentityProvider(provider, { protocol, usernameHint, peerIdentity } = {}) { 1292 this._checkClosed(); 1293 peerIdentity = peerIdentity || this._pc.peerIdentity; 1294 this._localIdp.setIdentityProvider( 1295 provider, 1296 protocol, 1297 usernameHint, 1298 peerIdentity 1299 ); 1300 } 1301 1302 async _getIdentityAssertion() { 1303 await this._certificateReady; 1304 return this._localIdp.getIdentityAssertion( 1305 this._pc.fingerprint, 1306 this._documentPrincipal.origin 1307 ); 1308 } 1309 1310 getIdentityAssertion() { 1311 this._checkClosed(); 1312 return this._win.Promise.resolve( 1313 this._chain(() => this._getIdentityAssertion()) 1314 ); 1315 } 1316 1317 get canTrickleIceCandidates() { 1318 return this._canTrickle; 1319 } 1320 1321 _updateCanTrickle() { 1322 let containsTrickle = section => { 1323 let lines = section.toLowerCase().split(/(?:\r\n?|\n)/); 1324 return lines.some(line => { 1325 let prefix = "a=ice-options:"; 1326 if (line.substring(0, prefix.length) !== prefix) { 1327 return false; 1328 } 1329 let tokens = line.substring(prefix.length).split(" "); 1330 return tokens.some(x => x === "trickle"); 1331 }); 1332 }; 1333 1334 let desc = null; 1335 try { 1336 // The getter for remoteDescription can throw if the pc is closed. 1337 desc = this.remoteDescription; 1338 } catch (e) {} 1339 if (!desc) { 1340 this._canTrickle = null; 1341 return; 1342 } 1343 1344 let sections = desc.sdp.split(/(?:\r\n?|\n)m=/); 1345 let topSection = sections.shift(); 1346 this._canTrickle = 1347 containsTrickle(topSection) || sections.every(containsTrickle); 1348 } 1349 1350 addIceCandidate(cand, onSucc, onErr) { 1351 if ( 1352 cand.candidate != "" && 1353 cand.sdpMid == null && 1354 cand.sdpMLineIndex == null 1355 ) { 1356 throw new this._win.TypeError( 1357 "Cannot add a candidate without specifying either sdpMid or sdpMLineIndex" 1358 ); 1359 } 1360 return this._auto(onSucc, onErr, () => this._addIceCandidate(cand)); 1361 } 1362 1363 async _addIceCandidate({ 1364 candidate, 1365 sdpMid, 1366 sdpMLineIndex, 1367 usernameFragment, 1368 }) { 1369 this._checkClosed(); 1370 return this._chain(async () => { 1371 if ( 1372 !this._pc.pendingRemoteDescription.length && 1373 !this._pc.currentRemoteDescription.length 1374 ) { 1375 throw new this._win.DOMException( 1376 "No remoteDescription.", 1377 "InvalidStateError" 1378 ); 1379 } 1380 return new Promise((resolve, reject) => { 1381 this._onAddIceCandidateSuccess = resolve; 1382 this._onAddIceCandidateError = reject; 1383 this._pc.addIceCandidate( 1384 candidate, 1385 sdpMid || "", 1386 usernameFragment || "", 1387 sdpMLineIndex 1388 ); 1389 }); 1390 }); 1391 } 1392 1393 restartIce() { 1394 this._pc.restartIce(); 1395 } 1396 1397 addStream(stream) { 1398 stream.getTracks().forEach(track => this.addTrack(track, stream)); 1399 } 1400 1401 addTrack(track, ...streams) { 1402 this._checkClosed(); 1403 1404 if ( 1405 this.getTransceivers().some( 1406 transceiver => transceiver.sender.track == track 1407 ) 1408 ) { 1409 throw new this._win.DOMException( 1410 "This track is already set on a sender.", 1411 "InvalidAccessError" 1412 ); 1413 } 1414 1415 let transceiver = this.getTransceivers().find(transceiver => { 1416 return ( 1417 transceiver.sender.track == null && 1418 transceiver.getKind() == track.kind && 1419 !transceiver.stopped && 1420 !transceiver.hasBeenUsedToSend() 1421 ); 1422 }); 1423 1424 if (transceiver) { 1425 transceiver.sender.setTrack(track); 1426 transceiver.sender.setStreamsImpl(...streams); 1427 if (transceiver.direction == "recvonly") { 1428 transceiver.setDirectionInternal("sendrecv"); 1429 } else if (transceiver.direction == "inactive") { 1430 transceiver.setDirectionInternal("sendonly"); 1431 } 1432 } else { 1433 transceiver = this._addTransceiverNoEvents( 1434 track, 1435 { 1436 streams, 1437 direction: "sendrecv", 1438 }, 1439 true 1440 ); 1441 } 1442 1443 this.updateNegotiationNeeded(); 1444 return transceiver.sender; 1445 } 1446 1447 removeTrack(sender) { 1448 this._checkClosed(); 1449 1450 if (!this._pc.createdSender(sender)) { 1451 throw new this._win.DOMException( 1452 "This sender was not created by this PeerConnection", 1453 "InvalidAccessError" 1454 ); 1455 } 1456 1457 let transceiver = this.getTransceivers().find( 1458 transceiver => !transceiver.stopped && transceiver.sender == sender 1459 ); 1460 1461 // If the transceiver was removed due to rollback, let it slide. 1462 if (!transceiver || !sender.track) { 1463 return; 1464 } 1465 1466 sender.setTrack(null); 1467 if (transceiver.direction == "sendrecv") { 1468 transceiver.setDirectionInternal("recvonly"); 1469 } else if (transceiver.direction == "sendonly") { 1470 transceiver.setDirectionInternal("inactive"); 1471 } 1472 1473 this.updateNegotiationNeeded(); 1474 } 1475 1476 _addTransceiverNoEvents(sendTrackOrKind, init, addTrackMagic) { 1477 let sendTrack = null; 1478 let kind; 1479 if (typeof sendTrackOrKind == "string") { 1480 kind = sendTrackOrKind; 1481 switch (kind) { 1482 case "audio": 1483 case "video": 1484 break; 1485 default: 1486 throw new this._win.TypeError("Invalid media kind"); 1487 } 1488 } else { 1489 sendTrack = sendTrackOrKind; 1490 kind = sendTrack.kind; 1491 } 1492 1493 try { 1494 return this._pc.addTransceiver(init, kind, sendTrack, addTrackMagic); 1495 } catch (e) { 1496 // Exceptions thrown by c++ code do not propagate. In most cases, that's 1497 // fine because we're using Promises, which can be copied. But this is 1498 // not promise-based, so we have to do this sketchy stuff. 1499 const holder = new StructuredCloneHolder( 1500 "", 1501 "", 1502 new ClonedErrorHolder(e) 1503 ); 1504 throw holder.deserialize(this._win); 1505 } 1506 } 1507 1508 addTransceiver(sendTrackOrKind, init) { 1509 this._checkClosed(); 1510 let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init); 1511 this.updateNegotiationNeeded(); 1512 return transceiver; 1513 } 1514 1515 updateNegotiationNeeded() { 1516 this._pc.updateNegotiationNeeded(); 1517 } 1518 1519 close() { 1520 if (this._closed) { 1521 return; 1522 } 1523 this._closed = true; 1524 this.changeIceConnectionState("closed"); 1525 if (this._localIdp) { 1526 this._localIdp.close(); 1527 } 1528 if (this._remoteIdp) { 1529 this._remoteIdp.close(); 1530 } 1531 this._pc.close(); 1532 this._suppressEvents = true; 1533 } 1534 1535 getLocalStreams() { 1536 this._checkClosed(); 1537 let localStreams = new Set(); 1538 this.getTransceivers().forEach(transceiver => { 1539 transceiver.sender.getStreams().forEach(stream => { 1540 localStreams.add(stream); 1541 }); 1542 }); 1543 return [...localStreams.values()]; 1544 } 1545 1546 getRemoteStreams() { 1547 this._checkClosed(); 1548 return this._pc.getRemoteStreams(); 1549 } 1550 1551 getSenders() { 1552 return this.getTransceivers() 1553 .filter(transceiver => !transceiver.stopped) 1554 .map(transceiver => transceiver.sender); 1555 } 1556 1557 getReceivers() { 1558 return this.getTransceivers() 1559 .filter(transceiver => !transceiver.stopped) 1560 .map(transceiver => transceiver.receiver); 1561 } 1562 1563 mozSetPacketCallback(callback) { 1564 this._onPacket = callback; 1565 } 1566 1567 mozEnablePacketDump(level, type, sending) { 1568 this._pc.enablePacketDump(level, type, sending); 1569 } 1570 1571 mozDisablePacketDump(level, type, sending) { 1572 this._pc.disablePacketDump(level, type, sending); 1573 } 1574 1575 getTransceivers() { 1576 return this._pc.getTransceivers(); 1577 } 1578 1579 get localDescription() { 1580 return this.pendingLocalDescription || this.currentLocalDescription; 1581 } 1582 1583 cacheDescription(name, type, sdp) { 1584 if ( 1585 !this[name] || 1586 this[name].type != type || 1587 this[name].sdp != sdp || 1588 this._legacyPref 1589 ) { 1590 this[name] = sdp.length 1591 ? new this._win.RTCSessionDescription({ type, sdp }) 1592 : null; 1593 } 1594 return this[name]; 1595 } 1596 1597 get currentLocalDescription() { 1598 this._checkClosed(); 1599 return this.cacheDescription( 1600 "_currentLocalDescription", 1601 this._pc.currentOfferer ? "offer" : "answer", 1602 this._pc.currentLocalDescription 1603 ); 1604 } 1605 1606 get pendingLocalDescription() { 1607 this._checkClosed(); 1608 return this.cacheDescription( 1609 "_pendingLocalDescription", 1610 this._pc.pendingOfferer ? "offer" : "answer", 1611 this._pc.pendingLocalDescription 1612 ); 1613 } 1614 1615 get remoteDescription() { 1616 return this.pendingRemoteDescription || this.currentRemoteDescription; 1617 } 1618 1619 get currentRemoteDescription() { 1620 this._checkClosed(); 1621 return this.cacheDescription( 1622 "_currentRemoteDescription", 1623 this._pc.currentOfferer ? "answer" : "offer", 1624 this._pc.currentRemoteDescription 1625 ); 1626 } 1627 1628 get pendingRemoteDescription() { 1629 this._checkClosed(); 1630 return this.cacheDescription( 1631 "_pendingRemoteDescription", 1632 this._pc.pendingOfferer ? "answer" : "offer", 1633 this._pc.pendingRemoteDescription 1634 ); 1635 } 1636 1637 get peerIdentity() { 1638 return this._peerIdentity; 1639 } 1640 get idpLoginUrl() { 1641 return this._localIdp.idpLoginUrl; 1642 } 1643 get id() { 1644 return this._pc.id; 1645 } 1646 set id(s) { 1647 this._pc.id = s; 1648 } 1649 get iceGatheringState() { 1650 return this._pc.iceGatheringState; 1651 } 1652 get iceConnectionState() { 1653 return this._iceConnectionState; 1654 } 1655 get connectionState() { 1656 return this._pc.connectionState; 1657 } 1658 1659 get signalingState() { 1660 // checking for our local pc closed indication 1661 // before invoking the pc methods. 1662 if (this._closed) { 1663 return "closed"; 1664 } 1665 return this._pc.signalingState; 1666 } 1667 1668 handleIceGatheringStateChange() { 1669 _globalPCList.notifyLifecycleObservers(this, "icegatheringstatechange"); 1670 this.dispatchEvent(new this._win.Event("icegatheringstatechange")); 1671 if (this.iceGatheringState === "complete") { 1672 this.dispatchEvent( 1673 new this._win.RTCPeerConnectionIceEvent("icecandidate", { 1674 candidate: null, 1675 }) 1676 ); 1677 } 1678 } 1679 1680 changeIceConnectionState(state) { 1681 if (state != this._iceConnectionState) { 1682 this._iceConnectionState = state; 1683 _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange"); 1684 if (!this._closed) { 1685 this.dispatchEvent(new this._win.Event("iceconnectionstatechange")); 1686 } 1687 } 1688 } 1689 1690 getStats(selector, onSucc, onErr) { 1691 if (selector !== null) { 1692 let matchingSenders = this.getSenders().filter(s => s.track === selector); 1693 let matchingReceivers = this.getReceivers().filter( 1694 r => r.track === selector 1695 ); 1696 1697 if (matchingSenders.length + matchingReceivers.length != 1) { 1698 throw new this._win.DOMException( 1699 "track must be associated with a unique sender or receiver, but " + 1700 " is associated with " + 1701 matchingSenders.length + 1702 " senders and " + 1703 matchingReceivers.length + 1704 " receivers.", 1705 "InvalidAccessError" 1706 ); 1707 } 1708 } 1709 1710 return this._auto(onSucc, onErr, () => this._pc.getStats(selector)); 1711 } 1712 1713 get sctp() { 1714 return this._pc.sctp; 1715 } 1716 1717 createDataChannel( 1718 label, 1719 { 1720 maxRetransmits, 1721 ordered, 1722 negotiated, 1723 id = null, 1724 maxRetransmitTime, 1725 maxPacketLifeTime, 1726 protocol, 1727 } = {} 1728 ) { 1729 this._checkClosed(); 1730 1731 if (maxPacketLifeTime === undefined) { 1732 maxPacketLifeTime = maxRetransmitTime; 1733 } 1734 1735 if (maxRetransmitTime !== undefined) { 1736 this.logWarning( 1737 "Use maxPacketLifeTime instead of deprecated maxRetransmitTime which will stop working soon in createDataChannel!" 1738 ); 1739 } 1740 1741 if (protocol.length > 32767) { 1742 // At least 65536/2 UTF-16 characters. UTF-8 might be too long. 1743 // Spec says to check how long |protocol| and |label| are in _bytes_. This 1744 // is a little ambiguous. For now, examine the length of the utf-8 encoding. 1745 const byteCounter = new TextEncoder(); 1746 1747 if (byteCounter.encode(protocol).length > 65535) { 1748 throw new this._win.TypeError( 1749 "protocol cannot be longer than 65535 bytes" 1750 ); 1751 } 1752 } 1753 1754 if (label.length > 32767) { 1755 const byteCounter = new TextEncoder(); 1756 if (byteCounter.encode(label).length > 65535) { 1757 throw new this._win.TypeError( 1758 "label cannot be longer than 65535 bytes" 1759 ); 1760 } 1761 } 1762 1763 if (!negotiated) { 1764 id = null; 1765 } else if (id === null) { 1766 throw new this._win.TypeError("id is required when negotiated is true"); 1767 } 1768 if (maxPacketLifeTime !== undefined && maxRetransmits !== undefined) { 1769 throw new this._win.TypeError( 1770 "Both maxPacketLifeTime and maxRetransmits cannot be provided" 1771 ); 1772 } 1773 if (id == 65535) { 1774 throw new this._win.TypeError("id cannot be 65535"); 1775 } 1776 // Must determine the type where we still know if entries are undefined. 1777 let type; 1778 if (maxPacketLifeTime !== undefined) { 1779 type = Ci.IPeerConnection.kDataChannelPartialReliableTimed; 1780 } else if (maxRetransmits !== undefined) { 1781 type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit; 1782 } else { 1783 type = Ci.IPeerConnection.kDataChannelReliable; 1784 } 1785 // Synchronous since it doesn't block. 1786 let dataChannel; 1787 try { 1788 dataChannel = this._pc.createDataChannel( 1789 label, 1790 protocol, 1791 type, 1792 ordered, 1793 maxPacketLifeTime, 1794 maxRetransmits, 1795 negotiated, 1796 id 1797 ); 1798 } catch (e) { 1799 if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { 1800 throw e; 1801 } 1802 1803 const msg = 1804 id === null ? "No available id could be generated" : "Id is in use"; 1805 throw new this._win.DOMException(msg, "OperationError"); 1806 } 1807 1808 // Spec says to only do this if this is the first DataChannel created, 1809 // but the c++ code that does the "is negotiation needed" checking will 1810 // only ever return true on the first one. 1811 this.updateNegotiationNeeded(); 1812 1813 return dataChannel; 1814 } 1815 } 1816 1817 setupPrototype(RTCPeerConnection, { 1818 classID: PC_CID, 1819 contractID: PC_CONTRACT, 1820 QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), 1821 _actions: { 1822 offer: Ci.IPeerConnection.kActionOffer, 1823 answer: Ci.IPeerConnection.kActionAnswer, 1824 pranswer: Ci.IPeerConnection.kActionPRAnswer, 1825 rollback: Ci.IPeerConnection.kActionRollback, 1826 }, 1827 }); 1828 1829 // This is a separate class because we don't want to expose it to DOM. 1830 1831 export class PeerConnectionObserver { 1832 init(win) { 1833 this._win = win; 1834 } 1835 1836 __init(dompc) { 1837 this._dompc = dompc._innerObject; 1838 } 1839 1840 newError({ message, name }) { 1841 return new this._dompc._win.DOMException(message, name); 1842 } 1843 1844 dispatchEvent(event) { 1845 this._dompc.dispatchEvent(event); 1846 } 1847 1848 onCreateOfferSuccess(sdp) { 1849 this._dompc._onCreateOfferSuccess(sdp); 1850 } 1851 1852 onCreateOfferError(error) { 1853 this._dompc._onCreateOfferFailure(this.newError(error)); 1854 } 1855 1856 onCreateAnswerSuccess(sdp) { 1857 this._dompc._onCreateAnswerSuccess(sdp); 1858 } 1859 1860 onCreateAnswerError(error) { 1861 this._dompc._onCreateAnswerFailure(this.newError(error)); 1862 } 1863 1864 onSetDescriptionSuccess() { 1865 this._dompc._onSetDescriptionSuccess(); 1866 } 1867 1868 onSetDescriptionError(error) { 1869 this._dompc._onSetDescriptionFailure(this.newError(error)); 1870 } 1871 1872 onAddIceCandidateSuccess() { 1873 this._dompc._onAddIceCandidateSuccess(); 1874 } 1875 1876 onAddIceCandidateError(error) { 1877 this._dompc._onAddIceCandidateError(this.newError(error)); 1878 } 1879 1880 onIceCandidate(sdpMLineIndex, sdpMid, candidate, usernameFragment) { 1881 let win = this._dompc._win; 1882 if (candidate || sdpMid || usernameFragment) { 1883 if (candidate.includes(" typ relay ")) { 1884 this._dompc._iceGatheredRelayCandidates = true; 1885 } 1886 candidate = new win.RTCIceCandidate({ 1887 candidate, 1888 sdpMid, 1889 sdpMLineIndex, 1890 usernameFragment, 1891 }); 1892 } 1893 this.dispatchEvent( 1894 new win.RTCPeerConnectionIceEvent("icecandidate", { candidate }) 1895 ); 1896 } 1897 1898 // This method is primarily responsible for updating iceConnectionState. 1899 // This state is defined in the WebRTC specification as follows: 1900 // 1901 // iceConnectionState: 1902 // ------------------- 1903 // new Any of the RTCIceTransports are in the new state and none 1904 // of them are in the checking, failed or disconnected state. 1905 // 1906 // checking Any of the RTCIceTransports are in the checking state and 1907 // none of them are in the failed or disconnected state. 1908 // 1909 // connected All RTCIceTransports are in the connected, completed or 1910 // closed state and at least one of them is in the connected 1911 // state. 1912 // 1913 // completed All RTCIceTransports are in the completed or closed state 1914 // and at least one of them is in the completed state. 1915 // 1916 // failed Any of the RTCIceTransports are in the failed state. 1917 // 1918 // disconnected Any of the RTCIceTransports are in the disconnected state 1919 // and none of them are in the failed state. 1920 // 1921 // closed All of the RTCIceTransports are in the closed state. 1922 1923 handleIceConnectionStateChange(iceConnectionState) { 1924 let pc = this._dompc; 1925 if (pc.iceConnectionState === iceConnectionState) { 1926 return; 1927 } 1928 1929 if (iceConnectionState === "failed") { 1930 if (!pc._hasStunServer) { 1931 pc.logError( 1932 "ICE failed, add a STUN server and see about:webrtc for more details" 1933 ); 1934 } else if (!pc._hasTurnServer) { 1935 pc.logError( 1936 "ICE failed, add a TURN server and see about:webrtc for more details" 1937 ); 1938 } else if (pc._hasTurnServer && !pc._iceGatheredRelayCandidates) { 1939 pc.logError( 1940 "ICE failed, your TURN server appears to be broken, see about:webrtc for more details" 1941 ); 1942 } else { 1943 pc.logError("ICE failed, see about:webrtc for more details"); 1944 } 1945 } 1946 1947 pc.changeIceConnectionState(iceConnectionState); 1948 } 1949 1950 onStateChange(state) { 1951 if (!this._dompc) { 1952 return; 1953 } 1954 1955 if (state == "SignalingState") { 1956 this.dispatchEvent(new this._win.Event("signalingstatechange")); 1957 return; 1958 } 1959 1960 if (!this._dompc._pc) { 1961 return; 1962 } 1963 1964 switch (state) { 1965 case "IceConnectionState": 1966 this.handleIceConnectionStateChange(this._dompc._pc.iceConnectionState); 1967 break; 1968 1969 case "IceGatheringState": 1970 this._dompc.handleIceGatheringStateChange(); 1971 break; 1972 1973 case "ConnectionState": 1974 _globalPCList.notifyLifecycleObservers(this, "connectionstatechange"); 1975 this.dispatchEvent(new this._win.Event("connectionstatechange")); 1976 break; 1977 1978 default: 1979 this._dompc.logWarning("Unhandled state type: " + state); 1980 break; 1981 } 1982 } 1983 1984 onTransceiverNeeded(kind, transceiverImpl) { 1985 this._dompc._onTransceiverNeeded(kind, transceiverImpl); 1986 } 1987 1988 notifyDataChannel(channel) { 1989 this.dispatchEvent( 1990 new this._dompc._win.RTCDataChannelEvent("datachannel", { channel }) 1991 ); 1992 } 1993 1994 fireTrackEvent(receiver, streams) { 1995 const pc = this._dompc; 1996 const transceiver = pc.getTransceivers().find(t => t.receiver == receiver); 1997 if (!transceiver) { 1998 return; 1999 } 2000 const track = receiver.track; 2001 this.dispatchEvent( 2002 new this._win.RTCTrackEvent("track", { 2003 transceiver, 2004 receiver, 2005 track, 2006 streams, 2007 }) 2008 ); 2009 // Fire legacy event as well for a little bit. 2010 this.dispatchEvent( 2011 new this._win.MediaStreamTrackEvent("addtrack", { track }) 2012 ); 2013 } 2014 2015 fireStreamEvent(stream) { 2016 const ev = new this._win.MediaStreamEvent("addstream", { stream }); 2017 this.dispatchEvent(ev); 2018 } 2019 2020 fireNegotiationNeededEvent() { 2021 this.dispatchEvent(new this._win.Event("negotiationneeded")); 2022 } 2023 2024 onPacket(level, type, sending, packet) { 2025 var pc = this._dompc; 2026 if (pc._onPacket) { 2027 pc._onPacket(level, type, sending, packet); 2028 } 2029 } 2030 } 2031 2032 setupPrototype(PeerConnectionObserver, { 2033 classID: PC_OBS_CID, 2034 contractID: PC_OBS_CONTRACT, 2035 QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), 2036 }); 2037 2038 export class RTCPeerConnectionStatic { 2039 init(win) { 2040 this._winID = win.windowGlobalChild.innerWindowId; 2041 } 2042 2043 registerPeerConnectionLifecycleCallback(cb) { 2044 _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb); 2045 } 2046 } 2047 2048 setupPrototype(RTCPeerConnectionStatic, { 2049 classID: PC_STATIC_CID, 2050 contractID: PC_STATIC_CONTRACT, 2051 QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]), 2052 }); 2053 2054 export class CreateOfferRequest { 2055 constructor(windowID, innerWindowID, callID, isSecure) { 2056 Object.assign(this, { windowID, innerWindowID, callID, isSecure }); 2057 } 2058 } 2059 2060 setupPrototype(CreateOfferRequest, { 2061 classID: PC_COREQUEST_CID, 2062 contractID: PC_COREQUEST_CONTRACT, 2063 QueryInterface: ChromeUtils.generateQI([]), 2064 });