Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  pc.js   Sprache: JAVA

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


"use strict";

const LOOPBACK_ADDR = "127.0.0.";

const iceStateTransitions = {
  new: ["checking""closed"], //Note: 'failed' might need to added here
  //      even though it is not in the standard
  checking: ["new""connected""failed""closed"], //Note: do we need to
  // allow 'completed' in
  // here as well?
  connected: ["new""checking""completed""disconnected""closed"],
  completed: ["new""checking""disconnected""closed"],
  disconnected: ["new""connected""completed""failed""closed"],
  failed: ["new""disconnected""closed"],
  closed: [],
};

const signalingStateTransitions = {
  stable: ["have-local-offer""have-remote-offer""closed"],
  "have-local-offer": [
    "have-remote-pranswer",
    "stable",
    "closed",
    "have-local-offer",
  ],
  "have-remote-pranswer": ["stable""closed""have-remote-pranswer"],
  "have-remote-offer": [
    "have-local-pranswer",
    "stable",
    "closed",
    "have-remote-offer",
  ],
  "have-local-pranswer": ["stable""closed""have-local-pranswer"],
  closed: [],
};

var makeDefaultCommands = () => {
  return [].concat(
    commandsPeerConnectionInitial,
    commandsGetUserMedia,
    commandsPeerConnectionOfferAnswer
  );
};

/**
 * This class handles tests for peer connections.
 *
 * @constructor
 * @param {object} [options={}]
 *        Optional options for the peer connection test
 * @param {object} [options.commands=commandsPeerConnection]
 *        Commands to run for the test
 * @param {bool}   [options.is_local=true]
 *        true if this test should run the tests for the "local" side.
 * @param {bool}   [options.is_remote=true]
 *        true if this test should run the tests for the "remote" side.
 * @param {object} [options.config_local=undefined]
 *        Configuration for the local peer connection instance
 * @param {object} [options.config_remote=undefined]
 *        Configuration for the remote peer connection instance. If not defined
 *        the configuration from the local instance will be used
 */

function PeerConnectionTest(options) {
  // If no options are specified make it an empty object
  options = options || {};
  options.commands = options.commands || makeDefaultCommands();
  options.is_local = "is_local" in options ? options.is_local : true;
  options.is_remote = "is_remote" in options ? options.is_remote : true;

  options.h264 = "h264" in options ? options.h264 : false;
  options.av1 = "av1" in options ? options.av1 : false;
  options.bundle = "bundle" in options ? options.bundle : true;
  options.rtcpmux = "rtcpmux" in options ? options.rtcpmux : true;
  options.opus = "opus" in options ? options.opus : true;
  options.ssrc = "ssrc" in options ? options.ssrc : true;

  options.config_local = options.config_local || {};
  options.config_remote = options.config_remote || {};

  if (!options.bundle) {
    // Make sure neither end tries to use bundle-only!
    options.config_local.bundlePolicy = "max-compat";
    options.config_remote.bundlePolicy = "max-compat";
  }

  if (iceServersArray.length) {
    if (!options.turn_disabled_local && !options.config_local.iceServers) {
      options.config_local.iceServers = iceServersArray;
    }
    if (!options.turn_disabled_remote && !options.config_remote.iceServers) {
      options.config_remote.iceServers = iceServersArray;
    }
  } else if (typeof turnServers !== "undefined") {
    if (!options.turn_disabled_local && turnServers.local) {
      if (!options.config_local.hasOwnProperty("iceServers")) {
        options.config_local.iceServers = turnServers.local.iceServers;
      }
    }
    if (!options.turn_disabled_remote && turnServers.remote) {
      if (!options.config_remote.hasOwnProperty("iceServers")) {
        options.config_remote.iceServers = turnServers.remote.iceServers;
      }
    }
  }

  if (options.is_local) {
    this.pcLocal = new PeerConnectionWrapper("pcLocal", options.config_local);
  } else {
    this.pcLocal = null;
  }

  if (options.is_remote) {
    this.pcRemote = new PeerConnectionWrapper(
      "pcRemote",
      options.config_remote || options.config_local
    );
  } else {
    this.pcRemote = null;
  }

  // Create command chain instance and assign default commands
  this.chain = new CommandChain(this, options.commands);

  this.testOptions = options;
}

/** TODO: consider removing this dependency on timeouts */
function timerGuard(p, time, message) {
  return Promise.race([
    p,
    wait(time).then(() => {
      throw new Error("timeout after " + time / 1000 + "s: " + message);
    }),
  ]);
}

/**
 * Closes the peer connection if it is active
 */

PeerConnectionTest.prototype.closePC = function () {
  info("Closing peer connections");

  var closeIt = pc => {
    if (!pc || pc.signalingState === "closed") {
      return Promise.resolve();
    }

    var promise = Promise.all([
      Promise.all(
        pc._pc
          .getReceivers()
          .filter(receiver => receiver.track.readyState == "live")
          .map(receiver => {
            info(
              "Waiting for track " +
                receiver.track.id +
                " (" +
                receiver.track.kind +
                ") to end."
            );
            return haveEvent(receiver.track, "ended", wait(50000)).then(
              event => {
                is(
                  event.target,
                  receiver.track,
                  "Event target should be the correct track"
                );
                info(pc + " ended fired for track " + receiver.track.id);
              },
              e =>
                e
                  ? Promise.reject(e)
                  : ok(
                      false,
                      "ended never fired for track " + receiver.track.id
                    )
            );
          })
      ),
    ]);
    pc.close();
    return promise;
  };

  return timerGuard(
    Promise.all([closeIt(this.pcLocal), closeIt(this.pcRemote)]),
    60000,
    "failed to close peer connection"
  );
};

/**
 * Close the open data channels, followed by the underlying peer connection
 */

PeerConnectionTest.prototype.close = function () {
  var allChannels = (this.pcLocal || this.pcRemote).dataChannels;
  return timerGuard(
    Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))),
    120000,
    "failed to close data channels"
  ).then(() => this.closePC());
};

/**
 * Close the specified data channels
 *
 * @param {Number} index
 *        Index of the data channels to close on both sides
 */

PeerConnectionTest.prototype.closeDataChannels = function (index) {
  info("closeDataChannels called with index: " + index);
  var localChannel = null;
  if (this.pcLocal) {
    localChannel = this.pcLocal.dataChannels[index];
  }
  var remoteChannel = null;
  if (this.pcRemote) {
    remoteChannel = this.pcRemote.dataChannels[index];
  }

  // We need to setup all the close listeners before calling close
  var setupClosePromise = channel => {
    if (!channel) {
      return Promise.resolve();
    }
    return new Promise(resolve => {
      channel.onclose = () => {
        is(
          channel.readyState,
          "closed",
          name + " channel " + index + " closed"
        );
        resolve();
      };
    });
  };

  // make sure to setup close listeners before triggering any actions
  var allClosed = Promise.all([
    setupClosePromise(localChannel),
    setupClosePromise(remoteChannel),
  ]);
  var complete = timerGuard(
    allClosed,
    120000,
    "failed to close data channel pair"
  );

  // triggering close on one side should suffice
  if (remoteChannel) {
    remoteChannel.close();
  } else if (localChannel) {
    localChannel.close();
  }

  return complete;
};

/**
 * Send data (message or blob) to the other peer
 *
 * @param {String|Blob} data
 *        Data to send to the other peer. For Blobs the MIME type will be lost.
 * @param {Object} [options={ }]
 *        Options to specify the data channels to be used
 * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]]
 *        Data channel to use for sending the message
 * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]]
 *        Data channel to use for receiving the message
 */

