tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

pc.js (76084B)


      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 "use strict";
      6 
      7 const LOOPBACK_ADDR = "127.0.0.";
      8 
      9 const iceStateTransitions = {
     10  new: ["checking", "closed"], //Note: 'failed' might need to added here
     11  //      even though it is not in the standard
     12  checking: ["new", "connected", "failed", "closed"], //Note: do we need to
     13  // allow 'completed' in
     14  // here as well?
     15  connected: ["new", "checking", "completed", "disconnected", "closed"],
     16  completed: ["new", "checking", "disconnected", "closed"],
     17  disconnected: ["new", "connected", "completed", "failed", "closed"],
     18  failed: ["new", "disconnected", "closed"],
     19  closed: [],
     20 };
     21 
     22 const signalingStateTransitions = {
     23  stable: ["have-local-offer", "have-remote-offer", "closed"],
     24  "have-local-offer": [
     25    "have-remote-pranswer",
     26    "stable",
     27    "closed",
     28    "have-local-offer",
     29  ],
     30  "have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"],
     31  "have-remote-offer": [
     32    "have-local-pranswer",
     33    "stable",
     34    "closed",
     35    "have-remote-offer",
     36  ],
     37  "have-local-pranswer": ["stable", "closed", "have-local-pranswer"],
     38  closed: [],
     39 };
     40 
     41 var makeDefaultCommands = () => {
     42  return [].concat(
     43    commandsPeerConnectionInitial,
     44    commandsGetUserMedia,
     45    commandsPeerConnectionOfferAnswer
     46  );
     47 };
     48 
     49 /**
     50 * This class handles tests for peer connections.
     51 *
     52 * @class
     53 * @param {object} [options={}]
     54 *        Optional options for the peer connection test
     55 * @param {object} [options.commands=commandsPeerConnection]
     56 *        Commands to run for the test
     57 * @param {bool}   [options.is_local=true]
     58 *        true if this test should run the tests for the "local" side.
     59 * @param {bool}   [options.is_remote=true]
     60 *        true if this test should run the tests for the "remote" side.
     61 * @param {object} [options.config_local=undefined]
     62 *        Configuration for the local peer connection instance
     63 * @param {object} [options.config_remote=undefined]
     64 *        Configuration for the remote peer connection instance. If not defined
     65 *        the configuration from the local instance will be used
     66 */
     67 function PeerConnectionTest(options) {
     68  // If no options are specified make it an empty object
     69  options = options || {};
     70  options.commands = options.commands || makeDefaultCommands();
     71  options.is_local = "is_local" in options ? options.is_local : true;
     72  options.is_remote = "is_remote" in options ? options.is_remote : true;
     73 
     74  options.h264 = "h264" in options ? options.h264 : false;
     75  options.av1 = "av1" in options ? options.av1 : false;
     76  options.bundle = "bundle" in options ? options.bundle : true;
     77  options.rtcpmux = "rtcpmux" in options ? options.rtcpmux : true;
     78  options.opus = "opus" in options ? options.opus : true;
     79  options.ssrc = "ssrc" in options ? options.ssrc : true;
     80 
     81  options.config_local = options.config_local || {};
     82  options.config_remote = options.config_remote || {};
     83 
     84  if (!options.bundle) {
     85    // Make sure neither end tries to use bundle-only!
     86    options.config_local.bundlePolicy = "max-compat";
     87    options.config_remote.bundlePolicy = "max-compat";
     88  }
     89 
     90  if (iceServersArray.length) {
     91    if (!options.turn_disabled_local && !options.config_local.iceServers) {
     92      options.config_local.iceServers = iceServersArray;
     93    }
     94    if (!options.turn_disabled_remote && !options.config_remote.iceServers) {
     95      options.config_remote.iceServers = iceServersArray;
     96    }
     97  } else if (typeof turnServers !== "undefined") {
     98    if (!options.turn_disabled_local && turnServers.local) {
     99      if (!options.config_local.hasOwnProperty("iceServers")) {
    100        options.config_local.iceServers = turnServers.local.iceServers;
    101      }
    102    }
    103    if (!options.turn_disabled_remote && turnServers.remote) {
    104      if (!options.config_remote.hasOwnProperty("iceServers")) {
    105        options.config_remote.iceServers = turnServers.remote.iceServers;
    106      }
    107    }
    108  }
    109 
    110  if (options.is_local) {
    111    this.pcLocal = new PeerConnectionWrapper("pcLocal", options.config_local);
    112  } else {
    113    this.pcLocal = null;
    114  }
    115 
    116  if (options.is_remote) {
    117    this.pcRemote = new PeerConnectionWrapper(
    118      "pcRemote",
    119      options.config_remote || options.config_local
    120    );
    121  } else {
    122    this.pcRemote = null;
    123  }
    124 
    125  // Create command chain instance and assign default commands
    126  this.chain = new CommandChain(this, options.commands);
    127 
    128  this.testOptions = options;
    129 }
    130 
    131 /** TODO: consider removing this dependency on timeouts */
    132 function timerGuard(p, time, message) {
    133  return Promise.race([
    134    p,
    135    wait(time).then(() => {
    136      throw new Error("timeout after " + time / 1000 + "s: " + message);
    137    }),
    138  ]);
    139 }
    140 
    141 /**
    142 * Closes the peer connection if it is active
    143 */
    144 PeerConnectionTest.prototype.closePC = function () {
    145  info("Closing peer connections");
    146 
    147  var closeIt = pc => {
    148    if (!pc || pc.signalingState === "closed") {
    149      return Promise.resolve();
    150    }
    151 
    152    var promise = Promise.all([
    153      Promise.all(
    154        pc._pc
    155          .getReceivers()
    156          .filter(receiver => receiver.track.readyState == "live")
    157          .map(receiver => {
    158            info(
    159              "Waiting for track " +
    160                receiver.track.id +
    161                " (" +
    162                receiver.track.kind +
    163                ") to end."
    164            );
    165            return haveEvent(receiver.track, "ended", wait(50000)).then(
    166              event => {
    167                is(
    168                  event.target,
    169                  receiver.track,
    170                  "Event target should be the correct track"
    171                );
    172                info(pc + " ended fired for track " + receiver.track.id);
    173              },
    174              e =>
    175                e
    176                  ? Promise.reject(e)
    177                  : ok(
    178                      false,
    179                      "ended never fired for track " + receiver.track.id
    180                    )
    181            );
    182          })
    183      ),
    184    ]);
    185    pc.close();
    186    return promise;
    187  };
    188 
    189  return timerGuard(
    190    Promise.all([closeIt(this.pcLocal), closeIt(this.pcRemote)]),
    191    60000,
    192    "failed to close peer connection"
    193  );
    194 };
    195 
    196 /**
    197 * Close the open data channels, followed by the underlying peer connection
    198 */
    199 PeerConnectionTest.prototype.close = function () {
    200  var allChannels = (this.pcLocal || this.pcRemote).dataChannels;
    201  return timerGuard(
    202    Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))),
    203    120000,
    204    "failed to close data channels"
    205  ).then(() => this.closePC());
    206 };
    207 
    208 /**
    209 * Close the specified data channels
    210 *
    211 * @param {number} index
    212 *        Index of the data channels to close on both sides
    213 */
    214 PeerConnectionTest.prototype.closeDataChannels = function (index) {
    215  info("closeDataChannels called with index: " + index);
    216  var localChannel = null;
    217  if (this.pcLocal) {
    218    localChannel = this.pcLocal.dataChannels[index];
    219  }
    220  var remoteChannel = null;
    221  if (this.pcRemote) {
    222    remoteChannel = this.pcRemote.dataChannels[index];
    223  }
    224 
    225  // We need to setup all the close listeners before calling close
    226  var setupClosePromise = channel => {
    227    if (!channel) {
    228      return Promise.resolve();
    229    }
    230    return new Promise(resolve => {
    231      channel.onclose = () => {
    232        is(
    233          channel.readyState,
    234          "closed",
    235          name + " channel " + index + " closed"
    236        );
    237        resolve();
    238      };
    239    });
    240  };
    241 
    242  // make sure to setup close listeners before triggering any actions
    243  var allClosed = Promise.all([
    244    setupClosePromise(localChannel),
    245    setupClosePromise(remoteChannel),
    246  ]);
    247  var complete = timerGuard(
    248    allClosed,
    249    120000,
    250    "failed to close data channel pair"
    251  );
    252 
    253  // triggering close on one side should suffice
    254  if (remoteChannel) {
    255    remoteChannel.close();
    256  } else if (localChannel) {
    257    localChannel.close();
    258  }
    259 
    260  return complete;
    261 };
    262 
    263 /**
    264 * Send data (message or blob) to the other peer
    265 *
    266 * @param {string | Blob} data
    267 *        Data to send to the other peer. For Blobs the MIME type will be lost.
    268 * @param {object} [options={ }]
    269 *        Options to specify the data channels to be used
    270 * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]]
    271 *        Data channel to use for sending the message
    272 * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]]
    273 *        Data channel to use for receiving the message
    274 */
    275 PeerConnectionTest.prototype.send = async function (data, options) {
    276  options = options || {};
    277  const source =
    278    options.sourceChannel ||
    279    this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
    280  const target =
    281    options.targetChannel ||
    282    this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
    283  source.bufferedAmountLowThreshold = options.bufferedAmountLowThreshold || 0;
    284 
    285  const getSizeInBytes = d => {
    286    if (d instanceof Blob) {
    287      return d.size;
    288    } else if (d instanceof ArrayBuffer) {
    289      return d.byteLength;
    290    } else if (d instanceof String || typeof d === "string") {
    291      return new TextEncoder().encode(d).length;
    292    } else {
    293      ok(false);
    294      throw new Error("Could not get size");
    295    }
    296  };
    297 
    298  const expectedSizeInBytes = getSizeInBytes(data);
    299  const bufferedAmount = source.bufferedAmount;
    300 
    301  source.send(data);
    302  is(
    303    source.bufferedAmount,
    304    expectedSizeInBytes + bufferedAmount,
    305    `Buffered amount should be ${expectedSizeInBytes}`
    306  );
    307 
    308  const resultReceived = new Promise(resolve => {
    309    // Register event handler for the target channel
    310    target.onmessage = e => {
    311      is(
    312        getSizeInBytes(e.data),
    313        expectedSizeInBytes,
    314        `Expected to receive the same number of bytes as we sent (${expectedSizeInBytes})`
    315      );
    316      resolve({ channel: target, data: e.data });
    317    };
    318  });
    319 
    320  await new Promise(resolve => (source.onbufferedamountlow = resolve));
    321 
    322  return resultReceived;
    323 };
    324 
    325 /**
    326 * Create a data channel
    327 *
    328 * @param {Dict} options
    329 *        Options for the data channel (see nsIPeerConnection)
    330 */
    331 PeerConnectionTest.prototype.createDataChannel = function (options) {
    332  var remotePromise;
    333  if (!options.negotiated) {
    334    this.pcRemote.expectDataChannel("pcRemote expected data channel");
    335    remotePromise = this.pcRemote.nextDataChannel;
    336  }
    337 
    338  // Create the datachannel
    339  var localChannel = this.pcLocal.createDataChannel(options);
    340  var localPromise = localChannel.opened;
    341 
    342  if (options.negotiated) {
    343    remotePromise = localPromise.then(localChannel => {
    344      // externally negotiated - we need to open from both ends
    345      options.id = options.id || channel.id; // allow for no id on options
    346      var remoteChannel = this.pcRemote.createDataChannel(options);
    347      return remoteChannel.opened;
    348    });
    349  }
    350 
    351  // pcRemote.observedNegotiationNeeded might be undefined if
    352  // !options.negotiated, which means we just wait on pcLocal
    353  return Promise.all([
    354    this.pcLocal.observedNegotiationNeeded,
    355    this.pcRemote.observedNegotiationNeeded,
    356  ]).then(() => {
    357    return Promise.all([localPromise, remotePromise]).then(result => {
    358      return { local: result[0], remote: result[1] };
    359    });
    360  });
    361 };
    362 
    363 /**
    364 * Creates an answer for the specified peer connection instance
    365 * and automatically handles the failure case.
    366 *
    367 * @param {PeerConnectionWrapper} peer
    368 *        The peer connection wrapper to run the command on
    369 */
    370 PeerConnectionTest.prototype.createAnswer = function (peer) {
    371  return peer.createAnswer().then(answer => {
    372    // make a copy so this does not get updated with ICE candidates
    373    this.originalAnswer = JSON.parse(JSON.stringify(answer));
    374    return answer;
    375  });
    376 };
    377 
    378 /**
    379 * Creates an offer for the specified peer connection instance
    380 * and automatically handles the failure case.
    381 *
    382 * @param {PeerConnectionWrapper} peer
    383 *        The peer connection wrapper to run the command on
    384 */
    385 PeerConnectionTest.prototype.createOffer = function (peer) {
    386  return peer.createOffer().then(offer => {
    387    // make a copy so this does not get updated with ICE candidates
    388    this.originalOffer = JSON.parse(JSON.stringify(offer));
    389    return offer;
    390  });
    391 };
    392 
    393 /**
    394 * Sets the local description for the specified peer connection instance
    395 * and automatically handles the failure case.
    396 *
    397 * @param {PeerConnectionWrapper} peer
    398          The peer connection wrapper to run the command on
    399 * @param {RTCSessionDescriptionInit} desc
    400 *        Session description for the local description request
    401 */
    402 PeerConnectionTest.prototype.setLocalDescription = function (
    403  peer,
    404  desc,
    405  stateExpected
    406 ) {
    407  var eventFired = new Promise(resolve => {
    408    peer.onsignalingstatechange = e => {
    409      info(peer + ": 'signalingstatechange' event received");
    410      var state = e.target.signalingState;
    411      if (stateExpected === state) {
    412        peer.setLocalDescStableEventDate = new Date();
    413        resolve();
    414      } else {
    415        ok(
    416          false,
    417          "This event has either already fired or there has been a " +
    418            "mismatch between event received " +
    419            state +
    420            " and event expected " +
    421            stateExpected
    422        );
    423      }
    424    };
    425  });
    426 
    427  var stateChanged = peer.setLocalDescription(desc).then(() => {
    428    peer.setLocalDescDate = new Date();
    429  });
    430 
    431  peer.endOfTrickleSdp = peer.endOfTrickleIce
    432    .then(() => {
    433      return peer._pc.localDescription;
    434    })
    435    .catch(e => ok(false, "Sending EOC message failed: " + e));
    436 
    437  return Promise.all([eventFired, stateChanged]);
    438 };
    439 
    440 /**
    441 * Sets the media constraints for both peer connection instances.
    442 *
    443 * @param {object} constraintsLocal
    444 *        Media constrains for the local peer connection instance
    445 * @param constraintsRemote
    446 */
    447 PeerConnectionTest.prototype.setMediaConstraints = function (
    448  constraintsLocal,
    449  constraintsRemote
    450 ) {
    451  if (this.pcLocal) {
    452    this.pcLocal.constraints = constraintsLocal;
    453  }
    454  if (this.pcRemote) {
    455    this.pcRemote.constraints = constraintsRemote;
    456  }
    457 };
    458 
    459 /**
    460 * Sets the media options used on a createOffer call in the test.
    461 *
    462 * @param {object} options the media constraints to use on createOffer
    463 */
    464 PeerConnectionTest.prototype.setOfferOptions = function (options) {
    465  if (this.pcLocal) {
    466    this.pcLocal.offerOptions = options;
    467  }
    468 };
    469 
    470 /**
    471 * Sets the remote description for the specified peer connection instance
    472 * and automatically handles the failure case.
    473 *
    474 * @param {PeerConnectionWrapper} peer
    475          The peer connection wrapper to run the command on
    476 * @param {RTCSessionDescriptionInit} desc
    477 *        Session description for the remote description request
    478 */
    479 PeerConnectionTest.prototype.setRemoteDescription = function (
    480  peer,
    481  desc,
    482  stateExpected
    483 ) {
    484  var eventFired = new Promise(resolve => {
    485    peer.onsignalingstatechange = e => {
    486      info(peer + ": 'signalingstatechange' event received");
    487      var state = e.target.signalingState;
    488      if (stateExpected === state) {
    489        peer.setRemoteDescStableEventDate = new Date();
    490        resolve();
    491      } else {
    492        ok(
    493          false,
    494          "This event has either already fired or there has been a " +
    495            "mismatch between event received " +
    496            state +
    497            " and event expected " +
    498            stateExpected
    499        );
    500      }
    501    };
    502  });
    503 
    504  var stateChanged = peer.setRemoteDescription(desc).then(() => {
    505    peer.setRemoteDescDate = new Date();
    506    peer.checkMediaTracks();
    507  });
    508 
    509  return Promise.all([eventFired, stateChanged]);
    510 };
    511 
    512 /**
    513 * Adds and removes steps to/from the execution chain based on the configured
    514 * testOptions.
    515 */
    516 PeerConnectionTest.prototype.updateChainSteps = function () {
    517  if (this.testOptions.h264) {
    518    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
    519      PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER,
    520    ]);
    521  }
    522  if (this.testOptions.av1) {
    523    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
    524      PC_LOCAL_REMOVE_ALL_BUT_AV1_FROM_OFFER,
    525    ]);
    526  }
    527  if (!this.testOptions.bundle) {
    528    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
    529      PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER,
    530    ]);
    531  }
    532  if (!this.testOptions.rtcpmux) {
    533    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
    534      PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER,
    535    ]);
    536  }
    537  if (!this.testOptions.ssrc) {
    538    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
    539      PC_LOCAL_REMOVE_SSRC_FROM_OFFER,
    540    ]);
    541    this.chain.insertAfterEach("PC_REMOTE_CREATE_ANSWER", [
    542      PC_REMOTE_REMOVE_SSRC_FROM_ANSWER,
    543    ]);
    544  }
    545  if (!this.testOptions.is_local) {
    546    this.chain.filterOut(/^PC_LOCAL/);
    547  }
    548  if (!this.testOptions.is_remote) {
    549    this.chain.filterOut(/^PC_REMOTE/);
    550  }
    551 };
    552 
    553 /**
    554 * Start running the tests as assigned to the command chain.
    555 */
    556 PeerConnectionTest.prototype.run = async function () {
    557  /* We have to modify the chain here to allow tests which modify the default
    558   * test chain instantiating a PeerConnectionTest() */
    559  this.updateChainSteps();
    560  try {
    561    await this.chain.execute();
    562    await this.close();
    563  } catch (e) {
    564    const stack =
    565      typeof e.stack === "string"
    566        ? ` ${e.stack.split("\n").join(" ... ")}`
    567        : "";
    568    ok(false, `Error in test execution: ${e} (${stack})`);
    569  }
    570 };
    571 
    572 /**
    573 * Routes ice candidates from one PCW to the other PCW
    574 */
    575 PeerConnectionTest.prototype.iceCandidateHandler = function (
    576  caller,
    577  candidate
    578 ) {
    579  info("Received: " + JSON.stringify(candidate) + " from " + caller);
    580 
    581  var target = null;
    582  if (caller.includes("pcLocal")) {
    583    if (this.pcRemote) {
    584      target = this.pcRemote;
    585    }
    586  } else if (caller.includes("pcRemote")) {
    587    if (this.pcLocal) {
    588      target = this.pcLocal;
    589    }
    590  } else {
    591    ok(false, "received event from unknown caller: " + caller);
    592    return;
    593  }
    594 
    595  if (target) {
    596    target.storeOrAddIceCandidate(candidate);
    597  } else {
    598    info("sending ice candidate to signaling server");
    599    send_message({ type: "ice_candidate", ice_candidate: candidate });
    600  }
    601 };
    602 
    603 /**
    604 * Installs a polling function for the socket.io client to read
    605 * all messages from the chat room into a message queue.
    606 */
    607 PeerConnectionTest.prototype.setupSignalingClient = function () {
    608  this.signalingMessageQueue = [];
    609  this.signalingCallbacks = {};
    610  this.signalingLoopRun = true;
    611 
    612  var queueMessage = message => {
    613    info("Received signaling message: " + JSON.stringify(message));
    614    var fired = false;
    615    Object.keys(this.signalingCallbacks).forEach(name => {
    616      if (name === message.type) {
    617        info("Invoking callback for message type: " + name);
    618        this.signalingCallbacks[name](message);
    619        fired = true;
    620      }
    621    });
    622    if (!fired) {
    623      this.signalingMessageQueue.push(message);
    624      info(
    625        "signalingMessageQueue.length: " + this.signalingMessageQueue.length
    626      );
    627    }
    628    if (this.signalingLoopRun) {
    629      wait_for_message().then(queueMessage);
    630    } else {
    631      info("Exiting signaling message event loop");
    632    }
    633  };
    634  wait_for_message().then(queueMessage);
    635 };
    636 
    637 /**
    638 * Sets a flag to stop reading further messages from the chat room.
    639 */
    640 PeerConnectionTest.prototype.signalingMessagesFinished = function () {
    641  this.signalingLoopRun = false;
    642 };
    643 
    644 /**
    645 * Register a callback function to deliver messages from the chat room
    646 * directly instead of storing them in the message queue.
    647 *
    648 * @param {string} messageType
    649 *        For which message types should the callback get invoked.
    650 *
    651 * @param {function} onMessage
    652 *        The function which gets invoked if a message of the messageType
    653 *        has been received from the chat room.
    654 */
    655 PeerConnectionTest.prototype.registerSignalingCallback = function (
    656  messageType,
    657  onMessage
    658 ) {
    659  this.signalingCallbacks[messageType] = onMessage;
    660 };
    661 
    662 /**
    663 * Searches the message queue for the first message of a given type
    664 * and invokes the given callback function, or registers the callback
    665 * function for future messages if the queue contains no such message.
    666 *
    667 * @param {string} messageType
    668 *        The type of message to search and register for.
    669 */
    670 PeerConnectionTest.prototype.getSignalingMessage = function (messageType) {
    671  var i = this.signalingMessageQueue.findIndex(m => m.type === messageType);
    672  if (i >= 0) {
    673    info(
    674      "invoking callback on message " +
    675        i +
    676        " from message queue, for message type:" +
    677        messageType
    678    );
    679    return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]);
    680  }
    681  return new Promise(resolve =>
    682    this.registerSignalingCallback(messageType, resolve)
    683  );
    684 };
    685 
    686 /**
    687 * This class acts as a wrapper around a DataChannel instance.
    688 *
    689 * @param dataChannel
    690 * @param peerConnectionWrapper
    691 * @class
    692 */
    693 function DataChannelWrapper(dataChannel, peerConnectionWrapper) {
    694  this._channel = dataChannel;
    695  this._pc = peerConnectionWrapper;
    696 
    697  info("Creating " + this);
    698 
    699  /**
    700   * Setup appropriate callbacks
    701   */
    702  createOneShotEventWrapper(this, this._channel, "close");
    703  createOneShotEventWrapper(this, this._channel, "error");
    704  createOneShotEventWrapper(this, this._channel, "message");
    705  createOneShotEventWrapper(this, this._channel, "bufferedamountlow");
    706 
    707  this.opened = timerGuard(
    708    new Promise(resolve => {
    709      this._channel.onopen = () => {
    710        this._channel.onopen = unexpectedEvent(this, "onopen");
    711        is(this.readyState, "open", "data channel is 'open' after 'onopen'");
    712        resolve(this);
    713      };
    714    }),
    715    180000,
    716    "channel didn't open in time"
    717  );
    718 }
    719 
    720 DataChannelWrapper.prototype = {
    721  /**
    722   * Returns the binary type of the channel
    723   *
    724   * @returns {string} The binary type
    725   */
    726  get binaryType() {
    727    return this._channel.binaryType;
    728  },
    729 
    730  /**
    731   * Sets the binary type of the channel
    732   *
    733   * @param {string} type
    734   *        The new binary type of the channel
    735   */
    736  set binaryType(type) {
    737    this._channel.binaryType = type;
    738  },
    739 
    740  /**
    741   * Returns the label of the underlying data channel
    742   *
    743   * @returns {string} The label
    744   */
    745  get label() {
    746    return this._channel.label;
    747  },
    748 
    749  /**
    750   * Returns the protocol of the underlying data channel
    751   *
    752   * @returns {string} The protocol
    753   */
    754  get protocol() {
    755    return this._channel.protocol;
    756  },
    757 
    758  /**
    759   * Returns the id of the underlying data channel
    760   *
    761   * @returns {number} The stream id
    762   */
    763  get id() {
    764    return this._channel.id;
    765  },
    766 
    767  /**
    768   * Returns the ordered attribute of the data channel
    769   *
    770   * @returns {bool} The ordered attribute
    771   */
    772  get ordered() {
    773    return this._channel.ordered;
    774  },
    775 
    776  /**
    777   * Returns the maxPacketLifeTime attribute of the data channel
    778   *
    779   * @returns {number} The maxPacketLifeTime attribute
    780   */
    781  get maxPacketLifeTime() {
    782    return this._channel.maxPacketLifeTime;
    783  },
    784 
    785  /**
    786   * Returns the maxRetransmits attribute of the data channel
    787   *
    788   * @returns {number} The maxRetransmits attribute
    789   */
    790  get maxRetransmits() {
    791    return this._channel.maxRetransmits;
    792  },
    793 
    794  /**
    795   * Returns the readyState bit of the data channel
    796   *
    797   * @returns {string} The state of the channel
    798   */
    799  get readyState() {
    800    return this._channel.readyState;
    801  },
    802 
    803  get bufferedAmount() {
    804    return this._channel.bufferedAmount;
    805  },
    806 
    807  /**
    808   * Sets the bufferlowthreshold of the channel
    809   *
    810   * @param {integer} amoutn
    811   *        The new threshold for the chanel
    812   */
    813  set bufferedAmountLowThreshold(amount) {
    814    this._channel.bufferedAmountLowThreshold = amount;
    815  },
    816 
    817  /**
    818   * Close the data channel
    819   */
    820  close() {
    821    info(this + ": Closing channel");
    822    this._channel.close();
    823  },
    824 
    825  /**
    826   * Send data through the data channel
    827   *
    828   * @param {string | object} data
    829   *        Data which has to be sent through the data channel
    830   */
    831  send(data) {
    832    info(this + ": Sending data '" + data + "'");
    833    this._channel.send(data);
    834  },
    835 
    836  /**
    837   * Returns the string representation of the class
    838   *
    839   * @returns {string} The string representation
    840   */
    841  toString() {
    842    return (
    843      "DataChannelWrapper (" + this._pc.label + "_" + this._channel.label + ")"
    844    );
    845  },
    846 };
    847 
    848 /**
    849 * This class acts as a wrapper around a PeerConnection instance.
    850 *
    851 * @class
    852 * @param {string} label
    853 *        Description for the peer connection instance
    854 * @param {object} configuration
    855 *        Configuration for the peer connection instance
    856 */
    857 function PeerConnectionWrapper(label, configuration) {
    858  this.configuration = configuration;
    859  if (configuration && configuration.label_suffix) {
    860    label = label + "_" + configuration.label_suffix;
    861  }
    862  this.label = label;
    863 
    864  this.constraints = [];
    865  this.offerOptions = {};
    866 
    867  this.dataChannels = [];
    868 
    869  this._local_ice_candidates = [];
    870  this._remote_ice_candidates = [];
    871  this.localRequiresTrickleIce = false;
    872  this.remoteRequiresTrickleIce = false;
    873  this.localMediaElements = [];
    874  this.remoteMediaElements = [];
    875  this.audioElementsOnly = false;
    876 
    877  this._sendStreams = [];
    878 
    879  this.expectedLocalTrackInfo = [];
    880  this.remoteStreamsByTrackId = new Map();
    881 
    882  this.disableRtpCountChecking = false;
    883 
    884  this.iceConnectedResolve;
    885  this.iceConnectedReject;
    886  this.iceConnected = new Promise((resolve, reject) => {
    887    this.iceConnectedResolve = resolve;
    888    this.iceConnectedReject = reject;
    889  });
    890  this.iceCheckingRestartExpected = false;
    891  this.iceCheckingIceRollbackExpected = false;
    892 
    893  info("Creating " + this);
    894  this._pc = new RTCPeerConnection(this.configuration);
    895 
    896  /**
    897   * Setup callback handlers
    898   */
    899  // This allows test to register their own callbacks for ICE connection state changes
    900  this.ice_connection_callbacks = {};
    901 
    902  this._pc.oniceconnectionstatechange = e => {
    903    isnot(
    904      typeof this._pc.iceConnectionState,
    905      "undefined",
    906      "iceConnectionState should not be undefined"
    907    );
    908    var iceState = this._pc.iceConnectionState;
    909    info(
    910      this + ": oniceconnectionstatechange fired, new state is: " + iceState
    911    );
    912    Object.keys(this.ice_connection_callbacks).forEach(name => {
    913      this.ice_connection_callbacks[name]();
    914    });
    915    if (iceState === "connected") {
    916      this.iceConnectedResolve();
    917    } else if (iceState === "failed") {
    918      this.iceConnectedReject(new Error("ICE failed"));
    919    }
    920  };
    921 
    922  this._pc.onicegatheringstatechange = e => {
    923    isnot(
    924      typeof this._pc.iceGatheringState,
    925      "undefined",
    926      "iceGetheringState should not be undefined"
    927    );
    928    var gatheringState = this._pc.iceGatheringState;
    929    info(
    930      this +
    931        ": onicegatheringstatechange fired, new state is: " +
    932        gatheringState
    933    );
    934  };
    935 
    936  createOneShotEventWrapper(this, this._pc, "datachannel");
    937  this._pc.addEventListener("datachannel", e => {
    938    var wrapper = new DataChannelWrapper(e.channel, this);
    939    this.dataChannels.push(wrapper);
    940  });
    941 
    942  createOneShotEventWrapper(this, this._pc, "signalingstatechange");
    943  createOneShotEventWrapper(this, this._pc, "negotiationneeded");
    944 }
    945 
    946 PeerConnectionWrapper.prototype = {
    947  /**
    948   * Returns the senders
    949   *
    950   * @returns {sequence<RTCRtpSender>} the senders
    951   */
    952  getSenders() {
    953    return this._pc.getSenders();
    954  },
    955 
    956  /**
    957   * Returns the getters
    958   *
    959   * @returns {sequence<RTCRtpReceiver>} the receivers
    960   */
    961  getReceivers() {
    962    return this._pc.getReceivers();
    963  },
    964 
    965  /**
    966   * Returns the local description.
    967   *
    968   * @returns {object} The local description
    969   */
    970  get localDescription() {
    971    return this._pc.localDescription;
    972  },
    973 
    974  /**
    975   * Returns the remote description.
    976   *
    977   * @returns {object} The remote description
    978   */
    979  get remoteDescription() {
    980    return this._pc.remoteDescription;
    981  },
    982 
    983  /**
    984   * Returns the signaling state.
    985   *
    986   * @returns {object} The local description
    987   */
    988  get signalingState() {
    989    return this._pc.signalingState;
    990  },
    991  /**
    992   * Returns the ICE connection state.
    993   *
    994   * @returns {object} The local description
    995   */
    996  get iceConnectionState() {
    997    return this._pc.iceConnectionState;
    998  },
    999 
   1000  setIdentityProvider(provider, options) {
   1001    this._pc.setIdentityProvider(provider, options);
   1002  },
   1003 
   1004  elementPrefix: direction => {
   1005    return [this.label, direction].join("_");
   1006  },
   1007 
   1008  getMediaElementForTrack(track, direction) {
   1009    var prefix = this.elementPrefix(direction);
   1010    return getMediaElementForTrack(track, prefix);
   1011  },
   1012 
   1013  createMediaElementForTrack(track, direction) {
   1014    var prefix = this.elementPrefix(direction);
   1015    return createMediaElementForTrack(track, prefix);
   1016  },
   1017 
   1018  ensureMediaElement(track, direction) {
   1019    var prefix = this.elementPrefix(direction);
   1020    var element = this.getMediaElementForTrack(track, direction);
   1021    if (!element) {
   1022      element = this.createMediaElementForTrack(track, direction);
   1023      if (direction == "local") {
   1024        this.localMediaElements.push(element);
   1025      } else if (direction == "remote") {
   1026        this.remoteMediaElements.push(element);
   1027      }
   1028    }
   1029 
   1030    // We do this regardless, because sometimes we end up with a new stream with
   1031    // an old id (ie; the rollback tests cause the same stream to be added
   1032    // twice)
   1033    element.srcObject = new MediaStream([track]);
   1034    element.play();
   1035  },
   1036 
   1037  addSendStream(stream) {
   1038    // The PeerConnection will not necessarily know about this stream
   1039    // automatically, because replaceTrack is not told about any streams the
   1040    // new track might be associated with. Only content really knows.
   1041    this._sendStreams.push(stream);
   1042  },
   1043 
   1044  getStreamForSendTrack(track) {
   1045    return this._sendStreams.find(str => str.getTrackById(track.id));
   1046  },
   1047 
   1048  getStreamForRecvTrack(track) {
   1049    return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id));
   1050  },
   1051 
   1052  /**
   1053   * Attaches a local track to this RTCPeerConnection using
   1054   * RTCPeerConnection.addTrack().
   1055   *
   1056   * Also creates a media element playing a MediaStream containing all
   1057   * tracks that have been added to `stream` using `attachLocalTrack()`.
   1058   *
   1059   * @param {MediaStreamTrack} track
   1060   *        MediaStreamTrack to handle
   1061   * @param {MediaStream} stream
   1062   *        MediaStream to use as container for `track` on remote side
   1063   */
   1064  attachLocalTrack(track, stream) {
   1065    info("Got a local " + track.kind + " track");
   1066 
   1067    this.expectNegotiationNeeded();
   1068    var sender = this._pc.addTrack(track, stream);
   1069    is(sender.track, track, "addTrack returns sender");
   1070    is(
   1071      this._pc.getSenders().pop(),
   1072      sender,
   1073      "Sender should be the last element in getSenders()"
   1074    );
   1075 
   1076    ok(track.id, "track has id");
   1077    ok(track.kind, "track has kind");
   1078    ok(stream.id, "stream has id");
   1079    this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
   1080    this.addSendStream(stream);
   1081 
   1082    // This will create one media element per track, which might not be how
   1083    // we set up things with the RTCPeerConnection. It's the only way
   1084    // we can ensure all sent tracks are flowing however.
   1085    this.ensureMediaElement(track, "local");
   1086 
   1087    return this.observedNegotiationNeeded;
   1088  },
   1089 
   1090  /**
   1091   * Callback when we get local media. Also an appropriate HTML media element
   1092   * will be created and added to the content node.
   1093   *
   1094   * @param {MediaStream} stream
   1095   *        Media stream to handle
   1096   */
   1097  attachLocalStream(stream, useAddTransceiver) {
   1098    info("Got local media stream: (" + stream.id + ")");
   1099 
   1100    this.expectNegotiationNeeded();
   1101    if (useAddTransceiver) {
   1102      info("Using addTransceiver (on PC).");
   1103      stream.getTracks().forEach(track => {
   1104        var transceiver = this._pc.addTransceiver(track, { streams: [stream] });
   1105        is(transceiver.sender.track, track, "addTransceiver returns sender");
   1106      });
   1107    }
   1108    // In order to test both the addStream and addTrack APIs, we do half one
   1109    // way, half the other, at random.
   1110    else if (Math.random() < 0.5) {
   1111      info("Using addStream.");
   1112      this._pc.addStream(stream);
   1113      ok(
   1114        this._pc
   1115          .getSenders()
   1116          .find(sender => sender.track == stream.getTracks()[0]),
   1117        "addStream returns sender"
   1118      );
   1119    } else {
   1120      info("Using addTrack (on PC).");
   1121      stream.getTracks().forEach(track => {
   1122        var sender = this._pc.addTrack(track, stream);
   1123        is(sender.track, track, "addTrack returns sender");
   1124      });
   1125    }
   1126 
   1127    this.addSendStream(stream);
   1128 
   1129    stream.getTracks().forEach(track => {
   1130      ok(track.id, "track has id");
   1131      ok(track.kind, "track has kind");
   1132      const sender = this._pc.getSenders().find(s => s.track == track);
   1133      ok(sender, "track has a sender");
   1134      this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
   1135      this.ensureMediaElement(track, "local");
   1136    });
   1137 
   1138    return this.observedNegotiationNeeded;
   1139  },
   1140 
   1141  removeSender(index) {
   1142    var sender = this._pc.getSenders()[index];
   1143    this.expectedLocalTrackInfo = this.expectedLocalTrackInfo.filter(
   1144      i => i.sender != sender
   1145    );
   1146    this.expectNegotiationNeeded();
   1147    this._pc.removeTrack(sender);
   1148    return this.observedNegotiationNeeded;
   1149  },
   1150 
   1151  senderReplaceTrack(sender, withTrack, stream) {
   1152    const info = this.expectedLocalTrackInfo.find(i => i.sender == sender);
   1153    if (!info) {
   1154      return undefined; // replaceTrack on a null track, probably
   1155    }
   1156    info.track = withTrack;
   1157    this.addSendStream(stream);
   1158    this.ensureMediaElement(withTrack, "local");
   1159    return sender.replaceTrack(withTrack);
   1160  },
   1161 
   1162  async getUserMedia(constraints) {
   1163    SpecialPowers.wrap(document).notifyUserGestureActivation();
   1164    var stream = await getUserMedia(constraints);
   1165    if (constraints.audio) {
   1166      stream.getAudioTracks().forEach(track => {
   1167        info(
   1168          this +
   1169            " gUM local stream " +
   1170            stream.id +
   1171            " with audio track " +
   1172            track.id
   1173        );
   1174      });
   1175    }
   1176    if (constraints.video) {
   1177      stream.getVideoTracks().forEach(track => {
   1178        info(
   1179          this +
   1180            " gUM local stream " +
   1181            stream.id +
   1182            " with video track " +
   1183            track.id
   1184        );
   1185      });
   1186    }
   1187    return stream;
   1188  },
   1189 
   1190  /**
   1191   * Requests all the media streams as specified in the constrains property.
   1192   *
   1193   * @param {Array} constraintsList
   1194   *        Array of constraints for GUM calls
   1195   */
   1196  getAllUserMedia(constraintsList) {
   1197    if (constraintsList.length === 0) {
   1198      info("Skipping GUM: no UserMedia requested");
   1199      return Promise.resolve();
   1200    }
   1201 
   1202    info("Get " + constraintsList.length + " local streams");
   1203    return Promise.all(
   1204      constraintsList.map(constraints => this.getUserMedia(constraints))
   1205    );
   1206  },
   1207 
   1208  async getAllUserMediaAndAddStreams(constraintsList) {
   1209    var streams = await this.getAllUserMedia(constraintsList);
   1210    if (!streams) {
   1211      return undefined;
   1212    }
   1213    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
   1214  },
   1215 
   1216  async getAllUserMediaAndAddTransceivers(constraintsList) {
   1217    var streams = await this.getAllUserMedia(constraintsList);
   1218    if (!streams) {
   1219      return undefined;
   1220    }
   1221    return Promise.all(
   1222      streams.map(stream => this.attachLocalStream(stream, true))
   1223    );
   1224  },
   1225 
   1226  /**
   1227   * Create a new data channel instance.  Also creates a promise called
   1228   * `this.nextDataChannel` that resolves when the next data channel arrives.
   1229   */
   1230  expectDataChannel(message) {
   1231    this.nextDataChannel = new Promise(resolve => {
   1232      this.ondatachannel = e => {
   1233        ok(e.channel, message);
   1234        is(
   1235          e.channel.readyState,
   1236          "open",
   1237          "data channel in 'open' after 'ondatachannel'"
   1238        );
   1239        resolve(e.channel);
   1240      };
   1241    });
   1242  },
   1243 
   1244  /**
   1245   * Create a new data channel instance
   1246   *
   1247   * @param {object} options
   1248   *        Options which get forwarded to nsIPeerConnection.createDataChannel
   1249   * @returns {DataChannelWrapper} The created data channel
   1250   */
   1251  createDataChannel(options) {
   1252    var label = "channel_" + this.dataChannels.length;
   1253    info(this + ": Create data channel '" + label);
   1254 
   1255    if (!this.dataChannels.length) {
   1256      this.expectNegotiationNeeded();
   1257    }
   1258    var channel = this._pc.createDataChannel(label, options);
   1259    is(channel.readyState, "connecting", "initial readyState is 'connecting'");
   1260    var wrapper = new DataChannelWrapper(channel, this);
   1261    this.dataChannels.push(wrapper);
   1262    return wrapper;
   1263  },
   1264 
   1265  /**
   1266   * Creates an offer and automatically handles the failure case.
   1267   */
   1268  createOffer() {
   1269    return this._pc.createOffer(this.offerOptions).then(offer => {
   1270      info("Got offer: " + JSON.stringify(offer));
   1271      // note: this might get updated through ICE gathering
   1272      this._latest_offer = offer;
   1273      return offer;
   1274    });
   1275  },
   1276 
   1277  /**
   1278   * Creates an answer and automatically handles the failure case.
   1279   */
   1280  createAnswer() {
   1281    return this._pc.createAnswer().then(answer => {
   1282      info(this + ": Got answer: " + JSON.stringify(answer));
   1283      this._last_answer = answer;
   1284      return answer;
   1285    });
   1286  },
   1287 
   1288  /**
   1289   * Sets the local description and automatically handles the failure case.
   1290   *
   1291   * @param {object} desc
   1292   *        RTCSessionDescriptionInit for the local description request
   1293   */
   1294  setLocalDescription(desc) {
   1295    this.observedNegotiationNeeded = undefined;
   1296    return this._pc.setLocalDescription(desc).then(() => {
   1297      info(this + ": Successfully set the local description");
   1298    });
   1299  },
   1300 
   1301  /**
   1302   * Tries to set the local description and expect failure. Automatically
   1303   * causes the test case to fail if the call succeeds.
   1304   *
   1305   * @param {object} desc
   1306   *        RTCSessionDescriptionInit for the local description request
   1307   * @returns {Promise}
   1308   *        A promise that resolves to the expected error
   1309   */
   1310  setLocalDescriptionAndFail(desc) {
   1311    return this._pc
   1312      .setLocalDescription(desc)
   1313      .then(
   1314        generateErrorCallback("setLocalDescription should have failed."),
   1315        err => {
   1316          info(this + ": As expected, failed to set the local description");
   1317          return err;
   1318        }
   1319      );
   1320  },
   1321 
   1322  /**
   1323   * Sets the remote description and automatically handles the failure case.
   1324   *
   1325   * @param {object} desc
   1326   *        RTCSessionDescriptionInit for the remote description request
   1327   */
   1328  setRemoteDescription(desc) {
   1329    this.observedNegotiationNeeded = undefined;
   1330    // This has to be done before calling sRD, otherwise a candidate in flight
   1331    // could end up in the PC's operations queue before sRD resolves.
   1332    if (desc.type == "rollback") {
   1333      this.holdIceCandidates = new Promise(
   1334        r => (this.releaseIceCandidates = r)
   1335      );
   1336    }
   1337    return this._pc.setRemoteDescription(desc).then(() => {
   1338      info(this + ": Successfully set remote description");
   1339      if (desc.type != "rollback") {
   1340        this.releaseIceCandidates();
   1341      }
   1342    });
   1343  },
   1344 
   1345  /**
   1346   * Tries to set the remote description and expect failure. Automatically
   1347   * causes the test case to fail if the call succeeds.
   1348   *
   1349   * @param {object} desc
   1350   *        RTCSessionDescriptionInit for the remote description request
   1351   * @returns {Promise}
   1352   *        a promise that resolve to the returned error
   1353   */
   1354  setRemoteDescriptionAndFail(desc) {
   1355    return this._pc
   1356      .setRemoteDescription(desc)
   1357      .then(
   1358        generateErrorCallback("setRemoteDescription should have failed."),
   1359        err => {
   1360          info(this + ": As expected, failed to set the remote description");
   1361          return err;
   1362        }
   1363      );
   1364  },
   1365 
   1366  /**
   1367   * Registers a callback for the signaling state change and
   1368   * appends the new state to an array for logging it later.
   1369   */
   1370  logSignalingState() {
   1371    this.signalingStateLog = [this._pc.signalingState];
   1372    this._pc.addEventListener("signalingstatechange", e => {
   1373      var newstate = this._pc.signalingState;
   1374      var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1];
   1375      if (Object.keys(signalingStateTransitions).includes(oldstate)) {
   1376        ok(
   1377          signalingStateTransitions[oldstate].includes(newstate),
   1378          this +
   1379            ": legal signaling state transition from " +
   1380            oldstate +
   1381            " to " +
   1382            newstate
   1383        );
   1384      } else {
   1385        ok(
   1386          false,
   1387          this +
   1388            ": old signaling state " +
   1389            oldstate +
   1390            " missing in signaling transition array"
   1391        );
   1392      }
   1393      this.signalingStateLog.push(newstate);
   1394    });
   1395  },
   1396 
   1397  allExpectedTracksAreObserved(expected, observed) {
   1398    return Object.keys(expected).every(trackId => observed[trackId]);
   1399  },
   1400 
   1401  setupStreamEventHandlers(stream) {
   1402    const myTrackIds = new Set(stream.getTracks().map(t => t.id));
   1403 
   1404    stream.addEventListener("addtrack", ({ track }) => {
   1405      ok(
   1406        !myTrackIds.has(track.id),
   1407        "Duplicate addtrack callback: " +
   1408          `stream id=${stream.id} track id=${track.id}`
   1409      );
   1410      myTrackIds.add(track.id);
   1411      // addtrack events happen before track events, so the track callback hasn't
   1412      // heard about this yet.
   1413      let streams = this.remoteStreamsByTrackId.get(track.id);
   1414      ok(
   1415        !streams || !streams.has(stream.id),
   1416        `In addtrack for stream id=${stream.id}` +
   1417          `there should not have been a track event for track id=${track.id} ` +
   1418          " containing this stream yet."
   1419      );
   1420      ok(
   1421        stream.getTracks().includes(track),
   1422        "In addtrack, stream id=" +
   1423          `${stream.id} should already contain track id=${track.id}`
   1424      );
   1425    });
   1426 
   1427    stream.addEventListener("removetrack", ({ track }) => {
   1428      ok(
   1429        myTrackIds.has(track.id),
   1430        "Duplicate removetrack callback: " +
   1431          `stream id=${stream.id} track id=${track.id}`
   1432      );
   1433      myTrackIds.delete(track.id);
   1434      // Also remove the association from remoteStreamsByTrackId
   1435      const streams = this.remoteStreamsByTrackId.get(track.id);
   1436      ok(
   1437        streams,
   1438        `In removetrack for stream id=${stream.id}, track id=` +
   1439          `${track.id} should have had a track callback for the stream.`
   1440      );
   1441      streams.delete(stream.id);
   1442      ok(
   1443        !stream.getTracks().includes(track),
   1444        "In removetrack, stream id=" +
   1445          `${stream.id} should not contain track id=${track.id}`
   1446      );
   1447    });
   1448  },
   1449 
   1450  setupTrackEventHandler() {
   1451    this._pc.addEventListener("track", ({ track, streams }) => {
   1452      info(`${this}: 'ontrack' event fired for ${track.id}`);
   1453      ok(
   1454        this._pc.getReceivers().some(r => r.track == track),
   1455        `Found track ${track.id}`
   1456      );
   1457 
   1458      let gratuitousEvent = true;
   1459      let streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
   1460      if (!streamsContainingTrack) {
   1461        gratuitousEvent = false; // Told us about a new track
   1462        this.remoteStreamsByTrackId.set(track.id, new Set());
   1463        streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
   1464      }
   1465 
   1466      for (const stream of streams) {
   1467        ok(
   1468          stream.getTracks().includes(track),
   1469          `In track event, track id=${track.id}` +
   1470            ` should already be in stream id=${stream.id}`
   1471        );
   1472 
   1473        if (!streamsContainingTrack.has(stream.id)) {
   1474          gratuitousEvent = false; // Told us about a new stream
   1475          streamsContainingTrack.add(stream.id);
   1476          this.setupStreamEventHandlers(stream);
   1477        }
   1478      }
   1479 
   1480      ok(!gratuitousEvent, "track event told us something new");
   1481 
   1482      // So far, we've verified consistency between the current state of the
   1483      // streams, addtrack/removetrack events on the streams, and track events
   1484      // on the peerconnection. We have also verified that we have not gotten
   1485      // any gratuitous events. We have not done anything to verify that the
   1486      // current state of affairs matches what we were expecting it to.
   1487 
   1488      this.ensureMediaElement(track, "remote");
   1489    });
   1490  },
   1491 
   1492  /**
   1493   * Either adds a given ICE candidate right away or stores it to be added
   1494   * later, depending on the state of the PeerConnection.
   1495   *
   1496   * @param {object} candidate
   1497   *        The RTCIceCandidate to be added or stored
   1498   */
   1499  storeOrAddIceCandidate(candidate) {
   1500    this._remote_ice_candidates.push(candidate);
   1501    if (this.signalingState === "closed") {
   1502      info("Received ICE candidate for closed PeerConnection - discarding");
   1503      return;
   1504    }
   1505    this.holdIceCandidates
   1506      .then(() => {
   1507        info(this + ": adding ICE candidate " + JSON.stringify(candidate));
   1508        return this._pc.addIceCandidate(candidate);
   1509      })
   1510      .then(() => ok(true, this + " successfully added an ICE candidate"))
   1511      .catch(e =>
   1512        // The onicecandidate callback runs independent of the test steps
   1513        // and therefore errors thrown from in there don't get caught by the
   1514        // race of the Promises around our test steps.
   1515        // Note: as long as we are queuing ICE candidates until the success
   1516        //       of sRD() this should never ever happen.
   1517        ok(false, this + " adding ICE candidate failed with: " + e.message)
   1518      );
   1519  },
   1520 
   1521  /**
   1522   * Registers a callback for the ICE connection state change and
   1523   * appends the new state to an array for logging it later.
   1524   */
   1525  logIceConnectionState() {
   1526    this.iceConnectionLog = [this._pc.iceConnectionState];
   1527    this.ice_connection_callbacks.logIceStatus = () => {
   1528      var newstate = this._pc.iceConnectionState;
   1529      var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1];
   1530      if (Object.keys(iceStateTransitions).includes(oldstate)) {
   1531        if (this.iceCheckingRestartExpected) {
   1532          is(
   1533            newstate,
   1534            "checking",
   1535            "iceconnectionstate event '" +
   1536              newstate +
   1537              "' matches expected state 'checking'"
   1538          );
   1539          this.iceCheckingRestartExpected = false;
   1540        } else if (this.iceCheckingIceRollbackExpected) {
   1541          is(
   1542            newstate,
   1543            "connected",
   1544            "iceconnectionstate event '" +
   1545              newstate +
   1546              "' matches expected state 'connected'"
   1547          );
   1548          this.iceCheckingIceRollbackExpected = false;
   1549        } else {
   1550          ok(
   1551            iceStateTransitions[oldstate].includes(newstate),
   1552            this +
   1553              ": legal ICE state transition from " +
   1554              oldstate +
   1555              " to " +
   1556              newstate
   1557          );
   1558        }
   1559      } else {
   1560        ok(
   1561          false,
   1562          this +
   1563            ": old ICE state " +
   1564            oldstate +
   1565            " missing in ICE transition array"
   1566        );
   1567      }
   1568      this.iceConnectionLog.push(newstate);
   1569    };
   1570  },
   1571 
   1572  /**
   1573   * Resets the ICE connected Promise and allows ICE connection state monitoring
   1574   * to go backwards to 'checking'.
   1575   */
   1576  expectIceChecking() {
   1577    this.iceCheckingRestartExpected = true;
   1578    this.iceConnected = new Promise((resolve, reject) => {
   1579      this.iceConnectedResolve = resolve;
   1580      this.iceConnectedReject = reject;
   1581    });
   1582  },
   1583 
   1584  /**
   1585   * Waits for ICE to either connect or fail.
   1586   *
   1587   * @returns {Promise}
   1588   *          resolves when connected, rejects on failure
   1589   */
   1590  waitForIceConnected() {
   1591    return this.iceConnected;
   1592  },
   1593 
   1594  /**
   1595   * Setup a onicecandidate handler
   1596   *
   1597   * @param {object} test
   1598   *        A PeerConnectionTest object to which the ice candidates gets
   1599   *        forwarded.
   1600   */
   1601  setupIceCandidateHandler(test, candidateHandler) {
   1602    candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test);
   1603 
   1604    var resolveEndOfTrickle;
   1605    this.endOfTrickleIce = new Promise(r => (resolveEndOfTrickle = r));
   1606    this.holdIceCandidates = new Promise(r => (this.releaseIceCandidates = r));
   1607    this._new_local_ice_candidates = [];
   1608 
   1609    this._pc.onicecandidate = anEvent => {
   1610      if (!anEvent.candidate) {
   1611        this._pc.onicecandidate = () =>
   1612          ok(
   1613            false,
   1614            this.label + " received ICE candidate after end of trickle"
   1615          );
   1616        info(this.label + ": received end of trickle ICE event");
   1617        ok(
   1618          this._pc.iceGatheringState === "complete",
   1619          "ICE gathering state has reached complete"
   1620        );
   1621        resolveEndOfTrickle(this.label);
   1622        return;
   1623      }
   1624 
   1625      info(
   1626        this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate)
   1627      );
   1628      ok(anEvent.candidate.sdpMid.length, "SDP mid not empty");
   1629      ok(
   1630        anEvent.candidate.usernameFragment.length,
   1631        "usernameFragment not empty"
   1632      );
   1633 
   1634      ok(
   1635        typeof anEvent.candidate.sdpMLineIndex === "number",
   1636        "SDP MLine Index needs to exist"
   1637      );
   1638      this._local_ice_candidates.push(anEvent.candidate);
   1639      this._new_local_ice_candidates.push(anEvent.candidate);
   1640      candidateHandler(this.label, anEvent.candidate);
   1641    };
   1642  },
   1643 
   1644  checkLocalMediaTracks() {
   1645    info(
   1646      `${this}: Checking local tracks ${JSON.stringify(
   1647        this.expectedLocalTrackInfo
   1648      )}`
   1649    );
   1650    const sendersWithTrack = this._pc.getSenders().filter(({ track }) => track);
   1651    is(
   1652      sendersWithTrack.length,
   1653      this.expectedLocalTrackInfo.length,
   1654      "The number of senders with a track should be equal to the number of " +
   1655        "expected local tracks."
   1656    );
   1657 
   1658    // expectedLocalTrackInfo is in the same order that the tracks were added, and
   1659    // so should the output of getSenders.
   1660    this.expectedLocalTrackInfo.forEach((info, i) => {
   1661      const sender = sendersWithTrack[i];
   1662      is(sender, info.sender, `Sender ${i} should match`);
   1663      is(sender.track, info.track, `Track ${i} should match`);
   1664    });
   1665  },
   1666 
   1667  /**
   1668   * Checks that we are getting the media tracks we expect.
   1669   */
   1670  checkMediaTracks() {
   1671    this.checkLocalMediaTracks();
   1672  },
   1673 
   1674  checkLocalMsids() {
   1675    const sdp = this.localDescription.sdp;
   1676    const msections = sdputils.getMSections(sdp);
   1677    const expectedStreamIdCounts = new Map();
   1678    for (const { track, sender, streamId } of this.expectedLocalTrackInfo) {
   1679      const transceiver = this._pc
   1680        .getTransceivers()
   1681        .find(t => t.sender == sender);
   1682      ok(transceiver, "There should be a transceiver for each sender");
   1683      if (transceiver.mid) {
   1684        const midFinder = new RegExp(`^a=mid:${transceiver.mid}$`, "m");
   1685        const msection = msections.find(m => m.match(midFinder));
   1686        ok(
   1687          msection,
   1688          `There should be a media section for mid = ${transceiver.mid}`
   1689        );
   1690        ok(
   1691          msection.startsWith(`m=${track.kind}`),
   1692          `Media section should be of type ${track.kind}`
   1693        );
   1694        const msidFinder = new RegExp(`^a=msid:${streamId} \\S+$`, "m");
   1695        ok(
   1696          msection.match(msidFinder),
   1697          `Should find a=msid:${streamId} in media section` +
   1698            " (with any track id for now)"
   1699        );
   1700        const count = expectedStreamIdCounts.get(streamId) || 0;
   1701        expectedStreamIdCounts.set(streamId, count + 1);
   1702      }
   1703    }
   1704 
   1705    // Check for any unexpected msids.
   1706    const allMsids = sdp.match(new RegExp("^a=msid:\\S+", "mg"));
   1707    if (!allMsids) {
   1708      return;
   1709    }
   1710    const allStreamIds = allMsids.map(msidAttr =>
   1711      msidAttr.replace("a=msid:", "")
   1712    );
   1713    allStreamIds.forEach(id => {
   1714      const count = expectedStreamIdCounts.get(id);
   1715      ok(count, `Unexpected stream id ${id} found in local description.`);
   1716      if (count) {
   1717        expectedStreamIdCounts.set(id, count - 1);
   1718      }
   1719    });
   1720  },
   1721 
   1722  /**
   1723   * Check that media flow is present for the given media element by checking
   1724   * that it reaches ready state HAVE_ENOUGH_DATA and progresses time further
   1725   * than the start of the check.
   1726   *
   1727   * This ensures, that the stream being played is producing
   1728   * data and, in case it contains a video track, that at least one video frame
   1729   * has been displayed.
   1730   *
   1731   * @param {HTMLMediaElement} track
   1732   *        The media element to check
   1733   * @returns {Promise}
   1734   *        A promise that resolves when media data is flowing.
   1735   */
   1736  waitForMediaElementFlow(element) {
   1737    info("Checking data flow for element: " + element.id);
   1738    is(
   1739      element.ended,
   1740      !element.srcObject.active,
   1741      "Element ended should be the inverse of the MediaStream's active state"
   1742    );
   1743    if (element.ended) {
   1744      is(
   1745        element.readyState,
   1746        element.HAVE_CURRENT_DATA,
   1747        "Element " + element.id + " is ended and should have had data"
   1748      );
   1749      return Promise.resolve();
   1750    }
   1751 
   1752    const haveEnoughData = (
   1753      element.readyState == element.HAVE_ENOUGH_DATA
   1754        ? Promise.resolve()
   1755        : haveEvent(
   1756            element,
   1757            "canplay",
   1758            wait(60000, new Error("Timeout for element " + element.id))
   1759          )
   1760    ).then(_ => info("Element " + element.id + " has enough data."));
   1761 
   1762    const startTime = element.currentTime;
   1763    // eslint-disable-next-line promise/valid-params
   1764    const timeProgressed = timeout(
   1765      listenUntil(element, "timeupdate", _ => element.currentTime > startTime),
   1766      60000,
   1767      "Element " + element.id + " should progress currentTime"
   1768    ).then();
   1769 
   1770    return Promise.all([haveEnoughData, timeProgressed]);
   1771  },
   1772 
   1773  /**
   1774   * Wait for RTP packet flow for the given MediaStreamTrack.
   1775   *
   1776   * @param {object} track
   1777   *        A MediaStreamTrack to wait for data flow on.
   1778   * @returns {Promise}
   1779   *        Returns a promise which yields a StatsReport object with RTP stats.
   1780   */
   1781  async _waitForRtpFlow(target, rtpType) {
   1782    const { track } = target;
   1783    info(`_waitForRtpFlow(${track.id}, ${rtpType})`);
   1784    const packets = `packets${rtpType == "outbound-rtp" ? "Sent" : "Received"}`;
   1785 
   1786    const retryInterval = 500; // Time between stats checks
   1787    const timeout = 30000; // Timeout in ms
   1788    const retries = timeout / retryInterval;
   1789 
   1790    for (let i = 0; i < retries; i++) {
   1791      info(`Checking ${rtpType} for ${track.kind} track ${track.id} try ${i}`);
   1792      for (const rtp of (await target.getStats()).values()) {
   1793        if (rtp.type != rtpType) {
   1794          continue;
   1795        }
   1796        if (rtp.kind != track.kind) {
   1797          continue;
   1798        }
   1799 
   1800        const numPackets = rtp[packets];
   1801        info(`Track ${track.id} has ${numPackets} ${packets}.`);
   1802        if (!numPackets) {
   1803          continue;
   1804        }
   1805 
   1806        ok(true, `RTP flowing for ${track.kind} track ${track.id}`);
   1807        return;
   1808      }
   1809      await wait(retryInterval);
   1810    }
   1811    throw new Error(
   1812      `Checking stats for track ${track.id} timed out after ${timeout} ms`
   1813    );
   1814  },
   1815 
   1816  /**
   1817   * Wait for inbound RTP packet flow for the given MediaStreamTrack.
   1818   *
   1819   * @param {object} receiver
   1820   *        An RTCRtpReceiver to wait for data flow on.
   1821   * @returns {Promise}
   1822   *        Returns a promise that resolves once data is flowing.
   1823   */
   1824  async waitForInboundRtpFlow(receiver) {
   1825    return this._waitForRtpFlow(receiver, "inbound-rtp");
   1826  },
   1827 
   1828  /**
   1829   * Wait for outbound RTP packet flow for the given MediaStreamTrack.
   1830   *
   1831   * @param {object} sender
   1832   *        An RTCRtpSender to wait for data flow on.
   1833   * @returns {Promise}
   1834   *        Returns a promise that resolves once data is flowing.
   1835   */
   1836  async waitForOutboundRtpFlow(sender) {
   1837    return this._waitForRtpFlow(sender, "outbound-rtp");
   1838  },
   1839 
   1840  getExpectedActiveReceivers() {
   1841    return this._pc
   1842      .getTransceivers()
   1843      .filter(
   1844        t =>
   1845          !t.stopped &&
   1846          t.currentDirection &&
   1847          t.currentDirection != "inactive" &&
   1848          t.currentDirection != "sendonly"
   1849      )
   1850      .filter(({ receiver }) => receiver.track)
   1851      .map(({ mid, currentDirection, receiver }) => {
   1852        info(
   1853          `Found transceiver that should be receiving RTP: mid=${mid}` +
   1854            ` currentDirection=${currentDirection}` +
   1855            ` kind=${receiver.track.kind} track-id=${receiver.track.id}`
   1856        );
   1857        return receiver;
   1858      });
   1859  },
   1860 
   1861  getExpectedSenders() {
   1862    return this._pc.getSenders().filter(({ track }) => track);
   1863  },
   1864 
   1865  /**
   1866   * Wait for presence of video flow on all media elements and rtp flow on
   1867   * all sending and receiving track involved in this test.
   1868   *
   1869   * @returns {Promise}
   1870   *        A promise that resolves when media flows for all elements and tracks
   1871   */
   1872  waitForMediaFlow() {
   1873    const receivers = this.getExpectedActiveReceivers();
   1874    return Promise.all([
   1875      ...this.localMediaElements.map(el => this.waitForMediaElementFlow(el)),
   1876      ...this.remoteMediaElements
   1877        .filter(({ srcObject }) =>
   1878          receivers.some(({ track }) =>
   1879            srcObject.getTracks().some(t => t == track)
   1880          )
   1881        )
   1882        .map(el => this.waitForMediaElementFlow(el)),
   1883      ...receivers.map(receiver => this.waitForInboundRtpFlow(receiver)),
   1884      ...this.getExpectedSenders().map(sender =>
   1885        this.waitForOutboundRtpFlow(sender)
   1886      ),
   1887    ]);
   1888  },
   1889 
   1890  /**
   1891   * Check that correct audio (typically a flat tone) is flowing to this
   1892   * PeerConnection for each transceiver that should be receiving. Uses
   1893   * WebAudio AnalyserNodes to compare input and output audio data in the
   1894   * frequency domain.
   1895   *
   1896   * @param {object} from
   1897   *        A PeerConnectionWrapper whose audio RTPSender we use as source for
   1898   *        the audio flow check.
   1899   * @returns {Promise}
   1900   *        A promise that resolves when we're receiving the tone/s from |from|.
   1901   */
   1902  async checkReceivingToneFrom(
   1903    audiocontext,
   1904    from,
   1905    cancel = wait(60000, new Error("Tone not detected"))
   1906  ) {
   1907    let localTransceivers = this._pc
   1908      .getTransceivers()
   1909      .filter(t => t.mid)
   1910      .filter(t => t.receiver.track.kind == "audio")
   1911      .sort((t1, t2) => t1.mid < t2.mid);
   1912    let remoteTransceivers = from._pc
   1913      .getTransceivers()
   1914      .filter(t => t.mid)
   1915      .filter(t => t.receiver.track.kind == "audio")
   1916      .sort((t1, t2) => t1.mid < t2.mid);
   1917 
   1918    is(
   1919      localTransceivers.length,
   1920      remoteTransceivers.length,
   1921      "Same number of associated audio transceivers on remote and local."
   1922    );
   1923 
   1924    for (let i = 0; i < localTransceivers.length; i++) {
   1925      is(
   1926        localTransceivers[i].mid,
   1927        remoteTransceivers[i].mid,
   1928        "Transceivers at index " + i + " have the same mid."
   1929      );
   1930 
   1931      if (!remoteTransceivers[i].sender.track) {
   1932        continue;
   1933      }
   1934 
   1935      if (
   1936        remoteTransceivers[i].currentDirection == "recvonly" ||
   1937        remoteTransceivers[i].currentDirection == "inactive"
   1938      ) {
   1939        continue;
   1940      }
   1941 
   1942      let sendTrack = remoteTransceivers[i].sender.track;
   1943      let inputElem = from.getMediaElementForTrack(sendTrack, "local");
   1944      ok(
   1945        inputElem,
   1946        "Remote wrapper should have a media element for track id " +
   1947          sendTrack.id
   1948      );
   1949      let inputAudioStream = from.getStreamForSendTrack(sendTrack);
   1950      ok(
   1951        inputAudioStream,
   1952        "Remote wrapper should have a stream for track id " + sendTrack.id
   1953      );
   1954      let inputAnalyser = new AudioStreamAnalyser(
   1955        audiocontext,
   1956        inputAudioStream
   1957      );
   1958 
   1959      let recvTrack = localTransceivers[i].receiver.track;
   1960      let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
   1961      ok(
   1962        outputAudioStream,
   1963        "Local wrapper should have a stream for track id " + recvTrack.id
   1964      );
   1965      let outputAnalyser = new AudioStreamAnalyser(
   1966        audiocontext,
   1967        outputAudioStream
   1968      );
   1969 
   1970      let error = null;
   1971      cancel.then(e => (error = e));
   1972 
   1973      let indexOfMax = data =>
   1974        data.reduce((max, val, i) => (val >= data[max] ? i : max), 0);
   1975 
   1976      await outputAnalyser.waitForAnalysisSuccess(() => {
   1977        if (error) {
   1978          throw error;
   1979        }
   1980 
   1981        let inputData = inputAnalyser.getByteFrequencyData();
   1982        let outputData = outputAnalyser.getByteFrequencyData();
   1983 
   1984        let inputMax = indexOfMax(inputData);
   1985        let outputMax = indexOfMax(outputData);
   1986        info(
   1987          `Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},` +
   1988            ` output[${outputMax}] = ${outputData[outputMax]}`
   1989        );
   1990        if (!inputData[inputMax] || !outputData[outputMax]) {
   1991          return false;
   1992        }
   1993 
   1994        // When the input and output maxima are within reasonable distance (2% of
   1995        // total length, which means ~10 for length 512) from each other, we can
   1996        // be sure that the input tone has made it through the peer connection.
   1997        info(`input data length: ${inputData.length}`);
   1998        return Math.abs(inputMax - outputMax) < inputData.length * 0.02;
   1999      });
   2000    }
   2001  },
   2002 
   2003  /**
   2004   * Check that stats are present by checking for known stats.
   2005   */
   2006  async getStats(selector) {
   2007    const stats = await this._pc.getStats(selector);
   2008    const dict = {};
   2009    for (const [k, v] of stats.entries()) {
   2010      dict[k] = v;
   2011    }
   2012    info(`${this}: Got stats: ${JSON.stringify(dict)}`);
   2013    return stats;
   2014  },
   2015 
   2016  /**
   2017   * Checks that we are getting the media streams we expect.
   2018   *
   2019   * @param {object} stats
   2020   *        The stats to check from this PeerConnectionWrapper
   2021   */
   2022  checkStats(stats) {
   2023    const isRemote = ({ type }) =>
   2024      ["remote-outbound-rtp", "remote-inbound-rtp"].includes(type);
   2025    var counters = {};
   2026    for (let [key, res] of stats) {
   2027      info("Checking stats for " + key + " : " + res);
   2028      // validate stats
   2029      ok(res.id == key, "Coherent stats id");
   2030      const now = performance.timeOrigin + performance.now();
   2031      const minimum = performance.timeOrigin;
   2032      const type = isRemote(res) ? "rtcp" : "rtp";
   2033      ok(
   2034        res.timestamp >= minimum,
   2035        `Valid ${type} timestamp ${res.timestamp} >= ${minimum} (
   2036            ${res.timestamp - minimum} ms)`
   2037      );
   2038      ok(
   2039        res.timestamp <= now,
   2040        `Valid ${type} timestamp ${res.timestamp} <= ${now} (
   2041            ${res.timestamp - now} ms)`
   2042      );
   2043      if (isRemote(res)) {
   2044        continue;
   2045      }
   2046      counters[res.type] = (counters[res.type] || 0) + 1;
   2047 
   2048      switch (res.type) {
   2049        case "inbound-rtp":
   2050        case "outbound-rtp":
   2051          {
   2052            // Inbound tracks won't have an ssrc if RTP is not flowing.
   2053            // (eg; negotiated inactive)
   2054            ok(
   2055              res.ssrc || res.type == "inbound-rtp",
   2056              "Outbound RTP stats has an ssrc."
   2057            );
   2058 
   2059            if (res.ssrc) {
   2060              // ssrc is a 32 bit number returned as an unsigned long
   2061              ok(!/[^0-9]/.test(`${res.ssrc}`), "SSRC is numeric");
   2062              ok(parseInt(res.ssrc) < Math.pow(2, 32), "SSRC is within limits");
   2063            }
   2064 
   2065            if (res.type == "outbound-rtp") {
   2066              ok(res.packetsSent !== undefined, "Rtp packetsSent");
   2067              // We assume minimum payload to be 1 byte (guess from RFC 3550)
   2068              ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
   2069            } else {
   2070              ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
   2071              ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived");
   2072            }
   2073            if (res.remoteId) {
   2074              var rem = stats.get(res.remoteId);
   2075              ok(isRemote(rem), "Remote is rtcp");
   2076              ok(rem.localId == res.id, "Remote backlink match");
   2077              if (res.type == "outbound-rtp") {
   2078                ok(rem.type == "remote-inbound-rtp", "Rtcp is inbound");
   2079                if (rem.packetsLost) {
   2080                  ok(
   2081                    rem.packetsLost >= 0,
   2082                    "Rtcp packetsLost " + rem.packetsLost + " >= 0"
   2083                  );
   2084                  ok(
   2085                    rem.packetsLost < 1000,
   2086                    "Rtcp packetsLost " + rem.packetsLost + " < 1000"
   2087                  );
   2088                }
   2089                if (!this.disableRtpCountChecking) {
   2090                  // no guarantee which one is newer!
   2091                  // Note: this must change when we add a timestamp field to remote RTCP reports
   2092                  // and make rem.timestamp be the reception time
   2093                  if (res.timestamp < rem.timestamp) {
   2094                    info(
   2095                      "REVERSED timestamps: rec:" +
   2096                        rem.packetsReceived +
   2097                        " time:" +
   2098                        rem.timestamp +
   2099                        " sent:" +
   2100                        res.packetsSent +
   2101                        " time:" +
   2102                        res.timestamp
   2103                    );
   2104                  }
   2105                }
   2106                if (rem.jitter) {
   2107                  ok(rem.jitter >= 0, "Rtcp jitter " + rem.jitter + " >= 0");
   2108                  ok(rem.jitter < 5, "Rtcp jitter " + rem.jitter + " < 5 sec");
   2109                }
   2110                if (rem.roundTripTime) {
   2111                  ok(
   2112                    rem.roundTripTime >= 0,
   2113                    "Rtcp rtt " + rem.roundTripTime + " >= 0"
   2114                  );
   2115                  ok(
   2116                    rem.roundTripTime < 60,
   2117                    "Rtcp rtt " + rem.roundTripTime + " < 1 min"
   2118                  );
   2119                }
   2120              } else {
   2121                ok(rem.type == "remote-outbound-rtp", "Rtcp is outbound");
   2122                ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
   2123                ok(rem.bytesSent !== undefined, "Rtcp bytesSent");
   2124              }
   2125              ok(rem.ssrc == res.ssrc, "Remote ssrc match");
   2126            } else {
   2127              info("No rtcp info received yet");
   2128            }
   2129          }
   2130          break;
   2131      }
   2132    }
   2133 
   2134    var nin = this._pc.getTransceivers().filter(t => {
   2135      return (
   2136        !t.stopped &&
   2137        t.currentDirection != "inactive" &&
   2138        t.currentDirection != "sendonly"
   2139      );
   2140    }).length;
   2141    const nout = Object.keys(this.expectedLocalTrackInfo).length;
   2142    var ndata = this.dataChannels.length;
   2143 
   2144    // TODO(Bug 957145): Restore stronger inbound-rtp test once Bug 948249 is fixed
   2145    //is((counters["inbound-rtp"] || 0), nin, "Have " + nin + " inbound-rtp stat(s)");
   2146    ok(
   2147      (counters["inbound-rtp"] || 0) >= nin,
   2148      "Have at least " + nin + " inbound-rtp stat(s) *"
   2149    );
   2150 
   2151    ok(
   2152      (counters["outbound-rtp"] || 0) >= nout,
   2153      "Have at least" + nout + " outbound-rtp stat(s)"
   2154    );
   2155 
   2156    var numLocalCandidates = counters["local-candidate"] || 0;
   2157    var numRemoteCandidates = counters["remote-candidate"] || 0;
   2158    // If there are no tracks, there will be no stats either.
   2159    if (nin + nout + ndata > 0) {
   2160      ok(numLocalCandidates, "Have local-candidate stat(s)");
   2161      ok(numRemoteCandidates, "Have remote-candidate stat(s)");
   2162    } else {
   2163      is(numLocalCandidates, 0, "Have no local-candidate stats");
   2164      is(numRemoteCandidates, 0, "Have no remote-candidate stats");
   2165    }
   2166  },
   2167 
   2168  /**
   2169   * Compares the Ice server configured for this PeerConnectionWrapper
   2170   * with the ICE candidates received in the RTCP stats.
   2171   *
   2172   * @param {object} stats
   2173   *        The stats to be verified for relayed vs. direct connection.
   2174   */
   2175  checkStatsIceConnectionType(stats, expectedLocalCandidateType) {
   2176    let lId;
   2177    let rId;
   2178    for (let stat of stats.values()) {
   2179      if (stat.type == "candidate-pair" && stat.selected) {
   2180        lId = stat.localCandidateId;
   2181        rId = stat.remoteCandidateId;
   2182        break;
   2183      }
   2184    }
   2185    isnot(
   2186      lId,
   2187      undefined,
   2188      "Got local candidate ID " + lId + " for selected pair"
   2189    );
   2190    isnot(
   2191      rId,
   2192      undefined,
   2193      "Got remote candidate ID " + rId + " for selected pair"
   2194    );
   2195    let lCand = stats.get(lId);
   2196    let rCand = stats.get(rId);
   2197    if (!lCand || !rCand) {
   2198      ok(
   2199        false,
   2200        "failed to find candidatepair IDs or stats for local: " +
   2201          lId +
   2202          " remote: " +
   2203          rId
   2204      );
   2205      return;
   2206    }
   2207 
   2208    info(
   2209      "checkStatsIceConnectionType verifying: local=" +
   2210        JSON.stringify(lCand) +
   2211        " remote=" +
   2212        JSON.stringify(rCand)
   2213    );
   2214    expectedLocalCandidateType = expectedLocalCandidateType || "host";
   2215    var candidateType = lCand.candidateType;
   2216    if (lCand.relayProtocol === "tcp" && candidateType === "relay") {
   2217      candidateType = "relay-tcp";
   2218    }
   2219 
   2220    if (lCand.relayProtocol === "tls" && candidateType === "relay") {
   2221      candidateType = "relay-tls";
   2222    }
   2223 
   2224    if (expectedLocalCandidateType === "srflx" && candidateType === "prflx") {
   2225      // Be forgiving of prflx when expecting srflx, since that can happen due
   2226      // to timing.
   2227      candidateType = "srflx";
   2228    }
   2229 
   2230    is(
   2231      candidateType,
   2232      expectedLocalCandidateType,
   2233      "Local candidate type is what we expected for selected pair"
   2234    );
   2235  },
   2236 
   2237  /**
   2238   * Compares amount of established ICE connection according to ICE candidate
   2239   * pairs in the stats reporting with the expected amount of connection based
   2240   * on the constraints.
   2241   *
   2242   * @param {object} stats
   2243   *        The stats to check for ICE candidate pairs
   2244   * @param {object} testOptions
   2245   *        The test options object from the PeerConnectionTest
   2246   */
   2247  checkStatsIceConnections(stats, testOptions) {
   2248    var numIceConnections = 0;
   2249    stats.forEach(stat => {
   2250      if (stat.type === "candidate-pair" && stat.selected) {
   2251        numIceConnections += 1;
   2252      }
   2253    });
   2254    info("ICE connections according to stats: " + numIceConnections);
   2255    isnot(
   2256      numIceConnections,
   2257      0,
   2258      "Number of ICE connections according to stats is not zero"
   2259    );
   2260    if (testOptions.bundle) {
   2261      if (testOptions.rtcpmux) {
   2262        is(numIceConnections, 1, "stats reports exactly 1 ICE connection");
   2263      } else {
   2264        ok(
   2265          numIceConnections >= 1,
   2266          `stats ICE connections should be at least 1`
   2267        );
   2268        ok(numIceConnections <= 2, `stats ICE connections should be at most 2`);
   2269      }
   2270    } else {
   2271      var numAudioTransceivers = this._pc
   2272        .getTransceivers()
   2273        .filter(transceiver => {
   2274          return (
   2275            !transceiver.stopped && transceiver.receiver.track.kind == "audio"
   2276          );
   2277        }).length;
   2278 
   2279      var numVideoTransceivers = this._pc
   2280        .getTransceivers()
   2281        .filter(transceiver => {
   2282          return (
   2283            !transceiver.stopped && transceiver.receiver.track.kind == "video"
   2284          );
   2285        }).length;
   2286 
   2287      var numExpectedTransports = numAudioTransceivers + numVideoTransceivers;
   2288 
   2289      if (this.dataChannels.length) {
   2290        ++numExpectedTransports;
   2291      }
   2292 
   2293      info(
   2294        "expected audio + video + data transports: " + numExpectedTransports
   2295      );
   2296      if (!testOptions.rtcpmux) {
   2297        // Without rtcp mux, the expected number of transports doubles, but
   2298        // there's no good way to check whether the rtcp transports are ready,
   2299        // since there's no way to expose the state of those extra transports.
   2300        ok(
   2301          numIceConnections >= numExpectedTransports,
   2302          `stats ICE connections should be at least ${numExpectedTransports}`
   2303        );
   2304        ok(
   2305          numIceConnections <= numExpectedTransports * 2,
   2306          `stats ICE connections should be at most ${numExpectedTransports * 2}`
   2307        );
   2308      } else {
   2309        is(
   2310          numIceConnections,
   2311          numExpectedTransports,
   2312          "stats ICE connections matches expected A/V transports"
   2313        );
   2314      }
   2315    }
   2316  },
   2317 
   2318  expectNegotiationNeeded() {
   2319    if (!this.observedNegotiationNeeded) {
   2320      this.observedNegotiationNeeded = new Promise(resolve => {
   2321        this.onnegotiationneeded = resolve;
   2322      });
   2323    }
   2324  },
   2325 
   2326  /**
   2327   * Property-matching function for finding a certain stat in passed-in stats
   2328   *
   2329   * @param {object} stats
   2330   *        The stats to check from this PeerConnectionWrapper
   2331   * @param {object} props
   2332   *        The properties to look for
   2333   * @returns {boolean} Whether an entry containing all match-props was found.
   2334   */
   2335  hasStat(stats, props) {
   2336    for (let res of stats.values()) {
   2337      var match = true;
   2338      for (let prop in props) {
   2339        if (res[prop] !== props[prop]) {
   2340          match = false;
   2341          break;
   2342        }
   2343      }
   2344      if (match) {
   2345        return true;
   2346      }
   2347    }
   2348    return false;
   2349  },
   2350 
   2351  /**
   2352   * Closes the connection
   2353   */
   2354  close() {
   2355    this._pc.close();
   2356    this.localMediaElements.forEach(e => e.pause());
   2357    info(this + ": Closed connection.");
   2358  },
   2359 
   2360  /**
   2361   * Returns the string representation of the class
   2362   *
   2363   * @returns {string} The string representation
   2364   */
   2365  toString() {
   2366    return "PeerConnectionWrapper (" + this.label + ")";
   2367  },
   2368 };
   2369 
   2370 // haxx to prevent SimpleTest from failing at window.onload
   2371 function addLoadEvent() {}
   2372 
   2373 function loadScript(...scripts) {
   2374  return Promise.all(
   2375    scripts.map(script => {
   2376      var el = document.createElement("script");
   2377      if (typeof scriptRelativePath === "string" && script.charAt(0) !== "/") {
   2378        script = scriptRelativePath + script;
   2379      }
   2380      el.src = script;
   2381      document.head.appendChild(el);
   2382      return new Promise(r => {
   2383        el.onload = r;
   2384        el.onerror = r;
   2385      });
   2386    })
   2387  );
   2388 }
   2389 
   2390 // Ensure SimpleTest.js is loaded before other scripts.
   2391 /* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
   2392 /* import-globals-from head.js */
   2393 /* import-globals-from templates.js */
   2394 /* import-globals-from turnConfig.js */
   2395 /* import-globals-from dataChannel.js */
   2396 /* import-globals-from network.js */
   2397 /* import-globals-from sdpUtils.js */
   2398 
   2399 var scriptsReady = loadScript("/tests/SimpleTest/SimpleTest.js").then(() => {
   2400  return loadScript(
   2401    "head.js",
   2402    "templates.js",
   2403    "turnConfig.js",
   2404    "dataChannel.js",
   2405    "network.js",
   2406    "sdpUtils.js"
   2407  );
   2408 });
   2409 
   2410 function createHTML(options) {
   2411  return scriptsReady.then(() => realCreateHTML(options));
   2412 }
   2413 
   2414 var iceServerWebsocket;
   2415 var iceServersArray = [];
   2416 
   2417 var addTurnsSelfsignedCerts = () => {
   2418  var gUrl = SimpleTest.getTestFileURL("addTurnsSelfsignedCert.js");
   2419  var gScript = SpecialPowers.loadChromeScript(gUrl);
   2420  var certs = [];
   2421  // If the ICE server is running TURNS, and includes a "cert" attribute in
   2422  // its JSON, we set up an override that will forgive things like
   2423  // self-signed for it.
   2424  iceServersArray.forEach(iceServer => {
   2425    if (iceServer.hasOwnProperty("cert")) {
   2426      iceServer.urls.forEach(url => {
   2427        if (url.startsWith("turns:")) {
   2428          // Assumes no port or params!
   2429          certs.push({ cert: iceServer.cert, hostname: url.substr(6) });
   2430        }
   2431      });
   2432    }
   2433  });
   2434 
   2435  return new Promise((resolve, reject) => {
   2436    gScript.addMessageListener("certs-added", () => {
   2437      resolve();
   2438    });
   2439 
   2440    gScript.sendAsyncMessage("add-turns-certs", certs);
   2441  });
   2442 };
   2443 
   2444 var setupIceServerConfig = useIceServer => {
   2445  // We disable ICE support for HTTP proxy when using a TURN server, because
   2446  // mochitest uses a fake HTTP proxy to serve content, which will eat our STUN
   2447  // packets for TURN TCP.
   2448  var enableHttpProxy = enable =>
   2449    SpecialPowers.pushPrefEnv({
   2450      set: [["media.peerconnection.disable_http_proxy", !enable]],
   2451    });
   2452 
   2453  var spawnIceServer = () =>
   2454    new Promise((resolve, reject) => {
   2455      iceServerWebsocket = new WebSocket("ws://localhost:8191/");
   2456      iceServerWebsocket.onopen = event => {
   2457        info("websocket/process bridge open, starting ICE Server...");
   2458        iceServerWebsocket.send("iceserver");
   2459      };
   2460 
   2461      iceServerWebsocket.onmessage = event => {
   2462        // The first message will contain the iceServers configuration, subsequent
   2463        // messages are just logging.
   2464        info("ICE Server: " + event.data);
   2465        resolve(event.data);
   2466      };
   2467 
   2468      iceServerWebsocket.onerror = () => {
   2469        reject("ICE Server error: Is the ICE server websocket up?");
   2470      };
   2471 
   2472      iceServerWebsocket.onclose = () => {
   2473        info("ICE Server websocket closed");
   2474        reject("ICE Server gone before getting configuration");
   2475      };
   2476    });
   2477 
   2478  if (!useIceServer) {
   2479    info("Skipping ICE Server for this test");
   2480    return enableHttpProxy(true);
   2481  }
   2482 
   2483  return enableHttpProxy(false)
   2484    .then(spawnIceServer)
   2485    .then(iceServersStr => {
   2486      iceServersArray = JSON.parse(iceServersStr);
   2487    })
   2488    .then(addTurnsSelfsignedCerts);
   2489 };
   2490 
   2491 async function runNetworkTest(testFunction, fixtureOptions = {}) {
   2492  let { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
   2493    "resource://gre/modules/AppConstants.sys.mjs"
   2494  );
   2495 
   2496  await scriptsReady;
   2497  await runTestWhenReady(async options => {
   2498    await startNetworkAndTest();
   2499    await setupIceServerConfig(fixtureOptions.useIceServer);
   2500    await testFunction(options);
   2501    await networkTestFinished();
   2502  });
   2503 }