tor-browser

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

RTCPeerConnection-setRemoteDescription-rollback.html (26293B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <title>RTCPeerConnection.prototype.setRemoteDescription rollback</title>
      4 <script src="/resources/testharness.js"></script>
      5 <script src="/resources/testharnessreport.js"></script>
      6 <script src="RTCPeerConnection-helper.js"></script>
      7 <script>
      8  'use strict';
      9 
     10  // Test is based on the following editor draft:
     11  // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
     12 
     13  // The following helper functions are called from RTCPeerConnection-helper.js:
     14  //   assert_session_desc_similar
     15  //   generateAudioReceiveOnlyOffer
     16  //   generateDataChannelOffer
     17 
     18  /*
     19    4.3.2.  Interface Definition
     20      [Constructor(optional RTCConfiguration configuration)]
     21      interface RTCPeerConnection : EventTarget {
     22        Promise<void>                      setLocalDescription(
     23            RTCSessionDescriptionInit description);
     24 
     25        readonly attribute RTCSessionDescription? localDescription;
     26        readonly attribute RTCSessionDescription? currentLocalDescription;
     27        readonly attribute RTCSessionDescription? pendingLocalDescription;
     28 
     29        Promise<void>                      setRemoteDescription(
     30            RTCSessionDescriptionInit description);
     31 
     32        readonly attribute RTCSessionDescription? remoteDescription;
     33        readonly attribute RTCSessionDescription? currentRemoteDescription;
     34        readonly attribute RTCSessionDescription? pendingRemoteDescription;
     35        ...
     36      };
     37 
     38    4.6.2.  RTCSessionDescription Class
     39      dictionary RTCSessionDescriptionInit {
     40        required RTCSdpType type;
     41                 DOMString  sdp = "";
     42      };
     43 
     44    4.6.1.  RTCSdpType
     45      enum RTCSdpType {
     46        "offer",
     47        "pranswer",
     48        "answer",
     49        "rollback"
     50      };
     51   */
     52 
     53  /*
     54    4.3.1.6.  Set the RTCSessionSessionDescription
     55      2.2.3.  Otherwise, if description is set as a remote description, then run one
     56              of the following steps:
     57        - If description is of type "rollback", then this is a rollback.
     58          Set connection.pendingRemoteDescription to null and signaling state to stable.
     59   */
     60  promise_test(t => {
     61    const pc = new RTCPeerConnection();
     62    t.add_cleanup(() => pc.close());
     63 
     64    const states = [];
     65    pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
     66 
     67    return generateDataChannelOffer(pc)
     68    .then(offer => pc.setRemoteDescription(offer))
     69    .then(() => {
     70      assert_equals(pc.signalingState, 'have-remote-offer');
     71      assert_not_equals(pc.remoteDescription, null);
     72      assert_equals(pc.pendingRemoteDescription, pc.remoteDescription);
     73      assert_equals(pc.currentRemoteDescription, null);
     74 
     75      return pc.setRemoteDescription({type: 'rollback'});
     76    })
     77    .then(() => {
     78      assert_equals(pc.signalingState, 'stable');
     79      assert_equals(pc.remoteDescription, null);
     80      assert_equals(pc.pendingRemoteDescription, null);
     81      assert_equals(pc.currentRemoteDescription, null);
     82 
     83      assert_array_equals(states, ['have-remote-offer', 'stable']);
     84    });
     85  }, 'setRemoteDescription(rollback) in have-remote-offer state should revert to stable state');
     86 
     87  /*
     88    4.3.1.6.  Set the RTCSessionSessionDescription
     89      2.3.  If the description's type is invalid for the current signaling state of
     90            connection, then reject p with a newly created InvalidStateError and abort
     91            these steps.
     92 
     93    [jsep]
     94      4.1.8.2.  Rollback
     95        - Rollback can only be used to cancel proposed changes;
     96          there is no support for rolling back from a stable state to a
     97          previous stable state
     98   */
     99  promise_test(t => {
    100    const pc = new RTCPeerConnection();
    101    t.add_cleanup(() => pc.close());
    102    return promise_rejects_dom(t, 'InvalidStateError',
    103      pc.setRemoteDescription({type: 'rollback'}));
    104  }, `setRemoteDescription(rollback) from stable state should reject with InvalidStateError`);
    105 
    106  promise_test(async t => {
    107    const pc = new RTCPeerConnection();
    108    t.add_cleanup(() => pc.close());
    109    await pc.setLocalDescription();
    110    await promise_rejects_dom(t, 'InvalidStateError', pc.setRemoteDescription({ type: 'rollback' }));
    111  }, `setRemoteDescription(rollback) after setting a local offer should reject with InvalidStateError`);
    112 
    113   promise_test(t => {
    114    const pc = new RTCPeerConnection();
    115    t.add_cleanup(() => pc.close());
    116    return generateAudioReceiveOnlyOffer(pc)
    117    .then(offer => pc.setRemoteDescription(offer))
    118    .then(() => pc.setRemoteDescription({
    119      type: 'rollback',
    120      sdp: '!<Invalid SDP Content>;'
    121    }));
    122  }, `setRemoteDescription(rollback) should ignore invalid sdp content and succeed`);
    123 
    124  promise_test(async t => {
    125    const pc1 = new RTCPeerConnection();
    126    const pc2 = new RTCPeerConnection();
    127    t.add_cleanup(() => pc1.close());
    128    t.add_cleanup(() => pc2.close());
    129 
    130    // We don't use this right away
    131    pc1.addTransceiver('audio', { direction: 'recvonly' });
    132    const offer1 = await pc1.createOffer();
    133 
    134    // Create offer from pc2, apply and rollback on pc1
    135    pc2.addTransceiver('audio', { direction: 'recvonly' });
    136    const offer2 = await pc2.createOffer();
    137    await pc1.setRemoteDescription(offer2);
    138    await pc1.setRemoteDescription({type: "rollback"});
    139 
    140    // Then try applying pc1's old offer
    141    await pc1.setLocalDescription(offer1);
    142  }, `local offer created before setRemoteDescription(remote offer) then rollback should still be usable`);
    143 
    144  promise_test(async t => {
    145    const pc1 = new RTCPeerConnection();
    146    const pc2 = new RTCPeerConnection();
    147    t.add_cleanup(() => pc1.close());
    148    t.add_cleanup(() => pc2.close());
    149 
    150    const stream = await getNoiseStream({video: true});
    151    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    152    pc1.addTrack(stream.getTracks()[0], stream);
    153 
    154    // We don't use this right away. pc1 has provisionally decided that the
    155    // (only) transceiver is bound to level 0.
    156    const offer1 = await pc1.createOffer();
    157 
    158    // Create offer from pc2, apply and rollback on pc1
    159    pc2.addTransceiver('audio', { direction: 'recvonly' });
    160    pc2.addTransceiver('video', { direction: 'recvonly' });
    161    const offer2 = await pc2.createOffer();
    162    // pc1 now should change its mind about what level its video transceiver is
    163    // bound to. It was 0, now it is 1.
    164    await pc1.setRemoteDescription(offer2);
    165 
    166    // Rolling back should put things back the way they were.
    167    await pc1.setRemoteDescription({type: "rollback"});
    168 
    169    // Then try applying pc1's old offer
    170    await pc1.setLocalDescription(offer1);
    171  }, "local offer created before setRemoteDescription(remote offer) with different transceiver level assignments then rollback should still be usable");
    172 
    173  promise_test(async t => {
    174    const pc1 = new RTCPeerConnection();
    175    t.add_cleanup(() => pc1.close());
    176    const pc2 = new RTCPeerConnection();
    177    t.add_cleanup(() => pc2.close());
    178 
    179    const stream = await getNoiseStream({video: true});
    180    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    181    pc1.addTrack(stream.getTracks()[0], stream);
    182 
    183    await pc2.setRemoteDescription(await pc1.createOffer());
    184    assert_equals(pc2.getTransceivers().length, 1);
    185    await pc2.setRemoteDescription({type: "rollback"});
    186    assert_equals(pc2.getTransceivers().length, 0);
    187  }, "rollback of a remote offer should remove a transceiver");
    188 
    189  promise_test(async t => {
    190    const pc1 = new RTCPeerConnection();
    191    t.add_cleanup(() => pc1.close());
    192    const pc2 = new RTCPeerConnection();
    193    t.add_cleanup(() => pc2.close());
    194 
    195    const stream = await getNoiseStream({video: true});
    196    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    197    pc1.addTrack(stream.getTracks()[0], stream);
    198 
    199    await pc2.setRemoteDescription(await pc1.createOffer());
    200    assert_equals(pc2.getTransceivers().length, 1);
    201 
    202    const stream2 = await getNoiseStream({video: true});
    203    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    204    const track = stream2.getVideoTracks()[0];
    205    await pc2.getTransceivers()[0].sender.replaceTrack(track);
    206 
    207    await pc2.setRemoteDescription({type: "rollback"});
    208    assert_equals(pc2.getTransceivers().length, 0);
    209  }, "rollback of a remote offer should remove touched transceiver");
    210 
    211  promise_test(async t => {
    212    const pc1 = new RTCPeerConnection();
    213    t.add_cleanup(() => pc1.close());
    214    const pc2 = new RTCPeerConnection();
    215    t.add_cleanup(() => pc2.close());
    216 
    217    const stream = await getNoiseStream({video: true});
    218    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    219    pc1.addTrack(stream.getTracks()[0], stream);
    220 
    221    await pc2.setRemoteDescription(await pc1.createOffer());
    222    assert_equals(pc2.getTransceivers().length, 1);
    223 
    224    const stream2 = await getNoiseStream({video: true});
    225    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    226    pc2.addTrack(stream2.getTracks()[0], stream2);
    227 
    228    await pc2.setRemoteDescription({type: "rollback"});
    229    assert_equals(pc2.getTransceivers().length, 1);
    230    assert_equals(pc2.getTransceivers()[0].mid, null);
    231    assert_equals(pc2.getTransceivers()[0].receiver.transport, null);
    232  }, "rollback of a remote offer should keep a transceiver");
    233 
    234  promise_test(async t => {
    235    const pc1 = new RTCPeerConnection();
    236    t.add_cleanup(() => pc1.close());
    237    const pc2 = new RTCPeerConnection();
    238    t.add_cleanup(() => pc2.close());
    239 
    240    const stream = await getNoiseStream({video: true});
    241    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    242    pc1.addTrack(stream.getTracks()[0], stream);
    243 
    244    const stream2 = await getNoiseStream({video: true});
    245    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    246    pc2.addTrack(stream2.getTracks()[0], stream2);
    247 
    248    await pc2.setRemoteDescription(await pc1.createOffer());
    249    assert_equals(pc2.getTransceivers().length, 1);
    250 
    251    await pc2.setRemoteDescription({type: "rollback"});
    252    assert_equals(pc2.getTransceivers().length, 1);
    253    assert_equals(pc2.getTransceivers()[0].mid, null);
    254    assert_equals(pc2.getTransceivers()[0].receiver.transport, null);
    255  }, "rollback of a remote offer should keep a transceiver created by addtrack");
    256 
    257  promise_test(async t => {
    258    const pc1 = new RTCPeerConnection();
    259    t.add_cleanup(() => pc1.close());
    260    const pc2 = new RTCPeerConnection();
    261    t.add_cleanup(() => pc2.close());
    262 
    263    const stream = await getNoiseStream({video: true});
    264    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    265    pc1.addTrack(stream.getTracks()[0], stream);
    266 
    267    await pc2.setRemoteDescription(await pc1.createOffer());
    268    assert_equals(pc2.getTransceivers().length, 1);
    269 
    270    const stream2 = await getNoiseStream({video: true});
    271    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    272    pc2.addTrack(stream2.getTracks()[0], stream2);
    273    await pc2.getTransceivers()[0].sender.replaceTrack(null);
    274    await pc2.setRemoteDescription({type: "rollback"});
    275    assert_equals(pc2.getTransceivers().length, 1);
    276  }, "rollback of a remote offer should keep a transceiver without tracks");
    277 
    278  promise_test(async t => {
    279    const pc = new RTCPeerConnection();
    280    t.add_cleanup(() => pc.close());
    281 
    282    const stream = await getNoiseStream({video: true});
    283    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    284    pc.addTrack(stream.getTracks()[0], stream);
    285 
    286    const states = [];
    287    const signalingstatechangeResolver = new Resolver();
    288    pc.onsignalingstatechange = () => {
    289      states.push(pc.signalingState);
    290      signalingstatechangeResolver.resolve();
    291    };
    292 
    293    const offer = await pc.createOffer();
    294    await pc.setLocalDescription(offer);
    295    assert_not_equals(pc.getTransceivers()[0].sender.transport, null);
    296    await pc.setLocalDescription({type: "rollback"});
    297    assert_equals(pc.getTransceivers().length, 1);
    298    assert_equals(pc.getTransceivers()[0].mid, null)
    299    assert_equals(pc.getTransceivers()[0].sender.transport, null);
    300    await pc.setLocalDescription(offer);
    301    assert_equals(pc.getTransceivers().length, 1);
    302    await signalingstatechangeResolver.promise;
    303    assert_array_equals(states, ['have-local-offer', 'stable', 'have-local-offer']);
    304  }, "explicit rollback of local offer should remove transceivers and transport");
    305 
    306  promise_test(async t => {
    307    const pc1 = new RTCPeerConnection();
    308    t.add_cleanup(() => pc1.close());
    309    const pc2 = new RTCPeerConnection();
    310    t.add_cleanup(() => pc2.close());
    311 
    312    const states = [];
    313    const signalingstatechangeResolver = new Resolver();
    314    pc1.onsignalingstatechange = () => {
    315      states.push(pc1.signalingState);
    316      signalingstatechangeResolver.resolve();
    317    };
    318    const stream1 = await getNoiseStream({audio: true});
    319    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    320    pc1.addTransceiver(stream1.getTracks()[0], stream1);
    321 
    322    const stream2 = await getNoiseStream({audio: true});
    323    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    324    pc2.addTransceiver(stream2.getTracks()[0], stream2);
    325 
    326    await pc1.setLocalDescription(await pc1.createOffer());
    327    pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event right now"));
    328    await pc1.setRemoteDescription(await pc2.createOffer());
    329    await pc1.setLocalDescription(await pc1.createAnswer());
    330    await signalingstatechangeResolver.promise;
    331    assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']);
    332    await new Promise(r => pc1.onnegotiationneeded = r);
    333  }, "when using addTransceiver, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded until we settle in stable");
    334 
    335  promise_test(async t => {
    336    const pc1 = new RTCPeerConnection();
    337    t.add_cleanup(() => pc1.close());
    338    const pc2 = new RTCPeerConnection();
    339    t.add_cleanup(() => pc2.close());
    340 
    341    const states = [];
    342    const signalingstatechangeResolver = new Resolver();
    343    pc1.onsignalingstatechange = () => {
    344      states.push(pc1.signalingState);
    345      signalingstatechangeResolver.resolve();
    346    };
    347    const stream1 = await getNoiseStream({audio: true});
    348    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    349    pc1.addTrack(stream1.getTracks()[0], stream1);
    350 
    351    const stream2 = await getNoiseStream({audio: true});
    352    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    353    pc2.addTrack(stream2.getTracks()[0], stream2);
    354 
    355    await pc1.setLocalDescription(await pc1.createOffer());
    356    pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event in this test"));
    357    await pc1.setRemoteDescription(await pc2.createOffer());
    358    await pc1.setLocalDescription(await pc1.createAnswer());
    359    assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']);
    360    await new Promise(r => t.step_timeout(r, 0));
    361  }, "when using addTrack, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded");
    362 
    363  promise_test(async t => {
    364    const pc1 = new RTCPeerConnection();
    365    const pc2 = new RTCPeerConnection();
    366    t.add_cleanup(() => pc1.close());
    367    t.add_cleanup(() => pc2.close());
    368 
    369    const stream = await getNoiseStream({audio: true});
    370    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    371    pc1.addTrack(stream.getTracks()[0], stream);
    372 
    373    await pc1.setLocalDescription(await pc1.createOffer());
    374    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    375 
    376    await pc2.setLocalDescription(await pc2.createAnswer());
    377    await pc1.setRemoteDescription(pc2.localDescription);
    378 
    379    // In stable state add video on both end and make sure video transceiver is not killed.
    380 
    381    const stream1 = await getNoiseStream({video: true});
    382    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    383    pc1.addTrack(stream1.getTracks()[0], stream1);
    384    await pc1.setLocalDescription(await pc1.createOffer());
    385 
    386    const stream2 = await getNoiseStream({video: true});
    387    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    388    pc2.addTrack(stream2.getTracks()[0], stream2);
    389    const offer2 = await pc2.createOffer();
    390    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    391    await pc2.setRemoteDescription({type: "rollback"});
    392    assert_equals(pc2.getTransceivers().length, 2);
    393    await pc2.setLocalDescription(offer2);
    394  }, "rollback of a remote offer to negotiated stable state should enable " +
    395     "applying of a local offer");
    396 
    397  promise_test(async t => {
    398    const pc1 = new RTCPeerConnection();
    399    const pc2 = new RTCPeerConnection();
    400    t.add_cleanup(() => pc1.close());
    401    t.add_cleanup(() => pc2.close());
    402 
    403    const stream = await getNoiseStream({audio: true});
    404    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    405    pc1.addTrack(stream.getTracks()[0], stream);
    406 
    407    await pc1.setLocalDescription(await pc1.createOffer());
    408    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    409 
    410    await pc2.setLocalDescription(await pc2.createAnswer());
    411    await pc1.setRemoteDescription(pc2.localDescription);
    412 
    413    // Both ends want to add video at the same time. pc2 rolls back.
    414 
    415    const stream2 = await getNoiseStream({video: true});
    416    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    417    pc2.addTrack(stream2.getTracks()[0], stream2);
    418    await pc2.setLocalDescription(await pc2.createOffer());
    419    assert_equals(pc2.getTransceivers().length, 2);
    420    assert_not_equals(pc2.getTransceivers()[1].sender.transport, null);
    421    await pc2.setLocalDescription({type: "rollback"});
    422    assert_equals(pc2.getTransceivers().length, 2);
    423    // Rollback didn't touch audio transceiver and transport is intact.
    424    assert_not_equals(pc2.getTransceivers()[0].sender.transport, null);
    425    // Video transport got killed.
    426    assert_equals(pc2.getTransceivers()[1].sender.transport, null);
    427    const stream1 = await getNoiseStream({video: true});
    428    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    429    pc1.addTrack(stream1.getTracks()[0], stream1);
    430    await pc1.setLocalDescription(await pc1.createOffer());
    431    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    432  }, "rollback of a local offer to negotiated stable state should enable " +
    433     "applying of a remote offer");
    434 
    435  promise_test(async t => {
    436    const pc1 = new RTCPeerConnection();
    437    const pc2 = new RTCPeerConnection();
    438    t.add_cleanup(() => pc1.close());
    439    t.add_cleanup(() => pc2.close());
    440 
    441    const stream = await getNoiseStream({audio: true});
    442    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    443    pc1.addTrack(stream.getTracks()[0], stream);
    444 
    445    await pc1.setLocalDescription(await pc1.createOffer());
    446    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    447 
    448    await pc2.setLocalDescription(await pc2.createAnswer());
    449    await pc1.setRemoteDescription(pc2.localDescription);
    450 
    451    // pc1 adds video and pc2 adds audio. pc2 rolls back.
    452    assert_equals(pc2.getTransceivers()[0].direction, "recvonly");
    453    const stream2 = await getNoiseStream({audio: true});
    454    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    455    pc2.addTrack(stream2.getTracks()[0], stream2);
    456    assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
    457    await pc2.setLocalDescription(await pc2.createOffer());
    458    assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
    459    await pc2.setLocalDescription({type: "rollback"});
    460    assert_equals(pc2.getTransceivers().length, 1);
    461    // setLocalDescription didn't change direction. So direction remains "sendrecv"
    462    assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
    463    // Rollback didn't touch audio transceiver and transport is intact. Still can receive audio.
    464    assert_not_equals(pc2.getTransceivers()[0].receiver.transport, null);
    465    const stream1 = await getNoiseStream({video: true});
    466    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    467    pc1.addTrack(stream1.getTracks()[0], stream1);
    468    await pc1.setLocalDescription(await pc1.createOffer());
    469    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    470  }, "rollback a local offer with audio direction change to negotiated " +
    471     "stable state and then add video receiver");
    472 
    473  promise_test(async t => {
    474    const pc1 = new RTCPeerConnection();
    475    const pc2 = new RTCPeerConnection();
    476    t.add_cleanup(() => pc1.close());
    477    t.add_cleanup(() => pc2.close());
    478 
    479    pc1.addTransceiver('video', {direction: 'sendonly'});
    480    pc2.addTransceiver('video', {direction: 'sendonly'});
    481    await pc1.setLocalDescription(await pc1.createOffer());
    482    const pc1FirstMid = pc1.getTransceivers()[0].mid;
    483    await pc2.setLocalDescription(await pc2.createOffer());
    484    const pc2FirstMid = pc2.getTransceivers()[0].mid;
    485    // I don't think it is mandated that this has to be true, but any implementation I know of would
    486    // have predictable mids (e.g. 0, 1, 2...) so pc1 and pc2 should offer with the same mids.
    487    assert_equals(pc1FirstMid, pc2FirstMid);
    488    await pc1.setRemoteDescription(pc2.pendingLocalDescription);
    489    // We've implicitly rolled back and the SRD caused a second transceiver to be created.
    490    // As such, the first transceiver's mid will now be null, and the second transceiver's mid will
    491    // match the remote offer.
    492    assert_equals(pc1.getTransceivers().length, 2);
    493    assert_equals(pc1.getTransceivers()[0].mid, null);
    494    assert_equals(pc1.getTransceivers()[1].mid, pc2FirstMid);
    495    // If we now do an offer the first transceiver will get a different mid than in the first
    496    // pc1.createOffer()!
    497    pc1.setLocalDescription(await pc1.createAnswer());
    498    await pc1.setLocalDescription(await pc1.createOffer());
    499    assert_not_equals(pc1.getTransceivers()[0].mid, pc1FirstMid);
    500  }, "two transceivers with same mids");
    501 
    502  promise_test(async t => {
    503    const pc1 = new RTCPeerConnection();
    504    const pc2 = new RTCPeerConnection();
    505    t.add_cleanup(() => pc1.close());
    506    t.add_cleanup(() => pc2.close());
    507    const stream = await getNoiseStream({audio: true, video: true});
    508    t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
    509    const audio = stream.getAudioTracks()[0];
    510    pc1.addTrack(audio, stream);
    511    const video = stream.getVideoTracks()[0];
    512    pc1.addTrack(video, stream);
    513 
    514    let remoteStream = null;
    515    pc2.ontrack = e => { remoteStream = e.streams[0]; }
    516    await pc2.setRemoteDescription(await pc1.createOffer());
    517    assert_true(remoteStream != null);
    518    let remoteTracks = remoteStream.getTracks();
    519    const removedTracks = [];
    520    remoteStream.onremovetrack = e => { removedTracks.push(e.track.id); }
    521    await pc2.setRemoteDescription({type: "rollback"});
    522    assert_equals(removedTracks.length, 2,
    523                  "Rollback should have removed two tracks");
    524    assert_true(removedTracks.includes(remoteTracks[0].id),
    525                "First track should be removed");
    526    assert_true(removedTracks.includes(remoteTracks[1].id),
    527                "Second track should be removed");
    528 
    529  }, "onremovetrack fires during remote rollback");
    530 
    531  promise_test(async t => {
    532    const pc1 = new RTCPeerConnection();
    533    t.add_cleanup(() => pc1.close());
    534    const pc2 = new RTCPeerConnection();
    535    t.add_cleanup(() => pc2.close());
    536 
    537    const stream1 = await getNoiseStream({audio: true});
    538    t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
    539    pc1.addTrack(stream1.getTracks()[0], stream1);
    540 
    541    const offer1 = await pc1.createOffer();
    542 
    543    const remoteStreams = [];
    544    pc2.ontrack = e => { remoteStreams.push(e.streams[0]); }
    545 
    546    await pc1.setLocalDescription(offer1);
    547    await pc2.setRemoteDescription(pc1.pendingLocalDescription);
    548    await pc2.setLocalDescription(await pc2.createAnswer());
    549    await pc1.setRemoteDescription(pc2.localDescription);
    550 
    551    assert_equals(remoteStreams.length, 1, "Number of remote streams");
    552    assert_equals(remoteStreams[0].getTracks().length, 1, "Number of remote tracks");
    553    const track = remoteStreams[0].getTracks()[0];
    554 
    555    const stream2 = await getNoiseStream({audio: true});
    556    t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
    557    pc1.getTransceivers()[0].sender.setStreams(stream2);
    558 
    559    const offer2 = await pc1.createOffer();
    560    await pc2.setRemoteDescription(offer2);
    561 
    562    assert_equals(remoteStreams.length, 2);
    563    assert_equals(remoteStreams[0].getTracks().length, 0);
    564    assert_equals(remoteStreams[1].getTracks()[0].id, track.id);
    565    await pc2.setRemoteDescription({type: "rollback"});
    566    assert_equals(remoteStreams.length, 3);
    567    assert_equals(remoteStreams[0].id, remoteStreams[2].id);
    568    assert_equals(remoteStreams[1].getTracks().length, 0);
    569    assert_equals(remoteStreams[2].getTracks().length, 1);
    570    assert_equals(remoteStreams[2].getTracks()[0].id, track.id);
    571 
    572  }, "rollback of a remote offer with stream changes");
    573 
    574  promise_test(async t => {
    575    const pc1 = new RTCPeerConnection();
    576    t.add_cleanup(() => pc1.close());
    577    const pc2 = new RTCPeerConnection();
    578    t.add_cleanup(() => pc2.close());
    579    pc2.addTransceiver('audio');
    580    const offer = await pc2.createOffer();
    581    await pc1.setRemoteDescription(offer);
    582    const [transceiver] = pc1.getTransceivers();
    583    pc1.setRemoteDescription({type:'rollback'});
    584    pc1.removeTrack(transceiver.sender);
    585  }, 'removeTrack() with a sender being rolled back does not crash or throw');
    586 
    587  promise_test(async t => {
    588    const pc1 = new RTCPeerConnection();
    589    t.add_cleanup(() => pc1.close());
    590    const pc2 = new RTCPeerConnection();
    591    t.add_cleanup(() => pc2.close());
    592    pc1.addTransceiver('video');
    593    const channel = pc2.createDataChannel('dummy');
    594    await pc2.setLocalDescription(await pc2.createOffer());
    595    await pc2.setRemoteDescription(await pc1.createOffer());
    596    assert_equals(pc2.signalingState, 'have-remote-offer');
    597    await pc2.setLocalDescription(await pc2.createAnswer());
    598    await pc2.setLocalDescription(await pc2.createOffer());
    599    assert_equals(channel.readyState, 'connecting');
    600  }, 'Implicit rollback with only a datachannel works');
    601 
    602 </script>