dom/media/PeerConnection.jsm
author Byron Campen [:bwc] <docfaraday@gmail.com>
Tue, 19 Mar 2019 16:49:05 +0000
changeset 465099 05ae948300ea8fd0b963bbd35828ed52bfee2a6e
parent 465097 703b266e352982cfbb6120f424fe6162bbffca05
child 465227 df88ddfaf8e33ad473c182c68c9844e4b1b22402
permissions -rw-r--r--
Bug 1535410: Check for null mid/level in addIceCandidate. r=jib Differential Revision: https://phabricator.services.mozilla.com/D23727

/* jshint moz:true, browser:true */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "PeerConnectionIdp",
  "resource://gre/modules/media/PeerConnectionIdp.jsm");
ChromeUtils.defineModuleGetter(this, "convertToRTCStatsReport",
  "resource://gre/modules/media/RTCStatsReport.jsm");

const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1";
const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1";
const PC_TRANSCEIVER_CONTRACT = "@mozilla.org/dom/rtptransceiver;1";
const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";

const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
const PC_TRANSCEIVER_CID = Components.ID("{09475754-103a-41f5-a2d0-e1f27eb0b537}");
const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}");
const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}");

const TELEMETRY_PC_CONNECTED = "webrtc.peerconnection.connected";
const TELEMETRY_PC_PROMISE_GETSTATS = "webrtc.peerconnection.promise_stats_used";
function logMsg(msg, file, line, flag, winID) {
  let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
  let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
  scriptError.initWithWindowID(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).
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([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
  classID: PC_MANAGER_CID,
  _xpcom_factory: {
    createInstance(outer, iid) {
      if (outer) {
        throw Cr.NS_ERROR_NO_AGGREGATION;
      }
      return _globalPCList.QueryInterface(iid);
    },
  },
});

var _globalPCList = new GlobalPCList();

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([Ci.nsIDOMGlobalPropertyInitializer]),
});

class RTCSessionDescription {
  init(win) {
    this._win = win;
    this._winID = this._win.windowUtils.currentInnerWindowID;
  }

  __init({ type, sdp }) {
    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([Ci.nsIDOMGlobalPropertyInitializer]),
});

class RTCStatsReport {
  constructor(pc, dict) {
    this._pc = pc;
    this._win = pc._win;
    this._pcid = dict.pcid;
    this._report = convertToRTCStatsReport(dict);
  }

  setInternal(aKey, aObj) {
    return this.__DOM_IMPL__.__set(aKey, aObj);
  }

  // Must be called after our webidl sandwich is made.

  makeStatsPublic() {
    for (const key in this._report) {
      const value = this._report[key];
      if (value.type == "local-candidate" || value.type == "remote-candidate") {
        delete value.transportId;
      }
      this.setInternal(key, Cu.cloneInto(value, this._win));
    }
  }

  get mozPcid() { return this._pcid; }

  __onget(key, stat) {
    return stat;
  }
}
setupPrototype(RTCStatsReport, {
  classID: PC_STATS_CID,
  contractID: PC_STATS_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([]),
});

// This is its own class so that it does not need to be exposed to the client.
class PeerConnectionTelemetry {
  // Record which style(s) of invocation for getStats are used
  recordGetStats() {
    Services.telemetry.scalarAdd(TELEMETRY_PC_PROMISE_GETSTATS, 1);
    this.recordGetStats = () => {};
  }
  // ICE connection state enters connected or completed.
  recordConnected() {
    Services.telemetry.scalarAdd(TELEMETRY_PC_CONNECTED, 1);
    this.recordConnected = () => {};
  }
}

// Cache for RTPSourceEntries
// Note: each cache is only valid for one JS event loop execution
class RTCRtpSourceCache {
  constructor() {
    // The time in RTP source time (ms)
    this.tsNowInRtpSourceTime = null;
    // The time in JS
    this.jsTimestamp = null;
    // Time difference between JS time and RTP source time
    this.timestampOffset = null;
    // RTPSourceEntries cached by track id
    this.rtpSourcesByTrackId = new Map();
    // Has a cache wipe already been scheduled
    this.scheduledClear = null;
  }
}

class RTCPeerConnection {
  constructor() {
    this._receiveStreams = new Map();
    // Used to fire onaddstream, remove when we don't do that anymore.
    this._newStreams = [];
    this._transceivers = [];

    this._pc = null;
    this._closed = false;
    this._currentRole = null;
    this._pendingRole = null;

    // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9
    // canTrickle == null means unknown; when a remote description is received it
    // is set to true or false based on the presence of the "trickle" ice-option
    this._canTrickle = null;

    // States
    this._iceGatheringState = this._iceConnectionState = "new";

    this._hasStunServer = this._hasTurnServer = false;
    this._iceGatheredRelayCandidates = false;
    // Stored webrtc timing information
    this._storedRtpSourceReferenceTime = null;
    // Stores cached RTP sources state
    this._rtpSourceCache = new RTCRtpSourceCache();
    // Records telemetry
    this._pcTelemetry = new PeerConnectionTelemetry();
  }

  init(win) {
    this._win = win;
  }

  __init(rtcConfig) {
    this._winID = this._win.windowUtils.currentInnerWindowID;
    // TODO: Update this code once we support pc.setConfiguration, to track
    // setting from content independently from pref (Bug 1181768).
    if (rtcConfig.iceTransportPolicy == "all" &&
        Services.prefs.getBoolPref("media.peerconnection.ice.relay_only")) {
      rtcConfig.iceTransportPolicy = "relay";
    }
    this._config = Object.assign({}, rtcConfig);

    if (!rtcConfig.iceServers ||
        !Services.prefs.getBoolPref("media.peerconnection.use_document_iceservers")) {
      try {
         rtcConfig.iceServers =
           JSON.parse(Services.prefs.getCharPref("media.peerconnection.default_iceservers") || "[]");
      } catch (e) {
        this.logWarning(
            "Ignoring invalid media.peerconnection.default_iceservers in about:config");
        rtcConfig.iceServers = [];
      }
      try {
        this._mustValidateRTCConfiguration(rtcConfig,
            "Ignoring invalid media.peerconnection.default_iceservers in about:config");
      } catch (e) {
        this.logWarning(e.message);
        rtcConfig.iceServers = [];
      }
    } else {
      // This gets executed in the typical case when iceServers
      // are passed in through the web page.
      this._mustValidateRTCConfiguration(rtcConfig,
        "RTCPeerConnection constructor passed invalid RTCConfiguration");
    }
    var principal = Cu.getWebIDLCallerPrincipal();
    this._isChrome = Services.scriptSecurityManager.isSystemPrincipal(principal);

    if (_globalPCList._networkdown) {
      throw new this._win.DOMException(
          "Can't create RTCPeerConnections when the network is down",
          "InvalidStateError");
    }

    this.makeGetterSetterEH("ontrack");
    this.makeLegacyGetterSetterEH("onaddstream", "Use peerConnection.ontrack instead.");
    this.makeLegacyGetterSetterEH("onaddtrack", "Use peerConnection.ontrack instead.");
    this.makeGetterSetterEH("onicecandidate");
    this.makeGetterSetterEH("onnegotiationneeded");
    this.makeGetterSetterEH("onsignalingstatechange");
    this.makeGetterSetterEH("ondatachannel");
    this.makeGetterSetterEH("oniceconnectionstatechange");
    this.makeGetterSetterEH("onicegatheringstatechange");
    this.makeGetterSetterEH("onidentityresult");
    this.makeGetterSetterEH("onpeeridentity");
    this.makeGetterSetterEH("onidpassertionerror");
    this.makeGetterSetterEH("onidpvalidationerror");

    this._pc = new this._win.PeerConnectionImpl();
    this._operationsChain = this._win.Promise.resolve();

    this.__DOM_IMPL__._innerObject = this;
    this._observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__);