PeerConnectionTest.prototype.send = async function (data, options) {
  options = options || {};
  const source =
    options.sourceChannel ||
    this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
  const target =
    options.targetChannel ||
    this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
  source.bufferedAmountLowThreshold = options.bufferedAmountLowThreshold || 0;

  const getSizeInBytes = d => {
    if (d instanceof Blob) {
      return d.size;
    } else if (d instanceof ArrayBuffer) {
      return d.byteLength;
    } else if (d instanceof String || typeof d === "string") {
      return new TextEncoder().encode(d).length;
    } else {
      ok(false);
      throw new Error("Could not get size");
    }
  };

  const expectedSizeInBytes = getSizeInBytes(data);
  const bufferedAmount = source.bufferedAmount;

  source.send(data);
  is(
    source.bufferedAmount,
    expectedSizeInBytes + bufferedAmount,
    `Buffered amount should be ${expectedSizeInBytes}`
  );

  await new Promise(resolve => (source.onbufferedamountlow = resolve));

  return new Promise(resolve => {
    // Register event handler for the target channel
    target.onmessage = e => {
      is(
        getSizeInBytes(e.data),
        expectedSizeInBytes,
        `Expected to receive the same number of bytes as we sent (${expectedSizeInBytes})`
      );
      resolve({ channel: target, data: e.data });
    };
  });
};

/**
 * Create a data channel
 *
 * @param {Dict} options
 *        Options for the data channel (see nsIPeerConnection)
 */

PeerConnectionTest.prototype.createDataChannel = function (options) {
  var remotePromise;
  if (!options.negotiated) {
    this.pcRemote.expectDataChannel("pcRemote expected data channel");
    remotePromise = this.pcRemote.nextDataChannel;
  }

  // Create the datachannel
  var localChannel = this.pcLocal.createDataChannel(options);
  var localPromise = localChannel.opened;

  if (options.negotiated) {
    remotePromise = localPromise.then(localChannel => {
      // externally negotiated - we need to open from both ends
      options.id = options.id || channel.id; // allow for no id on options
      var remoteChannel = this.pcRemote.createDataChannel(options);
      return remoteChannel.opened;
    });
  }

  // pcRemote.observedNegotiationNeeded might be undefined if
  // !options.negotiated, which means we just wait on pcLocal
  return Promise.all([
    this.pcLocal.observedNegotiationNeeded,
    this.pcRemote.observedNegotiationNeeded,
  ]).then(() => {
    return Promise.all([localPromise, remotePromise]).then(result => {
      return { local: result[0], remote: result[1] };
    });
  });
};

/**
 * Creates an answer for the specified peer connection instance
 * and automatically handles the failure case.
 *
 * @param {PeerConnectionWrapper} peer
 *        The peer connection wrapper to run the command on
 */

PeerConnectionTest.prototype.createAnswer = function (peer) {
  return peer.createAnswer().then(answer => {
    // make a copy so this does not get updated with ICE candidates
    this.originalAnswer = JSON.parse(JSON.stringify(answer));
    return answer;
  });
};

/**
 * Creates an offer for the specified peer connection instance
 * and automatically handles the failure case.
 *
 * @param {PeerConnectionWrapper} peer
 *        The peer connection wrapper to run the command on
 */

PeerConnectionTest.prototype.createOffer = function (peer) {
  return peer.createOffer().then(offer => {
    // make a copy so this does not get updated with ICE candidates
    this.originalOffer = JSON.parse(JSON.stringify(offer));
    return offer;
  });
};

/**
 * Sets the local description for the specified peer connection instance
 * and automatically handles the failure case.
 *
 * @param {PeerConnectionWrapper} peer
          The peer connection wrapper to run the command on
 * @param {RTCSessionDescriptionInit} desc
 *        Session description for the local description request
 */

PeerConnectionTest.prototype.setLocalDescription = function (
  peer,
  desc,
  stateExpected
) {
  var eventFired = new Promise(resolve => {
    peer.onsignalingstatechange = e => {
      info(peer + ": 'signalingstatechange' event received");
      var state = e.target.signalingState;
      if (stateExpected === state) {
        peer.setLocalDescStableEventDate = new Date();
        resolve();
      } else {
        ok(
          false,
          "This event has either already fired or there has been a " +
            "mismatch between event received " +
            state +
            " and event expected " +
            stateExpected
        );
      }
    };
  });

  var stateChanged = peer.setLocalDescription(desc).then(() => {
    peer.setLocalDescDate = new Date();
  });

  peer.endOfTrickleSdp = peer.endOfTrickleIce
    .then(() => {
      return peer._pc.localDescription;
    })
    .catch(e => ok(false"Sending EOC message failed: " + e));

  return Promise.all([eventFired, stateChanged]);
};

/**
 * Sets the media constraints for both peer connection instances.
 *
 * @param {object} constraintsLocal
 *        Media constrains for the local peer connection instance
 * @param constraintsRemote
 */

PeerConnectionTest.prototype.setMediaConstraints = function (
  constraintsLocal,
  constraintsRemote
) {
  if (this.pcLocal) {
    this.pcLocal.constraints = constraintsLocal;
  }
  if (this.pcRemote) {
    this.pcRemote.constraints = constraintsRemote;
  }
};

/**
 * Sets the media options used on a createOffer call in the test.
 *
 * @param {object} options the media constraints to use on createOffer
 */

PeerConnectionTest.prototype.setOfferOptions = function (options) {
  if (this.pcLocal) {
    this.pcLocal.offerOptions = options;
  }
};

/**
 * Sets the remote description for the specified peer connection instance
 * and automatically handles the failure case.
 *
 * @param {PeerConnectionWrapper} peer
          The peer connection wrapper to run the command on
 * @param {RTCSessionDescriptionInit} desc
 *        Session description for the remote description request
 */

PeerConnectionTest.prototype.setRemoteDescription = function (
  peer,
  desc,
  stateExpected
) {
  var eventFired = new Promise(resolve => {
    peer.onsignalingstatechange = e => {
      info(peer + ": 'signalingstatechange' event received");
      var state = e.target.signalingState;
      if (stateExpected === state) {
        peer.setRemoteDescStableEventDate = new Date();
        resolve();
      } else {
        ok(
          false,
          "This event has either already fired or there has been a " +
            "mismatch between event received " +
            state +
            " and event expected " +
            stateExpected
        );
      }
    };
  });

  var stateChanged = peer.setRemoteDescription(desc).then(() => {
    peer.setRemoteDescDate = new Date();
    peer.checkMediaTracks();
  });

  return Promise.all([eventFired, stateChanged]);
};

/**
 * Adds and removes steps to/from the execution chain based on the configured
 * testOptions.
 */

PeerConnectionTest.prototype.updateChainSteps = function () {
  if (this.testOptions.h264) {
    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
      PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER,
    ]);
  }
  if (this.testOptions.av1) {
    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
      PC_LOCAL_REMOVE_ALL_BUT_AV1_FROM_OFFER,
    ]);
  }
  if (!this.testOptions.bundle) {
    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
      PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER,
    ]);
  }
  if (!this.testOptions.rtcpmux) {
    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
      PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER,
    ]);
  }
  if (!this.testOptions.ssrc) {
    this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
      PC_LOCAL_REMOVE_SSRC_FROM_OFFER,
    ]);
    this.chain.insertAfterEach("PC_REMOTE_CREATE_ANSWER", [
      PC_REMOTE_REMOVE_SSRC_FROM_ANSWER,
    ]);
  }
  if (!this.testOptions.is_local) {
    this.chain.filterOut(/^PC_LOCAL/);
  }
  if (!this.testOptions.is_remote) {
    this.chain.filterOut(/^PC_REMOTE/);
  }
};

