Raw File
RTCRtpTransceiver.https.html
<!doctype html>
<meta charset=utf-8>
<title>RTCRtpTransceiver</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script>
  'use strict';

  const checkThrows = async (func, exceptionName, description) => {
    try {
      await func();
      assert_true(false, description + " throws " + exceptionName);
    } catch (e) {
      assert_equals(e.name, exceptionName, description + " throws " + exceptionName);
    }
  };

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

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

    target.addEventListener(name, handler);

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

    return {finish: finishCollecting};
  };

  const collectAddTrackEvents = stream => {
    const checkEvent = e => {
      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
      assert_true(stream.getTracks().includes(e.track),
        "track in addtrack event is in the stream");
    };
    return collectEvents(stream, "addtrack", checkEvent);
  };

  const collectRemoveTrackEvents = stream => {
    const checkEvent = e => {
      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
      assert_true(!stream.getTracks().includes(e.track),
        "track in removetrack event is not in the stream");
    };
    return collectEvents(stream, "removetrack", checkEvent);
  };

  const collectTrackEvents = pc => {
    const checkEvent = e => {
      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
      assert_true(e.receiver instanceof RTCRtpReceiver, "Receiver is set on event");
      assert_true(e.transceiver instanceof RTCRtpTransceiver, "Transceiver is set on event");
      assert_true(Array.isArray(e.streams), "Streams is set on event");
      e.streams.forEach(stream => {
        assert_true(stream.getTracks().includes(e.track),
           "Each stream in event contains the track");
      });
      assert_equals(e.receiver, e.transceiver.receiver,
                    "Receiver belongs to transceiver");
      assert_equals(e.track, e.receiver.track,
                    "Track belongs to receiver");
    };

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

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

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

  const trickle = (t, pc1, pc2) => {
    pc1.onicecandidate = t.step_func(async e => {
      if (e.candidate) {
        try {
          await pc2.addIceCandidate(e.candidate);
        } catch (e) {
          assert_true(false, "addIceCandidate threw error: " + e.name);
        }
      }
    });
  };

  const iceConnected = pc => {
    return new Promise((resolve, reject) => {
      const iceCheck = () => {
        if (pc.iceConnectionState == "connected") {
          assert_true(true, "ICE connected");
          resolve();
        }

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

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

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

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

  const gotMuteEvent = async track => {
    await new Promise(r => track.addEventListener("mute", r, {once: true}));

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

  const gotUnmuteEvent = async track => {
    await new Promise(r => track.addEventListener("unmute", r, {once: true}));

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

  // comparable() - produces copy of object that is JSON comparable.
  // o = original object (required)
  // t = template of what to examine. Useful if o is non-enumerable (optional)

  const comparable = (o, t = o) => {
    if (typeof o != 'object' || !o) {
      return o;
    }
    if (Array.isArray(t) && Array.isArray(o)) {
      return o.map((n, i) => comparable(n, t[i]));
    }
    return Object.keys(t).sort()
        .reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
  };

  const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");

  const hasProps = (observed, expected) => {
    const observable = comparable(observed, expected);
    assert_equals(stripKeyQuotes(JSON.stringify(observable)),
       stripKeyQuotes(JSON.stringify(comparable(expected))));
  };

  const hasPropsAndUniqueMids = (observed, expected) => {
    hasProps(observed, expected);

    const mids = [];
    observed.forEach((transceiver, i) => {
      if (!("mid" in expected[i])) {
        assert_not_equals(transceiver.mid, null);
        assert_equals(typeof transceiver.mid, "string");
      }
      if (transceiver.mid) {
        assert_false(mids.includes(transceiver.mid), "mid must be unique");
        mids.push(transceiver.mid);
      }
    });
  };

  const checkAddTransceiverNoTrack = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

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

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

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

  const checkAddTransceiverWithTrack = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
    t.add_cleanup(() => stopTracks(stream));
    const audio = stream.getAudioTracks()[0];
    const 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
        }
      ]);
  };

  const checkAddTransceiverWithAddTrack = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
    t.add_cleanup(() => stopTracks(stream));
    const audio = stream.getAudioTracks()[0];
    const 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
        }
      ]);
  };

  const checkAddTransceiverWithDirection = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

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

  const checkAddTransceiverWithSetRemoteOfferSending = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTransceiver(track, {streams: [stream]});

    const offer = await pc1.createOffer();

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


    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          currentDirection: null,
          stopped: false
        }
      ]);
  };

  const checkAddTransceiverWithSetRemoteOfferNoSend = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTransceiver(track);
    pc1.getTransceivers()[0].direction = "recvonly";

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

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

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

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

  const checkMsidNoTrackId = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    const offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    // Remove track-id from msid
    offer.sdp = offer.sdp.replace(/(a=msid:[^ \t]+).*\r\n/g, "$1\r\n");
    assert_true(offer.sdp.includes(`a=msid:${stream.id}\r\n`));
    await pc2.setRemoteDescription(offer);
    const answer = await pc2.createAnswer();
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);
  };

  const checkNoMidOffer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

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

    // Remove mid attr
    offer.sdp = offer.sdp.replace("a=mid:", "a=unknownattr:");
    await pc2.setRemoteDescription(offer);

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          currentDirection: null,
          stopped: false
        }
      ]);

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

  const checkAddTransceiverNoTrackDoesntPair = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

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

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

  const checkAddTransceiverWithTrackDoesntPair = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    pc1.addTransceiver("audio");

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc2.addTransceiver(track);

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

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

  const checkAddTransceiverThenReplaceTrackDoesntPair = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc2.getTransceivers()[0].sender.replaceTrack(track);

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

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

  const checkAddTransceiverThenAddTrackPairs = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    pc1.addTransceiver("audio");
    pc2.addTransceiver("audio");

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {sender: {track}}
      ]);
  };

  const checkAddTrackPairs = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    pc1.addTransceiver("audio");

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {sender: {track}}
      ]);
  };

  const checkReplaceTrackNullDoesntPreventPairing = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    pc1.addTransceiver("audio");

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc2.addTrack(track, stream);
    pc2.getTransceivers()[0].sender.replaceTrack(null);

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {sender: {track: null}}
      ]);
  };

  const checkRemoveAndReadd = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const 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"}
      ]);
  };

  const checkAddTrackExistingTransceiverThenRemove = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    pc.addTransceiver("audio");
    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    const audio = stream.getAudioTracks()[0];
    let sender = pc.addTrack(audio, stream);
    pc.removeTrack(sender);

    // Cause transceiver to be associated
    await pc.setLocalDescription(await pc.createOffer());

    // Make sure add/remove works still
    sender = pc.addTrack(audio, stream);
    pc.removeTrack(sender);

    stopTracks(stream);
  };

  const checkRemoveTrackNegotiation = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());
    const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
    t.add_cleanup(() => stopTracks(stream));
    const audio = stream.getAudioTracks()[0];
    pc1.addTrack(audio, stream);
    const video = stream.getVideoTracks()[0];
    pc1.addTrack(video, stream);
    // We want both a sendrecv and sendonly transceiver to test that the
    // appropriate direction changes happen.
    pc1.getTransceivers()[1].direction = "sendonly";

    let offer = await pc1.createOffer();

    // Get a reference to the stream
    let trackEventCollector = collectTrackEvents(pc2);
    await pc2.setRemoteDescription(offer);
    let pc2TrackEvents = trackEventCollector.finish();
    hasProps(pc2TrackEvents,
      [
        {streams: [{id: stream.id}]},
        {streams: [{id: stream.id}]}
      ]);
    const receiveStream = pc2TrackEvents[0].streams[0];

    // Verify that rollback causes onremovetrack to fire for the added tracks
    let removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    await pc2.setRemoteDescription({type: "rollback"});
    let removedtracks = removetrackEventCollector.finish().map(e => e.track);
    assert_equals(removedtracks.length, 2,
                  "Rollback should have removed two tracks");
    assert_true(removedtracks.includes(pc2TrackEvents[0].track),
                "First track should be removed");
    assert_true(removedtracks.includes(pc2TrackEvents[1].track),
                "Second track should be removed");

    offer = await pc1.createOffer();

    let addtrackEventCollector = collectAddTrackEvents(receiveStream);
    trackEventCollector = collectTrackEvents(pc2);
    await pc2.setRemoteDescription(offer);
    pc2TrackEvents = trackEventCollector.finish();
    let addedtracks = addtrackEventCollector.finish().map(e => e.track);
    assert_equals(addedtracks.length, 2,
      "pc2.setRemoteDescription(offer) should've added 2 tracks to receive stream");
    assert_true(addedtracks.includes(pc2TrackEvents[0].track),
                "First track should be added");
    assert_true(addedtracks.includes(pc2TrackEvents[1].track),
                "Second track should be added");

    await pc1.setLocalDescription(offer);
    let answer = await pc2.createAnswer();
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);
    pc1.removeTrack(pc1.getSenders()[0]);

    hasProps(pc1.getSenders(),
      [
        {track: null},
        {track: video}
      ]);

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

    await negotiationNeeded(pc1);

    pc1.removeTrack(pc1.getSenders()[1]);

    hasProps(pc1.getSenders(),
      [
        {track: null},
        {track: null}
      ]);

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

    // pc1 as offerer
    offer = await pc1.createOffer();

    removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    await pc2.setRemoteDescription(offer);
    removedtracks = removetrackEventCollector.finish().map(e => e.track);
    assert_equals(removedtracks.length, 2, "Should have two removed tracks");
    assert_true(removedtracks.includes(pc2TrackEvents[0].track),
                "First track should be removed");
    assert_true(removedtracks.includes(pc2TrackEvents[1].track),
                "Second track should be removed");

    addtrackEventCollector = collectAddTrackEvents(receiveStream);
    await pc2.setRemoteDescription({type: "rollback"});
    addedtracks = addtrackEventCollector.finish().map(e => e.track);
    assert_equals(addedtracks.length, 2, "Rollback should have added two tracks");

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

    removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    await pc2.setRemoteDescription(answer);
    removedtracks = removetrackEventCollector.finish().map(e => e.track);
    assert_equals(removedtracks.length, 2, "Should have two removed tracks");

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

  const checkSetDirection = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    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"}]);
  };

  const checkCurrentDirection = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const 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"}]);
  };

  const checkSendrecvWithNoSendTrack = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTransceiver("audio");
    pc1.getTransceivers()[0].direction = "sendrecv";
    pc2.addTrack(track, stream);

    const offer = await pc1.createOffer();

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

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

    const 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(t, pc2, pc1);
    await pc2.setLocalDescription(answer);

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

  const checkSendrecvWithTracklessStream = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

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

    const offer = await pc1.createOffer();

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

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

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

    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());
    const stream2 = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
    t.add_cleanup(() => stopTracks(stream2));
    const audio2 = stream2.getAudioTracks()[0];
    pc2.addTrack(audio2, stream2);
    const countMuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "mute");
    const countUnmuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "unmute");

    const video2 = stream2.getVideoTracks()[0];
    pc2.addTrack(video2, stream2);
    const countMuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "mute");
    const 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(t, pc1, pc2);
    await pc1.setLocalDescription(offer);
    let answer = await pc2.createAnswer();
    await pc1.setRemoteDescription(answer);
    trickle(t, 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();
    const gotMuteAudio1 = gotMuteEvent(pc1.getTransceivers()[0].receiver.track);
    const 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();
    const gotMuteAudio2 = gotMuteEvent(pc2.getTransceivers()[0].receiver.track);
    const gotMuteVideo2 = gotMuteEvent(pc2.getTransceivers()[1].receiver.track);
    await pc2.setRemoteDescription(offer);
    await gotMuteAudio2;
    await gotMuteVideo2;
    await pc1.setLocalDescription(offer);
    answer = await pc2.createAnswer();
    await pc1.setRemoteDescription(answer);
    await pc2.setLocalDescription(answer);

    // 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 new Promise(r => t.step_timeout(r, 100));

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

  const checkStop = async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

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

    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());
    await pc2.setRemoteDescription(offer);

    pc2.addTrack(track, stream);

    const 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();
    assert_equals(pc1.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver');
    assert_equals(pc1.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver');

    await onended;

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

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

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

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

    checkThrows(() => transceiver.direction = "sendrecv",
                "InvalidStateError", "set direction 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();
  };

  const checkStopAfterCreateOffer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const 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(t, pc1, pc2);
    await pc1.setLocalDescription(offer);

    let answer = await pc2.createAnswer();
    const 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}]
        }
      ]);

    hasPropsAndUniqueMids(pc1.getTransceivers(),
      [
        {
          stopped: true,
        }
      ]);

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

  const checkStopAfterSetLocalOffer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

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

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

    let answer = await pc2.createAnswer();
    const 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}]
        }
      ]);

    hasPropsAndUniqueMids(pc1.getTransceivers(),
      [
        {
          stopped: true,
        }
      ]);
    await negotiationNeeded(pc1);

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

  const checkStopAfterSetRemoteOffer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    const 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();

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

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

    await pc2.setLocalDescription(answer);
  };

  const checkStopAfterCreateAnswer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

    await pc2.setRemoteDescription(offer)
    trickle(t, 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();

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          stopped: true,
        }
      ]);

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

  const checkStopAfterSetLocalAnswer = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

    let offer = await pc1.createOffer();

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

    let answer = await pc2.createAnswer();

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

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

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          stopped: true,
        }
      ]);

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

  const checkStopAfterClose = async t => {
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    t.add_cleanup(() => pc2.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);

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

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

  const checkLocalRollback = async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc.addTrack(track, stream);

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

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

    // Verify that rollback doesn't stomp things it should not
    pc.getTransceivers()[0].direction = "sendonly";
    const stream2 = await navigator.mediaDevices.getUserMedia({audio: true});
    const 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 }]);
  };

  const checkRollbackAndSetRemoteOfferWithDifferentType = async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());

    const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(audioStream));
    const audioTrack = audioStream.getAudioTracks()[0];
    pc1.addTrack(audioTrack, audioStream);

    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
    t.add_cleanup(() => stopTracks(videoStream));
    const videoTrack = videoStream.getVideoTracks()[0];
    pc2.addTrack(videoTrack, videoStream);

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

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

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

    await offerAnswer(pc2, pc1);

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

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "video"}},
          sender: {track: videoTrack},
          direction: "sendrecv",
          currentDirection: "sendonly",
          stopped: false
        }
      ]);

    await offerAnswer(pc1, pc2);
  };

  const checkRemoteRollback = async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const track = stream.getAudioTracks()[0];
    pc1.addTrack(track, stream);

    let offer = await pc1.createOffer();

    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());
    await pc2.setRemoteDescription(offer);

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

    const 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);
    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          receiver: {track: {kind: "audio"}},
          sender: {track: null},
          direction: "recvonly",
          currentDirection: null,
          stopped: false
        }
      ]);

    const mid0 = pc2.getTransceivers()[0].mid;

    // Give pc2 a track with replaceTrack
    const stream2 = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream2));
    const 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: mid0,
          currentDirection: null,
          stopped: false
        }
      ]);

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

    // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
    // nor does setting direction.
    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: mid0,
          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: mid0,
          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}]
        }
      ]);

    const 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: mid0,
          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
        }
      ]);
  };

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

    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream));
    const 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
    const stoppedMid0 = pc2.getTransceivers()[0].mid;
    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
    const stream2 = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream2));
    const track2 = stream2.getAudioTracks()[0];

    pc1.addTrack(track2, stream2);
    offer = await pc1.createOffer();
    assert_equals(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}
        }
      ]);

    assert_not_equals(pc1.getTransceivers()[1].mid, stoppedMid0);

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

    assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid0);

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

    const mid0 = pc1.getTransceivers()[1].mid;

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

    // 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)
    const stoppedMid1 = pc1.getTransceivers()[1].mid;
    pc1.getTransceivers()[1].stop();
    const stream3 = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream3));
    const track3 = stream3.getAudioTracks()[0];
    pc1.addTrack(track3, stream3);
    offer = await pc1.createOffer();
    assert_equals(offer.sdp.match(/m=/g).length, 2,
                  "Exactly 2 m-lines in offer, because it is too early to reuse");
    assert_equals(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, []);

    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
          mid: null
        },
        {
          mid: null,
          stopped: true
        },
        {
          sender: {track: null},
          currentDirection: "recvonly"
        }
      ]);

    // Verify that we don't reuse the mid from the stopped transceiver
    const mid1 = pc2.getTransceivers()[2].mid;
    assert_not_equals(mid1, stoppedMid1);

    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();
    assert_equals(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
    assert_equals(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: mid1,
          sender: {track: track3},
          currentDirection: "recvonly",
          direction: "sendrecv"
        }
      ]);

    // Add _another_ track; this should reuse the disabled m-section
    const stream4 = await navigator.mediaDevices.getUserMedia({audio: true});
    t.add_cleanup(() => stopTracks(stream4));
    const track4 = stream4.getAudioTracks()[0];
    pc2.addTrack(track4, stream4);
    offer = await pc2.createOffer();
    await pc2.setLocalDescription(offer);
    hasPropsAndUniqueMids(pc2.getTransceivers(),
      [
        {
           mid: null
        },
        {
           mid: null
        },
        {
          mid: mid1
        },
        {
          sender: {track: track4},
        }
      ]);

    // Fourth transceiver should have a new mid
    assert_not_equals(pc2.getTransceivers()[3].mid, stoppedMid0);
    assert_not_equals(pc2.getTransceivers()[3].mid, stoppedMid1);

    assert_equals(offer.sdp.match(/m=/g).length, 2,
                  "Exactly 2 m-lines in offer, because m-section was reused");
    assert_equals(offer.sdp.match(/m=audio 0 /g), null,
                  "No rejected m-line, because it was reused");
  };

const tests = [
  checkAddTransceiverNoTrack,
  checkAddTransceiverWithTrack,
  checkAddTransceiverWithAddTrack,
  checkAddTransceiverWithDirection,
  checkMsidNoTrackId,
  checkAddTransceiverWithSetRemoteOfferSending,
  checkAddTransceiverWithSetRemoteOfferNoSend,
  checkAddTransceiverBadKind,
  checkNoMidOffer,
  checkSetDirection,
  checkCurrentDirection,
  checkSendrecvWithNoSendTrack,
  checkSendrecvWithTracklessStream,
  checkAddTransceiverNoTrackDoesntPair,
  checkAddTransceiverWithTrackDoesntPair,
  checkAddTransceiverThenReplaceTrackDoesntPair,
  checkAddTransceiverThenAddTrackPairs,
  checkAddTrackPairs,
  checkReplaceTrackNullDoesntPreventPairing,
  checkRemoveAndReadd,
  checkAddTrackExistingTransceiverThenRemove,
  checkRemoveTrackNegotiation,
  checkMute,
  checkStop,
  checkStopAfterCreateOffer,
  checkStopAfterSetLocalOffer,
  checkStopAfterSetRemoteOffer,
  checkStopAfterCreateAnswer,
  checkStopAfterSetLocalAnswer,
  checkStopAfterClose,
  checkLocalRollback,
  checkRollbackAndSetRemoteOfferWithDifferentType,
  checkRemoteRollback,
  checkMsectionReuse
].forEach(test => promise_test(test, test.name));

</script>
back to top