    this._warnDeprecatedStatsRemoteAccessNullable = { warn: (key) =>
      this.logWarning(`Detected soon-to-break getStats() use with key="${key}"! stat.isRemote goes away in Firefox 66, but won't warn there!\
 - See https://blog.mozilla.org/webrtc/getstats-isremote-66/`) };

    // Add a reference to the PeerConnection to global list (before init).
    _globalPCList.addPC(this);

    this._impl.initialize(this._observer, this._win, rtcConfig,
                          Services.tm.currentThread);

    this._certificateReady = this._initCertificate(rtcConfig.certificates);
    this._initIdp();
    _globalPCList.notifyLifecycleObservers(this, "initialized");
  }

  get _impl() {
    if (!this._pc) {
      throw new this._win.DOMException(
          "RTCPeerConnection is gone (did you enter Offline mode?)",
          "InvalidStateError");
    }
    return this._pc;
  }

  getConfiguration() {
    return this._config;
  }

  async _initCertificate(certificates = []) {
    let certificate;
    if (certificates.length > 1) {
      throw new this._win.DOMException(
        "RTCPeerConnection does not currently support multiple certificates",
        "NotSupportedError");
    }
    if (certificates.length) {
      certificate = certificates.find(c => c.expires > Date.now());
      if (!certificate) {
        throw new this._win.DOMException(
          "Unable to create RTCPeerConnection with an expired certificate",
          "InvalidParameterError");
      }
    }

    if (!certificate) {
      certificate = await this._win.RTCPeerConnection.generateCertificate({
        name: "ECDSA", namedCurve: "P-256",
      });
    }
    // Is the PC still around after the await?
    if (!this._closed) {
      this._impl.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 PeerConnectionIdp(this._win, idpTimeout);
    this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout);
  }

  // Add a function to the internal operations chain.

  async _chain(func) {
    let p = (async () => {
      await this._operationsChain;
      // Don't _checkClosed() inside the chain, because it throws, and spec
      // behavior is to NOT reject outstanding promises on close. This is what
      // happens most of the time anyways, as the c++ code stops calling us once
      // closed, hanging the chain. However, c++ may already have queued tasks
      // on us, so if we're one of those then sit back.
      if (this._closed) {
        return null;
      }
      return func();
    })();
    // don't propagate errors in the operations chain (this is a fork of p).
    this._operationsChain = p.catch(() => {});
    return p;
  }

  // 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.
   */
  _mustValidateRTCConfiguration({ 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.SyntaxError(`${msg} - malformed URI: ${uriStr}`);
        }
        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 == 0) {
        throw new this._win.SyntaxError(`${msg} - urls is empty`);
      }
      urls.map(url => nicerNewURI(url)).forEach(({ scheme, spec }) => {
        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;
          stunServers += 1;
        } else if (scheme in { stun: 1, stuns: 1 }) {
          this._hasStunServer = true;
          stunServers += 1;
        } else {
          throw new this._win.SyntaxError(`${msg} - improper scheme: ${scheme}`);
        }
        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.exceptionFlag);

    // Safely call onerror directly if present (necessary for testing)
    try {
      if (typeof this._win.onerror === "function") {
        this._win.onerror(e.message, e.fileName, e.lineNumber);
      }
    } catch (e) {
      // If onerror itself throws, service it.
      try {
        this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.errorFlag);
      } catch (e) {}
    }
  }

  logError(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) { return this.setEH(name, h); },
                          });
  }

  makeLegacyGetterSetterEH(name, msg) {
    Object.defineProperty(this, name,
                          {
                            get() { return this.getEH(name); },
                            set(h) {
                              this.logWarning(name + " is deprecated! " + msg);
                              return 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._transceivers.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._transceivers
      .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");
        }
      });
  }

  async _createOffer(options) {
    this._checkClosed();
    this._ensureTransceiversForOfferToReceive(options);
    this._syncTransceivers();
    let origin = Cu.getWebIDLCallerPrincipal().origin;
    return this._chain(async () => {
      let haveAssertion;
      if (this._localIdp.enabled) {
        haveAssertion = this._getIdentityAssertion(origin);
      }
      await this._getPermission();
      await this._certificateReady;
      let sdp = await new Promise((resolve, reject) => {
        this._onCreateOfferSuccess = resolve;
        this._onCreateOfferFailure = reject;
        this._impl.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));
  }

  async _createAnswer(options) {
    this._checkClosed();
    this._syncTransceivers();
    let origin = Cu.getWebIDLCallerPrincipal().origin;
    return this._chain(async () => {
      // We give up line-numbers in errors by doing this here, but do all
      // state-checks inside the chain, to support the legacy feature that
      // callers don't have to wait for setRemoteDescription to finish.
      if (!this.remoteDescription) {
        throw new this._win.DOMException("setRemoteDescription not called",
                                         "InvalidStateError");
      }
      if (this.remoteDescription.type != "offer") {
        throw new this._win.DOMException("No outstanding offer",
                                         "InvalidStateError");
      }
      let haveAssertion;
      if (this._localIdp.enabled) {
        haveAssertion = this._getIdentityAssertion(origin);
      }
      await this._getPermission();
      await this._certificateReady;
      let sdp = await new Promise((resolve, reject) => {
        this._onCreateAnswerSuccess = resolve;
        this._onCreateAnswerFailure = reject;
        this._impl.createAnswer();
      });
      if (haveAssertion) {
        await haveAssertion;
        sdp = this._localIdp.addIdentityAttribute(sdp);
      }
      return Cu.cloneInto({ type: "answer", sdp }, this._win);
    });
  }

  async _getPermission() {
    if (!this._havePermission) {
      let privileged = this._isChrome ||
          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.windowUtils.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(action, type, sdp) {
    if (action === undefined) {
      throw new this._win.DOMException(
          "Invalid type " + type + " provided to setLocalDescription",
          "InvalidParameterError");
    }
    if (action == Ci.IPeerConnection.kActionPRAnswer) {
      throw new this._win.DOMException("pranswer not yet implemented",
                                       "NotSupportedError");
    }

    if (!sdp && action != Ci.IPeerConnection.kActionRollback) {
      throw new this._win.DOMException(
          "Empty or null SDP provided to setLocalDescription",
          "InvalidParameterError");
    }

    // 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));
  }

  async _setLocalDescription({ type, sdp }) {
    this._checkClosed();

    let action = this._actions[type];

    this._sanityCheckSdp(action, type, sdp);

    return this._chain(async () => {
      await this._getPermission();
      await new Promise((resolve, reject) => {
        this._onSetLocalDescriptionSuccess = resolve;
        this._onSetLocalDescriptionFailure = reject;
        this._impl.setLocalDescription(action, sdp);
      });
      this._negotiationNeeded = false;
      if (type == "answer") {
        this._currentRole = "answerer";
        this._pendingRole = null;
      } else {
        this._pendingRole = "offerer";
      }
      this.updateNegotiationNeeded();
    });
  }

  async _validateIdentity(sdp, origin) {
    let expectedIdentity;

    // Only run a single identity verification at a time.  We have to do this to
    // avoid problems with the fact that identity validation doesn't block the
    // resolution of setRemoteDescription().
    let p = (async () => {
      try {
        await this._lastIdentityValidation;
        let msg = await this._remoteIdp.verifyIdentityFromSDP(sdp, origin);
        expectedIdentity = this._impl.peerIdentity;
        // If this pc has an identity already, then the identity in sdp must match
        if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
          this.close();
          throw new this._win.DOMException(
            "Peer Identity mismatch, expected: " + expectedIdentity,
            "IncompatibleSessionDescriptionError");
        }
        if (msg) {
          // Set new identity and generate an event.
          this._impl.peerIdentity = msg.identity;
          this._resolvePeerIdentity(Cu.cloneInto({
            idp: this._remoteIdp.provider,
            name: msg.identity,
          }, this._win));
        }
      } catch (e) {
        this._rejectPeerIdentity(e);
        // If we don't expect a specific peer identity, failure to get a valid
        // peer identity is not a terminal state, so replace the promise to
        // allow another attempt.
        if (!this._impl.peerIdentity) {
          this._resetPeerIdentityPromise();
        }
        throw e;
      }
    })();
    this._lastIdentityValidation = p.catch(() => {});

    // Only wait for IdP validation if we need identity matching
    if (expectedIdentity) {
      await p;
    }
  }

  setRemoteDescription(desc, onSucc, onErr) {
    return this._auto(onSucc, onErr, () => this._setRemoteDescription(desc));
  }

  async _setRemoteDescription({ type, sdp }) {
    this._checkClosed();

    let action = this._actions[type];

    this._sanityCheckSdp(action, type, sdp);

    // Get caller's origin before hitting the promise chain
    let origin = Cu.getWebIDLCallerPrincipal().origin;

    return this._chain(async () => {
      let haveSetRemote = (async () => {
        await this._getPermission();
        await new Promise((resolve, reject) => {
          this._onSetRemoteDescriptionSuccess = resolve;
          this._onSetRemoteDescriptionFailure = reject;
          this._impl.setRemoteDescription(action, sdp);
        });
        this._updateCanTrickle();
      })();

      if (action != Ci.IPeerConnection.kActionRollback) {
        // Do setRemoteDescription and identity validation in parallel
        await this._validateIdentity(sdp, origin);
      }
      await haveSetRemote;
      this._negotiationNeeded = false;
      if (type == "answer") {
        this._currentRole = "offerer";
        this._pendingRole = null;
      } else {
        this._pendingRole = "answerer";
      }
      this.updateNegotiationNeeded();
    });
  }

  setIdentityProvider(provider,
                      {protocol, usernameHint, peerIdentity} = {}) {
    this._checkClosed();
    this._localIdp.setIdentityProvider(provider,
                                       protocol, usernameHint, peerIdentity);
  }

  async _getIdentityAssertion(origin) {
    await this._certificateReady;
    return this._localIdp.getIdentityAssertion(this._impl.fingerprint, origin);
  }

  getIdentityAssertion() {
    this._checkClosed();
    let origin = Cu.getWebIDLCallerPrincipal().origin;
    return this._win.Promise.resolve(this._chain(() =>
        this._getIdentityAssertion(origin)));
  }

  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.DOMException(
        "Cannot add a candidate without specifying either sdpMid or sdpMLineIndex",
        "TypeError");
    }
    return this._auto(onSucc, onErr, () => this._addIceCandidate(cand));
  }

  async _addIceCandidate({ candidate, sdpMid, sdpMLineIndex, usernameFragment }) {
    this._checkClosed();
    return this._chain(() => {
      return new Promise((resolve, reject) => {
        this._onAddIceCandidateSuccess = resolve;
        this._onAddIceCandidateError = reject;
        this._impl.addIceCandidate(
          candidate, sdpMid || "", usernameFragment || "", sdpMLineIndex);
      });
    });
  }

  addStream(stream) {
    stream.getTracks().forEach(track => this.addTrack(track, stream));
  }

  addTrack(track, ...streams) {
    this._checkClosed();

    if (this._transceivers.some(
          transceiver => transceiver.sender.track == track)) {
      throw new this._win.DOMException("This track is already set on a sender.",
                                       "InvalidAccessError");
    }

    let transceiver = this._transceivers.find(transceiver => {
      return transceiver.sender.track == null &&
             transceiver.getKind() == track.kind &&
             !transceiver.stopped &&
             !transceiver.hasBeenUsedToSend();
    });

    if (transceiver) {
      transceiver.sender.setTrack(track);
      transceiver.sender.setStreams(streams);
      if (transceiver.direction == "recvonly") {
        transceiver.setDirectionInternal("sendrecv");
      } else if (transceiver.direction == "inactive") {
        transceiver.setDirectionInternal("sendonly");
      }
    } else {
      transceiver = this._addTransceiverNoEvents(track, {
        streams,
        direction: "sendrecv",
      });
    }

    transceiver.setAddTrackMagic();
    transceiver.sync();
    this.updateNegotiationNeeded();
    return transceiver.sender;
  }

  removeTrack(sender) {
    this._checkClosed();

    sender.checkWasCreatedByPc(this.__DOM_IMPL__);

    let transceiver =
      this._transceivers.find(transceiver => 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");
    }

    transceiver.sync();
    this.updateNegotiationNeeded();
  }

  _addTransceiverNoEvents(sendTrackOrKind, init) {
    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;
    }

    let transceiverImpl = this._impl.createTransceiverImpl(kind, sendTrack);
    let transceiver = this._win.RTCRtpTransceiver._create(
        this._win,
        new RTCRtpTransceiver(this, transceiverImpl, init, kind, sendTrack));
    transceiver.sync();
    this._transceivers.push(transceiver);
    return transceiver;
  }

  _onTransceiverNeeded(kind, transceiverImpl) {
    let init = {direction: "recvonly"};
    let transceiver = this._win.RTCRtpTransceiver._create(
        this._win,
        new RTCRtpTransceiver(this, transceiverImpl, init, kind, null));
    transceiver.sync();
    this._transceivers.push(transceiver);
  }

  /* Returns a dictionary with three keys:
   * sources: a list of contributing and synchronization sources
   * sourceClockOffset: an offset to apply to the source timestamp to get a
   * very close approximation of the sample time with respect to the local
   * clock.
   * jsTimestamp: the current JS time
   * Note: because the two clocks can drift with respect to each other, once
   *  a timestamp offset has been calculated it should not be recalculated
   *  until the timestamp changes, this way it will not appear as if a new
   *  audio level sample has arrived.
   */
  _getRtpSources(receiver) {
    let cache = this._rtpSourceCache;
    // Schedule cache invalidation
    if (!cache.scheduledClear) {
      cache.scheduledClear = true;
      Promise.resolve().then(() => {
        this._rtpSourceCache = new RTCRtpSourceCache();
      });
    }
    // Fetch the RTP source local time, store it for reuse, calculate
    // the local offset, likewise store it for reuse.
    if (cache.tsNowInRtpSourceTime !== undefined) {
      cache.tsNowInRtpSourceTime = this._impl.getNowInRtpSourceReferenceTime();
      cache.jsTimestamp = this._win.performance.now() + this._win.performance.timeOrigin;
      cache.timestampOffset = cache.jsTimestamp - cache.tsNowInRtpSourceTime;
    }
    let id = receiver.track.id;
    if (cache.rtpSourcesByTrackId[id] === undefined) {
      cache.rtpSourcesByTrackId[id] =
          this._impl.getRtpSources(receiver.track, cache.tsNowInRtpSourceTime);
    }
    return {
      sources: cache.rtpSourcesByTrackId[id],
      sourceClockOffset: cache.timestampOffset,
      jsTimestamp: cache.jsTimestamp,
    };
  }

  addTransceiver(sendTrackOrKind, init) {
    this._checkClosed();
    let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
    this.updateNegotiationNeeded();
    return transceiver;
  }

  _syncTransceivers() {
    this._transceivers.forEach(transceiver => transceiver.sync());
  }

  updateNegotiationNeeded() {
    if (this._closed || this.signalingState != "stable") {
      return;
    }

    let negotiationNeeded = this._impl.checkNegotiationNeeded();
    if (!negotiationNeeded) {
      this._negotiationNeeded = false;
      return;
    }

    if (this._negotiationNeeded) {
      return;
    }

    this._negotiationNeeded = true;

    this._queueTaskWithClosedCheck(() => {
      if (this._negotiationNeeded) {
        this.dispatchEvent(new this._win.Event("negotiationneeded"));
      }
    });
  }

  _processTrackAdditionsAndRemovals() {
    let postProcessing = {
      updateStreamFunctions: [],
      muteTracks: [],
      trackEvents: [],
    };

    for (let transceiver of this._transceivers) {
      transceiver.receiver.processTrackAdditionsAndRemovals(transceiver,
                                                            postProcessing);
    }

    for (let f of postProcessing.updateStreamFunctions) {
      f();
    }

    for (let t of postProcessing.muteTracks) {
      t.mutedChanged(true);
    }

    for (let ev of postProcessing.trackEvents) {
      this.dispatchEvent(ev);
    }
  }

  // TODO(Bug 1241291): Legacy event, remove eventually
  _fireLegacyAddStreamEvents() {
    for (let stream of this._newStreams) {
      let ev = new this._win.MediaStreamEvent("addstream", { stream });
      this.dispatchEvent(ev);
    }
    this._newStreams = [];
  }

  _getOrCreateStream(id) {
    if (!this._receiveStreams.has(id)) {
      let stream = new this._win.MediaStream();
      stream.assignId(id);
      this._newStreams.push(stream);
      this._receiveStreams.set(id, stream);
    }

    return this._receiveStreams.get(id);
  }

  _insertDTMF(transceiverImpl, tones, duration, interToneGap) {
    return this._impl.insertDTMF(transceiverImpl, tones, duration, interToneGap);
  }

  _getDTMFToneBuffer(sender) {
    return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
  }

  _replaceTrackNoRenegotiation(transceiverImpl, withTrack) {
    this._impl.replaceTrackNoRenegotiation(transceiverImpl, withTrack);
  }

  close() {
    if (this._closed) {
      return;
    }
    this._closed = true;
    this.changeIceConnectionState("closed");
    if (this._localIdp) {
        this._localIdp.close();
    }
    if (this._remoteIdp) {
        this._remoteIdp.close();
    }
    if (!this._suppressEvents) {
      this._transceivers.forEach(t => t.setStopped());
    }
    this._impl.close();
    this._suppressEvents = true;
    delete this._pc;
    delete this._observer;
  }

  getLocalStreams() {
    this._checkClosed();
    let localStreams = new Set();
    this._transceivers.forEach(transceiver => {
      transceiver.sender.getStreams().forEach(stream => {
        localStreams.add(stream);
      });
    });
    return [...localStreams.values()];
  }

  getRemoteStreams() {
    this._checkClosed();
    return [...this._receiveStreams.values()];
  }

  getSenders() {
    return this.getTransceivers()
      .filter(transceiver => !transceiver.stopped)
      .map(transceiver => transceiver.sender);
  }

  getReceivers() {
    return this.getTransceivers()
      .filter(transceiver => !transceiver.stopped)
      .map(transceiver => transceiver.receiver);
  }

  // test-only: get the current time using the webrtc clock
  mozGetNowInRtpSourceReferenceTime() {
    return this._impl.getNowInRtpSourceReferenceTime();
  }

  // test-only: insert a contributing source entry for a track
  mozInsertAudioLevelForContributingSource(receiver,
                                           source,
                                           timestamp,
                                           hasLevel,
                                           level) {
    this._impl.insertAudioLevelForContributingSource(receiver.track,
                                                     source,
                                                     timestamp,
                                                     hasLevel,
                                                     level);
  }

  mozAddRIDExtension(receiver, extensionId) {
    this._impl.addRIDExtension(receiver.track, extensionId);
  }

  mozAddRIDFilter(receiver, rid) {
    this._impl.addRIDFilter(receiver.track, rid);
  }

  mozSetPacketCallback(callback) {
    this._onPacket = callback;
  }

  mozEnablePacketDump(level, type, sending) {
    this._impl.enablePacketDump(level, type, sending);
  }

  mozDisablePacketDump(level, type, sending) {
    this._impl.disablePacketDump(level, type, sending);
  }

  getTransceivers() {
    return this._transceivers;
  }

  get localDescription() {
    return this.pendingLocalDescription || this.currentLocalDescription;
  }

  get currentLocalDescription() {
    this._checkClosed();
    let sdp = this._impl.currentLocalDescription;
    if (sdp.length == 0) {
      return null;
    }
    const type = this._currentRole == "answerer" ? "answer" : "offer";
    return new this._win.RTCSessionDescription({ type, sdp });
  }

  get pendingLocalDescription() {
    this._checkClosed();
    let sdp = this._impl.pendingLocalDescription;
    if (sdp.length == 0) {
      return null;
    }
    const type = this._pendingRole == "answerer" ? "answer" : "offer";
    return new this._win.RTCSessionDescription({ type, sdp });
  }


  get remoteDescription() {
    return this.pendingRemoteDescription || this.currentRemoteDescription;
  }

  get currentRemoteDescription() {
    this._checkClosed();
    let sdp = this._impl.currentRemoteDescription;
    if (sdp.length == 0) {
      return null;
    }
    const type = this._currentRole == "offerer" ? "answer" : "offer";
    return new this._win.RTCSessionDescription({ type, sdp });
  }

  get pendingRemoteDescription() {
    this._checkClosed();
    let sdp = this._impl.pendingRemoteDescription;
    if (sdp.length == 0) {
      return null;
    }
    const type = this._pendingRole == "offerer" ? "answer" : "offer";
    return new this._win.RTCSessionDescription({ type, sdp });
  }

  get peerIdentity() { return this._peerIdentity; }
  get idpLoginUrl() { return this._localIdp.idpLoginUrl; }
  get id() { return this._impl.id; }
  set id(s) { this._impl.id = s; }
  get iceGatheringState() { return this._iceGatheringState; }
  get iceConnectionState() { return this._iceConnectionState; }

  get signalingState() {
    // checking for our local pc closed indication
    // before invoking the pc methods.
    if (this._closed) {
      return "closed";
    }
    return {
      "SignalingInvalid":            "",
      "SignalingStable":             "stable",
      "SignalingHaveLocalOffer":     "have-local-offer",
      "SignalingHaveRemoteOffer":    "have-remote-offer",
      "SignalingHaveLocalPranswer":  "have-local-pranswer",
      "SignalingHaveRemotePranswer": "have-remote-pranswer",
      "SignalingClosed":             "closed",
    }[this._impl.signalingState];
  }

  changeIceGatheringState(state) {
    this._iceGatheringState = state;
    _globalPCList.notifyLifecycleObservers(this, "icegatheringstatechange");
    this.dispatchEvent(new this._win.Event("icegatheringstatechange"));
  }

  changeIceConnectionState(state) {
    if (state != this._iceConnectionState) {
      this._iceConnectionState = state;
      _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange");
      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");
      }
    }

    if (this._iceConnectionState === "completed" ||
        this._iceConnectionState === "connected") {
      this._pcTelemetry.recordGetStats();
    }

    return this._auto(onSucc, onErr, () => this._getStats(selector));
  }

  async _getStats(selector) {
    // getStats is allowed even in closed state.
    return this._chain(() => new Promise((resolve, reject) => {
      this._onGetStatsSuccess = resolve;
      this._onGetStatsFailure = reject;
      this._impl.getStats(selector);
    }));
  }

  createDataChannel(label, {
                      maxRetransmits, ordered, negotiated, id = 0xFFFF,
                      maxRetransmitTime, maxPacketLifeTime = maxRetransmitTime,
                      protocol,
                    } = {}) {
    this._checkClosed();

    if (maxRetransmitTime !== undefined) {
      this.logWarning("Use maxPacketLifeTime instead of deprecated maxRetransmitTime which will stop working soon in createDataChannel!");
    }
    if (maxPacketLifeTime !== undefined && maxRetransmits !== undefined) {
      throw new this._win.DOMException(
          "Both maxPacketLifeTime and maxRetransmits cannot be provided",
          "InvalidParameterError");
    }
    // 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 =
      this._impl.createDataChannel(label, protocol, type, ordered,
                                   maxPacketLifeTime, maxRetransmits,
                                   negotiated, id);

    // 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([Ci.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.

class PeerConnectionObserver {
  init(win) {
    this._win = win;
  }

  __init(dompc) {
    this._dompc = dompc._innerObject;
  }

  newError(message, code) {
    // These strings must match those defined in the WebRTC spec.
    const reasonName = [
      "",
      "InternalError",
      "InternalError",
      "InvalidParameterError",
      "InvalidStateError",
      "InvalidSessionDescriptionError",
      "IncompatibleSessionDescriptionError",
      "InternalError",
      "IncompatibleMediaStreamTrackError",
      "InternalError",
      "TypeError",
      "OperationError",
    ];
    let name = reasonName[Math.min(code, reasonName.length - 1)];
    return new this._dompc._win.DOMException(message, name);
  }

  dispatchEvent(event) {
    this._dompc.dispatchEvent(event);
  }

  onCreateOfferSuccess(sdp) {
    this._dompc._onCreateOfferSuccess(sdp);
  }

  onCreateOfferError(code, message) {
    this._dompc._onCreateOfferFailure(this.newError(message, code));
  }

  onCreateAnswerSuccess(sdp) {
    this._dompc._onCreateAnswerSuccess(sdp);
  }

  onCreateAnswerError(code, message) {
    this._dompc._onCreateAnswerFailure(this.newError(message, code));
  }

  onSetLocalDescriptionSuccess() {
    this._dompc._syncTransceivers();
    this._dompc._onSetLocalDescriptionSuccess();
  }

  onSetRemoteDescriptionSuccess() {
    this._dompc._syncTransceivers();
    this._dompc._processTrackAdditionsAndRemovals();
    this._dompc._fireLegacyAddStreamEvents();
    this._dompc._transceivers = this._dompc._transceivers.filter(t => !t.shouldRemove);
    this._dompc._onSetRemoteDescriptionSuccess();
  }

  onSetLocalDescriptionError(code, message) {
    this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
  }

  onSetRemoteDescriptionError(code, message) {
    this._dompc._onSetRemoteDescriptionFailure(this.newError(message, code));
  }

  onAddIceCandidateSuccess() {
    this._dompc._onAddIceCandidateSuccess();
  }

  onAddIceCandidateError(code, message) {
    this._dompc._onAddIceCandidateError(this.newError(message, code));
  }

  onIceCandidate(sdpMLineIndex, sdpMid, candidate, ufrag) {
    let win = this._dompc._win;
    if (candidate) {
      if (candidate.includes(" typ relay ")) {
        this._dompc._iceGatheredRelayCandidates = true;
      }
      candidate = new win.RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex, ufrag });
    } else {
      candidate = null;
    }
    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 (pc.iceConnectionState === "new") {
      var checking_histogram = Services.telemetry.getHistogramById("WEBRTC_ICE_CHECKING_RATE");
      if (iceConnectionState === "checking") {
        checking_histogram.add(true);
      } else if (iceConnectionState === "failed") {
        checking_histogram.add(false);
      }
    } else if (pc.iceConnectionState === "checking") {
      var success_histogram = Services.telemetry.getHistogramById("WEBRTC_ICE_SUCCESS_RATE");
      if (iceConnectionState === "completed" ||
          iceConnectionState === "connected") {
        success_histogram.add(true);
        pc._pcTelemetry.recordConnected();
      } else if (iceConnectionState === "failed") {
        success_histogram.add(false);
      }
    }

    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);
  }

  // This method is responsible for updating iceGatheringState. This
  // state is defined in the WebRTC specification as follows:
  //
  // iceGatheringState:
  // ------------------
  //   new        The object was just created, and no networking has occurred
  //              yet.
  //
  //   gathering  The ICE agent is in the process of gathering candidates for
  //              this RTCPeerConnection.
  //
  //   complete   The ICE agent has completed gathering. Events such as adding
  //              a new interface or a new TURN server will cause the state to
  //              go back to gathering.
  //
  handleIceGatheringStateChange(gatheringState) {
    let pc = this._dompc;
    if (pc.iceGatheringState === gatheringState) {
      return;
    }
    pc.changeIceGatheringState(gatheringState);
  }

  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.handleIceGatheringStateChange(this._dompc._pc.iceGatheringState);
        break;

      default:
        this._dompc.logWarning("Unhandled state type: " + state);
        break;
    }
  }

  onGetStatsSuccess(dict) {
    let pc = this._dompc;
    let chromeobj = new RTCStatsReport(pc, dict);
    let webidlobj = pc._win.RTCStatsReport._create(pc._win, chromeobj);
    chromeobj.makeStatsPublic();
    pc._onGetStatsSuccess(webidlobj);
  }

  onGetStatsError(code, message) {
    this._dompc._onGetStatsFailure(this.newError(message, code));
  }

  _getTransceiverWithRecvTrack(webrtcTrackId) {
    return this._dompc.getTransceivers().find(
        transceiver => transceiver.remoteTrackIdIs(webrtcTrackId));
  }

  onTransceiverNeeded(kind, transceiverImpl) {
    this._dompc._onTransceiverNeeded(kind, transceiverImpl);
  }

  notifyDataChannel(channel) {
    this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                { channel }));
  }

  onDTMFToneChange(track, tone) {
    var pc = this._dompc;
    var sender = pc.getSenders().find(sender => sender.track == track);
    sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
                                                                 { tone }));
  }

  onPacket(level, type, sending, packet) {
    var pc = this._dompc;
    if (pc._onPacket) {
      pc._onPacket(level, type, sending, packet);
    }
  }

  syncTransceivers() {
    this._dompc._syncTransceivers();
  }
}
setupPrototype(PeerConnectionObserver, {
  classID: PC_OBS_CID,
  contractID: PC_OBS_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
});