/**
 * Start running the tests as assigned to the command chain.
 */

PeerConnectionTest.prototype.run = async function () {
  /* We have to modify the chain here to allow tests which modify the default
   * test chain instantiating a PeerConnectionTest() */

  this.updateChainSteps();
  try {
    await this.chain.execute();
    await this.close();
  } catch (e) {
    const stack =
      typeof e.stack === "string"
        ? ` ${e.stack.split("\n").join(" ... ")}`
        : "";
    ok(false, `Error in test execution: ${e} (${stack})`);
  }
};

/**
 * Routes ice candidates from one PCW to the other PCW
 */

PeerConnectionTest.prototype.iceCandidateHandler = function (
  caller,
  candidate
) {
  info("Received: " + JSON.stringify(candidate) + " from " + caller);

  var target = null;
  if (caller.includes("pcLocal")) {
    if (this.pcRemote) {
      target = this.pcRemote;
    }
  } else if (caller.includes("pcRemote")) {
    if (this.pcLocal) {
      target = this.pcLocal;
    }
  } else {
    ok(false"received event from unknown caller: " + caller);
    return;
  }

  if (target) {
    target.storeOrAddIceCandidate(candidate);
  } else {
    info("sending ice candidate to signaling server");
    send_message({ type: "ice_candidate", ice_candidate: candidate });
  }
};

/**
 * Installs a polling function for the socket.io client to read
 * all messages from the chat room into a message queue.
 */

PeerConnectionTest.prototype.setupSignalingClient = function () {
  this.signalingMessageQueue = [];
  this.signalingCallbacks = {};
  this.signalingLoopRun = true;

  var queueMessage = message => {
    info("Received signaling message: " + JSON.stringify(message));
    var fired = false;
    Object.keys(this.signalingCallbacks).forEach(name => {
      if (name === message.type) {
        info("Invoking callback for message type: " + name);
        this.signalingCallbacks[name](message);
        fired = true;
      }
    });
    if (!fired) {
      this.signalingMessageQueue.push(message);
      info(
        "signalingMessageQueue.length: " + this.signalingMessageQueue.length
      );
    }
    if (this.signalingLoopRun) {
      wait_for_message().then(queueMessage);
    } else {
      info("Exiting signaling message event loop");
    }
  };
  wait_for_message().then(queueMessage);
};

/**
 * Sets a flag to stop reading further messages from the chat room.
 */

PeerConnectionTest.prototype.signalingMessagesFinished = function () {
  this.signalingLoopRun = false;
};

/**
 * Register a callback function to deliver messages from the chat room
 * directly instead of storing them in the message queue.
 *
 * @param {string} messageType
 *        For which message types should the callback get invoked.
 *
 * @param {function} onMessage
 *        The function which gets invoked if a message of the messageType
 *        has been received from the chat room.
 */

PeerConnectionTest.prototype.registerSignalingCallback = function (
  messageType,
  onMessage
) {
  this.signalingCallbacks[messageType] = onMessage;
};

/**
 * Searches the message queue for the first message of a given type
 * and invokes the given callback function, or registers the callback
 * function for future messages if the queue contains no such message.
 *
 * @param {string} messageType
 *        The type of message to search and register for.
 */

PeerConnectionTest.prototype.getSignalingMessage = function (messageType) {
  var i = this.signalingMessageQueue.findIndex(m => m.type === messageType);
  if (i >= 0) {
    info(
      "invoking callback on message " +
        i +
        " from message queue, for message type:" +
        messageType
    );
    return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]);
  }
  return new Promise(resolve =>
    this.registerSignalingCallback(messageType, resolve)
  );
};

/**
 * This class acts as a wrapper around a DataChannel instance.
 *
 * @param dataChannel
 * @param peerConnectionWrapper
 * @constructor
 */

function DataChannelWrapper(dataChannel, peerConnectionWrapper) {
  this._channel = dataChannel;
  this._pc = peerConnectionWrapper;

  info("Creating " + this);

  /**
   * Setup appropriate callbacks
   */

  createOneShotEventWrapper(thisthis._channel, "close");
  createOneShotEventWrapper(thisthis._channel, "error");
  createOneShotEventWrapper(thisthis._channel, "message");
  createOneShotEventWrapper(thisthis._channel, "bufferedamountlow");

  this.opened = timerGuard(
    new Promise(resolve => {
      this._channel.onopen = () => {
        this._channel.onopen = unexpectedEvent(this"onopen");
        is(this.readyState, "open""data channel is 'open' after 'onopen'");
        resolve(this);
      };
    }),
    180000,
    "channel didn't open in time"
  );
}

DataChannelWrapper.prototype = {
  /**
   * Returns the binary type of the channel
   *
   * @returns {String} The binary type
   */

  get binaryType() {
    return this._channel.binaryType;
  },

  /**
   * Sets the binary type of the channel
   *
   * @param {String} type
   *        The new binary type of the channel
   */

  set binaryType(type) {
    this._channel.binaryType = type;
  },

  /**
   * Returns the label of the underlying data channel
   *
   * @returns {String} The label
   */

  get label() {
    return this._channel.label;
  },

  /**
   * Returns the protocol of the underlying data channel
   *
   * @returns {String} The protocol
   */

  get protocol() {
    return this._channel.protocol;
  },

  /**
   * Returns the id of the underlying data channel
   *
   * @returns {number} The stream id
   */

  get id() {
    return this._channel.id;
  },

  /**
   * Returns the reliable state of the underlying data channel
   *
   * @returns {bool} The stream's reliable state
   */

  get reliable() {
    return this._channel.reliable;
  },

  /**
   * Returns the ordered attribute of the data channel
   *
   * @returns {bool} The ordered attribute
   */

  get ordered() {
    return this._channel.ordered;
  },

  /**
   * Returns the maxPacketLifeTime attribute of the data channel
   *
   * @returns {number} The maxPacketLifeTime attribute
   */

  get maxPacketLifeTime() {
    return this._channel.maxPacketLifeTime;
  },

  /**
   * Returns the maxRetransmits attribute of the data channel
   *
   * @returns {number} The maxRetransmits attribute
   */

  get maxRetransmits() {
    return this._channel.maxRetransmits;
  },

  /**
   * Returns the readyState bit of the data channel
   *
   * @returns {String} The state of the channel
   */

  get readyState() {
    return this._channel.readyState;
  },

  get bufferedAmount() {
    return this._channel.bufferedAmount;
  },

  /**
   * Sets the bufferlowthreshold of the channel
   *
   * @param {integer} amoutn
   *        The new threshold for the chanel
   */

  set bufferedAmountLowThreshold(amount) {
    this._channel.bufferedAmountLowThreshold = amount;
  },

  /**
   * Close the data channel
   */

  close() {
    info(this + ": Closing channel");
    this._channel.close();
  },

  /**
   * Send data through the data channel
   *
   * @param {String|Object} data
   *        Data which has to be sent through the data channel
   */

  send(data) {
    info(this + ": Sending data '" + data + "'");
    this._channel.send(data);
  },

  /**
   * Returns the string representation of the class
   *
   * @returns {String} The string representation
   */

  toString() {
    return (
      "DataChannelWrapper (" + this._pc.label + "_" + this._channel.label + ")"
    );
  },
};

/**
 * This class acts as a wrapper around a PeerConnection instance.
 *
 * @constructor
 * @param {string} label
 *        Description for the peer connection instance
 * @param {object} configuration
 *        Configuration for the peer connection instance
 */

