https://github.com/web-platform-tests/wpt
Raw File
Tip revision: 97317790d486c4b239e1e4f9942243a1991dce79 authored by Frédéric Wang on 14 March 2018, 14:14:29 UTC
use requestAnimationFrame instead of setTimeout(..., 0)
Tip revision: 9731779
RTCPeerConnection-addIceCandidate.html
<!doctype html>
<title>Test RTCPeerConnection.prototype.addIceCandidate</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
  'use strict';

  // Test is based on the following editor draft:
  // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.htm

  /*
    4.3.2.  Interface Definition
      interface RTCPeerConnection : EventTarget {
        ...
        Promise<void> addIceCandidate((RTCIceCandidateInit or RTCIceCandidate) candidate);
      };

      interface RTCIceCandidate {
        readonly attribute DOMString               candidate;
        readonly attribute DOMString?              sdpMid;
        readonly attribute unsigned short?         sdpMLineIndex;
        readonly attribute DOMString?              ufrag;
        ...
      };

      dictionary RTCIceCandidateInit {
        DOMString       candidate = "";
        DOMString?      sdpMid = null;
        unsigned short? sdpMLineIndex = null;
        DOMString       ufrag;
      };
   */

  // SDP copied from JSEP Example 7.1
  // It contains two media streams with different ufrags
  // to test if candidate is added to the correct stream
  const sdp = `v=0
o=- 4962303333179871722 1 IN IP4 0.0.0.0
s=-
t=0 0
a=ice-options:trickle
a=group:BUNDLE a1 v1
a=group:LS a1 v1
m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
c=IN IP4 203.0.113.100
a=mid:a1
a=sendrecv
a=rtpmap:96 opus/48000/2
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:97 telephone-event/8000
a=rtpmap:98 telephone-event/48000
a=maxptime:120
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
a=ice-ufrag:ETEn
a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
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
a=setup:actpass
a=dtls-id:1
a=rtcp:10101 IN IP4 203.0.113.100
a=rtcp-mux
a=rtcp-rsize
m=video 10102 UDP/TLS/RTP/SAVPF 100 101
c=IN IP4 203.0.113.100
a=mid:v1
a=sendrecv
a=rtpmap:100 VP8/90000
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
a=ice-ufrag:BGKk
a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
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
a=setup:actpass
a=dtls-id:1
a=rtcp:10103 IN IP4 203.0.113.100
a=rtcp-mux
a=rtcp-rsize
`;

  const sessionDesc = { type: 'offer', sdp };

  // valid candidate attributes
  const sdpMid = 'a1';
  const sdpMLineIndex = 0;
  const ufrag = 'ETEn';

  const sdpMid2 = 'v1';
  const sdpMLineIndex2 = 1;
  const ufrag2 = 'BGKk';

  const mediaLine1 = 'm=audio';
  const mediaLine2 = 'm=video';

  const candidateStr1 = 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
  const candidateStr2 = 'candidate:1 2 udp 2113929470 203.0.113.100 10101 typ host';
  const invalidCandidateStr = '(Invalid) candidate \r\n string';

  const candidateLine1 = `a=${candidateStr1}`;
  const candidateLine2 = `a=${candidateStr2}`;
  const endOfCandidateLine = 'a=end-of-candidates';

  // Copied from MDN
  function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  // Check that a candidate line is found after the first media line
  // but before the second, i.e. it belongs to the first media stream
  function assert_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
    const line1 = escapeRegExp(beforeMediaLine);
    const line2 = escapeRegExp(candidateLine);
    const line3 = escapeRegExp(afterMediaLine);

    const regex = new RegExp(`${line1}[^]+${line2}[^]+${line3}`);

    assert_true(regex.test(sdp),
      `Expect candidate line to be found between media lines ${beforeMediaLine} and ${afterMediaLine}`);
  }

  // Check that a candidate line is found after the second media line
  // i.e. it belongs to the second media stream
  function assert_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
    const line1 = escapeRegExp(beforeMediaLine);
    const line2 = escapeRegExp(candidateLine);

    const regex = new RegExp(`${line1}[^]+${line2}`);

    assert_true(regex.test(sdp),
      `Expect candidate line to be found after media line ${beforeMediaLine}`);
  }

  // Reject because WebIDL for addIceCandidate does not allow null argument
  // null can be accidentally passed from onicecandidate event handler
  // when null is used to indicate end of candidate
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate(null)));
  }, 'Add null candidate should reject with TypeError');

  /*
    4.3.2.  addIceCandidate
      4.  Return the result of enqueuing the following steps:
        1.  If remoteDescription is null return a promise rejected with a
            newly created InvalidStateError.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return promise_rejects(t, 'InvalidStateError',
      pc.addIceCandidate({
        candidate: candidateStr1,
        sdpMid, sdpMLineIndex, ufrag
      }));
  }, 'Add ICE candidate before setting remote description should reject with InvalidStateError');

  /*
    Success cases
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex, ufrag
    }));
  }, 'Add ICE candidate after setting remote description should succeed');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate(new RTCIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex, ufrag
    })));
  }, 'Add ICE candidate with RTCIceCandidate should succeed');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
      .then(() => pc.addIceCandidate({ sdpMid }));
  }, 'Add candidate with only valid sdpMid should succeed');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
      .then(() => pc.addIceCandidate({ sdpMLineIndex }));
  }, 'Add candidate with only valid sdpMLineIndex should succeed');

  /*
    4.3.2.  addIceCandidate
      4.6.2.  If candidate is applied successfully, the user agent MUST queue
              a task that runs the following steps:
        2.  Let remoteDescription be connection's pendingRemoteDescription
            if not null, otherwise connection's currentRemoteDescription.
        3.  Add candidate to remoteDescription.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex, ufrag
    }))
    .then(() => {
      assert_candidate_line_between(pc.remoteDescription.sdp,
        mediaLine1, candidateLine1, mediaLine2);
    });
  }, 'addIceCandidate with first sdpMid and sdpMLineIndex add candidate to first media stream');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr2,
      sdpMid: sdpMid2,
      sdpMLineIndex: sdpMLineIndex2,
      ufrag: ufrag2
    }))
    .then(() => {
      assert_candidate_line_after(pc.remoteDescription.sdp,
        mediaLine2, candidateLine2);
    });
  }, 'addIceCandidate with second sdpMid and sdpMLineIndex should add candidate to second media stream');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex,
      ufrag: null
    }))
    .then(() => {
      assert_candidate_line_between(pc.remoteDescription.sdp,
        mediaLine1, candidateLine1, mediaLine2);
    });
  }, 'Add candidate for first media stream with null ufrag should add candidate to first media stream');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex, ufrag
    }))
    .then(() => pc.addIceCandidate({
      candidate: candidateStr2,
      sdpMid: sdpMid2,
      sdpMLineIndex: sdpMLineIndex2,
      ufrag: ufrag2
    }))
    .then(() => {
      assert_candidate_line_between(pc.remoteDescription.sdp,
        mediaLine1, candidateLine1, mediaLine2);

      assert_candidate_line_after(pc.remoteDescription.sdp,
        mediaLine2, candidateLine2);
    });
  }, 'Adding multiple candidates should add candidates to their corresponding media stream');

  /*
    4.3.2.  addIceCandidate
      4.6.  If candidate.candidate is an empty string, process candidate as an
            end-of-candidates indication for the corresponding media description
            and ICE candidate generation.
        2.  If candidate is applied successfully, the user agent MUST queue
            a task that runs the following steps:
          2.  Let remoteDescription be connection's pendingRemoteDescription
              if not null, otherwise connection's currentRemoteDescription.
          3.  Add candidate to remoteDescription.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid, sdpMLineIndex, ufrag
    }))
    .then(() => pc.addIceCandidate({
      candidate: '',
      sdpMid, sdpMLineIndex,
      ufrag
    }))
    .then(() => {
      assert_candidate_line_between(pc.remoteDescription.sdp,
        mediaLine1, candidateLine1, mediaLine2);

      assert_candidate_line_between(pc.remoteDescription.sdp,
        mediaLine1, endOfCandidateLine, mediaLine2);
    });
  }, 'Add with empty candidate string (end of candidate) should succeed');

  /*
    4.3.2.  addIceCandidate
      3.  If both sdpMid and sdpMLineIndex are null, return a promise rejected
          with a newly created TypeError.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate({
          candidate: candidateStr1,
          sdpMid: null,
          sdpMLineIndex: null
        })));
  }, 'Add candidate with both sdpMid and sdpMLineIndex manually set to null should reject with TypeError');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate({
          candidate: candidateStr1
        })));
  }, 'Add candidate with only valid candidate string should reject with TypeError');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate({
          candidate: invalidCandidateStr,
          sdpMid: null,
          sdpMLineIndex: null
        })));
  }, 'Add candidate with invalid candidate string and both sdpMid and sdpMLineIndex null should reject with TypeError');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate({})));
  }, 'Add candidate with empty dict should reject with TypeError');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, new TypeError(),
        pc.addIceCandidate({
          candidate: '',
          sdpMid: null,
          sdpMLineIndex: null,
          ufrag: undefined
        })));
  }, 'Add candidate with manually filled default values should reject with TypeError');

  /*
    4.3.2.  addIceCandidate
      4.3.  If candidate.sdpMid is not null, run the following steps:
        1.  If candidate.sdpMid is not equal to the mid of any media
            description in remoteDescription , reject p with a newly
            created OperationError and abort these steps.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, 'OperationError',
        pc.addIceCandidate({
          candidate: candidateStr1,
          sdpMid: 'invalid', sdpMLineIndex, ufrag
        })));
  }, 'Add candidate with invalid sdpMid should reject with OperationError');

  /*
    4.3.2.  addIceCandidate
      4.4.  Else, if candidate.sdpMLineIndex is not null, run the following
          steps:
        1.  If candidate.sdpMLineIndex is equal to or larger than the
            number of media descriptions in remoteDescription , reject p
            with a newly created OperationError and abort these steps.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, 'OperationError',
        pc.addIceCandidate({
          candidate: candidateStr1,
          sdpMLineIndex: 2,
          ufrag
        })));
  }, 'Add candidate with invalid sdpMLineIndex should reject with OperationError');

  // There is an "Else" for the statement:
  // "Else, if candidate.sdpMLineIndex is not null, ..."
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr1,
      sdpMid,
      sdpMLineIndex: 2,
      ufrag
    }));
  }, 'Invalid sdpMLineIndex should be ignored if valid sdpMid is provided');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() => pc.addIceCandidate({
      candidate: candidateStr2,
      sdpMid: sdpMid2,
      sdpMLineIndex: sdpMLineIndex2,
      ufrag: null
    }))
    .then(() => {
      assert_candidate_line_after(pc.remoteDescription.sdp,
        mediaLine2, candidateLine2);
    });
  }, 'Add candidate for media stream 2 with null ufrag should succeed');

  /*
    4.3.2.  addIceCandidate
      4.5.  If candidate.ufrag is neither undefined nor null, and is not equal
            to any ufrag present in the corresponding media description of an
            applied remote description, reject p with a newly created
            OperationError and abort these steps.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, 'OperationError',
        pc.addIceCandidate({
          candidate: candidateStr1,
          sdpMid, sdpMLineIndex,
          ufrag: 'invalid'
        })));
  }, 'Add candidate with invalid ufrag should reject with OperationError');

  /*
    4.3.2.  addIceCandidate
      4.6.1.  If candidate could not be successfully added the user agent MUST
             queue a task that runs the following steps:
        2.  Reject p with a DOMException object whose name attribute has
            the value OperationError and abort these steps.
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, 'OperationError',
        pc.addIceCandidate({
          candidate: invalidCandidateStr,
          sdpMid, sdpMLineIndex, ufrag
        })));
  }, 'Add candidate with invalid candidate string should reject with OperationError');

  promise_test(t => {
    const pc = new RTCPeerConnection();

    return pc.setRemoteDescription(sessionDesc)
    .then(() =>
      promise_rejects(t, 'OperationError',
        pc.addIceCandidate({
          candidate: candidateStr2,
          sdpMid: sdpMid2,
          sdpMLineIndex: sdpMLineIndex2,
          ufrag: ufrag
        })));
  }, 'Add candidate with sdpMid belonging to different ufrag should reject with OperationError');

  /*
    TODO
    4.3.2.  addIceCandidate
      4.6.  In parallel, add the ICE candidate candidate as described in [JSEP]
            (section 4.1.17.). Use candidate.ufrag to identify the ICE generation;

            If the ufrag is null, process the candidate for the most recent ICE
            generation.

    - Call with candidate string containing partial malformed syntax, i.e. malformed IP.
      Some browsers may ignore the syntax error and add it to the SDP regardless.

    Non-Testable
    4.3.2.  addIceCandidate
      4.6.  (The steps are non-testable because the abort step in enqueue operation
            steps in before they can reach here):
        1.  If candidate could not be successfully added the user agent MUST
            queue a task that runs the following steps:
          1.  If connection's [[isClosed]] slot is true, then abort
              these steps.

        2.  If candidate is applied successfully, the user agent MUST queue
            a task that runs the following steps:
          1.  If connection's [[isClosed]] slot is true, then abort these steps.

    Issues
      w3c/webrtc-pc#1213
        addIceCandidate end of candidates woes

      w3c/webrtc-pc#1216
        Clarify addIceCandidate behavior when adding candidate after end of candidate

     w3c/webrtc-pc#1227
        addIceCandidate may add ice candidate to the wrong remote description

      w3c/webrtc-pc#1345
        Make promise rejection/enqueing consistent

    Coverage Report
      Total:        23
      Tested:       19
      Not Tested:    2
      Non-Testable:  2
   */
</script>
back to top