class RTCPeerConnectionStatic {
  init(win) {
    this._winID = win.windowUtils.currentInnerWindowID;
  }

  registerPeerConnectionLifecycleCallback(cb) {
    _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb);
  }
}
setupPrototype(RTCPeerConnectionStatic, {
  classID: PC_STATIC_CID,
  contractID: PC_STATIC_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
});

class RTCDTMFSender {
  constructor(sender) {
    this._sender = sender;
  }

  get toneBuffer() {
    return this._sender._pc._getDTMFToneBuffer(this._sender);
  }

  get ontonechange() {
    return this.__DOM_IMPL__.getEventHandler("ontonechange");
  }

  set ontonechange(handler) {
    this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
  }

  insertDTMF(tones, duration, interToneGap) {
    this._sender._pc._checkClosed();
    this._sender._transceiver.insertDTMF(tones, duration, interToneGap);
  }
}
setupPrototype(RTCDTMFSender, {
  classID: PC_DTMF_SENDER_CID,
  contractID: PC_DTMF_SENDER_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([]),
});

class RTCRtpSender {
  constructor(pc, transceiverImpl, transceiver, track, kind, streams) {
    let dtmf = null;
    if (kind == "audio") {
      dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
    }

    Object.assign(this, {
      _pc: pc,
      _transceiverImpl: transceiverImpl,
      _transceiver: transceiver,
      track,
      _streams: streams,
      dtmf });
  }