function PeerConnectionWrapper(label, configuration) {
  this.configuration = configuration;
  if (configuration && configuration.label_suffix) {
    label = label + "_" + configuration.label_suffix;
  }
  this.label = label;

  this.constraints = [];
  this.offerOptions = {};

  this.dataChannels = [];

  this._local_ice_candidates = [];
  this._remote_ice_candidates = [];
  this.localRequiresTrickleIce = false;
  this.remoteRequiresTrickleIce = false;
  this.localMediaElements = [];
  this.remoteMediaElements = [];
  this.audioElementsOnly = false;

  this._sendStreams = [];

  this.expectedLocalTrackInfo = [];
  this.remoteStreamsByTrackId = new Map();

  this.disableRtpCountChecking = false;

  this.iceConnectedResolve;
  this.iceConnectedReject;
  this.iceConnected = new Promise((resolve, reject) => {
    this.iceConnectedResolve = resolve;
    this.iceConnectedReject = reject;
  });
  this.iceCheckingRestartExpected = false;
  this.iceCheckingIceRollbackExpected = false;

  info("Creating " + this);
  this._pc = new RTCPeerConnection(this.configuration);

  /**
   * Setup callback handlers
   */

  // This allows test to register their own callbacks for ICE connection state changes
  this.ice_connection_callbacks = {};

  this._pc.oniceconnectionstatechange = e => {
    isnot(
      typeof this._pc.iceConnectionState,
      "undefined",
      "iceConnectionState should not be undefined"
    );
    var iceState = this._pc.iceConnectionState;
    info(
      this + ": oniceconnectionstatechange fired, new state is: " + iceState
    );
    Object.keys(this.ice_connection_callbacks).forEach(name => {
      this.ice_connection_callbacks[name]();
    });
    if (iceState === "connected") {
      this.iceConnectedResolve();
    } else if (iceState === "failed") {
      this.iceConnectedReject(new Error("ICE failed"));
    }
  };

  this._pc.onicegatheringstatechange = e => {
    isnot(
      typeof this._pc.iceGatheringState,
      "undefined",
      "iceGetheringState should not be undefined"
    );
    var gatheringState = this._pc.iceGatheringState;
    info(
      this +
        ": onicegatheringstatechange fired, new state is: " +
        gatheringState
    );
  };

  createOneShotEventWrapper(thisthis._pc, "datachannel");
  this._pc.addEventListener("datachannel", e => {
    var wrapper = new DataChannelWrapper(e.channel, this);
    this.dataChannels.push(wrapper);
  });

  createOneShotEventWrapper(thisthis._pc, "signalingstatechange");
  createOneShotEventWrapper(thisthis._pc, "negotiationneeded");
}

