dom/media/tests/mochitest/test_peerConnection_transceivers.html
author Byron Campen [:bwc] <docfaraday@gmail.com>
Mon, 18 Dec 2017 12:33:25 -0600
changeset 448785 17a32b697729c8a2f622f074fe6420e6ceba56a2
parent 448652 ae1d83fc9eab1af9c914b7aa8562924a53b78e77
child 448944 50619d5809b45e207bd6af827432b6bb90bed93f
permissions -rw-r--r--
Bug 1423842 - Part 1: Test-case: onaddstream should fire after tracks are added, but before SRD resolves. r=jib MozReview-Commit-ID: FOL1ueOR2xF

<!DOCTYPE HTML>
<html>
<head>
  <script type="application/javascript" src="pc.js"></script>
</head>
<body>
<pre id="test">
<script type="application/javascript">
  createHTML({
    bug: "1290948",
    title: "Transceivers API tests"
  });

  let checkThrows = async (func, exceptionName, description) => {
    try {
      await func();
      ok(false, description + " throws " + exceptionName);
    } catch (e) {
      is(e.name, exceptionName, description + " throws " + exceptionName);
    }
  };

  let stopTracks = (...streams) => {
    streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
  };

  let collectEvents = (target, name, check) => {
    let events = [];
    let handler = e => {
      check(e);
      events.push(e);
    };

    target.addEventListener(name, handler);

    let finishCollecting = () => {
      target.removeEventListener(name, handler);
      return events;
    };

    return {finish: finishCollecting};
  };

  let collectTrackEvents = pc => {
    let checkEvent = e => {
      ok(e.track, "Track is set on event");
      ok(e.receiver, "Receiver is set on event");
      ok(e.transceiver, "Transceiver is set on event");
      ok(e.streams, "Streams is set on event");
      e.streams.forEach(stream => {
        ok(stream.getTracks().includes(e.track),
           "Each stream in event contains the track");
      });
      is(e.receiver, e.transceiver.receiver, "Receiver belongs to transceiver");
      is(e.track, e.receiver.track, "Track belongs to receiver");
    };

    return collectEvents(pc, "track", checkEvent);
  };

  let setRemoteDescriptionReturnTrackEvents = async (pc, desc) => {
    let trackEventCollector = collectTrackEvents(pc);
    await pc.setRemoteDescription(desc);
    return trackEventCollector.finish();
  };

  let offerAnswer = async (offerer, answerer) => {
    let offer = await offerer.createOffer();
    await answerer.setRemoteDescription(offer);
    await offerer.setLocalDescription(offer);
    let answer = await answerer.createAnswer();
    await offerer.setRemoteDescription(answer);
    await answerer.setLocalDescription(answer);
  };

  let trickle = (pc1, pc2) => {
    pc1.onicecandidate = async e => {
      info("Adding ICE candidate: " + JSON.stringify(e.candidate));
      try {
        await pc2.addIceCandidate(e.candidate);
      } catch(e) {
        ok(false, "addIceCandidate threw error: " + e.name);
      }
    };
  };

  let iceConnected = pc => {
    info("Waiting for ICE connected...");
    return new Promise((resolve, reject) => {
      let iceCheck = () => {
        if (pc.iceConnectionState == "connected") {
          ok(true, "ICE connected");
          resolve();
        }

        if (pc.iceConnectionState == "failed") {
          ok(false, "ICE failed");
          reject();
        }
      };

      iceCheck();
      pc.oniceconnectionstatechange = iceCheck;
    });
  };

  let negotiationNeeded = pc => {
    return new Promise(resolve => pc.onnegotiationneeded = resolve);
  };

  let countEvents = (target, name) => {
    let result = {count: 0};
    target.addEventListener(name, e => result.count++);
    return result;
  };

  let gotMuteEvent = async track => {
    await haveEvent(track, "mute");

    ok(track.muted, "track should be muted after onmute");
  };

  let gotUnmuteEvent = async track => {
    await haveEvent(track, "unmute");

    ok(!track.muted, "track should not be muted after onunmute");
  };

  let logExpected = expected => {
    info("(expected " + JSON.stringify(expected) + ")");
  };

  let hasProps = (observed, expected) => {

    if (observed === expected) {
      return true;
    }

    // If we are expecting an array, iterate over it
    if (Array.isArray(expected)) {
      if (!Array.isArray(observed)) {
        ok(false, "Expected an array, but didn't get one.");
        logExpected(expected);
        return false;
      }

      if (observed.length !== expected.length) {
        ok(false, "Expected array to be " + expected.length + " long, but it was " + observed.length + " long instead");
        logExpected(expected);
        return false;
      }

      for (let i = 0; i < expected.length; i++) {
        if (!hasProps(observed[i], expected[i])) {
          logExpected(expected);
          return false;
        }
      }

      return true;
    }

    // If we are expecting an object, check its props
    if (typeof expected === "object" && expected !== null) {
      if (observed === null) {
        ok(false, "Expected non-null");
        return false;
      }
      let propsWeCareAbout = Object.getOwnPropertyNames(expected);
      for (let i in propsWeCareAbout) {
        let prop = propsWeCareAbout[i];
        if (!hasProps(observed[prop], expected[prop])) {
          logExpected(expected);
          return false;
        }
      }

      return true;
    }

    ok(false, "Expected (" + JSON.stringify(expected) + ") did not match " +
              "observed (" + JSON.stringify(observed) + ")");
    return false;
  };

  let checkAddTransceiverNoTrack = async () => {
    let pc = new RTCPeerConnection();
    hasProps(pc.getTransceivers(), []);

    pc.addTransceiver("audio");
    pc.addTransceiver("video");

    // NOTE: the w3c spec doesn't say anything about transceiver order, so this
    // may not necessarily be the same order we see on other browsers.
    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio", readyState: "live", muted: true}},
          sender: {track: null},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        },
        {
          receiver: {track: {kind: "video", readyState: "live", muted: true}},
          sender: {track: null},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    pc.close();
  };

  let checkAddTransceiverWithTrack = async () => {
    let pc = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true, video: true});
    let audio = stream.getAudioTracks()[0];
    let video = stream.getVideoTracks()[0];

    pc.addTransceiver(audio);
    pc.addTransceiver(video);

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: audio},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        },
        {
          receiver: {track: {kind: "video"}},
          sender: {track: video},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    pc.close();
    stopTracks(stream);
  };

  let checkAddTransceiverWithAddTrack = async () => {
    let pc = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true, video: true});
    let audio = stream.getAudioTracks()[0];
    let video = stream.getVideoTracks()[0];

    pc.addTrack(audio, stream);
    pc.addTrack(video, stream);

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: audio},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        },
        {
          receiver: {track: {kind: "video"}},
          sender: {track: video},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    pc.close();
    stopTracks(stream);
  };

  let checkAddTransceiverWithDirection = async () => {
    let pc = new RTCPeerConnection();

    pc.addTransceiver("audio", {direction: "recvonly"});
    pc.addTransceiver("video", {direction: "recvonly"});

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          mid: null,
          currentDirection: null,
          stopped: false
        },
        {
          receiver: {track: {kind: "video"}},
          sender: {track: null},
          direction: "recvonly",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    pc.close();
  };

  let checkAddTransceiverWithStream = async () => {
    let pc = new RTCPeerConnection();

    let audioStream = await getUserMedia({audio: true});
    let videoStream = await getUserMedia({video: true});
    let audio = audioStream.getAudioTracks()[0];
    let video = videoStream.getVideoTracks()[0];

    pc.addTransceiver(audio, {streams: [audioStream]});
    pc.addTransceiver(video, {streams: [videoStream]});

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: audio},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        },
        {
          receiver: {track: {kind: "video"}},
          sender: {track: video},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    let offer = await pc.createOffer();
    ok(offer.sdp.includes("a=msid:" + audioStream.id + " " + audio.id),
      "offer contains the expected audio msid");
    ok(offer.sdp.includes("a=msid:" + videoStream.id + " " + video.id),
      "offer contains the expected video msid");

    pc.close();
    stopTracks(audioStream, videoStream);
  };

  let checkAddTransceiverWithOfferToReceive = async kinds => {
    let pc = new RTCPeerConnection();

    let options = {};

    for (let kind of kinds) {
      if (kind == "audio") {
        options.offerToReceiveAudio = true;
      } else if (kind == "video") {
        options.offerToReceiveVideo = true;
      }
    }

    let offer = await pc.createOffer(options);

    let expected = [];

    // NOTE: The ordering here is not laid out in the spec at all, this is
    // firefox specific.
    if (options.offerToReceiveVideo) {
      expected.push(
        {
          receiver: {track: {kind: "video"}},
          sender: {track: null},
          direction: "recvonly",
          mid: null,
          currentDirection: null,
          stopped: false
        });
    }

    if (options.offerToReceiveAudio) {
      expected.push(
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          mid: null,
          currentDirection: null,
          stopped: false
        });
    }

    hasProps(pc.getTransceivers(), expected);

    pc.close();
  };

  let checkAddTransceiverWithSetRemoteOfferSending = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTransceiver(track, {streams: [stream]});

    let offer = await pc1.createOffer();

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);


    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkAddTransceiverWithSetRemoteOfferNoSend = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTransceiver(track);
    pc1.getTransceivers()[0].direction = "recvonly";

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents, []);

    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
          direction: "recvonly",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkAddTransceiverBadKind = async () => {
    let pc = new RTCPeerConnection();
    try {
      pc.addTransceiver("foo");
      ok(false, 'addTransceiver("foo") throws');
    }
    catch (e) {
      if (e instanceof TypeError) {
        ok(true, 'addTransceiver("foo") throws a TypeError');
      } else {
        ok(false, 'addTransceiver("foo") throws a TypeError');
      }
    }

    hasProps(pc.getTransceivers(), []);

    pc.close();
  };

  let checkAddTransceiverNoTrackDoesntPair = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[1].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: null}, // no addTrack magic, doesn't auto-pair
        {mid: "sdparta_0"} // Created by SRD
      ]);

    pc1.close();
    pc2.close();
  };

  let checkAddTransceiverWithTrackDoesntPair = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc2.addTransceiver(track);

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[1].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: null, sender: {track}},
        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkAddTransceiverThenReplaceTrackDoesntPair = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc2.getTransceivers()[0].sender.replaceTrack(track);

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[1].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: null, sender: {track}},
        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkAddTransceiverThenAddTrackPairs = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: "sdparta_0", sender: {track}}
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkAddTrackPairs = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: "sdparta_0", sender: {track}}
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkReplaceTrackNullDoesntPreventPairing = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    pc1.addTransceiver("audio");

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);
    pc2.getTransceivers()[0].sender.replaceTrack(null);

    let offer = await pc1.createOffer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: []
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {mid: "sdparta_0", sender: {track: null}}
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkRemoveAndReadd = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();
    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

    await offerAnswer(pc1, pc2);

    pc1.removeTrack(pc1.getSenders()[0]);
    pc1.addTrack(track, stream);

    hasProps(pc1.getTransceivers(),
      [
        {
          sender: {track: null},
          direction: "recvonly"
        },
        {
          sender: {track},
          direction: "sendrecv"
        }
      ]);

    // pc1 is offerer
    await offerAnswer(pc1, pc2);

    hasProps(pc2.getTransceivers(),
      [
        {currentDirection: "inactive"},
        {currentDirection: "recvonly"}
      ]);

    pc1.removeTrack(pc1.getSenders()[1]);
    pc1.addTrack(track, stream);

    hasProps(pc1.getTransceivers(),
      [
        {
          sender: {track: null},
          direction: "recvonly"
        },
        {
          sender: {track: null},
          direction: "recvonly"
        },
        {
          sender: {track},
          direction: "sendrecv"
        }
      ]);

    // pc1 is answerer. We need to create a new transceiver so pc1 will have
    // something to attach the re-added track to
    pc2.addTransceiver("audio");

    await offerAnswer(pc2, pc1);

    hasProps(pc2.getTransceivers(),
      [
        {currentDirection: "inactive"},
        {currentDirection: "inactive"},
        {currentDirection: "sendrecv"}
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkSetDirection = async () => {
    let pc = new RTCPeerConnection();
    pc.addTransceiver("audio");

    pc.getTransceivers()[0].direction = "sendonly";
    hasProps(pc.getTransceivers(),[{direction: "sendonly"}]);
    pc.getTransceivers()[0].direction = "recvonly";
    hasProps(pc.getTransceivers(),[{direction: "recvonly"}]);
    pc.getTransceivers()[0].direction = "inactive";
    hasProps(pc.getTransceivers(),[{direction: "inactive"}]);
    pc.getTransceivers()[0].direction = "sendrecv";
    hasProps(pc.getTransceivers(),[{direction: "sendrecv"}]);

    pc.close();
  };

  let checkCurrentDirection = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);
    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);

    let offer = await pc1.createOffer();
    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);

    await pc1.setLocalDescription(offer);
    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);

    let answer = await pc2.createAnswer();
    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);

    await pc2.setLocalDescription(answer);
    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);

    pc2.getTransceivers()[0].direction = "sendonly";

    offer = await pc2.createOffer();
    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);

    await pc2.setLocalDescription(offer);
    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
    hasProps(trackEvents, []);

    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);

    answer = await pc1.createAnswer();
    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);

    await pc1.setLocalDescription(answer);
    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
    hasProps(trackEvents, []);

    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);

    pc2.getTransceivers()[0].direction = "sendrecv";

    offer = await pc2.createOffer();
    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);

    await pc2.setLocalDescription(offer);
    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
    hasProps(trackEvents, []);

    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);

    answer = await pc1.createAnswer();
    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);

    await pc1.setLocalDescription(answer);
    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkSendrecvWithNoSendTrack = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTransceiver("audio");
    pc1.getTransceivers()[0].direction = "sendrecv";
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: []
        }
      ]);

    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);

    let answer = await pc2.createAnswer();
    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    // Spec language doesn't say anything about checking whether the transceiver
    // is stopped here.
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    await iceConnected(pc1);
    await iceConnected(pc2);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkSendrecvWithTracklessStream = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = new MediaStream();
    pc1.addTransceiver("audio", {streams: [stream]});

    let offer = await pc1.createOffer();

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    pc1.close();
    pc2.close();
  };

  let checkMute = async () => {
    let pc1 = new RTCPeerConnection();
    let stream1 = await getUserMedia({audio: true, video: true});
    let audio1 = stream1.getAudioTracks()[0];
    pc1.addTrack(audio1, stream1);
    let countMuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "mute");
    let countUnmuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "unmute");

    let video1 = stream1.getVideoTracks()[0];
    pc1.addTrack(video1, stream1);
    let countMuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "mute");
    let countUnmuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "unmute");

    let pc2 = new RTCPeerConnection();
    let stream2 = await getUserMedia({audio: true, video: true});
    let audio2 = stream2.getAudioTracks()[0];
    pc2.addTrack(audio2, stream2);
    let countMuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "mute");
    let countUnmuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "unmute");

    let video2 = stream2.getVideoTracks()[0];
    pc2.addTrack(video2, stream2);
    let countMuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "mute");
    let countUnmuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "unmute");


    // Check that receive tracks start muted
    hasProps(pc1.getTransceivers(),
      [
        {receiver: {track: {kind: "audio", muted: true}}},
        {receiver: {track: {kind: "video", muted: true}}}
      ]);

    hasProps(pc1.getTransceivers(),
      [
        {receiver: {track: {kind: "audio", muted: true}}},
        {receiver: {track: {kind: "video", muted: true}}}
      ]);

    let offer = await pc1.createOffer();
    await pc2.setRemoteDescription(offer);
    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);
    let answer = await pc2.createAnswer();
    await pc1.setRemoteDescription(answer);
    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    let gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
    let gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);

    let gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
    let gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);

    await iceConnected(pc1);
    await iceConnected(pc2);

    // Check that receive tracks are unmuted when RTP starts flowing
    await gotUnmuteAudio1;
    await gotUnmuteVideo1;
    await gotUnmuteAudio2;
    await gotUnmuteVideo2;

    // Check whether disabling recv locally causes onmute
    pc1.getTransceivers()[0].direction = "sendonly";
    pc1.getTransceivers()[1].direction = "sendonly";
    offer = await pc1.createOffer();
    await pc2.setRemoteDescription(offer);
    await pc1.setLocalDescription(offer);
    answer = await pc2.createAnswer();
    let gotMuteAudio1 = gotMuteEvent(pc1.getTransceivers()[0].receiver.track);
    let gotMuteVideo1 = gotMuteEvent(pc1.getTransceivers()[1].receiver.track);
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);
    await gotMuteAudio1;
    await gotMuteVideo1;

    // Check whether disabling on remote causes onmute
    pc1.getTransceivers()[0].direction = "inactive";
    pc1.getTransceivers()[1].direction = "inactive";
    offer = await pc1.createOffer();
    await pc2.setRemoteDescription(offer);
    await pc1.setLocalDescription(offer);
    answer = await pc2.createAnswer();
    let gotMuteAudio2 = gotMuteEvent(pc2.getTransceivers()[0].receiver.track);
    let gotMuteVideo2 = gotMuteEvent(pc2.getTransceivers()[1].receiver.track);
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);
    await gotMuteAudio2;
    await gotMuteVideo2;

    // Check whether onunmute fires when we turn everything on again
    pc1.getTransceivers()[0].direction = "sendrecv";
    pc1.getTransceivers()[1].direction = "sendrecv";
    offer = await pc1.createOffer();
    await pc2.setRemoteDescription(offer);
    await pc1.setLocalDescription(offer);
    answer = await pc2.createAnswer();
    gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
    gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
    gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
    gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);
    await gotUnmuteAudio1;
    await gotUnmuteVideo1;
    await gotUnmuteAudio2;
    await gotUnmuteVideo2;

    // Wait a little, just in case some stray events fire
    await wait(100);

    is(1, countMuteAudio1.count, "Got 1 mute event for pc1's audio track");
    is(1, countMuteVideo1.count, "Got 1 mute event for pc1's video track");
    is(1, countMuteAudio2.count, "Got 1 mute event for pc2's audio track");
    is(1, countMuteVideo2.count, "Got 1 mute event for pc2's video track");
    is(2, countUnmuteAudio1.count, "Got 2 unmute events for pc1's audio track");
    is(2, countUnmuteVideo1.count, "Got 2 unmute events for pc1's video track");
    is(2, countUnmuteAudio2.count, "Got 2 unmute events for pc2's audio track");
    is(2, countUnmuteVideo2.count, "Got 2 unmute events for pc2's video track");

    pc1.close();
    pc2.close();
    stopTracks(stream1);
    stopTracks(stream2);
  };

  let checkOnAddStream = async () => {
    let pc1 = new RTCPeerConnection();
    let stream1 = await getUserMedia({audio: true, video: true});
    let audio1 = stream1.getAudioTracks()[0];
    pc1.addTrack(audio1, stream1);
    let video1 = stream1.getVideoTracks()[0];
    pc1.addTrack(video1, stream1);

    let pc2 = new RTCPeerConnection();
    let stream2 = await getUserMedia({audio: true, video: true});
    let audio2 = stream2.getAudioTracks()[0];
    pc2.addTrack(audio2, stream2);
    let video2 = stream2.getVideoTracks()[0];
    pc2.addTrack(video2, stream2);

    let offer = await pc1.createOffer();

    let trackEventCollector = collectTrackEvents(pc2);
    let addstreamEventCollector = collectEvents(pc2, "addstream", e => {
      hasProps(e, {stream: {id: stream1.id}});
      is(e.stream.getAudioTracks().length, 1, "One audio track");
      is(e.stream.getVideoTracks().length, 1, "One video track");
    });

    await pc2.setRemoteDescription(offer);

    let addstreamEvents = addstreamEventCollector.finish();
    is(addstreamEvents.length, 1, "Should have 1 addstream event");

    let trackEvents = trackEventCollector.finish();

    hasProps(trackEvents,
      [
        {streams: [addstreamEvents[0].stream]},
        {streams: [addstreamEvents[0].stream]}
      ]);

    await pc1.setLocalDescription(offer);
    let answer = await pc2.createAnswer();

    trackEventCollector = collectTrackEvents(pc1);
    addstreamEventCollector = collectEvents(pc1, "addstream", e => {
      hasProps(e, {stream: {id: stream2.id}});
      is(e.stream.getAudioTracks().length, 1, "One audio track");
      is(e.stream.getVideoTracks().length, 1, "One video track");
    });

    await pc1.setRemoteDescription(answer);
    addstreamEvents = addstreamEventCollector.finish();
    is(addstreamEvents.length, 1, "Should have 1 addstream event");

    trackEvents = trackEventCollector.finish();

    hasProps(trackEvents,
      [
        {streams: [addstreamEvents[0].stream]},
        {streams: [addstreamEvents[0].stream]}
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream1);
    stopTracks(stream2);
  };

  let checkStop = async () => {
    let pc1 = new RTCPeerConnection();
    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

    let offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);

    let pc2 = new RTCPeerConnection();
    await pc2.setRemoteDescription(offer);

    pc2.addTrack(track, stream);

    let answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    let stoppedTransceiver = pc1.getTransceivers()[0];
    let onended = new Promise(resolve => {
      stoppedTransceiver.receiver.track.onended = resolve;
    });
    stoppedTransceiver.stop();

    await onended;

    hasProps(pc1.getTransceivers(),
      [
        {
          sender: {track: {kind: "audio"}},
          receiver: {track: {kind: "audio", readyState: "ended"}},
          stopped: true,
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          direction: "sendrecv"
        }
      ]);

    let transceiver = pc1.getTransceivers()[0];

    checkThrows(() => transceiver.sender.setParameters(
                        transceiver.sender.getParameters()),
                "InvalidStateError", "setParameters on stopped transceiver");

    let stream2 = await getUserMedia({audio: true});
    let track2 = stream.getAudioTracks()[0];
    checkThrows(() => transceiver.sender.replaceTrack(track2),
                "InvalidStateError", "replaceTrack on stopped transceiver");

    checkThrows(() => transceiver.direction = "sendrecv",
                "InvalidStateError", "setDirection on stopped transceiver");

    checkThrows(() => transceiver.sender.dtmf.insertDTMF("111"),
                "InvalidStateError", "insertDTMF on stopped transceiver");

    // Shouldn't throw
    stoppedTransceiver.stop();

    offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);

    stoppedTransceiver = pc2.getTransceivers()[0];
    onended = new Promise(resolve => {
      stoppedTransceiver.receiver.track.onended = resolve;
    });

    await pc2.setRemoteDescription(offer);

    await onended;

    hasProps(pc2.getTransceivers(),
      [
        {
          sender: {track: {kind: "audio"}},
          receiver: {track: {kind: "audio", readyState: "ended"}},
          stopped: true,
          mid: null,
          currentDirection: null,
          direction: "sendrecv"
        }
      ]);

    // Shouldn't throw either
    stoppedTransceiver.stop();

    pc1.close();
    pc2.close();

    // Still shouldn't throw
    stoppedTransceiver.stop();

    stopTracks(stream);
  };

  let checkStopAfterCreateOffer = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    pc1.getTransceivers()[0].stop();

    await pc2.setRemoteDescription(offer)
    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);

    let answer = await pc2.createAnswer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    // Spec language doesn't say anything about checking whether the transceiver
    // is stopped here.
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: "sdparta_0"
        }
      ]);

    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    await negotiationNeeded(pc1);
    await iceConnected(pc1);
    await iceConnected(pc2);

    offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);
    answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkStopAfterSetLocalOffer = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer)
    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);

    pc1.getTransceivers()[0].stop();

    let answer = await pc2.createAnswer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    // Spec language doesn't say anything about checking whether the transceiver
    // is stopped here.
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: "sdparta_0"
        }
      ]);
    await negotiationNeeded(pc1);

    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    await iceConnected(pc1);
    await iceConnected(pc2);

    offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);
    answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkStopAfterSetRemoteOffer = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer)
    await pc1.setLocalDescription(offer);

    // Stop on _answerer_side now. Should take effect in answer.
    pc2.getTransceivers()[0].stop();

    let answer = await pc2.createAnswer();
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents, []);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    await pc2.setLocalDescription(answer);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkStopAfterCreateAnswer = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer)
    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);

    let answer = await pc2.createAnswer();

    // Too late for this to go in the answer. ICE should succeed.
    pc2.getTransceivers()[0].stop();

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: "sdparta_0"
        }
      ]);

    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    await negotiationNeeded(pc2);
    await iceConnected(pc1);
    await iceConnected(pc2);

    offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);
    answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkStopAfterSetLocalAnswer = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer)
    trickle(pc1, pc2);
    await pc1.setLocalDescription(offer);

    let answer = await pc2.createAnswer();

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    trickle(pc2, pc1);
    await pc2.setLocalDescription(answer);

    // ICE should succeed.
    pc2.getTransceivers()[0].stop();

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: "sdparta_0"
        }
      ]);

    await negotiationNeeded(pc2);
    await iceConnected(pc1);
    await iceConnected(pc2);

    offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);
    answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true,
          mid: null
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream);
  };

  let checkStopAfterClose = async () => {
    let pc1 = new RTCPeerConnection();
    let pc2 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();
    await pc2.setRemoteDescription(offer)
    await pc1.setLocalDescription(offer);
    let answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    pc1.close();
    pc2.close();
    await checkThrows(() => pc1.getTransceivers()[0].stop(),
                      "InvalidStateError",
                      "Stopping a transceiver on a closed PC should throw.");
    stopTracks(stream);
  };

  let checkLocalRollback = async () => {
    let pc = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc.addTrack(track, stream);

    let offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track},
          direction: "sendrecv",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    // Verify that rollback doesn't stomp things it should not
    pc.getTransceivers()[0].direction = "sendonly";
    let stream2 = await getUserMedia({audio: true});
    let track2 = stream2.getAudioTracks()[0];
    await pc.getTransceivers()[0].sender.replaceTrack(track2);

    await pc.setLocalDescription({type: "rollback"});

    hasProps(pc.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: track2},
          direction: "sendonly",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    // Make sure stop() isn't rolled back either.
    offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    pc.getTransceivers()[0].stop();
    await pc.setLocalDescription({type: "rollback"});

    hasProps(pc.getTransceivers(), [{ stopped: true }]);

    stopTracks(stream);
    pc.close();
  };

  let checkRemoteRollback = async () => {
    let pc1 = new RTCPeerConnection();

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

    let offer = await pc1.createOffer();

    let pc2 = new RTCPeerConnection();
    await pc2.setRemoteDescription(offer);

    let removedTransceiver = pc2.getTransceivers()[0];

    let onended = new Promise(resolve => {
      removedTransceiver.receiver.track.onended = resolve;
    });

    await pc2.setRemoteDescription({type: "rollback"});

    // Transceiver should be _gone_
    hasProps(pc2.getTransceivers(), []);

    hasProps(removedTransceiver,
      {
        stopped: true,
        mid: null,
        currentDirection: null
      }
    );

    await onended;

    hasProps(removedTransceiver,
      {
        receiver: {track: {readyState: "ended"}},
        stopped: true,
        mid: null,
        currentDirection: null
      }
    );

    // Setting the same offer again should do the same thing as before
    await pc2.setRemoteDescription(offer);
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    // Give pc2 a track with replaceTrack
    let stream2 = await getUserMedia({audio: true});
    let track2 = stream2.getAudioTracks()[0];
    await pc2.getTransceivers()[0].sender.replaceTrack(track2);
    pc2.getTransceivers()[0].direction = "sendrecv";
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: track2},
          direction: "sendrecv",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    await pc2.setRemoteDescription({type: "rollback"});

    // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
    // nor does setDirection.
    hasProps(pc2.getTransceivers(), []);

    // Setting the same offer for a _third_ time should do the same thing
    await pc2.setRemoteDescription(offer);
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    // We should be able to add the same track again
    pc2.addTrack(track2, stream2);
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: track2},
          direction: "sendrecv",
          mid: "sdparta_0", // Firefox-specific
          currentDirection: null,
          stopped: false
        }
      ]);

    await pc2.setRemoteDescription({type: "rollback"});
    // Transceiver should _not_ be gone this time, because addTrack touched it.
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: track2},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: false
        }
      ]);

    // Complete negotiation so we can test interactions with transceiver.stop()
    await pc1.setLocalDescription(offer);

    // After all this SRD/rollback, we should still get the track event
    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    let answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);

    // Make sure all this rollback hasn't messed up the signaling
    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents,
      [
        {
          track: pc1.getTransceivers()[0].receiver.track,
          streams: [{id: stream2.id}]
        }
      ]);
    hasProps(pc1.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track},
          direction: "sendrecv",
          mid: "sdparta_0",
          currentDirection: "sendrecv",
          stopped: false
        }
      ]);

    // Don't bother waiting for ICE and such

    // Check to see whether rolling back a remote track removal works
    pc1.getTransceivers()[0].direction = "recvonly";
    offer = await pc1.createOffer();

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents, []);

    trackEvents =
      await setRemoteDescriptionReturnTrackEvents(pc2, {type: "rollback"});
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[0].receiver.track,
          streams: [{id: stream.id}]
        }
      ]);

    // Check to see that stop() cannot be rolled back
    pc1.getTransceivers()[0].stop();
    offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer);
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: track2},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: true
        }
      ]);

    // stop() cannot be rolled back!
    await pc2.setRemoteDescription({type: "rollback"});
    hasProps(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: {kind: "audio"}},
          direction: "sendrecv",
          mid: null,
          currentDirection: null,
          stopped: true
        }
      ]);

    pc1.close();
    pc2.close();
    stopTracks(stream, stream2);
  };

  let checkMsectionReuse = async () => {
    // Use max-compat to make it easier to check for disabled m-sections
    let pc1 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
    let pc2 = new RTCPeerConnection({ bundlePolicy: "max-compat" });

    let stream = await getUserMedia({audio: true});
    let track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

    let offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);

    // answerer stops transceiver to reject m-section
    pc2.getTransceivers()[0].stop();

    let answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    hasProps(pc1.getTransceivers(),
      [
        {
          mid: null,
          currentDirection: null,
          stopped: true
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {
          mid: null,
          currentDirection: null,
          stopped: true
        }
      ]);

    // Check that m-section is reused on both ends
    let stream2 = await getUserMedia({audio: true});
    let track2 = stream2.getAudioTracks()[0];

    pc1.addTrack(track2, stream2);
    offer = await pc1.createOffer();
    is(offer.sdp.match(/m=/g).length, 1, "Exactly one m-line in offer, because it was reused");
    hasProps(pc1.getTransceivers(),
      [
        {
          stopped: true
        },
        {
          sender: {track: track2}
        }
      ]);


    pc2.addTrack(track, stream);
    offer = await pc2.createOffer();
    is(offer.sdp.match(/m=/g).length, 1, "Exactly one m-line in offer, because it was reused");
    hasProps(pc2.getTransceivers(),
      [
        {
          stopped: true
        },
        {
          sender: {track}
        }
      ]);

    await pc2.setLocalDescription(offer);
    await pc1.setRemoteDescription(offer);
    answer = await pc1.createAnswer();
    await pc1.setLocalDescription(answer);
    await pc2.setRemoteDescription(answer);
    hasProps(pc1.getTransceivers(),
      [
        {},
        {
          sender: {track: track2},
          currentDirection: "sendrecv"
        }
      ]);

    hasProps(pc2.getTransceivers(),
      [
        {},
        {
          sender: {track},
          currentDirection: "sendrecv"
        }
      ]);

    // stop the transceiver, and add a track. Verify that we don't reuse
    // prematurely in our offer. (There should be one rejected m-section, and a
    // new one for the new track)
    pc1.getTransceivers()[1].stop();
    let stream3 = await getUserMedia({audio: true});
    let track3 = stream3.getAudioTracks()[0];
    pc1.addTrack(track3, stream3);
    offer = await pc1.createOffer();
    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer, because it is too early to reuse");
    is(offer.sdp.match(/m=audio 0 /g).length, 1, "One m-line is rejected");

    await pc1.setLocalDescription(offer);

    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    hasProps(trackEvents,
      [
        {
          track: pc2.getTransceivers()[2].receiver.track,
          streams: [{id: stream3.id}]
        }
      ]);

    answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);

    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    hasProps(trackEvents, []);

    hasProps(pc2.getTransceivers(),
      [
        {},
        {
          stopped: true
        },
        {
          mid: "sdparta_1", // Firefox-specific
          sender: {track: null},
          currentDirection: "recvonly"
        }
      ]);

    pc2.addTrack(track3, stream3);
    // There are two ways to handle this new track; reuse the recvonly
    // transceiver created above, or create a new transceiver and reuse the
    // disabled m-section. We're supposed to do the former.
    offer = await pc2.createOffer();
    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
    is(offer.sdp.match(/m=audio 0 /g).length, 1, "One m-line is rejected, because the other was used");

    hasProps(pc2.getTransceivers(),
      [
        {},
        {
          stopped: true
        },
        {
          mid: "sdparta_1", // Firefox-specific
          sender: {track: track3},
          currentDirection: "recvonly",
          direction: "sendrecv"
        }
      ]);

    // Add _another_ track; this should reuse the disabled m-section
    let stream4 = await getUserMedia({audio: true});
    let track4 = stream4.getAudioTracks()[0];
    pc2.addTrack(track4, stream4);
    offer = await pc2.createOffer();
    await pc2.setLocalDescription(offer);
    hasProps(pc2.getTransceivers(),
      [
        {}, {},
        {
          mid: "sdparta_1", // Firefox-specific
        },
        {
          sender: {track: track4},
          mid: "sdparta_0" // Firefox-specific
        }
      ]);
    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer, because m-section was reused");
    is(offer.sdp.match(/m=audio 0 /g), null, "No rejected m-line, because it was reused");

    pc1.close();
    pc2.close();
    stopTracks(stream, stream2, stream3, stream4);
  };

  runNetworkTest(async () => {
    await checkAddTransceiverNoTrack();
    await checkAddTransceiverWithTrack();
    await checkAddTransceiverWithAddTrack();
    await checkAddTransceiverWithDirection();
    await checkAddTransceiverWithStream();
    await checkAddTransceiverWithOfferToReceive(["audio"]);
    await checkAddTransceiverWithOfferToReceive(["video"]);
    await checkAddTransceiverWithOfferToReceive(["audio", "video"]);
    await checkAddTransceiverWithSetRemoteOfferSending();
    await checkAddTransceiverWithSetRemoteOfferNoSend();
    await checkAddTransceiverBadKind();
    await checkSetDirection();
    await checkCurrentDirection();
    await checkSendrecvWithNoSendTrack();
    await checkSendrecvWithTracklessStream();
    await checkAddTransceiverNoTrackDoesntPair();
    await checkAddTransceiverWithTrackDoesntPair();
    await checkAddTransceiverThenReplaceTrackDoesntPair();
    await checkAddTransceiverThenAddTrackPairs();
    await checkAddTrackPairs();
    await checkReplaceTrackNullDoesntPreventPairing();
    await checkRemoveAndReadd();
    await checkMute();
    await checkOnAddStream();
    await checkStop();
    await checkStopAfterCreateOffer();
    await checkStopAfterSetLocalOffer();
    await checkStopAfterSetRemoteOffer();
    await checkStopAfterCreateAnswer();
    await checkStopAfterSetLocalAnswer();
    await checkStopAfterClose();
    await checkLocalRollback();
    await checkRemoteRollback();
    await checkMsectionReuse();
    return SimpleTest.finish();
  });
</script>
</pre>
</body>
</html>