  replaceTrack(withTrack) {
    // async functions in here return a chrome promise, which is not something
    // content can use. This wraps that promise in something content can use.
    return this._pc._win.Promise.resolve(this._replaceTrack(withTrack));
  }

  async _replaceTrack(withTrack) {
    let pc = this._pc;
    if (withTrack && (withTrack.kind != this._transceiver.getKind())) {
      throw new pc._win.DOMException(
          "Cannot replaceTrack with a different kind!",
          "TypeError");
    }

    pc._checkClosed();

    await pc._chain(async () => {
      if (this._transceiver.stopped) {
        throw new pc._win.DOMException(
            "Cannot call replaceTrack when transceiver is stopped",
            "InvalidStateError");
      }

      await pc._queueTaskWithClosedCheck(() => {
        // Updates the track on the MediaPipeline, will throw on failure.
        try {
          pc._replaceTrackNoRenegotiation(this._transceiverImpl, withTrack);
        } catch (e) {
          throw new pc._win.DOMException("Track could not be replaced without renegotiation",
                                         "InvalidModificationError");
        }
        this.track = withTrack;
        this._transceiver.sync();
      });
    });
  }

  setParameters(parameters) {
    return this._pc._win.Promise.resolve(this._setParameters(parameters));
  }

  async _setParameters(parameters) {
    this._pc._checkClosed();

    if (this._transceiver.stopped) {
      throw new this._pc._win.DOMException(
          "This sender's transceiver is stopped", "InvalidStateError");
    }

    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
      return;
    }