PeerConnectionWrapper.prototype = {
  /**
   * Returns the senders
   *
   * @returns {sequence<RTCRtpSender>} the senders
   */

  getSenders() {
    return this._pc.getSenders();
  },

  /**
   * Returns the getters
   *
   * @returns {sequence<RTCRtpReceiver>} the receivers
   */

  getReceivers() {
    return this._pc.getReceivers();
  },

  /**
   * Returns the local description.
   *
   * @returns {object} The local description
   */

  get localDescription() {
    return this._pc.localDescription;
  },

  /**
   * Returns the remote description.
   *
   * @returns {object} The remote description
   */

  get remoteDescription() {
    return this._pc.remoteDescription;
  },

  /**
   * Returns the signaling state.
   *
   * @returns {object} The local description
   */

  get signalingState() {
    return this._pc.signalingState;
  },
  /**
   * Returns the ICE connection state.
   *
   * @returns {object} The local description
   */

  get iceConnectionState() {
    return this._pc.iceConnectionState;
  },

  setIdentityProvider(provider, options) {
    this._pc.setIdentityProvider(provider, options);
  },

  elementPrefix: direction => {
    return [this.label, direction].join("_");
  },

  getMediaElementForTrack(track, direction) {
    var prefix = this.elementPrefix(direction);
    return getMediaElementForTrack(track, prefix);
  },

  createMediaElementForTrack(track, direction) {
    var prefix = this.elementPrefix(direction);
    return createMediaElementForTrack(track, prefix);
  },

  ensureMediaElement(track, direction) {
    var prefix = this.elementPrefix(direction);
    var element = this.getMediaElementForTrack(track, direction);
    if (!element) {
      element = this.createMediaElementForTrack(track, direction);
      if (direction == "local") {
        this.localMediaElements.push(element);
      } else if (direction == "remote") {
        this.remoteMediaElements.push(element);
      }
    }

    // We do this regardless, because sometimes we end up with a new stream with
    // an old id (ie; the rollback tests cause the same stream to be added
    // twice)
    element.srcObject = new MediaStream([track]);
    element.play();
  },

  addSendStream(stream) {
    // The PeerConnection will not necessarily know about this stream
    // automatically, because replaceTrack is not told about any streams the
    // new track might be associated with. Only content really knows.
    this._sendStreams.push(stream);
  },

  getStreamForSendTrack(track) {
    return this._sendStreams.find(str => str.getTrackById(track.id));
  },

  getStreamForRecvTrack(track) {
    return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id));
  },

  /**
   * Attaches a local track to this RTCPeerConnection using
   * RTCPeerConnection.addTrack().
   *
   * Also creates a media element playing a MediaStream containing all
   * tracks that have been added to `stream` using `attachLocalTrack()`.
   *
   * @param {MediaStreamTrack} track
   *        MediaStreamTrack to handle
   * @param {MediaStream} stream
   *        MediaStream to use as container for `track` on remote side
   */

  attachLocalTrack(track, stream) {
    info("Got a local " + track.kind + " track");

    this.expectNegotiationNeeded();
    var sender = this._pc.addTrack(track, stream);
    is(sender.track, track, "addTrack returns sender");
    is(
      this._pc.getSenders().pop(),
      sender,
      "Sender should be the last element in getSenders()"
    );

    ok(track.id, "track has id");
    ok(track.kind, "track has kind");
    ok(stream.id, "stream has id");
    this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
    this.addSendStream(stream);

    // This will create one media element per track, which might not be how
    // we set up things with the RTCPeerConnection. It's the only way
    // we can ensure all sent tracks are flowing however.
    this.ensureMediaElement(track, "local");

    return this.observedNegotiationNeeded;
  },

  /**
   * Callback when we get local media. Also an appropriate HTML media element
   * will be created and added to the content node.
   *
   * @param {MediaStream} stream
   *        Media stream to handle
   */

  attachLocalStream(stream, useAddTransceiver) {
    info("Got local media stream: (" + stream.id + ")");

    this.expectNegotiationNeeded();
    if (useAddTransceiver) {
      info("Using addTransceiver (on PC).");
      stream.getTracks().forEach(track => {
        var transceiver = this._pc.addTransceiver(track, { streams: [stream] });
        is(transceiver.sender.track, track, "addTransceiver returns sender");
      });
    }
    // In order to test both the addStream and addTrack APIs, we do half one
    // way, half the other, at random.
    else if (Math.random() < 0.5) {
      info("Using addStream.");
      this._pc.addStream(stream);
      ok(
        this._pc
          .getSenders()
          .find(sender => sender.track == stream.getTracks()[0]),
        "addStream returns sender"
      );
    } else {
      info("Using addTrack (on PC).");
      stream.getTracks().forEach(track => {
        var sender = this._pc.addTrack(track, stream);
        is(sender.track, track, "addTrack returns sender");
      });
    }

    this.addSendStream(stream);

    stream.getTracks().forEach(track => {
      ok(track.id, "track has id");
      ok(track.kind, "track has kind");
      const sender = this._pc.getSenders().find(s => s.track == track);
      ok(sender, "track has a sender");
      this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
      this.ensureMediaElement(track, "local");
    });

    return this.observedNegotiationNeeded;
  },

  removeSender(index) {
    var sender = this._pc.getSenders()[index];
    this.expectedLocalTrackInfo = this.expectedLocalTrackInfo.filter(
      i => i.sender != sender
    );
    this.expectNegotiationNeeded();
    this._pc.removeTrack(sender);
    return this.observedNegotiationNeeded;
  },

  senderReplaceTrack(sender, withTrack, stream) {
    const info = this.expectedLocalTrackInfo.find(i => i.sender == sender);
    if (!info) {
      return undefined; // replaceTrack on a null track, probably
    }
    info.track = withTrack;
    this.addSendStream(stream);
    this.ensureMediaElement(withTrack, "local");
    return sender.replaceTrack(withTrack);
  },

  async getUserMedia(constraints) {
    SpecialPowers.wrap(document).notifyUserGestureActivation();
    var stream = await getUserMedia(constraints);
    if (constraints.audio) {
      stream.getAudioTracks().forEach(track => {
        info(
          this +
            " gUM local stream " +
            stream.id +
            " with audio track " +
            track.id
        );
      });
    }
    if (constraints.video) {
      stream.getVideoTracks().forEach(track => {
        info(
          this +
            " gUM local stream " +
            stream.id +
            " with video track " +
            track.id
        );
      });
    }
    return stream;
  },

  /**
   * Requests all the media streams as specified in the constrains property.
   *
   * @param {array} constraintsList
   *        Array of constraints for GUM calls
   */

  getAllUserMedia(constraintsList) {
    if (constraintsList.length === 0) {
      info("Skipping GUM: no UserMedia requested");
      return Promise.resolve();
    }

    info("Get " + constraintsList.length + " local streams");
    return Promise.all(
      constraintsList.map(constraints => this.getUserMedia(constraints))
    );
  },

  async getAllUserMediaAndAddStreams(constraintsList) {
    var streams = await this.getAllUserMedia(constraintsList);
    if (!streams) {
      return undefined;
    }
    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
  },

  async getAllUserMediaAndAddTransceivers(constraintsList) {
    var streams = await this.getAllUserMedia(constraintsList);
    if (!streams) {
      return undefined;
    }
    return Promise.all(
      streams.map(stream => this.attachLocalStream(stream, true))
    );
  },

  /**
   * Create a new data channel instance.  Also creates a promise called
   * `this.nextDataChannel` that resolves when the next data channel arrives.
   */

  expectDataChannel(message) {
    this.nextDataChannel = new Promise(resolve => {
      this.ondatachannel = e => {
        ok(e.channel, message);
        is(
          e.channel.readyState,
          "open",
          "data channel in 'open' after 'ondatachannel'"
        );
        resolve(e.channel);
      };
    });
  },

  /**
   * Create a new data channel instance
   *
   * @param {Object} options
   *        Options which get forwarded to nsIPeerConnection.createDataChannel
   * @returns {DataChannelWrapper} The created data channel
   */

  createDataChannel(options) {
    var label = "channel_" + this.dataChannels.length;
    info(this + ": Create data channel '" + label);

    if (!this.dataChannels.length) {
      this.expectNegotiationNeeded();
    }
    var channel = this._pc.createDataChannel(label, options);
    is(channel.readyState, "connecting""initial readyState is 'connecting'");
    var wrapper = new DataChannelWrapper(channel, this);
    this.dataChannels.push(wrapper);
    return wrapper;
  },

  /**
   * Creates an offer and automatically handles the failure case.
   */

  createOffer() {
    return this._pc.createOffer(this.offerOptions).then(offer => {
      info("Got offer: " + JSON.stringify(offer));
      // note: this might get updated through ICE gathering
      this._latest_offer = offer;
      return offer;
    });
  },

  /**
   * Creates an answer and automatically handles the failure case.
   */

  createAnswer() {
    return this._pc.createAnswer().then(answer => {
      info(this + ": Got answer: " + JSON.stringify(answer));
      this._last_answer = answer;
      return answer;
    });
  },

  /**
   * Sets the local description and automatically handles the failure case.
   *
   * @param {object} desc
   *        RTCSessionDescriptionInit for the local description request
   */

  setLocalDescription(desc) {
    this.observedNegotiationNeeded = undefined;
    return this._pc.setLocalDescription(desc).then(() => {
      info(this + ": Successfully set the local description");
    });
  },

  /**
   * Tries to set the local description and expect failure. Automatically
   * causes the test case to fail if the call succeeds.
   *
   * @param {object} desc
   *        RTCSessionDescriptionInit for the local description request
   * @returns {Promise}
   *        A promise that resolves to the expected error
   */

  setLocalDescriptionAndFail(desc) {
    return this._pc
      .setLocalDescription(desc)
      .then(
        generateErrorCallback("setLocalDescription should have failed."),
        err => {
          info(this + ": As expected, failed to set the local description");
          return err;
        }
      );
  },

  /**
   * Sets the remote description and automatically handles the failure case.
   *
   * @param {object} desc
   *        RTCSessionDescriptionInit for the remote description request
   */

  setRemoteDescription(desc) {
    this.observedNegotiationNeeded = undefined;
    // This has to be done before calling sRD, otherwise a candidate in flight
    // could end up in the PC's operations queue before sRD resolves.
    if (desc.type == "rollback") {
      this.holdIceCandidates = new Promise(
        r => (this.releaseIceCandidates = r)
      );
    }
    return this._pc.setRemoteDescription(desc).then(() => {
      info(this + ": Successfully set remote description");
      if (desc.type != "rollback") {
        this.releaseIceCandidates();
      }
    });
  },

  /**
   * Tries to set the remote description and expect failure. Automatically
   * causes the test case to fail if the call succeeds.
   *
   * @param {object} desc
   *        RTCSessionDescriptionInit for the remote description request
   * @returns {Promise}
   *        a promise that resolve to the returned error
   */

  setRemoteDescriptionAndFail(desc) {
    return this._pc
      .setRemoteDescription(desc)
      .then(
        generateErrorCallback("setRemoteDescription should have failed."),
        err => {
          info(this + ": As expected, failed to set the remote description");
          return err;
        }
      );
  },

  /**
   * Registers a callback for the signaling state change and
   * appends the new state to an array for logging it later.
   */

  logSignalingState() {
    this.signalingStateLog = [this._pc.signalingState];
    this._pc.addEventListener("signalingstatechange", e => {
      var newstate = this._pc.signalingState;
      var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1];
      if (Object.keys(signalingStateTransitions).includes(oldstate)) {
        ok(
          signalingStateTransitions[oldstate].includes(newstate),
          this +
            ": legal signaling state transition from " +
            oldstate +
            " to " +
            newstate
        );
      } else {
        ok(
          false,
          this +
            ": old signaling state " +
            oldstate +
            " missing in signaling transition array"
        );
      }
      this.signalingStateLog.push(newstate);
    });
  },

  allExpectedTracksAreObserved(expected, observed) {
    return Object.keys(expected).every(trackId => observed[trackId]);
  },

  setupStreamEventHandlers(stream) {
    const myTrackIds = new Set(stream.getTracks().map(t => t.id));

    stream.addEventListener("addtrack", ({ track }) => {
      ok(
        !myTrackIds.has(track.id),
        "Duplicate addtrack callback: " +
          `stream id=${stream.id} track id=${track.id}`
      );
      myTrackIds.add(track.id);
      // addtrack events happen before track events, so the track callback hasn't
      // heard about this yet.
      let streams = this.remoteStreamsByTrackId.get(track.id);
      ok(
        !streams || !streams.has(stream.id),
        `In addtrack for stream id=${stream.id}` +
          `there should not have been a track event for track id=${track.id} ` +
          " containing this stream yet."
      );
      ok(
        stream.getTracks().includes(track),
        "In addtrack, stream id=" +
          `${stream.id} should already contain track id=${track.id}`
      );
    });

    stream.addEventListener("removetrack", ({ track }) => {
      ok(
        myTrackIds.has(track.id),
        "Duplicate removetrack callback: " +
          `stream id=${stream.id} track id=${track.id}`
      );
      myTrackIds.delete(track.id);
      // Also remove the association from remoteStreamsByTrackId
      const streams = this.remoteStreamsByTrackId.get(track.id);
      ok(
        streams,
        `In removetrack for stream id=${stream.id}, track id=` +
          `${track.id} should have had a track callback for the stream.`
      );
      streams.delete(stream.id);
      ok(
        !stream.getTracks().includes(track),
        "In removetrack, stream id=" +
          `${stream.id} should not contain track id=${track.id}`
      );
    });
  },

  setupTrackEventHandler() {
    this._pc.addEventListener("track", ({ track, streams }) => {
      info(`${this}: 'ontrack' event fired for ${track.id}`);
      ok(
        this._pc.getReceivers().some(r => r.track == track),
        `Found track ${track.id}`
      );

      let gratuitousEvent = true;
      let streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
      if (!streamsContainingTrack) {
        gratuitousEvent = false// Told us about a new track
        this.remoteStreamsByTrackId.set(track.id, new Set());
        streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
      }

      for (const stream of streams) {
        ok(
          stream.getTracks().includes(track),
          `In track event, track id=${track.id}` +
            ` should already be in stream id=${stream.id}`
        );

        if (!streamsContainingTrack.has(stream.id)) {
          gratuitousEvent = false// Told us about a new stream
          streamsContainingTrack.add(stream.id);
          this.setupStreamEventHandlers(stream);
        }
      }

      ok(!gratuitousEvent, "track event told us something new");

      // So far, we've verified consistency between the current state of the
      // streams, addtrack/removetrack events on the streams, and track events
      // on the peerconnection. We have also verified that we have not gotten
      // any gratuitous events. We have not done anything to verify that the
      // current state of affairs matches what we were expecting it to.

      this.ensureMediaElement(track, "remote");
    });
  },

  /**
   * Either adds a given ICE candidate right away or stores it to be added
   * later, depending on the state of the PeerConnection.
   *
   * @param {object} candidate
   *        The RTCIceCandidate to be added or stored
   */

  storeOrAddIceCandidate(candidate) {
    this._remote_ice_candidates.push(candidate);
    if (this.signalingState === "closed") {
      info("Received ICE candidate for closed PeerConnection - discarding");
      return;
    }
    this.holdIceCandidates
      .then(() => {
        info(this + ": adding ICE candidate " + JSON.stringify(candidate));
        return this._pc.addIceCandidate(candidate);
      })
      .then(() => ok(truethis + " successfully added an ICE candidate"))
      .catch(e =>
        // The onicecandidate callback runs independent of the test steps
        // and therefore errors thrown from in there don't get caught by the
        // race of the Promises around our test steps.
        // Note: as long as we are queuing ICE candidates until the success
        //       of sRD() this should never ever happen.
        ok(falsethis + " adding ICE candidate failed with: " + e.message)
      );
  },

  /**
   * Registers a callback for the ICE connection state change and
   * appends the new state to an array for logging it later.
   */

  logIceConnectionState() {
    this.iceConnectionLog = [this._pc.iceConnectionState];
    this.ice_connection_callbacks.logIceStatus = () => {
      var newstate = this._pc.iceConnectionState;
      var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1];
      if (Object.keys(iceStateTransitions).includes(oldstate)) {
        if (this.iceCheckingRestartExpected) {
          is(
            newstate,
            "checking",
            "iceconnectionstate event '" +
              newstate +
              "' matches expected state 'checking'"
          );
          this.iceCheckingRestartExpected = false;
        } else if (this.iceCheckingIceRollbackExpected) {
          is(
            newstate,
            "connected",
            "iceconnectionstate event '" +
              newstate +
              "' matches expected state 'connected'"
          );
          this.iceCheckingIceRollbackExpected = false;
        } else {
          ok(
            iceStateTransitions[oldstate].includes(newstate),
            this +
              ": legal ICE state transition from " +
              oldstate +
              " to " +
              newstate
          );
        }
      } else {
        ok(
          false,
          this +
            ": old ICE state " +
            oldstate +
            " missing in ICE transition array"
        );
      }
      this.iceConnectionLog.push(newstate);
    };
  },

  /**
   * Resets the ICE connected Promise and allows ICE connection state monitoring
   * to go backwards to 'checking'.
   */

  expectIceChecking() {
    this.iceCheckingRestartExpected = true;
    this.iceConnected = new Promise((resolve, reject) => {
      this.iceConnectedResolve = resolve;
      this.iceConnectedReject = reject;
    });
  },

  /**
   * Waits for ICE to either connect or fail.
   *
   * @returns {Promise}
   *          resolves when connected, rejects on failure
   */

  waitForIceConnected() {
    return this.iceConnected;
  },

  /**
   * Setup a onicecandidate handler
   *
   * @param {object} test
   *        A PeerConnectionTest object to which the ice candidates gets
   *        forwarded.
   */

  setupIceCandidateHandler(test, candidateHandler) {
    candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test);

    var resolveEndOfTrickle;
    this.endOfTrickleIce = new Promise(r => (resolveEndOfTrickle = r));
    this.holdIceCandidates = new Promise(r => (this.releaseIceCandidates = r));
    this._new_local_ice_candidates = [];

    this._pc.onicecandidate = anEvent => {
      if (!anEvent.candidate) {
        this._pc.onicecandidate = () =>
          ok(
            false,
            this.label + " received ICE candidate after end of trickle"
          );
        info(this.label + ": received end of trickle ICE event");
        ok(
          this._pc.iceGatheringState === "complete",
          "ICE gathering state has reached complete"
        );
        resolveEndOfTrickle(this.label);
        return;
      }

      info(
        this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate)
      );
      ok(anEvent.candidate.sdpMid.length, "SDP mid not empty");
      ok(
        anEvent.candidate.usernameFragment.length,
        "usernameFragment not empty"
      );

      ok(
        typeof anEvent.candidate.sdpMLineIndex === "number",
        "SDP MLine Index needs to exist"
      );
      this._local_ice_candidates.push(anEvent.candidate);
      this._new_local_ice_candidates.push(anEvent.candidate);
      candidateHandler(this.label, anEvent.candidate);
    };
  },

  checkLocalMediaTracks() {
    info(
      `${this}: Checking local tracks ${JSON.stringify(
        this.expectedLocalTrackInfo
      )}`
    );
    const sendersWithTrack = this._pc.getSenders().filter(({ track }) => track);
    is(
      sendersWithTrack.length,
      this.expectedLocalTrackInfo.length,
      "The number of senders with a track should be equal to the number of " +
        "expected local tracks."
    );

    // expectedLocalTrackInfo is in the same order that the tracks were added, and
    // so should the output of getSenders.
    this.expectedLocalTrackInfo.forEach((info, i) => {
      const sender = sendersWithTrack[i];
      is(sender, info.sender, `Sender ${i} should match`);
      is(sender.track, info.track, `Track ${i} should match`);
    });
  },

  /**
   * Checks that we are getting the media tracks we expect.
   */

  checkMediaTracks() {
    this.checkLocalMediaTracks();
  },

  checkLocalMsids() {
    const sdp = this.localDescription.sdp;
    const msections = sdputils.getMSections(sdp);
    const expectedStreamIdCounts = new Map();
    for (const { track, sender, streamId } of this.expectedLocalTrackInfo) {
      const transceiver = this._pc
        .getTransceivers()
        .find(t => t.sender == sender);
      ok(transceiver, "There should be a transceiver for each sender");
      if (transceiver.mid) {
        const midFinder = new RegExp(`^a=mid:${transceiver.mid}$`, "m");
        const msection = msections.find(m => m.match(midFinder));
        ok(
          msection,
          `There should be a media section for mid = ${transceiver.mid}`
        );
        ok(
          msection.startsWith(`m=${track.kind}`),
          `Media section should be of type ${track.kind}`
        );
        const msidFinder = new RegExp(`^a=msid:${streamId} \\S+$`, "m");
        ok(
          msection.match(msidFinder),
          `Should find a=msid:${streamId} in media section` +
            " (with any track id for now)"
        );
        const count = expectedStreamIdCounts.get(streamId) || 0;
        expectedStreamIdCounts.set(streamId, count + 1);
      }
    }

    // Check for any unexpected msids.
    const allMsids = sdp.match(new RegExp("^a=msid:\\S+""mg"));
    if (!allMsids) {
      return;
    }
    const allStreamIds = allMsids.map(msidAttr =>
      msidAttr.replace("a=msid:""")
    );
    allStreamIds.forEach(id => {
      const count = expectedStreamIdCounts.get(id);
      ok(count, `Unexpected stream id ${id} found in local description.`);
      if (count) {
        expectedStreamIdCounts.set(id, count - 1);
      }
    });
  },

  /**
   * Check that media flow is present for the given media element by checking
   * that it reaches ready state HAVE_ENOUGH_DATA and progresses time further
   * than the start of the check.
   *
   * This ensures, that the stream being played is producing
   * data and, in case it contains a video track, that at least one video frame
   * has been displayed.
   *
   * @param {HTMLMediaElement} track
   *        The media element to check
   * @returns {Promise}
   *        A promise that resolves when media data is flowing.
   */

  waitForMediaElementFlow(element) {
    info("Checking data flow for element: " + element.id);
    is(
      element.ended,
      !element.srcObject.active,
      "Element ended should be the inverse of the MediaStream's active state"
    );
    if (element.ended) {
      is(
        element.readyState,
        element.HAVE_CURRENT_DATA,
        "Element " + element.id + " is ended and should have had data"
      );
      return Promise.resolve();
    }

    const haveEnoughData = (
      element.readyState == element.HAVE_ENOUGH_DATA
        ? Promise.resolve()
        : haveEvent(
            element,
            "canplay",
            wait(60000, new Error("Timeout for element " + element.id))
          )
    ).then(_ => info("Element " + element.id + " has enough data."));

    const startTime = element.currentTime;
    // eslint-disable-next-line promise/valid-params
    const timeProgressed = timeout(
      listenUntil(element, "timeupdate", _ => element.currentTime > startTime),
      60000,
      "Element " + element.id + " should progress currentTime"
    ).then();

    return Promise.all([haveEnoughData, timeProgressed]);
  },

  /**
   * Wait for RTP packet flow for the given MediaStreamTrack.
   *
   * @param {object} track
   *        A MediaStreamTrack to wait for data flow on.
   * @returns {Promise}
   *        Returns a promise which yields a StatsReport object with RTP stats.
   */

  async _waitForRtpFlow(target, rtpType) {
    const { track } = target;
    info(`_waitForRtpFlow(${track.id}, ${rtpType})`);
    const packets = `packets${rtpType == "outbound-rtp" ? "Sent" : "Received"}`;

    const retryInterval = 500; // Time between stats checks
    const timeout = 30000; // Timeout in ms
    const retries = timeout / retryInterval;

    for (let i = 0; i < retries; i++) {
      info(`Checking ${rtpType} for ${track.kind} track ${track.id} try ${i}`);
      for (const rtp of (await target.getStats()).values()) {
        if (rtp.type != rtpType) {
          continue;
        }
        if (rtp.kind != track.kind) {
          continue;
        }

        const numPackets = rtp[packets];
        info(`Track ${track.id} has ${numPackets} ${packets}.`);
        if (!numPackets) {
          continue;
        }

        ok(true, `RTP flowing for ${track.kind} track ${track.id}`);
        return;
      }
      await wait(retryInterval);
    }
    throw new Error(
      `Checking stats for track ${track.id} timed out after ${timeout} ms`
    );
  },

  /**
   * Wait for inbound RTP packet flow for the given MediaStreamTrack.
   *
   * @param {object} receiver
   *        An RTCRtpReceiver to wait for data flow on.
   * @returns {Promise}
   *        Returns a promise that resolves once data is flowing.
   */

  async waitForInboundRtpFlow(receiver) {
    return this._waitForRtpFlow(receiver, "inbound-rtp");
  },

  /**
   * Wait for outbound RTP packet flow for the given MediaStreamTrack.
   *
   * @param {object} sender
   *        An RTCRtpSender to wait for data flow on.
   * @returns {Promise}
   *        Returns a promise that resolves once data is flowing.
   */

  async waitForOutboundRtpFlow(sender) {
    return this._waitForRtpFlow(sender, "outbound-rtp");
  },

  getExpectedActiveReceivers() {
    return this._pc
      .getTransceivers()
      .filter(
        t =>
          !t.stopped &&
          t.currentDirection &&
          t.currentDirection != "inactive" &&
          t.currentDirection != "sendonly"
      )
      .filter(({ receiver }) => receiver.track)
      .map(({ mid, currentDirection, receiver }) => {
        info(
          `Found transceiver that should be receiving RTP: mid=${mid}` +
            ` currentDirection=${currentDirection}` +
            ` kind=${receiver.track.kind} track-id=${receiver.track.id}`
        );
        return receiver;
      });
  },

  getExpectedSenders() {
    return this._pc.getSenders().filter(({ track }) => track);
  },

  /**
   * Wait for presence of video flow on all media elements and rtp flow on
   * all sending and receiving track involved in this test.
   *
   * @returns {Promise}
   *        A promise that resolves when media flows for all elements and tracks
   */

  waitForMediaFlow() {
    const receivers = this.getExpectedActiveReceivers();
    return Promise.all([
      ...this.localMediaElements.map(el => this.waitForMediaElementFlow(el)),
      ...this.remoteMediaElements
        .filter(({ srcObject }) =>
          receivers.some(({ track }) =>
            srcObject.getTracks().some(t => t == track)
          )
        )
        .map(el => this.waitForMediaElementFlow(el)),
      ...receivers.map(receiver => this.waitForInboundRtpFlow(receiver)),
      ...this.getExpectedSenders().map(sender =>
        this.waitForOutboundRtpFlow(sender)
      ),
    ]);
  },

  /**
   * Check that correct audio (typically a flat tone) is flowing to this
   * PeerConnection for each transceiver that should be receiving. Uses
   * WebAudio AnalyserNodes to compare input and output audio data in the
   * frequency domain.
   *
   * @param {object} from
   *        A PeerConnectionWrapper whose audio RTPSender we use as source for
   *        the audio flow check.
   * @returns {Promise}
   *        A promise that resolves when we're receiving the tone/s from |from|.
   */

  async checkReceivingToneFrom(
    audiocontext,
    from,
    cancel = wait(60000, new Error("Tone not detected"))
  ) {
    let localTransceivers = this._pc
      .getTransceivers()
      .filter(t => t.mid)
      .filter(t => t.receiver.track.kind == "audio")
      .sort((t1, t2) => t1.mid < t2.mid);
    let remoteTransceivers = from._pc
      .getTransceivers()
      .filter(t => t.mid)
      .filter(t => t.receiver.track.kind == "audio")
      .sort((t1, t2) => t1.mid < t2.mid);

    is(
      localTransceivers.length,
      remoteTransceivers.length,
      "Same number of associated audio transceivers on remote and local."
    );

    for (let i = 0; i < localTransceivers.length; i++) {
      is(
        localTransceivers[i].mid,
        remoteTransceivers[i].mid,
        "Transceivers at index " + i + " have the same mid."
      );

      if (!remoteTransceivers[i].sender.track) {
        continue;
      }

      if (
        remoteTransceivers[i].currentDirection == "recvonly" ||
        remoteTransceivers[i].currentDirection == "inactive"
      ) {
        continue;
      }

      let sendTrack = remoteTransceivers[i].sender.track;
      let inputElem = from.getMediaElementForTrack(sendTrack, "local");
      ok(
        inputElem,
        "Remote wrapper should have a media element for track id " +
          sendTrack.id
      );
      let inputAudioStream = from.getStreamForSendTrack(sendTrack);
      ok(
        inputAudioStream,
        "Remote wrapper should have a stream for track id " + sendTrack.id
      );
      let inputAnalyser = new AudioStreamAnalyser(
        audiocontext,
        inputAudioStream
      );

      let recvTrack = localTransceivers[i].receiver.track;
      let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
      ok(
        outputAudioStream,
        "Local wrapper should have a stream for track id " + recvTrack.id
      );
      let outputAnalyser = new AudioStreamAnalyser(
        audiocontext,
        outputAudioStream
      );

      let error = null;
      cancel.then(e => (error = e));

      let indexOfMax = data =>
        data.reduce((max, val, i) => (val >= data[max] ? i : max), 0);

      await outputAnalyser.waitForAnalysisSuccess(() => {
        if (error) {
          throw error;
        }

        let inputData = inputAnalyser.getByteFrequencyData();
        let outputData = outputAnalyser.getByteFrequencyData();

        let inputMax = indexOfMax(inputData);
        let outputMax = indexOfMax(outputData);
        info(
          `Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},` +
            ` output[${outputMax}] = ${outputData[outputMax]}`
        );
        if (!inputData[inputMax] || !outputData[outputMax]) {
          return false;
        }

        // When the input and output maxima are within reasonable distance (2% of
        // total length, which means ~10 for length 512) from each other, we can
        // be sure that the input tone has made it through the peer connection.
        info(`input data length: ${inputData.length}`);
        return Math.abs(inputMax - outputMax) < inputData.length * 0.02;
      });
    }
  },

  /**
   * Check that stats are present by checking for known stats.
   */

  async getStats(selector) {
    const stats = await this._pc.getStats(selector);
    const dict = {};
    for (const [k, v] of stats.entries()) {
      dict[k] = v;
    }
    info(`${this}: Got stats: ${JSON.stringify(dict)}`);
    return stats;
  },

  /**
   * Checks that we are getting the media streams we expect.
   *
   * @param {object} stats
   *        The stats to check from this PeerConnectionWrapper
   */

  checkStats(stats) {
    const isRemote = ({ type }) =>
      ["remote-outbound-rtp""remote-inbound-rtp"].includes(type);
    var counters = {};
    for (let [key, res] of stats) {
      info("Checking stats for " + key + " : " + res);
      // validate stats
      ok(res.id == key, "Coherent stats id");
      const now = performance.timeOrigin + performance.now();
      const minimum = performance.timeOrigin;
      const type = isRemote(res) ? "rtcp" : "rtp";
      ok(
        res.timestamp >= minimum,
        `Valid ${type} timestamp ${res.timestamp} >= ${minimum} (
            ${res.timestamp - minimum} ms)`
      );
      ok(
        res.timestamp <= now,
        `Valid ${type} timestamp ${res.timestamp} <= ${now} (
            ${res.timestamp - now} ms)`
      );
      if (isRemote(res)) {
        continue;
      }
      counters[res.type] = (counters[res.type] || 0) + 1;

      switch (res.type) {
        case "inbound-rtp":
        case "outbound-rtp":
          {
            // Inbound tracks won't have an ssrc if RTP is not flowing.
            // (eg; negotiated inactive)
            ok(
              res.ssrc || res.type == "inbound-rtp",
              "Outbound RTP stats has an ssrc."
            );

            if (res.ssrc) {
              // ssrc is a 32 bit number returned as an unsigned long
              ok(!/[^0-9]/.test(`${res.ssrc}`), "SSRC is numeric");
              ok(parseInt(res.ssrc) < Math.pow(2, 32), "SSRC is within limits");
            }

            if (res.type == "outbound-rtp") {
              ok(res.packetsSent !== undefined, "Rtp packetsSent");
              // We assume minimum payload to be 1 byte (guess from RFC 3550)
              ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
            } else {
              ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
              ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived");
            }
            if (res.remoteId) {
              var rem = stats.get(res.remoteId);
              ok(isRemote(rem), "Remote is rtcp");
              ok(rem.localId == res.id, "Remote backlink match");
              if (res.type == "outbound-rtp") {
                ok(rem.type == "remote-inbound-rtp""Rtcp is inbound");
                if (rem.packetsLost) {
                  ok(
                    rem.packetsLost >= 0,
                    "Rtcp packetsLost " + rem.packetsLost + " >= 0"
                  );
                  ok(
                    rem.packetsLost < 1000,
                    "Rtcp packetsLost " + rem.packetsLost + " < 1000"
                  );
                }
                if (!this.disableRtpCountChecking) {
                  // no guarantee which one is newer!
                  // Note: this must change when we add a timestamp field to remote RTCP reports
                  // and make rem.timestamp be the reception time
                  if (res.timestamp < rem.timestamp) {
                    info(
                      "REVERSED timestamps: rec:" +
                        rem.packetsReceived +
                        " time:" +
                        rem.timestamp +
                        " sent:" +
                        res.packetsSent +
                        " time:" +
                        res.timestamp
                    );
                  }
                }
                if (rem.jitter) {
                  ok(rem.jitter >= 0, "Rtcp jitter " + rem.jitter + " >= 0");
                  ok(rem.jitter < 5, "Rtcp jitter " + rem.jitter + " < 5 sec");
                }
                if (rem.roundTripTime) {
                  ok(
                    rem.roundTripTime >= 0,
                    "Rtcp rtt " + rem.roundTripTime + " >= 0"
                  );
                  ok(
                    rem.roundTripTime < 60,
                    "Rtcp rtt " + rem.roundTripTime + " < 1 min"
                  );
                }
              } else {
                ok(rem.type == "remote-outbound-rtp""Rtcp is outbound");
                ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
                ok(rem.bytesSent !== undefined, "Rtcp bytesSent");
              }
              ok(rem.ssrc == res.ssrc, "Remote ssrc match");
            } else {
              info("No rtcp info received yet");
            }
          }
          break;
      }
    }

    var nin = this._pc.getTransceivers().filter(t => {
      return (
        !t.stopped &&
        t.currentDirection != "inactive" &&
        t.currentDirection != "sendonly"
      );
    }).length;
    const nout = Object.keys(this.expectedLocalTrackInfo).length;
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=95 H=97 G=95

¤ Dauer der Verarbeitung: 0.27 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge