tor-browser

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

RTCPeerConnection-addIceCandidate.html (22642B)


      1 <!doctype html>
      2 <title>Test RTCPeerConnection.prototype.addIceCandidate</title>
      3 <script src="/resources/testharness.js"></script>
      4 <script src="/resources/testharnessreport.js"></script>
      5 <script src="RTCPeerConnection-helper.js"></script>
      6 <script>
      7  'use strict';
      8 
      9  // SDP copied from JSEP Example 7.1
     10  // It contains two media streams with different ufrags
     11  // to test if candidate is added to the correct stream
     12  const sdp = `v=0
     13 o=- 4962303333179871722 1 IN IP4 0.0.0.0
     14 s=-
     15 t=0 0
     16 a=ice-options:trickle
     17 a=group:BUNDLE a1 v1
     18 a=group:LS a1 v1
     19 m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
     20 c=IN IP4 203.0.113.100
     21 a=mid:a1
     22 a=sendrecv
     23 a=rtpmap:96 opus/48000/2
     24 a=rtpmap:0 PCMU/8000
     25 a=rtpmap:8 PCMA/8000
     26 a=rtpmap:97 telephone-event/8000
     27 a=rtpmap:98 telephone-event/48000
     28 a=maxptime:120
     29 a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
     30 a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
     31 a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
     32 a=ice-ufrag:ETEn
     33 a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
     34 a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
     35 a=setup:actpass
     36 a=dtls-id:1
     37 a=rtcp:10101 IN IP4 203.0.113.100
     38 a=rtcp-mux
     39 a=rtcp-rsize
     40 m=video 10102 UDP/TLS/RTP/SAVPF 100 101
     41 c=IN IP4 203.0.113.100
     42 a=mid:v1
     43 a=sendrecv
     44 a=rtpmap:100 VP8/90000
     45 a=rtpmap:101 rtx/90000
     46 a=fmtp:101 apt=100
     47 a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
     48 a=rtcp-fb:100 ccm fir
     49 a=rtcp-fb:100 nack
     50 a=rtcp-fb:100 nack pli
     51 a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
     52 a=ice-ufrag:BGKk
     53 a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
     54 a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
     55 a=setup:actpass
     56 a=dtls-id:1
     57 a=rtcp:10103 IN IP4 203.0.113.100
     58 a=rtcp-mux
     59 a=rtcp-rsize
     60 `;
     61 
     62  const sessionDesc = { type: 'offer', sdp };
     63 
     64  // valid candidate attributes
     65  const sdpMid1 = 'a1';
     66  const sdpMLineIndex1 = 0;
     67  const usernameFragment1 = 'ETEn';
     68 
     69  const sdpMid2 = 'v1';
     70  const sdpMLineIndex2 = 1;
     71  const usernameFragment2 = 'BGKk';
     72 
     73  const mediaLine1 = 'm=audio';
     74  const mediaLine2 = 'm=video';
     75 
     76  const candidateStr1 = 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
     77  const candidateStr2 = 'candidate:1 2 udp 2113929470 203.0.113.100 10101 typ host';
     78  const invalidCandidateStr = '(Invalid) candidate \r\n string';
     79 
     80  const candidateLine1 = `a=${candidateStr1}`;
     81  const candidateLine2 = `a=${candidateStr2}`;
     82  const endOfCandidateLine = 'a=end-of-candidates';
     83 
     84  // Copied from MDN
     85  function escapeRegExp(string) {
     86    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
     87  }
     88 
     89  function is_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
     90    const line1 = escapeRegExp(beforeMediaLine);
     91    const line2 = escapeRegExp(candidateLine);
     92    const line3 = escapeRegExp(afterMediaLine);
     93 
     94    const regex = new RegExp(`${line1}[^]+${line2}[^]+${line3}`);
     95    return regex.test(sdp);
     96  }
     97 
     98  // Check that a candidate line is found after the first media line
     99  // but before the second, i.e. it belongs to the first media stream
    100  function assert_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
    101    assert_true(is_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine),
    102      `Expect candidate line to be found between media lines ${beforeMediaLine} and ${afterMediaLine}`);
    103  }
    104 
    105  // Check that a candidate line is found after the second media line
    106  // i.e. it belongs to the second media stream
    107  function is_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
    108    const line1 = escapeRegExp(beforeMediaLine);
    109    const line2 = escapeRegExp(candidateLine);
    110 
    111    const regex = new RegExp(`${line1}[^]+${line2}`);
    112 
    113    return regex.test(sdp);
    114  }
    115 
    116  function assert_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
    117    assert_true(is_candidate_line_after(sdp, beforeMediaLine, candidateLine),
    118      `Expect candidate line to be found after media line ${beforeMediaLine}`);
    119  }
    120 
    121  /*
    122    4.4.2.  addIceCandidate
    123      4.  Return the result of enqueuing the following steps:
    124        1.  If remoteDescription is null return a promise rejected with a
    125            newly created InvalidStateError.
    126   */
    127  promise_test(t => {
    128    const pc = new RTCPeerConnection();
    129 
    130    t.add_cleanup(() => pc.close());
    131 
    132    return promise_rejects_dom(t, 'InvalidStateError',
    133      pc.addIceCandidate({
    134        candidate: candidateStr1,
    135        sdpMid: sdpMid1,
    136        sdpMLineIndex: sdpMLineIndex1,
    137        usernameFragment: usernameFragment1
    138      }));
    139  }, 'Add ICE candidate before setting remote description should reject with InvalidStateError');
    140 
    141  promise_test(t => {
    142    const pc = new RTCPeerConnection();
    143    pc.setRemoteDescription(sessionDesc);
    144    pc.close();
    145    return promise_rejects_dom(t, 'InvalidStateError',
    146      pc.addIceCandidate({
    147        candidate: candidateStr1,
    148        sdpMid: sdpMid1,
    149        sdpMLineIndex: sdpMLineIndex1,
    150        usernameFragment: usernameFragment1
    151      }));
    152  }, 'addIceCandidate after close should reject with InvalidStateError');
    153 
    154  /*
    155    Success cases
    156   */
    157 
    158  // All of these should work, because all of these end up being equivalent to the
    159  // same thing; an end-of-candidates signal for all levels/mids/ufrags.
    160  [
    161    // This is just the default. Everything else here is equivalent to this.
    162    {
    163      candidate: '',
    164      sdpMid: null,
    165      sdpMLineIndex: null,
    166      usernameFragment: undefined
    167    },
    168    // The arg is optional, so when passing undefined we'll just get the default
    169    undefined,
    170    // The arg is optional, but not nullable, so we get the default again
    171    null,
    172    // Members in the dictionary take their default values
    173    {}
    174  ].forEach(init => {
    175    promise_test(async t => {
    176      const pc = new RTCPeerConnection();
    177 
    178      t.add_cleanup(() => pc.close());
    179 
    180      await pc.setRemoteDescription(sessionDesc);
    181      await pc.addIceCandidate(init);
    182    }, `addIceCandidate(${JSON.stringify(init)}) works`);
    183    promise_test(async t => {
    184      const pc = new RTCPeerConnection();
    185 
    186      t.add_cleanup(() => pc.close());
    187 
    188      await pc.setRemoteDescription(sessionDesc);
    189      await pc.addIceCandidate(init);
    190      assert_candidate_line_between(pc.remoteDescription.sdp,
    191        mediaLine1, endOfCandidateLine, mediaLine2);
    192      assert_candidate_line_after(pc.remoteDescription.sdp,
    193        mediaLine2, endOfCandidateLine);
    194    }, `addIceCandidate(${JSON.stringify(init)}) adds a=end-of-candidates to both m-sections`);
    195  });
    196 
    197  promise_test(async t => {
    198    const pc = new RTCPeerConnection();
    199    t.add_cleanup(() => pc.close());
    200    await pc.setRemoteDescription(sessionDesc);
    201    await pc.setLocalDescription(await pc.createAnswer());
    202    await pc.addIceCandidate({});
    203    assert_candidate_line_between(pc.remoteDescription.sdp,
    204      mediaLine1, endOfCandidateLine, mediaLine2);
    205    assert_candidate_line_after(pc.remoteDescription.sdp,
    206      mediaLine2, endOfCandidateLine);
    207  }, 'addIceCandidate({}) in stable should work, and add a=end-of-candidates to both m-sections');
    208 
    209  promise_test(async t => {
    210    const pc = new RTCPeerConnection();
    211 
    212    t.add_cleanup(() => pc.close());
    213 
    214    await pc.setRemoteDescription(sessionDesc);
    215    await pc.addIceCandidate({
    216      usernameFragment: usernameFragment1,
    217      sdpMid: sdpMid1
    218    });
    219    assert_candidate_line_between(pc.remoteDescription.sdp,
    220      mediaLine1, endOfCandidateLine, mediaLine2);
    221    assert_false(is_candidate_line_after(pc.remoteDescription.sdp,
    222      mediaLine2, endOfCandidateLine));
    223  }, 'addIceCandidate({usernameFragment: usernameFragment1, sdpMid: sdpMid1}) should work, and add a=end-of-candidates to the first m-section');
    224 
    225  promise_test(async t => {
    226    const pc = new RTCPeerConnection();
    227 
    228    t.add_cleanup(() => pc.close());
    229 
    230    await pc.setRemoteDescription(sessionDesc);
    231    await pc.addIceCandidate({
    232      usernameFragment: usernameFragment2,
    233      sdpMLineIndex: 1
    234    });
    235    assert_false(is_candidate_line_between(pc.remoteDescription.sdp,
    236      mediaLine1, endOfCandidateLine, mediaLine2));
    237    assert_true(is_candidate_line_after(pc.remoteDescription.sdp,
    238      mediaLine2, endOfCandidateLine));
    239  }, 'addIceCandidate({usernameFragment: usernameFragment2, sdpMLineIndex: 1}) should work, and add a=end-of-candidates to the first m-section');
    240 
    241  promise_test(async t => {
    242    const pc = new RTCPeerConnection();
    243 
    244    t.add_cleanup(() => pc.close());
    245 
    246    await pc.setRemoteDescription(sessionDesc);
    247    await promise_rejects_dom(t, 'OperationError',
    248      pc.addIceCandidate({usernameFragment: "no such ufrag"}));
    249  }, 'addIceCandidate({usernameFragment: "no such ufrag"}) should not work');
    250 
    251  promise_test(async t => {
    252    const pc = new RTCPeerConnection();
    253 
    254    t.add_cleanup(() => pc.close());
    255 
    256    await pc.setRemoteDescription(sessionDesc)
    257    await pc.addIceCandidate({
    258      candidate: candidateStr1,
    259      sdpMid: sdpMid1,
    260      sdpMLineIndex: sdpMLineIndex1,
    261      usernameFragement: usernameFragment1
    262    });
    263    assert_candidate_line_after(pc.remoteDescription.sdp,
    264                                mediaLine1, candidateStr1);
    265  }, 'Add ICE candidate after setting remote description should succeed');
    266 
    267  promise_test(t => {
    268    const pc = new RTCPeerConnection();
    269 
    270    t.add_cleanup(() => pc.close());
    271 
    272    return pc.setRemoteDescription(sessionDesc)
    273    .then(() => pc.addIceCandidate(new RTCIceCandidate({
    274      candidate: candidateStr1,
    275      sdpMid: sdpMid1,
    276      sdpMLineIndex: sdpMLineIndex1,
    277      usernameFragement: usernameFragment1
    278    })));
    279  }, 'Add ICE candidate with RTCIceCandidate should succeed');
    280 
    281  promise_test(t => {
    282    const pc = new RTCPeerConnection();
    283 
    284    t.add_cleanup(() => pc.close());
    285    return pc.setRemoteDescription(sessionDesc)
    286      .then(() => pc.addIceCandidate({
    287        candidate: candidateStr1,
    288        sdpMid: sdpMid1 }));
    289  }, 'Add candidate with only valid sdpMid should succeed');
    290 
    291  promise_test(t => {
    292    const pc = new RTCPeerConnection();
    293 
    294    t.add_cleanup(() => pc.close());
    295 
    296    return pc.setRemoteDescription(sessionDesc)
    297      .then(() => pc.addIceCandidate(new RTCIceCandidate({
    298        candidate: candidateStr1,
    299        sdpMid: sdpMid1 })));
    300  }, 'Add candidate with only valid sdpMid and RTCIceCandidate should succeed');
    301 
    302  promise_test(t => {
    303    const pc = new RTCPeerConnection();
    304 
    305    t.add_cleanup(() => pc.close());
    306 
    307    return pc.setRemoteDescription(sessionDesc)
    308      .then(() => pc.addIceCandidate({
    309        candidate: candidateStr1,
    310        sdpMLineIndex: sdpMLineIndex1 }));
    311  }, 'Add candidate with only valid sdpMLineIndex should succeed');
    312 
    313  /*
    314    4.4.2.  addIceCandidate
    315      4.6.2.  If candidate is applied successfully, the user agent MUST queue
    316              a task that runs the following steps:
    317        2.  If connection.pendingRemoteDescription is non-null, and represents
    318            the ICE generation for which candidate was processed, add
    319            candidate to connection.pendingRemoteDescription.
    320        3.  If connection.currentRemoteDescription is non-null, and represents
    321            the ICE generation for which candidate was processed, add
    322            candidate to connection.currentRemoteDescription.
    323   */
    324  promise_test(t => {
    325    const pc = new RTCPeerConnection();
    326 
    327    t.add_cleanup(() => pc.close());
    328 
    329    return pc.setRemoteDescription(sessionDesc)
    330    .then(() => pc.addIceCandidate({
    331      candidate: candidateStr1,
    332      sdpMid: sdpMid1,
    333      sdpMLineIndex: sdpMLineIndex1,
    334      usernameFragement: usernameFragment1
    335    }))
    336    .then(() => {
    337      assert_candidate_line_between(pc.remoteDescription.sdp,
    338        mediaLine1, candidateLine1, mediaLine2);
    339    });
    340  }, 'addIceCandidate with first sdpMid and sdpMLineIndex add candidate to first media stream');
    341 
    342  promise_test(t => {
    343    const pc = new RTCPeerConnection();
    344 
    345    t.add_cleanup(() => pc.close());
    346 
    347    return pc.setRemoteDescription(sessionDesc)
    348    .then(() => pc.addIceCandidate({
    349      candidate: candidateStr2,
    350      sdpMid: sdpMid2,
    351      sdpMLineIndex: sdpMLineIndex2,
    352      usernameFragment: usernameFragment2
    353    }))
    354    .then(() => {
    355      assert_candidate_line_after(pc.remoteDescription.sdp,
    356        mediaLine2, candidateLine2);
    357    });
    358  }, 'addIceCandidate with second sdpMid and sdpMLineIndex should add candidate to second media stream');
    359 
    360  promise_test(t => {
    361    const pc = new RTCPeerConnection();
    362 
    363    t.add_cleanup(() => pc.close());
    364 
    365    return pc.setRemoteDescription(sessionDesc)
    366    .then(() => pc.addIceCandidate({
    367      candidate: candidateStr1,
    368      sdpMid: sdpMid1,
    369      sdpMLineIndex: sdpMLineIndex1,
    370      usernameFragment: null
    371    }))
    372    .then(() => {
    373      assert_candidate_line_between(pc.remoteDescription.sdp,
    374        mediaLine1, candidateLine1, mediaLine2);
    375    });
    376  }, 'Add candidate for first media stream with null usernameFragment should add candidate to first media stream');
    377 
    378  promise_test(t => {
    379    const pc = new RTCPeerConnection();
    380 
    381    t.add_cleanup(() => pc.close());
    382 
    383    return pc.setRemoteDescription(sessionDesc)
    384    .then(() => pc.addIceCandidate({
    385      candidate: candidateStr1,
    386      sdpMid: sdpMid1,
    387      sdpMLineIndex: sdpMLineIndex1,
    388      usernameFragement: usernameFragment1
    389    }))
    390    .then(() => pc.addIceCandidate({
    391      candidate: candidateStr2,
    392      sdpMid: sdpMid2,
    393      sdpMLineIndex: sdpMLineIndex2,
    394      usernameFragment: usernameFragment2
    395    }))
    396    .then(() => {
    397      assert_candidate_line_between(pc.remoteDescription.sdp,
    398        mediaLine1, candidateLine1, mediaLine2);
    399 
    400      assert_candidate_line_after(pc.remoteDescription.sdp,
    401        mediaLine2, candidateLine2);
    402    });
    403  }, 'Adding multiple candidates should add candidates to their corresponding media stream');
    404 
    405  /*
    406    4.4.2.  addIceCandidate
    407      4.6.  If candidate.candidate is an empty string, process candidate as an
    408            end-of-candidates indication for the corresponding media description
    409            and ICE candidate generation.
    410        2.  If candidate is applied successfully, the user agent MUST queue
    411            a task that runs the following steps:
    412          2.  If connection.pendingRemoteDescription is non-null, and represents
    413              the ICE generation for which candidate was processed, add
    414              candidate to connection.pendingRemoteDescription.
    415          3.  If connection.currentRemoteDescription is non-null, and represents
    416              the ICE generation for which candidate was processed, add
    417              candidate to connection.currentRemoteDescription.
    418   */
    419  promise_test(t => {
    420    const pc = new RTCPeerConnection();
    421 
    422    t.add_cleanup(() => pc.close());
    423 
    424    return pc.setRemoteDescription(sessionDesc)
    425    .then(() => pc.addIceCandidate({
    426      candidate: candidateStr1,
    427      sdpMid: sdpMid1,
    428      sdpMLineIndex: sdpMLineIndex1,
    429      usernameFragement: usernameFragment1
    430    }))
    431    .then(() => pc.addIceCandidate({
    432      candidate: '',
    433      sdpMid: sdpMid1,
    434      sdpMLineIndex: sdpMLineIndex1,
    435      usernameFragement: usernameFragment1
    436    }))
    437    .then(() => {
    438      assert_candidate_line_between(pc.remoteDescription.sdp,
    439        mediaLine1, candidateLine1, mediaLine2);
    440 
    441      assert_candidate_line_between(pc.remoteDescription.sdp,
    442        mediaLine1, endOfCandidateLine, mediaLine2);
    443    });
    444  }, 'Add with empty candidate string (end of candidates) should succeed');
    445 
    446  /*
    447    4.4.2.  addIceCandidate
    448      3.  If both sdpMid and sdpMLineIndex are null, return a promise rejected
    449          with a newly created TypeError.
    450   */
    451  promise_test(t => {
    452    const pc = new RTCPeerConnection();
    453 
    454    t.add_cleanup(() => pc.close());
    455 
    456    return pc.setRemoteDescription(sessionDesc)
    457    .then(() =>
    458      promise_rejects_js(t, TypeError,
    459        pc.addIceCandidate({
    460          candidate: candidateStr1,
    461          sdpMid: null,
    462          sdpMLineIndex: null
    463        })));
    464  }, 'Add candidate with both sdpMid and sdpMLineIndex manually set to null should reject with TypeError');
    465 
    466  promise_test(async t => {
    467    const pc = new RTCPeerConnection();
    468    t.add_cleanup(() => pc.close());
    469 
    470    await pc.setRemoteDescription(sessionDesc);
    471    promise_rejects_js(t, TypeError,
    472      pc.addIceCandidate({candidate: candidateStr1}));
    473  }, 'addIceCandidate with a candidate and neither sdpMid nor sdpMLineIndex should reject with TypeError');
    474 
    475  promise_test(t => {
    476    const pc = new RTCPeerConnection();
    477 
    478    t.add_cleanup(() => pc.close());
    479 
    480    return pc.setRemoteDescription(sessionDesc)
    481    .then(() =>
    482      promise_rejects_js(t, TypeError,
    483        pc.addIceCandidate({
    484          candidate: candidateStr1
    485        })));
    486  }, 'Add candidate with only valid candidate string should reject with TypeError');
    487 
    488  promise_test(t => {
    489    const pc = new RTCPeerConnection();
    490 
    491    t.add_cleanup(() => pc.close());
    492 
    493    return pc.setRemoteDescription(sessionDesc)
    494    .then(() =>
    495      promise_rejects_js(t, TypeError,
    496        pc.addIceCandidate({
    497          candidate: invalidCandidateStr,
    498          sdpMid: null,
    499          sdpMLineIndex: null
    500        })));
    501  }, 'Add candidate with invalid candidate string and both sdpMid and sdpMLineIndex null should reject with TypeError');
    502 
    503  /*
    504    4.4.2.  addIceCandidate
    505      4.3.  If candidate.sdpMid is not null, run the following steps:
    506        1.  If candidate.sdpMid is not equal to the mid of any media
    507            description in remoteDescription , reject p with a newly
    508            created OperationError and abort these steps.
    509   */
    510  promise_test(t => {
    511    const pc = new RTCPeerConnection();
    512 
    513    t.add_cleanup(() => pc.close());
    514 
    515    return pc.setRemoteDescription(sessionDesc)
    516    .then(() =>
    517      promise_rejects_dom(t, 'OperationError',
    518        pc.addIceCandidate({
    519          candidate: candidateStr1,
    520          sdpMid: 'invalid',
    521          sdpMLineIndex: sdpMLineIndex1,
    522          usernameFragement: usernameFragment1
    523        })));
    524  }, 'Add candidate with invalid sdpMid should reject with OperationError');
    525 
    526  /*
    527    4.4.2.  addIceCandidate
    528      4.4.  Else, if candidate.sdpMLineIndex is not null, run the following
    529          steps:
    530        1.  If candidate.sdpMLineIndex is equal to or larger than the
    531            number of media descriptions in remoteDescription , reject p
    532            with a newly created OperationError and abort these steps.
    533   */
    534  promise_test(t => {
    535    const pc = new RTCPeerConnection();
    536 
    537    t.add_cleanup(() => pc.close());
    538 
    539    return pc.setRemoteDescription(sessionDesc)
    540    .then(() =>
    541      promise_rejects_dom(t, 'OperationError',
    542        pc.addIceCandidate({
    543          candidate: candidateStr1,
    544          sdpMLineIndex: 2,
    545          usernameFragement: usernameFragment1
    546        })));
    547  }, 'Add candidate with invalid sdpMLineIndex should reject with OperationError');
    548 
    549  // There is an "Else" for the statement:
    550  // "Else, if candidate.sdpMLineIndex is not null, ..."
    551  promise_test(t => {
    552    const pc = new RTCPeerConnection();
    553 
    554    t.add_cleanup(() => pc.close());
    555 
    556    return pc.setRemoteDescription(sessionDesc)
    557    .then(() => pc.addIceCandidate({
    558      candidate: candidateStr1,
    559      sdpMid: sdpMid1,
    560      sdpMLineIndex: 2,
    561      usernameFragement: usernameFragment1
    562    }));
    563  }, 'Invalid sdpMLineIndex should be ignored if valid sdpMid is provided');
    564 
    565  promise_test(t => {
    566    const pc = new RTCPeerConnection();
    567 
    568    t.add_cleanup(() => pc.close());
    569 
    570    return pc.setRemoteDescription(sessionDesc)
    571    .then(() => pc.addIceCandidate({
    572      candidate: candidateStr2,
    573      sdpMid: sdpMid2,
    574      sdpMLineIndex: sdpMLineIndex2,
    575      usernameFragment: null
    576    }))
    577    .then(() => {
    578      assert_candidate_line_after(pc.remoteDescription.sdp,
    579        mediaLine2, candidateLine2);
    580    });
    581  }, 'Add candidate for media stream 2 with null usernameFragment should succeed');
    582 
    583  /*
    584    4.3.2.  addIceCandidate
    585      4.5.  If candidate.usernameFragment is neither undefined nor null, and is not equal
    586            to any usernameFragment present in the corresponding media description of an
    587            applied remote description, reject p with a newly created
    588            OperationError and abort these steps.
    589   */
    590  promise_test(t => {
    591    const pc = new RTCPeerConnection();
    592 
    593    t.add_cleanup(() => pc.close());
    594 
    595    return pc.setRemoteDescription(sessionDesc)
    596    .then(() =>
    597      promise_rejects_dom(t, 'OperationError',
    598        pc.addIceCandidate({
    599          candidate: candidateStr1,
    600          sdpMid: sdpMid1,
    601          sdpMLineIndex: sdpMLineIndex1,
    602          usernameFragment: 'invalid'
    603        })));
    604  }, 'Add candidate with invalid usernameFragment should reject with OperationError');
    605 
    606  /*
    607    4.4.2.  addIceCandidate
    608      4.6.1.  If candidate could not be successfully added the user agent MUST
    609             queue a task that runs the following steps:
    610        2.  Reject p with a DOMException object whose name attribute has
    611            the value OperationError and abort these steps.
    612   */
    613  promise_test(t => {
    614    const pc = new RTCPeerConnection();
    615 
    616    t.add_cleanup(() => pc.close());
    617 
    618    return pc.setRemoteDescription(sessionDesc)
    619    .then(() =>
    620      promise_rejects_dom(t, 'OperationError',
    621        pc.addIceCandidate({
    622          candidate: invalidCandidateStr,
    623          sdpMid: sdpMid1,
    624          sdpMLineIndex: sdpMLineIndex1,
    625          usernameFragement: usernameFragment1
    626        })));
    627  }, 'Add candidate with invalid candidate string should reject with OperationError');
    628 
    629  promise_test(t => {
    630    const pc = new RTCPeerConnection();
    631 
    632    t.add_cleanup(() => pc.close());
    633 
    634    return pc.setRemoteDescription(sessionDesc)
    635    .then(() =>
    636      promise_rejects_dom(t, 'OperationError',
    637        pc.addIceCandidate({
    638          candidate: candidateStr2,
    639          sdpMid: sdpMid2,
    640          sdpMLineIndex: sdpMLineIndex2,
    641          usernameFragment: usernameFragment1
    642        })));
    643  }, 'Add candidate with sdpMid belonging to different usernameFragment should reject with OperationError');
    644 
    645  promise_test(async t => {
    646    const pc = new RTCPeerConnection();
    647    t.add_cleanup(() => pc.close());
    648    await pc.setRemoteDescription(sessionDesc);
    649    const recognized = [];
    650    await pc.addIceCandidate({
    651      candidate: candidateStr1,
    652      sdpMid: sdpMid1,
    653      get relayProtocol() {
    654        recognized.push("relayProtocol");
    655        return null;
    656      },
    657      get url() {
    658        recognized.push("url");
    659        return null;
    660      },
    661      get usernameFragment() {
    662        recognized.push("usernameFragment");
    663        return null;
    664      },
    665    });
    666    assert_array_equals(recognized, ['usernameFragment']);
    667  }, 'addIceCandidate should not recognize relayProtocol or url');
    668 </script>