    parameters.encodings = parameters.encodings || [];

    parameters.encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
      if (scaleResolutionDownBy < 1.0) {
        throw new this._pc._win.RangeError("scaleResolutionDownBy must be >= 1.0");
      }
      if (!rid && parameters.encodings.length > 1) {
        throw new this._pc._win.DOMException("Missing rid", "TypeError");
      }
      if (uniqueRids[rid]) {
        throw new this._pc._win.DOMException("Duplicate rid", "TypeError");
      }
      uniqueRids[rid] = true;
      return uniqueRids;
    }, {});

    // TODO(bug 1401592): transaction ids, timing changes

    await this._pc._queueTaskWithClosedCheck(() => {
      this.parameters = parameters;
      this._transceiver.sync();
    });
  }

  getParameters() {
    // TODO(bug 1401592): transaction ids

    // All the other stuff that the spec says to update is handled when
    // transceivers are synced.
    return this.parameters;
  }

  setStreams(streams) {
    this._streams = streams;
  }

  getStreams() {
    return this._streams;
  }

  setTrack(track) {
    this._pc._replaceTrackNoRenegotiation(this._transceiverImpl, track);
    this.track = track;
  }

  getStats() {
    if (this.track) {
      return this._pc._async(
        async () => this._pc._getStats(this.track));
    }
    return this._pc._win.Promise.resolve().then(
      () => this._pc._win.RTCStatsReport._create(this._pc._win, new Map())
    );
  }

  checkWasCreatedByPc(pc) {
    if (pc != this._pc.__DOM_IMPL__) {
      throw new this._pc._win.DOMException(
          "This sender was not created by this PeerConnection",
          "InvalidAccessError");
    }
  }
}
setupPrototype(RTCRtpSender, {
  classID: PC_SENDER_CID,
  contractID: PC_SENDER_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([]),
});

