tor-browser

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

RTCRtpTransceiver.https.html (72866B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <meta name="timeout" content="long">
      4 <title>RTCRtpTransceiver</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="RTCPeerConnection-helper.js"></script>
      8 <script>
      9  'use strict';
     10 
     11  const checkThrows = async (func, exceptionName, description) => {
     12    try {
     13      await func();
     14      assert_true(false, description + " throws " + exceptionName);
     15    } catch (e) {
     16      assert_equals(e.name, exceptionName, description + " throws " + exceptionName);
     17    }
     18  };
     19 
     20  const stopTracks = (...streams) => {
     21    streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
     22  };
     23 
     24  const collectEvents = (target, name, check) => {
     25    const events = [];
     26    const handler = e => {
     27      check(e);
     28      events.push(e);
     29    };
     30 
     31    target.addEventListener(name, handler);
     32 
     33    const finishCollecting = () => {
     34      target.removeEventListener(name, handler);
     35      return events;
     36    };
     37 
     38    return {finish: finishCollecting};
     39  };
     40 
     41  const collectAddTrackEvents = stream => {
     42    const checkEvent = e => {
     43      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
     44      assert_true(stream.getTracks().includes(e.track),
     45        "track in addtrack event is in the stream");
     46    };
     47    return collectEvents(stream, "addtrack", checkEvent);
     48  };
     49 
     50  const collectRemoveTrackEvents = stream => {
     51    const checkEvent = e => {
     52      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
     53      assert_true(!stream.getTracks().includes(e.track),
     54        "track in removetrack event is not in the stream");
     55    };
     56    return collectEvents(stream, "removetrack", checkEvent);
     57  };
     58 
     59  const collectTrackEvents = pc => {
     60    const checkEvent = e => {
     61      assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
     62      assert_true(e.receiver instanceof RTCRtpReceiver, "Receiver is set on event");
     63      assert_true(e.transceiver instanceof RTCRtpTransceiver, "Transceiver is set on event");
     64      assert_true(Array.isArray(e.streams), "Streams is set on event");
     65      e.streams.forEach(stream => {
     66        assert_true(stream.getTracks().includes(e.track),
     67           "Each stream in event contains the track");
     68      });
     69      assert_equals(e.receiver, e.transceiver.receiver,
     70                    "Receiver belongs to transceiver");
     71      assert_equals(e.track, e.receiver.track,
     72                    "Track belongs to receiver");
     73    };
     74 
     75    return collectEvents(pc, "track", checkEvent);
     76  };
     77 
     78  const setRemoteDescriptionReturnTrackEvents = async (pc, desc) => {
     79    const trackEventCollector = collectTrackEvents(pc);
     80    await pc.setRemoteDescription(desc);
     81    return trackEventCollector.finish();
     82  };
     83 
     84  const offerAnswer = async (offerer, answerer) => {
     85    const offer = await offerer.createOffer();
     86    await answerer.setRemoteDescription(offer);
     87    await offerer.setLocalDescription(offer);
     88    const answer = await answerer.createAnswer();
     89    await offerer.setRemoteDescription(answer);
     90    await answerer.setLocalDescription(answer);
     91  };
     92 
     93  const trickle = (t, pc1, pc2) => {
     94    pc1.onicecandidate = t.step_func(async e => {
     95      try {
     96        await pc2.addIceCandidate(e.candidate);
     97      } catch (e) {
     98        assert_true(false, "addIceCandidate threw error: " + e.name);
     99      }
    100    });
    101  };
    102 
    103  const iceConnected = pc => {
    104    return new Promise((resolve, reject) => {
    105      const iceCheck = () => {
    106        if (pc.iceConnectionState == "connected") {
    107          assert_true(true, "ICE connected");
    108          resolve();
    109        }
    110 
    111        if (pc.iceConnectionState == "failed") {
    112          assert_true(false, "ICE failed");
    113          reject();
    114        }
    115      };
    116 
    117      iceCheck();
    118      pc.oniceconnectionstatechange = iceCheck;
    119    });
    120  };
    121 
    122  const negotiationNeeded = pc => {
    123    return new Promise(resolve => pc.onnegotiationneeded = resolve);
    124  };
    125 
    126  const countEvents = (target, name) => {
    127    const result = {count: 0};
    128    target.addEventListener(name, e => result.count++);
    129    return result;
    130  };
    131 
    132  const gotMuteEvent = async track => {
    133    await new Promise(r => track.addEventListener("mute", r, {once: true}));
    134 
    135    assert_true(track.muted, "track should be muted after onmute");
    136  };
    137 
    138  const gotUnmuteEvent = async track => {
    139    await new Promise(r => track.addEventListener("unmute", r, {once: true}));
    140 
    141    assert_true(!track.muted, "track should not be muted after onunmute");
    142  };
    143 
    144  // comparable() - produces copy of object that is JSON comparable.
    145  // o = original object (required)
    146  // t = template of what to examine. Useful if o is non-enumerable (optional)
    147 
    148  const comparable = (o, t = o) => {
    149    if (typeof o != 'object' || !o) {
    150      return o;
    151    }
    152    if (Array.isArray(t) && Array.isArray(o)) {
    153      return o.map((n, i) => comparable(n, t[i]));
    154    }
    155    return Object.keys(t).sort()
    156        .reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
    157  };
    158 
    159  const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");
    160 
    161  const hasProps = (observed, expected) => {
    162    const observable = comparable(observed, expected);
    163    assert_equals(stripKeyQuotes(JSON.stringify(observable)),
    164       stripKeyQuotes(JSON.stringify(comparable(expected))));
    165  };
    166 
    167  const hasPropsAndUniqueMids = (observed, expected) => {
    168    hasProps(observed, expected);
    169 
    170    const mids = [];
    171    observed.forEach((transceiver, i) => {
    172      if (!("mid" in expected[i])) {
    173        assert_not_equals(transceiver.mid, null);
    174        assert_equals(typeof transceiver.mid, "string");
    175      }
    176      if (transceiver.mid) {
    177        assert_false(mids.includes(transceiver.mid), "mid must be unique");
    178        mids.push(transceiver.mid);
    179      }
    180    });
    181  };
    182 
    183  const checkAddTransceiverNoTrack = async t => {
    184    const pc = new RTCPeerConnection();
    185    t.add_cleanup(() => pc.close());
    186 
    187    hasProps(pc.getTransceivers(), []);
    188 
    189    pc.addTransceiver("audio");
    190    pc.addTransceiver("video");
    191 
    192    hasProps(pc.getTransceivers(),
    193      [
    194        {
    195          receiver: {track: {kind: "audio", readyState: "live", muted: true}},
    196          sender: {track: null},
    197          direction: "sendrecv",
    198          mid: null,
    199          currentDirection: null,
    200        },
    201        {
    202          receiver: {track: {kind: "video", readyState: "live", muted: true}},
    203          sender: {track: null},
    204          direction: "sendrecv",
    205          mid: null,
    206          currentDirection: null,
    207        }
    208      ]);
    209  };
    210 
    211  const checkAddTransceiverWithTrack = async t => {
    212    const pc = new RTCPeerConnection();
    213    t.add_cleanup(() => pc.close());
    214 
    215    const stream = await getNoiseStream({audio: true, video: true});
    216    t.add_cleanup(() => stopTracks(stream));
    217    const audio = stream.getAudioTracks()[0];
    218    const video = stream.getVideoTracks()[0];
    219 
    220    pc.addTransceiver(audio);
    221    pc.addTransceiver(video);
    222 
    223    hasProps(pc.getTransceivers(),
    224      [
    225        {
    226          receiver: {track: {kind: "audio"}},
    227          sender: {track: audio},
    228          direction: "sendrecv",
    229          mid: null,
    230          currentDirection: null,
    231        },
    232        {
    233          receiver: {track: {kind: "video"}},
    234          sender: {track: video},
    235          direction: "sendrecv",
    236          mid: null,
    237          currentDirection: null,
    238        }
    239      ]);
    240  };
    241 
    242  const checkAddTransceiverWithAddTrack = async t => {
    243    const pc = new RTCPeerConnection();
    244    t.add_cleanup(() => pc.close());
    245 
    246    const stream = await getNoiseStream({audio: true, video: true});
    247    t.add_cleanup(() => stopTracks(stream));
    248    const audio = stream.getAudioTracks()[0];
    249    const video = stream.getVideoTracks()[0];
    250 
    251    pc.addTrack(audio, stream);
    252    pc.addTrack(video, stream);
    253 
    254    hasProps(pc.getTransceivers(),
    255      [
    256        {
    257          receiver: {track: {kind: "audio"}},
    258          sender: {track: audio},
    259          direction: "sendrecv",
    260          mid: null,
    261          currentDirection: null,
    262        },
    263        {
    264          receiver: {track: {kind: "video"}},
    265          sender: {track: video},
    266          direction: "sendrecv",
    267          mid: null,
    268          currentDirection: null,
    269        }
    270      ]);
    271  };
    272 
    273  const checkAddTransceiverWithDirection = async t => {
    274    const pc = new RTCPeerConnection();
    275    t.add_cleanup(() => pc.close());
    276 
    277    pc.addTransceiver("audio", {direction: "recvonly"});
    278    pc.addTransceiver("video", {direction: "recvonly"});
    279 
    280    hasProps(pc.getTransceivers(),
    281      [
    282        {
    283          receiver: {track: {kind: "audio"}},
    284          sender: {track: null},
    285          direction: "recvonly",
    286          mid: null,
    287          currentDirection: null,
    288        },
    289        {
    290          receiver: {track: {kind: "video"}},
    291          sender: {track: null},
    292          direction: "recvonly",
    293          mid: null,
    294          currentDirection: null,
    295        }
    296      ]);
    297  };
    298 
    299  const checkAddTransceiverWithSetRemoteOfferSending = async t => {
    300    const pc1 = new RTCPeerConnection();
    301    const pc2 = new RTCPeerConnection();
    302    t.add_cleanup(() => pc1.close());
    303    t.add_cleanup(() => pc2.close());
    304 
    305    const stream = await getNoiseStream({audio: true});
    306    t.add_cleanup(() => stopTracks(stream));
    307    const track = stream.getAudioTracks()[0];
    308    pc1.addTransceiver(track, {streams: [stream]});
    309 
    310    const offer = await pc1.createOffer();
    311 
    312    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    313    hasProps(trackEvents,
    314      [
    315        {
    316          track: pc2.getTransceivers()[0].receiver.track,
    317          streams: [{id: stream.id}]
    318        }
    319      ]);
    320 
    321 
    322    hasPropsAndUniqueMids(pc2.getTransceivers(),
    323      [
    324        {
    325          receiver: {track: {kind: "audio"}},
    326          sender: {track: null},
    327          direction: "recvonly",
    328          currentDirection: null,
    329        }
    330      ]);
    331  };
    332 
    333  const checkAddTransceiverWithSetRemoteOfferNoSend = async t => {
    334    const pc1 = new RTCPeerConnection();
    335    const pc2 = new RTCPeerConnection();
    336    t.add_cleanup(() => pc1.close());
    337    t.add_cleanup(() => pc2.close());
    338 
    339    const stream = await getNoiseStream({audio: true});
    340    t.add_cleanup(() => stopTracks(stream));
    341    const track = stream.getAudioTracks()[0];
    342    pc1.addTransceiver(track);
    343    pc1.getTransceivers()[0].direction = "recvonly";
    344 
    345    const offer = await pc1.createOffer();
    346    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    347    hasProps(trackEvents, []);
    348 
    349    hasPropsAndUniqueMids(pc2.getTransceivers(),
    350      [
    351        {
    352          receiver: {track: {kind: "audio"}},
    353          sender: {track: null},
    354          // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
    355          direction: "recvonly",
    356          currentDirection: null,
    357        }
    358      ]);
    359  };
    360 
    361  const checkAddTransceiverBadKind = async t => {
    362    const pc = new RTCPeerConnection();
    363    t.add_cleanup(() => pc.close());
    364    try {
    365      pc.addTransceiver("foo");
    366      assert_true(false, 'addTransceiver("foo") throws');
    367    }
    368    catch (e) {
    369      if (e instanceof TypeError) {
    370        assert_true(true, 'addTransceiver("foo") throws a TypeError');
    371      } else {
    372        assert_true(false, 'addTransceiver("foo") throws a TypeError');
    373      }
    374    }
    375 
    376    hasProps(pc.getTransceivers(), []);
    377  };
    378 
    379  const checkNoMidOffer = async t => {
    380    const pc1 = new RTCPeerConnection();
    381    const pc2 = new RTCPeerConnection();
    382    t.add_cleanup(() => pc1.close());
    383    t.add_cleanup(() => pc2.close());
    384 
    385    const stream = await getNoiseStream({audio: true});
    386    t.add_cleanup(() => stopTracks(stream));
    387    const track = stream.getAudioTracks()[0];
    388    pc1.addTrack(track, stream);
    389 
    390    const offer = await pc1.createOffer();
    391    await pc1.setLocalDescription(offer);
    392 
    393    // Remove mid attr
    394    offer.sdp = offer.sdp.replace("a=mid:", "a=unknownattr:");
    395    offer.sdp = offer.sdp.replace("a=group:", "a=unknownattr:");
    396    await pc2.setRemoteDescription(offer);
    397 
    398    hasPropsAndUniqueMids(pc2.getTransceivers(),
    399      [
    400        {
    401          receiver: {track: {kind: "audio"}},
    402          sender: {track: null},
    403          direction: "recvonly",
    404          currentDirection: null,
    405        }
    406      ]);
    407 
    408    const answer = await pc2.createAnswer();
    409    await pc2.setLocalDescription(answer);
    410    await pc1.setRemoteDescription(answer);
    411  };
    412 
    413  const checkNoMidAnswer = async t => {
    414    const pc1 = new RTCPeerConnection();
    415    const pc2 = new RTCPeerConnection();
    416    t.add_cleanup(() => pc1.close());
    417    t.add_cleanup(() => pc2.close());
    418 
    419    const stream = await getNoiseStream({audio: true});
    420    t.add_cleanup(() => stopTracks(stream));
    421    const track = stream.getAudioTracks()[0];
    422    pc1.addTrack(track, stream);
    423 
    424    const offer = await pc1.createOffer();
    425    await pc1.setLocalDescription(offer);
    426    await pc2.setRemoteDescription(offer);
    427 
    428    hasPropsAndUniqueMids(pc1.getTransceivers(),
    429      [
    430        {
    431          receiver: {track: {kind: "audio"}},
    432          sender: {track: {kind: "audio"}},
    433          direction: "sendrecv",
    434          currentDirection: null,
    435        }
    436      ]);
    437 
    438    const lastMid = pc1.getTransceivers()[0].mid;
    439 
    440    let answer = await pc2.createAnswer();
    441    // Remove mid attr
    442    answer.sdp = answer.sdp.replace("a=mid:", "a=unknownattr:");
    443    // Remove group attr also
    444    answer.sdp = answer.sdp.replace("a=group:", "a=unknownattr:");
    445    await pc1.setRemoteDescription(answer);
    446 
    447    hasProps(pc1.getTransceivers(),
    448      [
    449        {
    450          receiver: {track: {kind: "audio"}},
    451          sender: {track: {kind: "audio"}},
    452          direction: "sendrecv",
    453          currentDirection: "sendonly",
    454          mid: lastMid
    455        }
    456      ]);
    457 
    458    const reoffer = await pc1.createOffer();
    459    await pc1.setLocalDescription(reoffer);
    460    hasProps(pc1.getTransceivers(),
    461      [
    462        {
    463          receiver: {track: {kind: "audio"}},
    464          sender: {track: {kind: "audio"}},
    465          direction: "sendrecv",
    466          currentDirection: "sendonly",
    467          mid: lastMid
    468        }
    469      ]);
    470  };
    471 
    472  const checkAddTransceiverNoTrackDoesntPair = async t => {
    473    const pc1 = new RTCPeerConnection();
    474    const pc2 = new RTCPeerConnection();
    475    t.add_cleanup(() => pc1.close());
    476    t.add_cleanup(() => pc2.close());
    477 
    478    pc1.addTransceiver("audio");
    479    pc2.addTransceiver("audio");
    480 
    481    const offer = await pc1.createOffer();
    482    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    483    hasProps(trackEvents,
    484      [
    485        {
    486          track: pc2.getTransceivers()[1].receiver.track,
    487          streams: []
    488        }
    489      ]);
    490 
    491    hasPropsAndUniqueMids(pc2.getTransceivers(),
    492      [
    493        {mid: null}, // no addTrack magic, doesn't auto-pair
    494        {} // Created by SRD
    495      ]);
    496  };
    497 
    498  const checkAddTransceiverWithTrackDoesntPair = async t => {
    499    const pc1 = new RTCPeerConnection();
    500    const pc2 = new RTCPeerConnection();
    501    t.add_cleanup(() => pc1.close());
    502    t.add_cleanup(() => pc2.close());
    503    pc1.addTransceiver("audio");
    504 
    505    const stream = await getNoiseStream({audio: true});
    506    t.add_cleanup(() => stopTracks(stream));
    507    const track = stream.getAudioTracks()[0];
    508    pc2.addTransceiver(track);
    509 
    510    const offer = await pc1.createOffer();
    511    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    512    hasProps(trackEvents,
    513      [
    514        {
    515          track: pc2.getTransceivers()[1].receiver.track,
    516          streams: []
    517        }
    518      ]);
    519 
    520    hasPropsAndUniqueMids(pc2.getTransceivers(),
    521      [
    522        {mid: null, sender: {track}},
    523        {sender: {track: null}} // Created by SRD
    524      ]);
    525  };
    526 
    527  const checkAddTransceiverThenReplaceTrackDoesntPair = async t => {
    528    const pc1 = new RTCPeerConnection();
    529    const pc2 = new RTCPeerConnection();
    530    t.add_cleanup(() => pc1.close());
    531    t.add_cleanup(() => pc2.close());
    532    pc1.addTransceiver("audio");
    533    pc2.addTransceiver("audio");
    534 
    535    const stream = await getNoiseStream({audio: true});
    536    t.add_cleanup(() => stopTracks(stream));
    537    const track = stream.getAudioTracks()[0];
    538    await pc2.getTransceivers()[0].sender.replaceTrack(track);
    539 
    540    const offer = await pc1.createOffer();
    541    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    542    hasProps(trackEvents,
    543      [
    544        {
    545          track: pc2.getTransceivers()[1].receiver.track,
    546          streams: []
    547        }
    548      ]);
    549 
    550    hasPropsAndUniqueMids(pc2.getTransceivers(),
    551      [
    552        {mid: null, sender: {track}},
    553        {sender: {track: null}} // Created by SRD
    554      ]);
    555  };
    556 
    557  const checkAddTransceiverThenAddTrackPairs = async t => {
    558    const pc1 = new RTCPeerConnection();
    559    const pc2 = new RTCPeerConnection();
    560    t.add_cleanup(() => pc1.close());
    561    t.add_cleanup(() => pc2.close());
    562    pc1.addTransceiver("audio");
    563    pc2.addTransceiver("audio");
    564 
    565    const stream = await getNoiseStream({audio: true});
    566    t.add_cleanup(() => stopTracks(stream));
    567    const track = stream.getAudioTracks()[0];
    568    pc2.addTrack(track, stream);
    569 
    570    const offer = await pc1.createOffer();
    571    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    572    hasProps(trackEvents,
    573      [
    574        {
    575          track: pc2.getTransceivers()[0].receiver.track,
    576          streams: []
    577        }
    578      ]);
    579 
    580    // addTransceiver-transceivers cannot attach to a remote offers, so a second
    581    // transceiver is created and associated whilst the first transceiver
    582    // remains unassociated.
    583    assert_equals(pc2.getTransceivers()[0].mid, null);
    584    assert_not_equals(pc2.getTransceivers()[1].mid, null);
    585  };
    586 
    587  const checkAddTrackPairs = async t => {
    588    const pc1 = new RTCPeerConnection();
    589    const pc2 = new RTCPeerConnection();
    590    t.add_cleanup(() => pc1.close());
    591    t.add_cleanup(() => pc2.close());
    592    pc1.addTransceiver("audio");
    593 
    594    const stream = await getNoiseStream({audio: true});
    595    t.add_cleanup(() => stopTracks(stream));
    596    const track = stream.getAudioTracks()[0];
    597    pc2.addTrack(track, stream);
    598 
    599    const offer = await pc1.createOffer();
    600    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    601    hasProps(trackEvents,
    602      [
    603        {
    604          track: pc2.getTransceivers()[0].receiver.track,
    605          streams: []
    606        }
    607      ]);
    608 
    609    hasPropsAndUniqueMids(pc2.getTransceivers(),
    610      [
    611        {sender: {track}}
    612      ]);
    613  };
    614 
    615  const checkReplaceTrackNullDoesntPreventPairing = async t => {
    616    const pc1 = new RTCPeerConnection();
    617    const pc2 = new RTCPeerConnection();
    618    t.add_cleanup(() => pc1.close());
    619    t.add_cleanup(() => pc2.close());
    620    pc1.addTransceiver("audio");
    621 
    622    const stream = await getNoiseStream({audio: true});
    623    t.add_cleanup(() => stopTracks(stream));
    624    const track = stream.getAudioTracks()[0];
    625    pc2.addTrack(track, stream);
    626    await pc2.getTransceivers()[0].sender.replaceTrack(null);
    627 
    628    const offer = await pc1.createOffer();
    629    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    630    hasProps(trackEvents,
    631      [
    632        {
    633          track: pc2.getTransceivers()[0].receiver.track,
    634          streams: []
    635        }
    636      ]);
    637 
    638    hasPropsAndUniqueMids(pc2.getTransceivers(),
    639      [
    640        {sender: {track: null}}
    641      ]);
    642  };
    643 
    644  const checkRemoveAndReadd = async t => {
    645    const pc1 = new RTCPeerConnection();
    646    const pc2 = new RTCPeerConnection();
    647    t.add_cleanup(() => pc1.close());
    648    t.add_cleanup(() => pc2.close());
    649    const stream = await getNoiseStream({audio: true});
    650    t.add_cleanup(() => stopTracks(stream));
    651    const track = stream.getAudioTracks()[0];
    652    pc1.addTrack(track, stream);
    653 
    654    await offerAnswer(pc1, pc2);
    655 
    656    pc1.removeTrack(pc1.getSenders()[0]);
    657    pc1.addTrack(track, stream);
    658 
    659    hasProps(pc1.getTransceivers(),
    660      [
    661        {
    662          sender: {track: null},
    663          direction: "recvonly"
    664        },
    665        {
    666          sender: {track},
    667          direction: "sendrecv"
    668        }
    669      ]);
    670 
    671    // pc1 is offerer
    672    await offerAnswer(pc1, pc2);
    673 
    674    hasProps(pc2.getTransceivers(),
    675      [
    676        {currentDirection: "inactive"},
    677        {currentDirection: "recvonly"}
    678      ]);
    679 
    680    pc1.removeTrack(pc1.getSenders()[1]);
    681    pc1.addTrack(track, stream);
    682 
    683    hasProps(pc1.getTransceivers(),
    684      [
    685        {
    686          sender: {track: null},
    687          direction: "recvonly"
    688        },
    689        {
    690          sender: {track: null},
    691          direction: "recvonly"
    692        },
    693        {
    694          sender: {track},
    695          direction: "sendrecv"
    696        }
    697      ]);
    698 
    699    // pc1 is answerer. We need to create a new transceiver so pc1 will have
    700    // something to attach the re-added track to
    701    pc2.addTransceiver("audio");
    702 
    703    await offerAnswer(pc2, pc1);
    704 
    705    hasProps(pc2.getTransceivers(),
    706      [
    707        {currentDirection: "inactive"},
    708        {currentDirection: "inactive"},
    709        {currentDirection: "sendrecv"}
    710      ]);
    711  };
    712 
    713  const checkAddTrackExistingTransceiverThenRemove = async t => {
    714    const pc = new RTCPeerConnection();
    715    t.add_cleanup(() => pc.close());
    716    pc.addTransceiver("audio");
    717    const stream = await getNoiseStream({audio: true});
    718    const audio = stream.getAudioTracks()[0];
    719    let sender = pc.addTrack(audio, stream);
    720    pc.removeTrack(sender);
    721 
    722    // Cause transceiver to be associated
    723    await pc.setLocalDescription(await pc.createOffer());
    724 
    725    // Make sure add/remove works still
    726    sender = pc.addTrack(audio, stream);
    727    pc.removeTrack(sender);
    728 
    729    stopTracks(stream);
    730  };
    731 
    732  const checkRemoveTrackNegotiation = async t => {
    733    const pc1 = new RTCPeerConnection();
    734    const pc2 = new RTCPeerConnection();
    735    t.add_cleanup(() => pc1.close());
    736    t.add_cleanup(() => pc2.close());
    737    const stream = await getNoiseStream({audio: true, video: true});
    738    t.add_cleanup(() => stopTracks(stream));
    739    const audio = stream.getAudioTracks()[0];
    740    pc1.addTrack(audio, stream);
    741    const video = stream.getVideoTracks()[0];
    742    pc1.addTrack(video, stream);
    743    // We want both a sendrecv and sendonly transceiver to test that the
    744    // appropriate direction changes happen.
    745    pc1.getTransceivers()[1].direction = "sendonly";
    746 
    747    let offer = await pc1.createOffer();
    748 
    749    // Get a reference to the stream
    750    let trackEventCollector = collectTrackEvents(pc2);
    751    await pc2.setRemoteDescription(offer);
    752    let pc2TrackEvents = trackEventCollector.finish();
    753    hasProps(pc2TrackEvents,
    754      [
    755        {streams: [{id: stream.id}]},
    756        {streams: [{id: stream.id}]}
    757      ]);
    758    const receiveStream = pc2TrackEvents[0].streams[0];
    759 
    760    // Verify that rollback causes onremovetrack to fire for the added tracks
    761    let removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    762    await pc2.setRemoteDescription({type: "rollback"});
    763    let removedtracks = removetrackEventCollector.finish().map(e => e.track);
    764    assert_equals(removedtracks.length, 2,
    765                  "Rollback should have removed two tracks");
    766    assert_true(removedtracks.includes(pc2TrackEvents[0].track),
    767                "First track should be removed");
    768    assert_true(removedtracks.includes(pc2TrackEvents[1].track),
    769                "Second track should be removed");
    770 
    771    offer = await pc1.createOffer();
    772 
    773    let addtrackEventCollector = collectAddTrackEvents(receiveStream);
    774    trackEventCollector = collectTrackEvents(pc2);
    775    await pc2.setRemoteDescription(offer);
    776    pc2TrackEvents = trackEventCollector.finish();
    777    let addedtracks = addtrackEventCollector.finish().map(e => e.track);
    778    assert_equals(addedtracks.length, 2,
    779      "pc2.setRemoteDescription(offer) should've added 2 tracks to receive stream");
    780    assert_true(addedtracks.includes(pc2TrackEvents[0].track),
    781                "First track should be added");
    782    assert_true(addedtracks.includes(pc2TrackEvents[1].track),
    783                "Second track should be added");
    784 
    785    await pc1.setLocalDescription(offer);
    786    let answer = await pc2.createAnswer();
    787    await pc1.setRemoteDescription(answer);
    788    await pc2.setLocalDescription(answer);
    789    pc1.removeTrack(pc1.getSenders()[0]);
    790 
    791    hasProps(pc1.getSenders(),
    792      [
    793        {track: null},
    794        {track: video}
    795      ]);
    796 
    797    hasProps(pc1.getTransceivers(),
    798      [
    799        {
    800          sender: {track: null},
    801          direction: "recvonly"
    802        },
    803        {
    804          sender: {track: video},
    805          direction: "sendonly"
    806        }
    807      ]);
    808 
    809    await negotiationNeeded(pc1);
    810 
    811    pc1.removeTrack(pc1.getSenders()[1]);
    812 
    813    hasProps(pc1.getSenders(),
    814      [
    815        {track: null},
    816        {track: null}
    817      ]);
    818 
    819    hasProps(pc1.getTransceivers(),
    820      [
    821        {
    822          sender: {track: null},
    823          direction: "recvonly"
    824        },
    825        {
    826          sender: {track: null},
    827          direction: "inactive"
    828        }
    829      ]);
    830 
    831    // pc1 as offerer
    832    offer = await pc1.createOffer();
    833 
    834    removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    835    await pc2.setRemoteDescription(offer);
    836    removedtracks = removetrackEventCollector.finish().map(e => e.track);
    837    assert_equals(removedtracks.length, 2, "Should have two removed tracks");
    838    assert_true(removedtracks.includes(pc2TrackEvents[0].track),
    839                "First track should be removed");
    840    assert_true(removedtracks.includes(pc2TrackEvents[1].track),
    841                "Second track should be removed");
    842 
    843    addtrackEventCollector = collectAddTrackEvents(receiveStream);
    844    await pc2.setRemoteDescription({type: "rollback"});
    845    addedtracks = addtrackEventCollector.finish().map(e => e.track);
    846    assert_equals(addedtracks.length, 2, "Rollback should have added two tracks");
    847 
    848    // pc2 as offerer
    849    offer = await pc2.createOffer();
    850    await pc2.setLocalDescription(offer);
    851    await pc1.setRemoteDescription(offer);
    852    answer = await pc1.createAnswer();
    853    await pc1.setLocalDescription(answer);
    854 
    855    removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
    856    await pc2.setRemoteDescription(answer);
    857    removedtracks = removetrackEventCollector.finish().map(e => e.track);
    858    assert_equals(removedtracks.length, 2, "Should have two removed tracks");
    859 
    860    hasProps(pc2.getTransceivers(),
    861      [
    862        {
    863          currentDirection: "inactive"
    864        },
    865        {
    866          currentDirection: "inactive"
    867        }
    868      ]);
    869  };
    870 
    871  const checkSetDirection = async t => {
    872    const pc = new RTCPeerConnection();
    873    t.add_cleanup(() => pc.close());
    874    pc.addTransceiver("audio");
    875 
    876    pc.getTransceivers()[0].direction = "sendonly";
    877    hasProps(pc.getTransceivers(),[{direction: "sendonly"}]);
    878    pc.getTransceivers()[0].direction = "recvonly";
    879    hasProps(pc.getTransceivers(),[{direction: "recvonly"}]);
    880    pc.getTransceivers()[0].direction = "inactive";
    881    hasProps(pc.getTransceivers(),[{direction: "inactive"}]);
    882    pc.getTransceivers()[0].direction = "sendrecv";
    883    hasProps(pc.getTransceivers(),[{direction: "sendrecv"}]);
    884  };
    885 
    886  const checkCurrentDirection = async t => {
    887    const pc1 = new RTCPeerConnection();
    888    const pc2 = new RTCPeerConnection();
    889    t.add_cleanup(() => pc1.close());
    890    t.add_cleanup(() => pc2.close());
    891 
    892    const stream = await getNoiseStream({audio: true});
    893    t.add_cleanup(() => stopTracks(stream));
    894    const track = stream.getAudioTracks()[0];
    895    pc1.addTrack(track, stream);
    896    pc2.addTrack(track, stream);
    897    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
    898 
    899    let offer = await pc1.createOffer();
    900    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
    901 
    902    await pc1.setLocalDescription(offer);
    903    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
    904 
    905    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
    906    hasProps(trackEvents,
    907      [
    908        {
    909          track: pc2.getTransceivers()[0].receiver.track,
    910          streams: [{id: stream.id}]
    911        }
    912      ]);
    913 
    914    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
    915 
    916    let answer = await pc2.createAnswer();
    917    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
    918 
    919    await pc2.setLocalDescription(answer);
    920    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
    921 
    922    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
    923    hasProps(trackEvents,
    924      [
    925        {
    926          track: pc1.getTransceivers()[0].receiver.track,
    927          streams: [{id: stream.id}]
    928        }
    929      ]);
    930 
    931    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
    932 
    933    pc2.getTransceivers()[0].direction = "sendonly";
    934 
    935    offer = await pc2.createOffer();
    936    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
    937 
    938    await pc2.setLocalDescription(offer);
    939    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
    940 
    941    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
    942    hasProps(trackEvents, []);
    943 
    944    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
    945 
    946    answer = await pc1.createAnswer();
    947    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
    948 
    949    await pc1.setLocalDescription(answer);
    950    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
    951 
    952    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
    953    hasProps(trackEvents, []);
    954 
    955    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
    956 
    957    pc2.getTransceivers()[0].direction = "sendrecv";
    958 
    959    offer = await pc2.createOffer();
    960    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
    961 
    962    await pc2.setLocalDescription(offer);
    963    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
    964 
    965    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
    966    hasProps(trackEvents, []);
    967 
    968    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
    969 
    970    answer = await pc1.createAnswer();
    971    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
    972 
    973    await pc1.setLocalDescription(answer);
    974    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
    975 
    976    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
    977    hasProps(trackEvents,
    978      [
    979        {
    980          track: pc2.getTransceivers()[0].receiver.track,
    981          streams: [{id: stream.id}]
    982        }
    983      ]);
    984 
    985    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
    986 
    987    pc2.close();
    988    hasProps(pc2.getTransceivers(), [{currentDirection: "stopped"}]);
    989  };
    990 
    991  const checkSendrecvWithNoSendTrack = async t => {
    992    const pc1 = new RTCPeerConnection();
    993    const pc2 = new RTCPeerConnection();
    994    t.add_cleanup(() => pc1.close());
    995    t.add_cleanup(() => pc2.close());
    996 
    997    const stream = await getNoiseStream({audio: true});
    998    t.add_cleanup(() => stopTracks(stream));
    999    const track = stream.getAudioTracks()[0];
   1000    pc1.addTransceiver("audio");
   1001    pc1.getTransceivers()[0].direction = "sendrecv";
   1002    pc2.addTrack(track, stream);
   1003 
   1004    const offer = await pc1.createOffer();
   1005 
   1006    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
   1007    hasProps(trackEvents,
   1008      [
   1009        {
   1010          track: pc2.getTransceivers()[0].receiver.track,
   1011          streams: []
   1012        }
   1013      ]);
   1014 
   1015    trickle(t, pc1, pc2);
   1016    await pc1.setLocalDescription(offer);
   1017 
   1018    const answer = await pc2.createAnswer();
   1019    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1020    hasProps(trackEvents,
   1021      [
   1022        {
   1023          track: pc1.getTransceivers()[0].receiver.track,
   1024          streams: [{id: stream.id}]
   1025        }
   1026      ]);
   1027 
   1028    trickle(t, pc2, pc1);
   1029    await pc2.setLocalDescription(answer);
   1030 
   1031    await iceConnected(pc1);
   1032    await iceConnected(pc2);
   1033  };
   1034 
   1035  const checkSendrecvWithTracklessStream = async t => {
   1036    const pc1 = new RTCPeerConnection();
   1037    const pc2 = new RTCPeerConnection();
   1038    t.add_cleanup(() => pc1.close());
   1039    t.add_cleanup(() => pc2.close());
   1040 
   1041    const stream = new MediaStream();
   1042    pc1.addTransceiver("audio", {streams: [stream]});
   1043 
   1044    const offer = await pc1.createOffer();
   1045 
   1046    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
   1047    hasProps(trackEvents,
   1048      [
   1049        {
   1050          track: pc2.getTransceivers()[0].receiver.track,
   1051          streams: [{id: stream.id}]
   1052        }
   1053      ]);
   1054  };
   1055 
   1056  const checkStop = async t => {
   1057    const pc1 = new RTCPeerConnection();
   1058    t.add_cleanup(() => pc1.close());
   1059    const stream = await getNoiseStream({audio: true});
   1060    t.add_cleanup(() => stopTracks(stream));
   1061    const track = stream.getAudioTracks()[0];
   1062    pc1.addTrack(track, stream);
   1063 
   1064    let offer = await pc1.createOffer();
   1065    await pc1.setLocalDescription(offer);
   1066 
   1067    const pc2 = new RTCPeerConnection();
   1068    t.add_cleanup(() => pc2.close());
   1069    await pc2.setRemoteDescription(offer);
   1070 
   1071    pc2.addTrack(track, stream);
   1072 
   1073    const answer = await pc2.createAnswer();
   1074    await pc2.setLocalDescription(answer);
   1075    await pc1.setRemoteDescription(answer);
   1076 
   1077    let stoppedTransceiver = pc1.getTransceivers()[0];
   1078    let onended = new Promise(resolve => {
   1079      stoppedTransceiver.receiver.track.onended = resolve;
   1080    });
   1081    stoppedTransceiver.stop();
   1082    assert_equals(pc1.getReceivers().length, 1, 'getReceivers exposes a receiver of a stopped transceiver before negotiation');
   1083    assert_equals(pc1.getSenders().length, 1, 'getSenders exposes a sender of a stopped transceiver before negotiation');
   1084    await onended;
   1085    // The transceiver has [[stopping]] = true, [[stopped]] = false
   1086    hasPropsAndUniqueMids(pc1.getTransceivers(),
   1087      [
   1088        {
   1089          sender: {track: {kind: "audio"}},
   1090          receiver: {track: {kind: "audio", readyState: "ended"}},
   1091          currentDirection: "sendrecv",
   1092          direction: "stopped"
   1093        }
   1094      ]);
   1095 
   1096    const transceiver = pc1.getTransceivers()[0];
   1097 
   1098    checkThrows(() => transceiver.sender.setParameters(
   1099                        transceiver.sender.getParameters()),
   1100                "InvalidStateError", "setParameters on stopped transceiver");
   1101 
   1102    const stream2 = await getNoiseStream({audio: true});
   1103    const track2 = stream.getAudioTracks()[0];
   1104    checkThrows(() => transceiver.sender.replaceTrack(track2),
   1105                "InvalidStateError", "replaceTrack on stopped transceiver");
   1106 
   1107    checkThrows(() => transceiver.direction = "sendrecv",
   1108                "InvalidStateError", "set direction on stopped transceiver");
   1109 
   1110    checkThrows(() => transceiver.sender.dtmf.insertDTMF("111"),
   1111                "InvalidStateError", "insertDTMF on stopped transceiver");
   1112 
   1113    // Shouldn't throw
   1114    stoppedTransceiver.stop();
   1115 
   1116    offer = await pc1.createOffer();
   1117    await pc1.setLocalDescription(offer);
   1118 
   1119    const stoppedCalleeTransceiver = pc2.getTransceivers()[0];
   1120    onended = new Promise(resolve => {
   1121      stoppedCalleeTransceiver.receiver.track.onended = resolve;
   1122    });
   1123 
   1124    await pc2.setRemoteDescription(offer);
   1125 
   1126    await onended;
   1127    // pc2's transceiver was stopped remotely.
   1128    // The track ends when setRemeoteDescription(offer) is set.
   1129    hasProps(pc2.getTransceivers(),
   1130      [
   1131        {
   1132          sender: {track: {kind: "audio"}},
   1133          receiver: {track: {kind: "audio", readyState: "ended"}},
   1134          currentDirection: "stopped",
   1135          direction: "stopped"
   1136        }
   1137      ]);
   1138    // After setLocalDescription(answer), the transceiver has
   1139    // [[stopping]] = true, [[stopped]] = true, and is removed from pc2.
   1140    const stoppingAnswer = await pc2.createAnswer();
   1141    await pc2.setLocalDescription(stoppingAnswer);
   1142    assert_equals(pc2.getTransceivers().length, 0);
   1143    assert_equals(pc2.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
   1144    assert_equals(pc2.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
   1145 
   1146    // Shouldn't throw either
   1147    stoppedTransceiver.stop();
   1148    await pc1.setRemoteDescription(stoppingAnswer);
   1149    assert_equals(pc1.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
   1150    assert_equals(pc1.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
   1151 
   1152    pc1.close();
   1153    pc2.close();
   1154 
   1155    // Spec says the closed check comes before the stopped check, so this
   1156    // should throw now.
   1157    checkThrows(() => stoppedTransceiver.stop(),
   1158                "InvalidStateError", "RTCRtpTransceiver.stop() with closed PC");
   1159  };
   1160 
   1161  const checkStopAfterCreateOffer = async t => {
   1162    const pc1 = new RTCPeerConnection();
   1163    const pc2 = new RTCPeerConnection();
   1164    t.add_cleanup(() => pc1.close());
   1165    t.add_cleanup(() => pc2.close());
   1166 
   1167    const stream = await getNoiseStream({audio: true});
   1168    t.add_cleanup(() => stopTracks(stream));
   1169    const track = stream.getAudioTracks()[0];
   1170    pc1.addTrack(track, stream);
   1171    pc2.addTrack(track, stream);
   1172 
   1173    let offer = await pc1.createOffer();
   1174 
   1175    const transceiverThatWasStopped = pc1.getTransceivers()[0];
   1176    transceiverThatWasStopped.stop();
   1177    await pc2.setRemoteDescription(offer)
   1178    trickle(t, pc1, pc2);
   1179    await pc1.setLocalDescription(offer);
   1180 
   1181    let answer = await pc2.createAnswer();
   1182    const negotiationNeededAwaiter = negotiationNeeded(pc1);
   1183    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1184    // Spec language doesn't say anything about checking whether the transceiver
   1185    // is stopped here.
   1186    hasProps(trackEvents,
   1187      [
   1188        {
   1189          track: pc1.getTransceivers()[0].receiver.track,
   1190          streams: [{id: stream.id}]
   1191        }
   1192      ]);
   1193 
   1194    assert_equals(transceiverThatWasStopped, pc1.getTransceivers()[0]);
   1195    // The transceiver should still be [[stopping]]=true, [[stopped]]=false.
   1196    hasPropsAndUniqueMids(pc1.getTransceivers(),
   1197      [
   1198        {
   1199          currentDirection: "sendrecv",
   1200          direction: "stopped"
   1201        }
   1202      ]);
   1203 
   1204    await negotiationNeededAwaiter;
   1205 
   1206    trickle(t, pc2, pc1);
   1207 
   1208    await pc2.setLocalDescription(answer);
   1209 
   1210    await iceConnected(pc1);
   1211    await iceConnected(pc2);
   1212 
   1213    offer = await pc1.createOffer();
   1214    await pc1.setLocalDescription(offer);
   1215    await pc2.setRemoteDescription(offer);
   1216    answer = await pc2.createAnswer();
   1217    await pc2.setLocalDescription(answer);
   1218    await pc1.setRemoteDescription(answer);
   1219    assert_equals(pc1.getTransceivers().length, 0);
   1220    assert_equals(pc2.getTransceivers().length, 0);
   1221  };
   1222 
   1223  const checkStopAfterSetLocalOffer = async t => {
   1224    const pc1 = new RTCPeerConnection();
   1225    const pc2 = new RTCPeerConnection();
   1226    t.add_cleanup(() => pc1.close());
   1227    t.add_cleanup(() => pc2.close());
   1228 
   1229    const stream = await getNoiseStream({audio: true});
   1230    t.add_cleanup(() => stopTracks(stream));
   1231    const track = stream.getAudioTracks()[0];
   1232    pc1.addTrack(track, stream);
   1233    pc2.addTrack(track, stream);
   1234 
   1235    let offer = await pc1.createOffer();
   1236 
   1237    await pc2.setRemoteDescription(offer)
   1238    trickle(t, pc1, pc2);
   1239    await pc1.setLocalDescription(offer);
   1240 
   1241    pc1.getTransceivers()[0].stop();
   1242 
   1243    let answer = await pc2.createAnswer();
   1244    const negotiationNeededAwaiter = negotiationNeeded(pc1);
   1245    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1246    // Spec language doesn't say anything about checking whether the transceiver
   1247    // is stopped here.
   1248    hasProps(trackEvents,
   1249      [
   1250        {
   1251          track: pc1.getTransceivers()[0].receiver.track,
   1252          streams: [{id: stream.id}]
   1253        }
   1254      ]);
   1255 
   1256    hasPropsAndUniqueMids(pc1.getTransceivers(),
   1257      [
   1258        {
   1259          direction: "stopped",
   1260          currentDirection: "sendrecv"
   1261        }
   1262      ]);
   1263    await negotiationNeededAwaiter;
   1264 
   1265    trickle(t, pc2, pc1);
   1266    await pc2.setLocalDescription(answer);
   1267 
   1268    await iceConnected(pc1);
   1269    await iceConnected(pc2);
   1270 
   1271    offer = await pc1.createOffer();
   1272    await pc1.setLocalDescription(offer);
   1273    await pc2.setRemoteDescription(offer);
   1274    answer = await pc2.createAnswer();
   1275    await pc2.setLocalDescription(answer);
   1276    await pc1.setRemoteDescription(answer);
   1277 
   1278    assert_equals(pc1.getTransceivers().length, 0);
   1279    assert_equals(pc2.getTransceivers().length, 0);
   1280  };
   1281 
   1282  const checkStopAfterSetRemoteOffer = async t => {
   1283    const pc1 = new RTCPeerConnection();
   1284    const pc2 = new RTCPeerConnection();
   1285    t.add_cleanup(() => pc1.close());
   1286    t.add_cleanup(() => pc2.close());
   1287 
   1288    const stream = await getNoiseStream({audio: true});
   1289    t.add_cleanup(() => stopTracks(stream));
   1290    const track = stream.getAudioTracks()[0];
   1291    pc1.addTrack(track, stream);
   1292    pc2.addTrack(track, stream);
   1293 
   1294    const offer = await pc1.createOffer();
   1295 
   1296    await pc2.setRemoteDescription(offer)
   1297    await pc1.setLocalDescription(offer);
   1298 
   1299    // Stop on _answerer_ side now. Should not stop transceiver in answer,
   1300    // but cause firing of negotiationNeeded at pc2, and disabling
   1301    // of the transceiver with direction = inactive in answer.
   1302    pc2.getTransceivers()[0].stop();
   1303    assert_equals(pc2.getTransceivers()[0].direction, 'stopped');
   1304 
   1305    const answer = await pc2.createAnswer();
   1306    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1307    hasProps(trackEvents, []);
   1308 
   1309    hasProps(pc2.getTransceivers(),
   1310      [
   1311        {
   1312          direction: "stopped",
   1313          currentDirection: null,
   1314        }
   1315      ]);
   1316 
   1317    const negotiationNeededAwaiter = negotiationNeeded(pc2);
   1318    await pc2.setLocalDescription(answer);
   1319    hasProps(pc2.getTransceivers(),
   1320      [
   1321        {
   1322          direction: "stopped",
   1323          currentDirection: "inactive",
   1324        }
   1325      ]);
   1326 
   1327    await negotiationNeededAwaiter;
   1328  };
   1329 
   1330  const checkStopAfterCreateAnswer = async t => {
   1331    const pc1 = new RTCPeerConnection();
   1332    const pc2 = new RTCPeerConnection();
   1333    t.add_cleanup(() => pc1.close());
   1334    t.add_cleanup(() => pc2.close());
   1335 
   1336    const stream = await getNoiseStream({audio: true});
   1337    t.add_cleanup(() => stopTracks(stream));
   1338    const track = stream.getAudioTracks()[0];
   1339    pc1.addTrack(track, stream);
   1340    pc2.addTrack(track, stream);
   1341 
   1342    let offer = await pc1.createOffer();
   1343 
   1344    await pc2.setRemoteDescription(offer)
   1345    trickle(t, pc1, pc2);
   1346    await pc1.setLocalDescription(offer);
   1347 
   1348    let answer = await pc2.createAnswer();
   1349 
   1350    // Too late for this to go in the answer. ICE should succeed.
   1351    pc2.getTransceivers()[0].stop();
   1352 
   1353    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1354    hasProps(trackEvents,
   1355      [
   1356        {
   1357          track: pc1.getTransceivers()[0].receiver.track,
   1358          streams: [{id: stream.id}]
   1359        }
   1360      ]);
   1361 
   1362    hasPropsAndUniqueMids(pc2.getTransceivers(),
   1363      [
   1364        {
   1365          direction: "stopped",
   1366          currentDirection: null,
   1367        }
   1368      ]);
   1369 
   1370    trickle(t, pc2, pc1);
   1371    // The negotiationneeded event is fired during processing of
   1372    // setLocalDescription()
   1373    const negotiationNeededAwaiter = negotiationNeeded(pc2);
   1374    await pc2.setLocalDescription(answer);
   1375    hasPropsAndUniqueMids(pc2.getTransceivers(),
   1376      [
   1377        {
   1378          direction: "stopped",
   1379          currentDirection: "sendrecv",
   1380        }
   1381      ]);
   1382 
   1383    await negotiationNeededAwaiter;
   1384    await iceConnected(pc1);
   1385    await iceConnected(pc2);
   1386 
   1387    offer = await pc1.createOffer();
   1388    await pc1.setLocalDescription(offer);
   1389    await pc2.setRemoteDescription(offer);
   1390    answer = await pc2.createAnswer();
   1391    await pc2.setLocalDescription(answer);
   1392    await pc1.setRemoteDescription(answer);
   1393 
   1394    // Since this offer/answer exchange was initiated from pc1,
   1395    // pc2 still doesn't get to say that it has a stopped transceiver,
   1396    // but does get to set it to inactive.
   1397    hasProps(pc1.getTransceivers(),
   1398      [
   1399        {
   1400          direction: "sendrecv",
   1401          currentDirection: "inactive",
   1402        }
   1403      ]);
   1404 
   1405    hasProps(pc2.getTransceivers(),
   1406      [
   1407        {
   1408          direction: "stopped",
   1409          currentDirection: "inactive",
   1410        }
   1411      ]);
   1412  };
   1413 
   1414  const checkStopAfterSetLocalAnswer = async t => {
   1415    const pc1 = new RTCPeerConnection();
   1416    const pc2 = new RTCPeerConnection();
   1417    t.add_cleanup(() => pc1.close());
   1418    t.add_cleanup(() => pc2.close());
   1419 
   1420    const stream = await getNoiseStream({audio: true});
   1421    t.add_cleanup(() => stopTracks(stream));
   1422    const track = stream.getAudioTracks()[0];
   1423    pc1.addTrack(track, stream);
   1424    pc2.addTrack(track, stream);
   1425 
   1426    let offer = await pc1.createOffer();
   1427 
   1428    await pc2.setRemoteDescription(offer)
   1429    trickle(t, pc1, pc2);
   1430    await pc1.setLocalDescription(offer);
   1431 
   1432    let answer = await pc2.createAnswer();
   1433 
   1434    const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1435    hasProps(trackEvents,
   1436      [
   1437        {
   1438          track: pc1.getTransceivers()[0].receiver.track,
   1439          streams: [{id: stream.id}]
   1440        }
   1441      ]);
   1442 
   1443    trickle(t, pc2, pc1);
   1444    await pc2.setLocalDescription(answer);
   1445 
   1446    // ICE should succeed.
   1447    pc2.getTransceivers()[0].stop();
   1448 
   1449    hasPropsAndUniqueMids(pc2.getTransceivers(),
   1450      [
   1451        {
   1452          direction: "stopped",
   1453          currentDirection: "sendrecv",
   1454        }
   1455      ]);
   1456 
   1457    await negotiationNeeded(pc2);
   1458    await iceConnected(pc1);
   1459    await iceConnected(pc2);
   1460 
   1461    // Initiate an offer/answer exchange from pc2 in order
   1462    // to negotiate the stopped transceiver.
   1463    offer = await pc2.createOffer();
   1464    await pc2.setLocalDescription(offer);
   1465    await pc1.setRemoteDescription(offer);
   1466    answer = await pc1.createAnswer();
   1467    await pc1.setLocalDescription(answer);
   1468    await pc2.setRemoteDescription(answer);
   1469 
   1470    assert_equals(pc1.getTransceivers().length, 0);
   1471    assert_equals(pc2.getTransceivers().length, 0);
   1472  };
   1473 
   1474  const checkStopAfterClose = async t => {
   1475    const pc1 = new RTCPeerConnection();
   1476    const pc2 = new RTCPeerConnection();
   1477    t.add_cleanup(() => pc1.close());
   1478    t.add_cleanup(() => pc2.close());
   1479 
   1480    const stream = await getNoiseStream({audio: true});
   1481    t.add_cleanup(() => stopTracks(stream));
   1482    const track = stream.getAudioTracks()[0];
   1483    pc1.addTrack(track, stream);
   1484    pc2.addTrack(track, stream);
   1485 
   1486    const offer = await pc1.createOffer();
   1487    await pc2.setRemoteDescription(offer)
   1488    await pc1.setLocalDescription(offer);
   1489    const answer = await pc2.createAnswer();
   1490    await pc2.setLocalDescription(answer);
   1491    await pc1.setRemoteDescription(answer);
   1492 
   1493    pc1.close();
   1494    await checkThrows(() => pc1.getTransceivers()[0].stop(),
   1495                      "InvalidStateError",
   1496                      "Stopping a transceiver on a closed PC should throw.");
   1497  };
   1498 
   1499  const checkLocalRollback = async t => {
   1500    const pc = new RTCPeerConnection();
   1501    t.add_cleanup(() => pc.close());
   1502 
   1503    const stream = await getNoiseStream({audio: true});
   1504    t.add_cleanup(() => stopTracks(stream));
   1505    const track = stream.getAudioTracks()[0];
   1506    pc.addTrack(track, stream);
   1507 
   1508    let offer = await pc.createOffer();
   1509    await pc.setLocalDescription(offer);
   1510 
   1511    hasPropsAndUniqueMids(pc.getTransceivers(),
   1512      [
   1513        {
   1514          receiver: {track: {kind: "audio"}},
   1515          sender: {track},
   1516          direction: "sendrecv",
   1517          currentDirection: null,
   1518        }
   1519      ]);
   1520 
   1521    // Verify that rollback doesn't stomp things it should not
   1522    pc.getTransceivers()[0].direction = "sendonly";
   1523    const stream2 = await getNoiseStream({audio: true});
   1524    const track2 = stream2.getAudioTracks()[0];
   1525    await pc.getTransceivers()[0].sender.replaceTrack(track2);
   1526 
   1527    await pc.setLocalDescription({type: "rollback"});
   1528 
   1529    hasProps(pc.getTransceivers(),
   1530      [
   1531        {
   1532          receiver: {track: {kind: "audio"}},
   1533          sender: {track: track2},
   1534          direction: "sendonly",
   1535          mid: null,
   1536          currentDirection: null,
   1537        }
   1538      ]);
   1539 
   1540    // Make sure stop() isn't rolled back either.
   1541    offer = await pc.createOffer();
   1542    await pc.setLocalDescription(offer);
   1543    pc.getTransceivers()[0].stop();
   1544    await pc.setLocalDescription({type: "rollback"});
   1545 
   1546    hasProps(pc.getTransceivers(), [
   1547      {
   1548        direction: "stopped",
   1549      }
   1550    ]);
   1551  };
   1552 
   1553  const checkRollbackAndSetRemoteOfferWithDifferentType = async t => {
   1554    const pc1 = new RTCPeerConnection();
   1555    t.add_cleanup(() => pc1.close());
   1556 
   1557    const audioStream = await getNoiseStream({audio: true});
   1558    t.add_cleanup(() => stopTracks(audioStream));
   1559    const audioTrack = audioStream.getAudioTracks()[0];
   1560    pc1.addTrack(audioTrack, audioStream);
   1561 
   1562    const pc2 = new RTCPeerConnection();
   1563    t.add_cleanup(() => pc2.close());
   1564 
   1565    const videoStream = await getNoiseStream({video: true});
   1566    t.add_cleanup(() => stopTracks(videoStream));
   1567    const videoTrack = videoStream.getVideoTracks()[0];
   1568    pc2.addTrack(videoTrack, videoStream);
   1569 
   1570    await pc1.setLocalDescription(await pc1.createOffer());
   1571    await pc1.setLocalDescription({type: "rollback"});
   1572 
   1573    hasProps(pc1.getTransceivers(),
   1574      [
   1575        {
   1576          receiver: {track: {kind: "audio"}},
   1577          sender: {track: audioTrack},
   1578          direction: "sendrecv",
   1579          mid: null,
   1580          currentDirection: null,
   1581        }
   1582      ]);
   1583 
   1584    hasProps(pc2.getTransceivers(),
   1585      [
   1586        {
   1587          receiver: {track: {kind: "video"}},
   1588          sender: {track: videoTrack},
   1589          direction: "sendrecv",
   1590          mid: null,
   1591          currentDirection: null,
   1592        }
   1593      ]);
   1594 
   1595    await offerAnswer(pc2, pc1);
   1596 
   1597    hasPropsAndUniqueMids(pc1.getTransceivers(),
   1598      [
   1599        {
   1600          receiver: {track: {kind: "audio"}},
   1601          sender: {track: audioTrack},
   1602          direction: "sendrecv",
   1603          mid: null,
   1604          currentDirection: null,
   1605        },
   1606        {
   1607          receiver: {track: {kind: "video"}},
   1608          sender: {track: null},
   1609          direction: "recvonly",
   1610          currentDirection: "recvonly",
   1611        }
   1612      ]);
   1613 
   1614    hasPropsAndUniqueMids(pc2.getTransceivers(),
   1615      [
   1616        {
   1617          receiver: {track: {kind: "video"}},
   1618          sender: {track: videoTrack},
   1619          direction: "sendrecv",
   1620          currentDirection: "sendonly",
   1621        }
   1622      ]);
   1623 
   1624    await offerAnswer(pc1, pc2);
   1625  };
   1626 
   1627  const checkRemoteRollback = async t => {
   1628    const pc1 = new RTCPeerConnection();
   1629    t.add_cleanup(() => pc1.close());
   1630 
   1631    const stream = await getNoiseStream({audio: true});
   1632    t.add_cleanup(() => stopTracks(stream));
   1633    const track = stream.getAudioTracks()[0];
   1634    pc1.addTrack(track, stream);
   1635 
   1636    let offer = await pc1.createOffer();
   1637 
   1638    const pc2 = new RTCPeerConnection();
   1639    t.add_cleanup(() => pc2.close());
   1640    await pc2.setRemoteDescription(offer);
   1641 
   1642    const removedTransceiver = pc2.getTransceivers()[0];
   1643 
   1644    const onended = new Promise(resolve => {
   1645      removedTransceiver.receiver.track.onended = resolve;
   1646    });
   1647 
   1648    await pc2.setRemoteDescription({type: "rollback"});
   1649 
   1650    // Transceiver should be _gone_
   1651    hasProps(pc2.getTransceivers(), []);
   1652 
   1653    hasProps(removedTransceiver,
   1654      {
   1655        mid: null,
   1656        currentDirection: "stopped"
   1657      }
   1658    );
   1659 
   1660    await onended;
   1661 
   1662    hasProps(removedTransceiver,
   1663      {
   1664        receiver: {track: {readyState: "ended"}},
   1665        mid: null,
   1666        currentDirection: "stopped"
   1667      }
   1668    );
   1669 
   1670    // Setting the same offer again should do the same thing as before
   1671    await pc2.setRemoteDescription(offer);
   1672    hasPropsAndUniqueMids(pc2.getTransceivers(),
   1673      [
   1674        {
   1675          receiver: {track: {kind: "audio"}},
   1676          sender: {track: null},
   1677          direction: "recvonly",
   1678          currentDirection: null,
   1679        }
   1680      ]);
   1681 
   1682    const mid0 = pc2.getTransceivers()[0].mid;
   1683 
   1684    // Give pc2 a track with replaceTrack
   1685    const stream2 = await getNoiseStream({audio: true});
   1686    t.add_cleanup(() => stopTracks(stream2));
   1687    const track2 = stream2.getAudioTracks()[0];
   1688    await pc2.getTransceivers()[0].sender.replaceTrack(track2);
   1689    pc2.getTransceivers()[0].direction = "sendrecv";
   1690    hasProps(pc2.getTransceivers(),
   1691      [
   1692        {
   1693          receiver: {track: {kind: "audio"}},
   1694          sender: {track: track2},
   1695          direction: "sendrecv",
   1696          mid: mid0,
   1697          currentDirection: null,
   1698        }
   1699      ]);
   1700 
   1701    await pc2.setRemoteDescription({type: "rollback"});
   1702 
   1703    // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
   1704    // nor does setting direction.
   1705    hasProps(pc2.getTransceivers(), []);
   1706 
   1707    // Setting the same offer for a _third_ time should do the same thing
   1708    await pc2.setRemoteDescription(offer);
   1709    hasProps(pc2.getTransceivers(),
   1710      [
   1711        {
   1712          receiver: {track: {kind: "audio"}},
   1713          sender: {track: null},
   1714          direction: "recvonly",
   1715          mid: mid0,
   1716          currentDirection: null,
   1717        }
   1718      ]);
   1719 
   1720    // We should be able to add the same track again
   1721    pc2.addTrack(track2, stream2);
   1722    hasProps(pc2.getTransceivers(),
   1723      [
   1724        {
   1725          receiver: {track: {kind: "audio"}},
   1726          sender: {track: track2},
   1727          direction: "sendrecv",
   1728          mid: mid0,
   1729          currentDirection: null,
   1730        }
   1731      ]);
   1732 
   1733    await pc2.setRemoteDescription({type: "rollback"});
   1734    // Transceiver should _not_ be gone this time, because addTrack touched it.
   1735    hasProps(pc2.getTransceivers(),
   1736      [
   1737        {
   1738          receiver: {track: {kind: "audio"}},
   1739          sender: {track: track2},
   1740          direction: "sendrecv",
   1741          mid: null,
   1742          currentDirection: null,
   1743        }
   1744      ]);
   1745 
   1746    // Complete negotiation so we can test interactions with transceiver.stop()
   1747    await pc1.setLocalDescription(offer);
   1748 
   1749    // After all this SRD/rollback, we should still get the track event
   1750    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
   1751 
   1752    assert_equals(trackEvents.length, 1);
   1753    hasProps(trackEvents,
   1754      [
   1755        {
   1756          track: pc2.getTransceivers()[0].receiver.track,
   1757          streams: [{id: stream.id}]
   1758        }
   1759      ]);
   1760 
   1761    const answer = await pc2.createAnswer();
   1762    await pc2.setLocalDescription(answer);
   1763 
   1764    // Make sure all this rollback hasn't messed up the signaling
   1765    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1766    assert_equals(trackEvents.length, 1);
   1767    hasProps(trackEvents,
   1768      [
   1769        {
   1770          track: pc1.getTransceivers()[0].receiver.track,
   1771          streams: [{id: stream2.id}]
   1772        }
   1773      ]);
   1774    hasProps(pc1.getTransceivers(),
   1775      [
   1776        {
   1777          receiver: {track: {kind: "audio"}},
   1778          sender: {track},
   1779          direction: "sendrecv",
   1780          mid: mid0,
   1781          currentDirection: "sendrecv",
   1782        }
   1783      ]);
   1784 
   1785    // Don't bother waiting for ICE and such
   1786 
   1787    // Check to see whether rolling back a remote track removal works
   1788    pc1.getTransceivers()[0].direction = "recvonly";
   1789    offer = await pc1.createOffer();
   1790 
   1791    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
   1792    hasProps(trackEvents, []);
   1793 
   1794    trackEvents =
   1795      await setRemoteDescriptionReturnTrackEvents(pc2, {type: "rollback"});
   1796 
   1797    assert_equals(trackEvents.length, 1, 'track event from remote rollback');
   1798    hasProps(trackEvents,
   1799      [
   1800        {
   1801          track: pc2.getTransceivers()[0].receiver.track,
   1802          streams: [{id: stream.id}]
   1803        }
   1804      ]);
   1805 
   1806    // Check to see that stop() cannot be rolled back
   1807    pc1.getTransceivers()[0].stop();
   1808    offer = await pc1.createOffer();
   1809 
   1810    await pc2.setRemoteDescription(offer);
   1811    hasProps(pc2.getTransceivers(),
   1812      [
   1813        {
   1814          receiver: {track: {kind: "audio"}},
   1815          sender: {track: track2},
   1816          direction: "stopped",
   1817          mid: mid0,
   1818          currentDirection: "stopped",
   1819        }
   1820      ]);
   1821 
   1822    // stop() cannot be rolled back!
   1823    // Transceiver should have [[stopping]]=true, [[stopped]]=false.
   1824    await pc2.setRemoteDescription({type: "rollback"});
   1825    hasProps(pc2.getTransceivers(),
   1826      [
   1827        {
   1828          receiver: {track: {kind: "audio"}},
   1829          sender: {track: {kind: "audio"}},
   1830          direction: "stopped",
   1831          mid: mid0,
   1832          currentDirection: "stopped",
   1833        }
   1834      ]);
   1835  };
   1836 
   1837  const checkBundleTagRejected = async t => {
   1838    const pc1 = new RTCPeerConnection();
   1839    t.add_cleanup(() => pc1.close());
   1840    const pc2 = new RTCPeerConnection();
   1841    t.add_cleanup(() => pc2.close());
   1842 
   1843    const stream1 = await getNoiseStream({audio: true});
   1844    t.add_cleanup(() => stopTracks(stream1));
   1845    const track1 = stream1.getAudioTracks()[0];
   1846    const stream2 = await getNoiseStream({audio: true});
   1847    t.add_cleanup(() => stopTracks(stream2));
   1848    const track2 = stream2.getAudioTracks()[0];
   1849 
   1850    pc1.addTrack(track1, stream1);
   1851    pc1.addTrack(track2, stream2);
   1852 
   1853    await offerAnswer(pc1, pc2);
   1854 
   1855    pc2.getTransceivers()[0].stop();
   1856 
   1857    await offerAnswer(pc1, pc2);
   1858    await offerAnswer(pc2, pc1);
   1859  };
   1860 
   1861  const checkMsectionReuse = async t => {
   1862    // Use max-compat to make it easier to check for disabled m-sections
   1863    const pc1 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
   1864    const pc2 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
   1865    t.add_cleanup(() => pc1.close());
   1866    t.add_cleanup(() => pc2.close());
   1867 
   1868    const stream = await getNoiseStream({audio: true});
   1869    t.add_cleanup(() => stopTracks(stream));
   1870    const track = stream.getAudioTracks()[0];
   1871    pc1.addTrack(track, stream);
   1872    const [pc1Transceiver] = pc1.getTransceivers();
   1873 
   1874    await pc1.setLocalDescription();
   1875    await pc2.setRemoteDescription(pc1.localDescription);
   1876 
   1877    // Answerer stops transceiver. The m-section is not immediately rejected
   1878    // (a follow-up O/A exchange is needed) but it should become inactive in
   1879    // the meantime.
   1880    const stoppedMid0 = pc2.getTransceivers()[0].mid;
   1881    const [pc2Transceiver] = pc2.getTransceivers();
   1882    pc2Transceiver.stop();
   1883    assert_equals(pc2.getTransceivers()[0].direction, "stopped");
   1884    assert_not_equals(pc2.getTransceivers()[0].currentDirection, "stopped");
   1885 
   1886    await pc2.setLocalDescription();
   1887    await pc1.setRemoteDescription(pc2.localDescription);
   1888 
   1889    // Still not stopped - but inactive is reflected!
   1890    assert_equals(pc1Transceiver.mid, stoppedMid0);
   1891    assert_equals(pc1Transceiver.direction, "sendrecv");
   1892    assert_equals(pc1Transceiver.currentDirection, "inactive");
   1893    assert_equals(pc2Transceiver.mid, stoppedMid0);
   1894    assert_equals(pc2Transceiver.direction, "stopped");
   1895    assert_equals(pc2Transceiver.currentDirection, "inactive");
   1896 
   1897    // Now do the follow-up O/A exchange pc2 -> pc1.
   1898    await pc2.setLocalDescription();
   1899    await pc1.setRemoteDescription(pc2.localDescription);
   1900    await pc1.setLocalDescription();
   1901    await pc2.setRemoteDescription(pc1.localDescription);
   1902 
   1903    // Now they're stopped, and have been removed from the PCs.
   1904    assert_equals(pc1.getTransceivers().length, 0);
   1905    assert_equals(pc2.getTransceivers().length, 0);
   1906    assert_equals(pc1Transceiver.mid, null);
   1907    assert_equals(pc1Transceiver.direction, "stopped");
   1908    assert_equals(pc1Transceiver.currentDirection, "stopped");
   1909    assert_equals(pc2Transceiver.mid, null);
   1910    assert_equals(pc2Transceiver.direction, "stopped");
   1911    assert_equals(pc2Transceiver.currentDirection, "stopped");
   1912 
   1913    // Check that m-section is reused on both ends
   1914    const stream2 = await getNoiseStream({audio: true});
   1915    t.add_cleanup(() => stopTracks(stream2));
   1916    const track2 = stream2.getAudioTracks()[0];
   1917 
   1918    pc1.addTrack(track2, stream2);
   1919    let offer = await pc1.createOffer();
   1920    assert_equals(offer.sdp.match(/m=/g).length, 1,
   1921                  "Exactly one m-line in offer, because it was reused");
   1922    hasProps(pc1.getTransceivers(),
   1923      [
   1924        {
   1925          sender: {track: track2}
   1926        }
   1927      ]);
   1928 
   1929    assert_not_equals(pc1.getTransceivers()[0].mid, stoppedMid0);
   1930 
   1931    pc2.addTrack(track, stream);
   1932    offer = await pc2.createOffer();
   1933    assert_equals(offer.sdp.match(/m=/g).length, 1,
   1934                  "Exactly one m-line in offer, because it was reused");
   1935    hasProps(pc2.getTransceivers(),
   1936      [
   1937        {
   1938          sender: {track}
   1939        }
   1940      ]);
   1941 
   1942    assert_not_equals(pc2.getTransceivers()[0].mid, stoppedMid0);
   1943 
   1944    await pc2.setLocalDescription(offer);
   1945    await pc1.setRemoteDescription(offer);
   1946    let answer = await pc1.createAnswer();
   1947    await pc1.setLocalDescription(answer);
   1948    await pc2.setRemoteDescription(answer);
   1949    hasPropsAndUniqueMids(pc1.getTransceivers(),
   1950      [
   1951        {
   1952          sender: {track: track2},
   1953          currentDirection: "sendrecv"
   1954        }
   1955      ]);
   1956 
   1957    const mid0 = pc1.getTransceivers()[0].mid;
   1958 
   1959    hasProps(pc2.getTransceivers(),
   1960      [
   1961        {
   1962          sender: {track},
   1963          currentDirection: "sendrecv",
   1964          mid: mid0
   1965        }
   1966      ]);
   1967 
   1968    // stop the transceiver, and add a track. Verify that we don't reuse
   1969    // prematurely in our offer. (There should be one rejected m-section, and a
   1970    // new one for the new track)
   1971    const stoppedMid1 = pc1.getTransceivers()[0].mid;
   1972    pc1.getTransceivers()[0].stop();
   1973    const stream3 = await getNoiseStream({audio: true});
   1974    t.add_cleanup(() => stopTracks(stream3));
   1975    const track3 = stream3.getAudioTracks()[0];
   1976    pc1.addTrack(track3, stream3);
   1977    offer = await pc1.createOffer();
   1978    assert_equals(offer.sdp.match(/m=/g).length, 2,
   1979                  "Exactly 2 m-lines in offer, because it is too early to reuse");
   1980    assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
   1981                  "One m-line is rejected");
   1982 
   1983    await pc1.setLocalDescription(offer);
   1984 
   1985    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
   1986    hasProps(trackEvents,
   1987      [
   1988        {
   1989          track: pc2.getTransceivers()[1].receiver.track,
   1990          streams: [{id: stream3.id}]
   1991        }
   1992      ]);
   1993 
   1994    answer = await pc2.createAnswer();
   1995    await pc2.setLocalDescription(answer);
   1996 
   1997    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
   1998    hasProps(trackEvents, []);
   1999 
   2000    hasPropsAndUniqueMids(pc2.getTransceivers(),
   2001      [
   2002        {
   2003          sender: {track: null},
   2004          currentDirection: "recvonly"
   2005        }
   2006      ]);
   2007 
   2008    // Verify that we don't reuse the mid from the stopped transceiver
   2009    const mid1 = pc2.getTransceivers()[0].mid;
   2010    assert_not_equals(mid1, stoppedMid1);
   2011 
   2012    pc2.addTrack(track3, stream3);
   2013    // There are two ways to handle this new track; reuse the recvonly
   2014    // transceiver created above, or create a new transceiver and reuse the
   2015    // disabled m-section. We're supposed to do the former.
   2016    offer = await pc2.createOffer();
   2017    assert_equals(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
   2018    assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
   2019                  "One m-line is rejected, because the other was used");
   2020 
   2021    hasProps(pc2.getTransceivers(),
   2022      [
   2023        {
   2024          mid: mid1,
   2025          sender: {track: track3},
   2026          currentDirection: "recvonly",
   2027          direction: "sendrecv"
   2028        }
   2029      ]);
   2030 
   2031    // Add _another_ track; this should reuse the disabled m-section
   2032    const stream4 = await getNoiseStream({audio: true});
   2033    t.add_cleanup(() => stopTracks(stream4));
   2034    const track4 = stream4.getAudioTracks()[0];
   2035    pc2.addTrack(track4, stream4);
   2036    offer = await pc2.createOffer();
   2037    await pc2.setLocalDescription(offer);
   2038    hasPropsAndUniqueMids(pc2.getTransceivers(),
   2039      [
   2040        {
   2041          mid: mid1
   2042        },
   2043        {
   2044          sender: {track: track4},
   2045        }
   2046      ]);
   2047 
   2048    // Fourth transceiver should have a new mid
   2049    assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid0);
   2050    assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid1);
   2051 
   2052    assert_equals(offer.sdp.match(/m=/g).length, 2,
   2053                  "Exactly 2 m-lines in offer, because m-section was reused");
   2054    assert_equals(offer.sdp.match(/m=audio 0 /g), null,
   2055                  "No rejected m-line, because it was reused");
   2056  };
   2057 
   2058  const checkStopAfterCreateOfferWithReusedMsection = async t => {
   2059    const pc1 = new RTCPeerConnection();
   2060    t.add_cleanup(() => pc1.close());
   2061    const pc2 = new RTCPeerConnection();
   2062    t.add_cleanup(() => pc2.close());
   2063 
   2064    const stream = await getNoiseStream({audio: true, video: true});
   2065    t.add_cleanup(() => stopTracks(stream));
   2066    const audio = stream.getAudioTracks()[0];
   2067    const video = stream.getVideoTracks()[0];
   2068 
   2069    pc1.addTrack(audio, stream);
   2070    pc1.addTrack(video, stream);
   2071 
   2072    await offerAnswer(pc1, pc2);
   2073    pc1.getTransceivers()[1].stop();
   2074    await offerAnswer(pc1, pc2);
   2075 
   2076    // Second (video) m-section has been negotiated disabled.
   2077    const transceiver = pc1.addTransceiver("video");
   2078    const offer = await pc1.createOffer();
   2079    transceiver.stop();
   2080    await pc1.setLocalDescription(offer);
   2081    await pc2.setRemoteDescription(offer);
   2082 
   2083    const answer = await pc2.createAnswer();
   2084    await pc2.setLocalDescription(answer);
   2085    await pc1.setRemoteDescription(answer);
   2086  };
   2087 
   2088  const checkAddIceCandidateToStoppedTransceiver = async t => {
   2089    const pc1 = new RTCPeerConnection();
   2090    t.add_cleanup(() => pc1.close());
   2091    const pc2 = new RTCPeerConnection();
   2092    t.add_cleanup(() => pc2.close());
   2093 
   2094    const stream = await getNoiseStream({audio: true, video: true});
   2095    t.add_cleanup(() => stopTracks(stream));
   2096    const audio = stream.getAudioTracks()[0];
   2097    const video = stream.getVideoTracks()[0];
   2098 
   2099    pc1.addTrack(audio, stream);
   2100    pc1.addTrack(video, stream);
   2101 
   2102    pc2.addTrack(audio, stream);
   2103    pc2.addTrack(video, stream);
   2104 
   2105    await pc1.setLocalDescription(await pc1.createOffer());
   2106    pc1.getTransceivers()[1].stop();
   2107    pc1.setLocalDescription({type: "rollback"});
   2108 
   2109    const offer = await pc2.createOffer();
   2110    await pc2.setLocalDescription(offer);
   2111    await pc1.setRemoteDescription(offer);
   2112 
   2113    await pc1.addIceCandidate(
   2114      {
   2115        candidate: "candidate:0 1 UDP 2122252543 192.168.1.112 64261 typ host",
   2116        sdpMid: pc2.getTransceivers()[1].mid
   2117      });
   2118  };
   2119 
   2120 const tests = [
   2121  checkAddTransceiverNoTrack,
   2122  checkAddTransceiverWithTrack,
   2123  checkAddTransceiverWithAddTrack,
   2124  checkAddTransceiverWithDirection,
   2125  checkAddTransceiverWithSetRemoteOfferSending,
   2126  checkAddTransceiverWithSetRemoteOfferNoSend,
   2127  checkAddTransceiverBadKind,
   2128  checkNoMidOffer,
   2129  checkNoMidAnswer,
   2130  checkSetDirection,
   2131  checkCurrentDirection,
   2132  checkSendrecvWithNoSendTrack,
   2133  checkSendrecvWithTracklessStream,
   2134  checkAddTransceiverNoTrackDoesntPair,
   2135  checkAddTransceiverWithTrackDoesntPair,
   2136  checkAddTransceiverThenReplaceTrackDoesntPair,
   2137  checkAddTransceiverThenAddTrackPairs,
   2138  checkAddTrackPairs,
   2139  checkReplaceTrackNullDoesntPreventPairing,
   2140  checkRemoveAndReadd,
   2141  checkAddTrackExistingTransceiverThenRemove,
   2142  checkRemoveTrackNegotiation,
   2143  checkStop,
   2144  checkStopAfterCreateOffer,
   2145  checkStopAfterSetLocalOffer,
   2146  checkStopAfterSetRemoteOffer,
   2147  checkStopAfterCreateAnswer,
   2148  checkStopAfterSetLocalAnswer,
   2149  checkStopAfterClose,
   2150  checkLocalRollback,
   2151  checkRollbackAndSetRemoteOfferWithDifferentType,
   2152  checkRemoteRollback,
   2153  checkMsectionReuse,
   2154  checkStopAfterCreateOfferWithReusedMsection,
   2155  checkAddIceCandidateToStoppedTransceiver,
   2156  checkBundleTagRejected
   2157 ].forEach(test => promise_test(test, test.name));
   2158 
   2159 ['audio', 'video'].forEach(kind => {
   2160  const waitUntilTrackEventWithTimeout = (obj, name, t, timeout = 1000/*ms*/) => {
   2161    return new Promise((resolve) => {
   2162      obj.addEventListener(name, resolve, {once: true});
   2163      t.step_timeout(resolve, timeout)
   2164    });
   2165  }
   2166 
   2167  promise_test(async t => {
   2168    const pc1 = new RTCPeerConnection();
   2169    const pc2 = new RTCPeerConnection();
   2170    t.add_cleanup(() => pc1.close());
   2171    t.add_cleanup(() => pc2.close());
   2172 
   2173    pc1.addTransceiver(kind);
   2174    pc2.ontrack = t.step_func(async e => {
   2175      assert_true(e.track.muted, `track is muted in ontrack`);
   2176    });
   2177    const offer = await pc1.createOffer();
   2178    await pc2.setRemoteDescription(offer);
   2179    assert_true(pc2.getReceivers()[0].track.muted, `track is muted after SRD`);
   2180  }, `track with ${kind} is muted after SRD`);
   2181 
   2182  promise_test(async t => {
   2183    const pc1 = new RTCPeerConnection();
   2184    const pc2 = new RTCPeerConnection();
   2185    t.add_cleanup(() => pc1.close());
   2186    t.add_cleanup(() => pc2.close());
   2187 
   2188    let stream;
   2189    if (kind === 'audio') {
   2190      stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
   2191    } else {
   2192      stream = await getNoiseStream({[kind]: true});
   2193    }
   2194    t.add_cleanup(() => stream.getTracks().forEach(t => t.stop()));
   2195 
   2196    pc1.addTrack(stream.getTracks()[0], stream);
   2197    pc2.ontrack = t.step_func(async e => {
   2198      assert_true(e.track.muted, `track is muted in ontrack`);
   2199    });
   2200 
   2201    await exchangeIceCandidates(pc1, pc2);
   2202    await exchangeOfferAnswer(pc1, pc2);
   2203    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2204    assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`);
   2205  }, `track with ${kind} gets unmuted when packets flow.`);
   2206 
   2207  promise_test(async t => {
   2208    const pc1 = new RTCPeerConnection();
   2209    const pc2 = new RTCPeerConnection();
   2210    t.add_cleanup(() => pc1.close());
   2211    t.add_cleanup(() => pc2.close());
   2212 
   2213    let stream;
   2214    if (kind === 'audio') {
   2215      stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
   2216    } else {
   2217      stream = await getNoiseStream({[kind]: true});
   2218    }
   2219    t.add_cleanup(() => stream.getTracks().forEach(t => t.stop()));
   2220 
   2221    pc1.addTrack(stream.getTracks()[0], stream);
   2222    pc2.ontrack = t.step_func(async e => {
   2223      assert_true(e.track.muted, `track is muted in ontrack`);
   2224    });
   2225 
   2226    await exchangeIceCandidates(pc1, pc2);
   2227    await exchangeOfferAnswer(pc1, pc2);
   2228    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2229    assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`);
   2230 
   2231    pc1.getTransceivers()[0].direction = 'inactive';
   2232    await exchangeOfferAnswer(pc1, pc2);
   2233    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t);
   2234    assert_true(pc2.getReceivers()[0].track.muted, `track is muted`);
   2235  }, `track with ${kind} gets muted when setting the transceiver direction to 'inactive'`);
   2236 
   2237  promise_test(async t => {
   2238    const pc1 = new RTCPeerConnection();
   2239    const pc2 = new RTCPeerConnection();
   2240    t.add_cleanup(() => pc1.close());
   2241    t.add_cleanup(() => pc2.close());
   2242 
   2243    let stream;
   2244    if (kind === 'audio') {
   2245      stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
   2246    } else {
   2247      stream = await getNoiseStream({[kind]: true});
   2248    }
   2249    t.add_cleanup(() => stream.getTracks().forEach(t => t.stop()));
   2250 
   2251    pc1.addTrack(stream.getTracks()[0], stream);
   2252    pc2.ontrack = t.step_func(async e => {
   2253      assert_true(e.track.muted, `track is muted in ontrack`);
   2254    });
   2255 
   2256    await exchangeIceCandidates(pc1, pc2);
   2257    await exchangeOfferAnswer(pc1, pc2);
   2258    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2259    assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`);
   2260 
   2261    pc1.getTransceivers()[0].direction = 'inactive';
   2262    await exchangeOfferAnswer(pc1, pc2);
   2263    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t);
   2264    assert_true(pc2.getReceivers()[0].track.muted, `track is muted`);
   2265 
   2266    pc1.getTransceivers()[0].direction = 'sendrecv';
   2267    await exchangeOfferAnswer(pc1, pc2);
   2268    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2269    assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`);
   2270  }, `track with ${kind} gets unmuted when setting the transceiver direction to 'sendrecv' after 'inactive'`);
   2271 
   2272  promise_test(async t => {
   2273    const pc1 = new RTCPeerConnection();
   2274    const pc2 = new RTCPeerConnection();
   2275    t.add_cleanup(() => pc1.close());
   2276    t.add_cleanup(() => pc2.close());
   2277 
   2278    let stream;
   2279    if (kind === 'audio') {
   2280      stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
   2281    } else {
   2282      stream = await getNoiseStream({[kind]: true});
   2283    }
   2284    t.add_cleanup(() => stream.getTracks().forEach(t => t.stop()));
   2285 
   2286    pc1.addTrack(stream.getTracks()[0], stream);
   2287    pc2.ontrack = t.step_func(async e => {
   2288      assert_true(e.track.muted, `track is muted in ontrack`);
   2289    });
   2290 
   2291    await exchangeIceCandidates(pc1, pc2);
   2292    await exchangeOfferAnswer(pc1, pc2);
   2293    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2294    assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`);
   2295    pc1.getSenders()[0].replaceTrack(null);
   2296 
   2297    pc1.getTransceivers()[0].direction = 'inactive';
   2298    await exchangeOfferAnswer(pc1, pc2);
   2299    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t);
   2300    assert_true(pc2.getReceivers()[0].track.muted, `track is muted`);
   2301 
   2302    pc1.getTransceivers()[0].direction = 'sendrecv';
   2303    await exchangeOfferAnswer(pc1, pc2);
   2304    await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t);
   2305    assert_true(pc2.getReceivers()[0].track.muted, `track remains muted`);
   2306  }, `track with ${kind} stays muted when setting the transceiver direction to 'sendrecv' after 'inactive' but no packets flow`);
   2307 });
   2308 
   2309 </script>