class RTCRtpReceiver {
  constructor(pc, transceiverImpl) {
    // We do not set the track here; that is done when _transceiverImpl is set
    Object.assign(this,
        {
          _pc: pc,
          _transceiverImpl: transceiverImpl,
          track: transceiverImpl.getReceiveTrack(),
          _remoteSetSendBit: false,
          _ontrackFired: false,
          streamIds: [],
          // Sync and contributing sources must be kept cached so that timestamps
          // remain stable, as the timestamp offset can vary
          // note key = entry.source + entry.sourceType
          _rtpSources: new Map(),
          _rtpSourcesJsTimestamp: null,
        });
  }

  // TODO(bug 1401983): Create a getStats binding on TransceiverImpl, and use
  // that here.
  getStats() {
    return this._pc._async(
      async () => this._pc.getStats(this.track));
  }

  _getRtpSource(source, type) {
    this._fetchRtpSources();
    return this._rtpSources.get(type + source).entry;
  }

  /* Fetch all of the RTP Contributing and Sync sources for the receiver
   * and store them so they are available when asked for.
   */
  _fetchRtpSources() {
    if (this._rtpSourcesJsTimestamp !== null) {
      return;
    }
    // Queue microtask to mark the cache as stale after this task completes
    Promise.resolve().then(() => this._rtpSourcesJsTimestamp = null);
    let {sources, sourceClockOffset, jsTimestamp} =
        this._pc._getRtpSources(this);
    this._rtpSourcesJsTimestamp = jsTimestamp;
    for (let entry of sources) {
      // Set the clock offset for calculating the 10-second window
      entry.sourceClockOffset = sourceClockOffset;
      // Store the new entries or update existing entries
      let key =  entry.source + entry.sourceType;
      let cached = this._rtpSources.get(key);
      if (cached === undefined) {
        this._rtpSources.set(key, entry);
      } else if (cached.timestamp != entry.timestamp) {
        // Only update if the timestamp has changed
        // This also prevents the sourceClockOffset from changing unecessarily
        // which could cause a value to flutter at the edge of the 10 second
        // window.
        this._rtpSources.set(key, entry);
      }
    }
    // Clear old entries
    let cutoffTime = this._rtpSourcesJsTimestamp - 10 * 1000;
    let removeKeys = [];
    for (let entry of this._rtpSources.values()) {
      if ((entry.timestamp + entry.sourceClockOffset) < cutoffTime) {
        removeKeys.push(entry.source + entry.sourceType);
      }
    }
    for (let delKey of removeKeys) {
      this._rtpSources.delete(delKey);
    }
  }

  _getRtpSourcesByType(type) {
    this._fetchRtpSources();
    // Only return the values from within the last 10 seconds as per the spec
    let cutoffTime = this._rtpSourcesJsTimestamp - 10 * 1000;
    let sources = [...this._rtpSources.values()].filter(
      (entry) => {
        return entry.sourceType == type &&
            (entry.timestamp + entry.sourceClockOffset) >= cutoffTime;
      }).map(e => {
        let newEntry = {
          source: e.source,
          timestamp: e.timestamp + e.sourceClockOffset,
          audioLevel: e.audioLevel,
        };
        if (e.voiceActivityFlag !== undefined) {
          Object.assign(newEntry, {voiceActivityFlag: e.voiceActivityFlag});
        }
        return newEntry;
      });
      return sources;
  }

  getContributingSources() {
    return this._getRtpSourcesByType("contributing");
  }

  getSynchronizationSources() {
    return this._getRtpSourcesByType("synchronization");
  }

  setStreamIds(streamIds) {
    this.streamIds = streamIds;
  }

  setRemoteSendBit(sendBit) {
    this._remoteSetSendBit = sendBit;
  }

  processTrackAdditionsAndRemovals(transceiver,
                                   {updateStreamFunctions, muteTracks, trackEvents}) {
    let streamsWithTrack = this.streamIds
      .map(id => this._pc._getOrCreateStream(id));

    let streamsWithoutTrack = this._pc.getRemoteStreams()
      .filter(s => !this.streamIds.includes(s.id));

    updateStreamFunctions.push(...streamsWithTrack.map(stream => () => {
      if (!stream.getTracks().includes(this.track)) {
        stream.addTrack(this.track);
        // Adding tracks from JS does not result in the stream getting
        // onaddtrack, so we need to do that here.
        stream.dispatchEvent(
            new this._pc._win.MediaStreamTrackEvent(
              "addtrack", { track: this.track }));
      }
    }));

    updateStreamFunctions.push(...streamsWithoutTrack.map(stream => () => {
      // Content JS might remove this track from the stream before this function fires (ugh)
      if (stream.getTracks().includes(this.track)) {
        stream.removeTrack(this.track);
        // Removing tracks from JS does not result in the stream getting
        // onremovetrack, so we need to do that here.
        stream.dispatchEvent(
            new this._pc._win.MediaStreamTrackEvent(
              "removetrack", { track: this.track }));
      }
    }));

    if (!this._remoteSetSendBit) {
      // remote used "recvonly" or "inactive"
      this._ontrackFired = false;
      if (!this.track.muted) {
        muteTracks.push(this.track);
      }
    } else if (!this._ontrackFired) {
      // remote used "sendrecv" or "sendonly", and we haven't fired ontrack
      let ev = new this._pc._win.RTCTrackEvent("track", {
        receiver: this.__DOM_IMPL__,
        track: this.track,
        streams: streamsWithTrack,
        transceiver });
      trackEvents.push(ev);
      this._ontrackFired = true;

      // Fire legacy event as well for a little bit.
      ev = new this._pc._win.MediaStreamTrackEvent("addtrack",
          { track: this.track });
      trackEvents.push(ev);
    }
  }
}
setupPrototype(RTCRtpReceiver, {
  classID: PC_RECEIVER_CID,
  contractID: PC_RECEIVER_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([]),
});

class RTCRtpTransceiver {
  constructor(pc, transceiverImpl, init, kind, sendTrack) {
    let receiver = pc._win.RTCRtpReceiver._create(
        pc._win, new RTCRtpReceiver(pc, transceiverImpl, kind));
    let streams = (init && init.streams) || [];
    let sender = pc._win.RTCRtpSender._create(
        pc._win, new RTCRtpSender(pc, transceiverImpl, this, sendTrack, kind, streams));

    let direction = (init && init.direction) || "sendrecv";
    Object.assign(this,
        {
          _pc: pc,
          mid: null,
          sender,
          receiver,
          stopped: false,
          _direction: direction,
          currentDirection: null,
          _remoteTrackId: null,
          addTrackMagic: false,
          shouldRemove: false,
          _hasBeenUsedToSend: false,
          // the receiver starts out without a track, so record this here
          _kind: kind,
          _transceiverImpl: transceiverImpl,
        });
  }

  set direction(direction) {
    this._pc._checkClosed();

    if (this.stopped) {
      throw new this._pc._win.DOMException("Transceiver is stopped!",
                                           "InvalidStateError");
    }

    if (this._direction == direction) {
      return;
    }

    this._direction = direction;
    this.sync();
    this._pc.updateNegotiationNeeded();
  }

  get direction() {
    return this._direction;
  }

  setDirectionInternal(direction) {
    this._direction = direction;
  }

  stop() {
    this._pc._checkClosed();

    if (this.stopped) {
      return;
    }

    this.setStopped();
    this.sync();
    this._pc.updateNegotiationNeeded();
  }

  setStopped() {
    this.stopped = true;
    this.currentDirection = null;
  }

  getKind() {
    return this._kind;
  }

  hasBeenUsedToSend() {
    return this._hasBeenUsedToSend;
  }

  setRemoteTrackId(webrtcTrackId) {
    this._remoteTrackId = webrtcTrackId;
  }

  remoteTrackIdIs(webrtcTrackId) {
    return this._remoteTrackId == webrtcTrackId;
  }

  getRemoteTrackId() {
    return this._remoteTrackId;
  }

  setAddTrackMagic() {
    this.addTrackMagic = true;
  }

  sync() {
    if (this._syncing) {
      throw new DOMException("Reentrant sync! This is a bug!", "InternalError");
    }
    this._syncing = true;
    this._transceiverImpl.syncWithJS(this.__DOM_IMPL__);
    this._syncing = false;
  }

  // Used by _transceiverImpl.syncWithJS, don't call sync again!
  setCurrentDirection(direction) {
    if (this.stopped) {
      return;
    }

    switch (direction) {
      case "sendrecv":
      case "sendonly":
        this._hasBeenUsedToSend = true;
        break;
      default:
    }

    this.currentDirection = direction;
  }

  // Used by _transceiverImpl.syncWithJS, don't call sync again!
  setMid(mid) {
    this.mid = mid;
  }

  // Used by _transceiverImpl.syncWithJS, don't call sync again!
  unsetMid() {
    this.mid = null;
  }

  insertDTMF(tones, duration, interToneGap) {
    if (this.stopped) {
      throw new this._pc._win.DOMException("Transceiver is stopped!",
                                           "InvalidStateError");
    }

    if (!this.sender.track) {
      throw new this._pc._win.DOMException("RTCRtpSender has no track",
                                           "InvalidStateError");
    }

    duration = Math.max(40, Math.min(duration, 6000));
    if (interToneGap < 30) interToneGap = 30;

    tones = tones.toUpperCase();

    if (tones.match(/[^0-9A-D#*,]/)) {
      throw new this._pc._win.DOMException("Invalid DTMF characters",
                                           "InvalidCharacterError");
    }

    // TODO (bug 1401983): Move this API to TransceiverImpl so we don't need the
    // extra hops through RTCPeerConnection and PeerConnectionImpl
    this._pc._insertDTMF(this._transceiverImpl, tones, duration, interToneGap);
  }
}

setupPrototype(RTCRtpTransceiver, {
  classID: PC_TRANSCEIVER_CID,
  contractID: PC_TRANSCEIVER_CONTRACT,
  QueryInterface: ChromeUtils.generateQI([]),
});

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([]),
});

var EXPORTED_SYMBOLS =
  ["GlobalPCList",
   "RTCDTMFSender",
   "RTCIceCandidate",
   "RTCSessionDescription",
   "RTCPeerConnection",
   "RTCPeerConnectionStatic",
   "RTCRtpReceiver",
   "RTCRtpSender",
   "RTCRtpTransceiver",
   "RTCStatsReport",
   "PeerConnectionObserver",
   "CreateOfferRequest"];