blob: e55cc61b558a1d950a6f5291e2481d66e164946d [file] [log] [blame]
Soares Chenb20cf6b2017-05-19 12:28:431<!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>
6 'use strict';
7
Soares Chen25c23902017-06-27 15:32:578 // Test is based on the following editor draft:
9 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.htm
10
Soares Chen25c23902017-06-27 15:32:5711 /*
12 4.3.2. Interface Definition
13 interface RTCPeerConnection : EventTarget {
14 ...
15 Promise<void> addIceCandidate((RTCIceCandidateInit or RTCIceCandidate) candidate);
16 };
17
18 interface RTCIceCandidate {
19 readonly attribute DOMString candidate;
20 readonly attribute DOMString? sdpMid;
21 readonly attribute unsigned short? sdpMLineIndex;
22 readonly attribute DOMString? ufrag;
23 ...
24 };
25
26 dictionary RTCIceCandidateInit {
27 DOMString candidate = "";
28 DOMString? sdpMid = null;
29 unsigned short? sdpMLineIndex = null;
30 DOMString ufrag;
31 };
32 */
33
Soares Chenb20cf6b2017-05-19 12:28:4334 // SDP copied from JSEP Example 7.1
35 // It contains two media streams with different ufrags
36 // to test if candidate is added to the correct stream
37 const sdp = `v=0
38o=- 4962303333179871722 1 IN IP4 0.0.0.0
39s=-
40t=0 0
41a=ice-options:trickle
42a=group:BUNDLE a1 v1
43a=group:LS a1 v1
44m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
45c=IN IP4 203.0.113.100
46a=mid:a1
47a=sendrecv
48a=rtpmap:96 opus/48000/2
49a=rtpmap:0 PCMU/8000
50a=rtpmap:8 PCMA/8000
51a=rtpmap:97 telephone-event/8000
52a=rtpmap:98 telephone-event/48000
53a=maxptime:120
54a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
55a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
56a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
57a=ice-ufrag:ETEn
58a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
59a=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
60a=setup:actpass
61a=dtls-id:1
62a=rtcp:10101 IN IP4 203.0.113.100
63a=rtcp-mux
64a=rtcp-rsize
65m=video 10102 UDP/TLS/RTP/SAVPF 100 101
66c=IN IP4 203.0.113.100
67a=mid:v1
68a=sendrecv
69a=rtpmap:100 VP8/90000
70a=rtpmap:101 rtx/90000
71a=fmtp:101 apt=100
72a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
73a=rtcp-fb:100 ccm fir
74a=rtcp-fb:100 nack
75a=rtcp-fb:100 nack pli
76a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
77a=ice-ufrag:BGKk
78a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
79a=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
80a=setup:actpass
81a=dtls-id:1
82a=rtcp:10103 IN IP4 203.0.113.100
83a=rtcp-mux
84a=rtcp-rsize
85`;
86
87 const sessionDesc = { type: 'offer', sdp };
88
89 // valid candidate attributes
90 const sdpMid = 'a1';
91 const sdpMLineIndex = 0;
92 const ufrag = 'ETEn';
93
94 const sdpMid2 = 'v1';
95 const sdpMLineIndex2 = 1;
96 const ufrag2 = 'BGKk';
97
98 const mediaLine1 = 'm=audio';
99 const mediaLine2 = 'm=video';
100
101 const candidateStr1 = 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
102 const candidateStr2 = 'candidate:1 2 udp 2113929470 203.0.113.100 10101 typ host';
103 const invalidCandidateStr = '(Invalid) candidate \r\n string';
104
105 const candidateLine1 = `a=${candidateStr1}`;
106 const candidateLine2 = `a=${candidateStr2}`;
107 const endOfCandidateLine = 'a=end-of-candidates';
108
109 // Copied from MDN
110 function escapeRegExp(string) {
111 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112 }
113
114 // Check that a candidate line is found after the first media line
115 // but before the second, i.e. it belongs to the first media stream
116 function assert_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
117 const line1 = escapeRegExp(beforeMediaLine);
118 const line2 = escapeRegExp(candidateLine);
119 const line3 = escapeRegExp(afterMediaLine);
120
121 const regex = new RegExp(`${line1}[^]+${line2}[^]+${line3}`);
122
123 assert_true(regex.test(sdp),
124 `Expect candidate line to be found between media lines ${beforeMediaLine} and ${afterMediaLine}`);
125 }
126
127 // Check that a candidate line is found after the second media line
128 // i.e. it belongs to the second media stream
129 function assert_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
130 const line1 = escapeRegExp(beforeMediaLine);
131 const line2 = escapeRegExp(candidateLine);
132
133 const regex = new RegExp(`${line1}[^]+${line2}`);
134
135 assert_true(regex.test(sdp),
136 `Expect candidate line to be found after media line ${beforeMediaLine}`);
137 }
138
Soares Chen25c23902017-06-27 15:32:57139 // Reject because WebIDL for addIceCandidate does not allow null argument
140 // null can be accidentally passed from onicecandidate event handler
141 // when null is used to indicate end of candidate
142 promise_test(t => {
143 const pc = new RTCPeerConnection();
144
145 return pc.setRemoteDescription(sessionDesc)
146 .then(() =>
147 promise_rejects(t, new TypeError(),
148 pc.addIceCandidate(null)));
149 }, 'Add null candidate should reject with TypeError');
150
151 /*
152 4.3.2. addIceCandidate
153 4. Return the result of enqueuing the following steps:
154 1. If remoteDescription is null return a promise rejected with a
155 newly created InvalidStateError.
156 */
Soares Chenb20cf6b2017-05-19 12:28:43157 promise_test(t => {
158 const pc = new RTCPeerConnection();
159
160 return promise_rejects(t, 'InvalidStateError',
161 pc.addIceCandidate({
162 candidate: candidateStr1,
163 sdpMid, sdpMLineIndex, ufrag
164 }));
165 }, 'Add ICE candidate before setting remote description should reject with InvalidStateError');
166
Soares Chen25c23902017-06-27 15:32:57167 /*
Soares Chen25c23902017-06-27 15:32:57168 Success cases
169 */
Soares Chenb20cf6b2017-05-19 12:28:43170 promise_test(t => {
171 const pc = new RTCPeerConnection();
172
173 return pc.setRemoteDescription(sessionDesc)
174 .then(() => pc.addIceCandidate({
175 candidate: candidateStr1,
176 sdpMid, sdpMLineIndex, ufrag
177 }));
178 }, 'Add ICE candidate after setting remote description should succeed');
179
180 promise_test(t => {
181 const pc = new RTCPeerConnection();
182
183 return pc.setRemoteDescription(sessionDesc)
184 .then(() => pc.addIceCandidate(new RTCIceCandidate({
185 candidate: candidateStr1,
186 sdpMid, sdpMLineIndex, ufrag
187 })));
188 }, 'Add ICE candidate with RTCIceCandidate should succeed');
189
190 promise_test(t => {
191 const pc = new RTCPeerConnection();
192
193 return pc.setRemoteDescription(sessionDesc)
Soares Chen25c23902017-06-27 15:32:57194 .then(() => pc.addIceCandidate({ sdpMid }));
195 }, 'Add candidate with only valid sdpMid should succeed');
196
197 promise_test(t => {
198 const pc = new RTCPeerConnection();
199
200 return pc.setRemoteDescription(sessionDesc)
201 .then(() => pc.addIceCandidate({ sdpMLineIndex }));
202 }, 'Add candidate with only valid sdpMLineIndex should succeed');
203
204 /*
205 4.3.2. addIceCandidate
206 4.6.2. If candidate is applied successfully, the user agent MUST queue
207 a task that runs the following steps:
208 2. Let remoteDescription be connection's pendingRemoteDescription
209 if not null, otherwise connection's currentRemoteDescription.
210 3. Add candidate to remoteDescription.
211 */
212 promise_test(t => {
213 const pc = new RTCPeerConnection();
214
215 return pc.setRemoteDescription(sessionDesc)
Soares Chenb20cf6b2017-05-19 12:28:43216 .then(() => pc.addIceCandidate({
217 candidate: candidateStr1,
218 sdpMid, sdpMLineIndex, ufrag
219 }))
220 .then(() => {
221 assert_candidate_line_between(pc.remoteDescription.sdp,
222 mediaLine1, candidateLine1, mediaLine2);
223 });
Soares Chen25c23902017-06-27 15:32:57224 }, 'addIceCandidate with first sdpMid and sdpMLineIndex add candidate to first media stream');
Soares Chenb20cf6b2017-05-19 12:28:43225
226 promise_test(t => {
227 const pc = new RTCPeerConnection();
228
229 return pc.setRemoteDescription(sessionDesc)
230 .then(() => pc.addIceCandidate({
231 candidate: candidateStr2,
232 sdpMid: sdpMid2,
233 sdpMLineIndex: sdpMLineIndex2,
234 ufrag: ufrag2
235 }))
236 .then(() => {
237 assert_candidate_line_after(pc.remoteDescription.sdp,
238 mediaLine2, candidateLine2);
239 });
Soares Chen25c23902017-06-27 15:32:57240 }, 'addIceCandidate with second sdpMid and sdpMLineIndex should add candidate to second media stream');
241
242 promise_test(t => {
243 const pc = new RTCPeerConnection();
244
245 return pc.setRemoteDescription(sessionDesc)
246 .then(() => pc.addIceCandidate({
247 candidate: candidateStr1,
248 sdpMid, sdpMLineIndex,
249 ufrag: null
250 }))
251 .then(() => {
252 assert_candidate_line_between(pc.remoteDescription.sdp,
253 mediaLine1, candidateLine1, mediaLine2);
254 });
255 }, 'Add candidate for first media stream with null ufrag should add candidate to first media stream');
Soares Chenb20cf6b2017-05-19 12:28:43256
257 promise_test(t => {
258 const pc = new RTCPeerConnection();
259
260 return pc.setRemoteDescription(sessionDesc)
261 .then(() => pc.addIceCandidate({
262 candidate: candidateStr1,
263 sdpMid, sdpMLineIndex, ufrag
264 }))
265 .then(() => pc.addIceCandidate({
266 candidate: candidateStr2,
267 sdpMid: sdpMid2,
268 sdpMLineIndex: sdpMLineIndex2,
269 ufrag: ufrag2
270 }))
271 .then(() => {
272 assert_candidate_line_between(pc.remoteDescription.sdp,
273 mediaLine1, candidateLine1, mediaLine2);
274
275 assert_candidate_line_after(pc.remoteDescription.sdp,
276 mediaLine2, candidateLine2);
277 });
Soares Chen25c23902017-06-27 15:32:57278 }, 'Adding multiple candidates should add candidates to their corresponding media stream');
Soares Chenb20cf6b2017-05-19 12:28:43279
Soares Chen25c23902017-06-27 15:32:57280 /*
281 4.3.2. addIceCandidate
282 4.6. If candidate.candidate is an empty string, process candidate as an
283 end-of-candidates indication for the corresponding media description
284 and ICE candidate generation.
285 2. If candidate is applied successfully, the user agent MUST queue
286 a task that runs the following steps:
287 2. Let remoteDescription be connection's pendingRemoteDescription
288 if not null, otherwise connection's currentRemoteDescription.
289 3. Add candidate to remoteDescription.
290 */
Soares Chenb20cf6b2017-05-19 12:28:43291 promise_test(t => {
292 const pc = new RTCPeerConnection();
293
294 return pc.setRemoteDescription(sessionDesc)
295 .then(() => pc.addIceCandidate({
296 candidate: candidateStr1,
297 sdpMid, sdpMLineIndex, ufrag
298 }))
299 .then(() => pc.addIceCandidate({
300 candidate: '',
301 sdpMid, sdpMLineIndex,
302 ufrag
303 }))
304 .then(() => {
305 assert_candidate_line_between(pc.remoteDescription.sdp,
306 mediaLine1, candidateLine1, mediaLine2);
307
308 assert_candidate_line_between(pc.remoteDescription.sdp,
309 mediaLine1, endOfCandidateLine, mediaLine2);
310 });
311 }, 'Add with empty candidate string (end of candidate) should succeed');
312
Soares Chen25c23902017-06-27 15:32:57313 /*
314 4.3.2. addIceCandidate
315 3. If both sdpMid and sdpMLineIndex are null, return a promise rejected
316 with a newly created TypeError.
317 */
Soares Chenb20cf6b2017-05-19 12:28:43318 promise_test(t => {
319 const pc = new RTCPeerConnection();
320
321 return pc.setRemoteDescription(sessionDesc)
322 .then(() =>
323 promise_rejects(t, new TypeError(),
324 pc.addIceCandidate({
325 candidate: candidateStr1,
326 sdpMid: null,
327 sdpMLineIndex: null
328 })));
329 }, 'Add candidate with both sdpMid and sdpMLineIndex manually set to null should reject with TypeError');
330
331 promise_test(t => {
332 const pc = new RTCPeerConnection();
333
334 return pc.setRemoteDescription(sessionDesc)
335 .then(() =>
Soares Chen25c23902017-06-27 15:32:57336 promise_rejects(t, new TypeError(),
Soares Chenb20cf6b2017-05-19 12:28:43337 pc.addIceCandidate({
Soares Chen25c23902017-06-27 15:32:57338 candidate: candidateStr1
Soares Chenb20cf6b2017-05-19 12:28:43339 })));
Soares Chen25c23902017-06-27 15:32:57340 }, 'Add candidate with only valid candidate string should reject with TypeError');
Soares Chenb20cf6b2017-05-19 12:28:43341
342 promise_test(t => {
343 const pc = new RTCPeerConnection();
344
345 return pc.setRemoteDescription(sessionDesc)
346 .then(() =>
347 promise_rejects(t, new TypeError(),
348 pc.addIceCandidate({
349 candidate: invalidCandidateStr,
350 sdpMid: null,
351 sdpMLineIndex: null
352 })));
353 }, 'Add candidate with invalid candidate string and both sdpMid and sdpMLineIndex null should reject with TypeError');
354
355 promise_test(t => {
356 const pc = new RTCPeerConnection();
357
358 return pc.setRemoteDescription(sessionDesc)
359 .then(() =>
360 promise_rejects(t, new TypeError(),
Soares Chenb20cf6b2017-05-19 12:28:43361 pc.addIceCandidate({})));
362 }, 'Add candidate with empty dict should reject with TypeError');
363
364 promise_test(t => {
365 const pc = new RTCPeerConnection();
366
367 return pc.setRemoteDescription(sessionDesc)
368 .then(() =>
369 promise_rejects(t, new TypeError(),
370 pc.addIceCandidate({
371 candidate: '',
372 sdpMid: null,
373 sdpMLineIndex: null,
374 ufrag: undefined
375 })));
376 }, 'Add candidate with manually filled default values should reject with TypeError');
377
Soares Chen25c23902017-06-27 15:32:57378 /*
379 4.3.2. addIceCandidate
380 4.3. If candidate.sdpMid is not null, run the following steps:
381 1. If candidate.sdpMid is not equal to the mid of any media
382 description in remoteDescription , reject p with a newly
383 created OperationError and abort these steps.
384 */
Soares Chenb20cf6b2017-05-19 12:28:43385 promise_test(t => {
386 const pc = new RTCPeerConnection();
387
388 return pc.setRemoteDescription(sessionDesc)
389 .then(() =>
390 promise_rejects(t, 'OperationError',
391 pc.addIceCandidate({
392 candidate: candidateStr1,
393 sdpMid: 'invalid', sdpMLineIndex, ufrag
394 })));
395 }, 'Add candidate with invalid sdpMid should reject with OperationError');
396
Soares Chen25c23902017-06-27 15:32:57397 /*
398 4.3.2. addIceCandidate
399 4.4. Else, if candidate.sdpMLineIndex is not null, run the following
400 steps:
401 1. If candidate.sdpMLineIndex is equal to or larger than the
402 number of media descriptions in remoteDescription , reject p
403 with a newly created OperationError and abort these steps.
404 */
Soares Chenb20cf6b2017-05-19 12:28:43405 promise_test(t => {
406 const pc = new RTCPeerConnection();
407
408 return pc.setRemoteDescription(sessionDesc)
409 .then(() =>
410 promise_rejects(t, 'OperationError',
411 pc.addIceCandidate({
412 candidate: candidateStr1,
413 sdpMLineIndex: 2,
414 ufrag
415 })));
416 }, 'Add candidate with invalid sdpMLineIndex should reject with OperationError');
417
418 // There is an "Else" for the statement:
419 // "Else, if candidate.sdpMLineIndex is not null, ..."
420 promise_test(t => {
421 const pc = new RTCPeerConnection();
422
423 return pc.setRemoteDescription(sessionDesc)
424 .then(() => pc.addIceCandidate({
425 candidate: candidateStr1,
426 sdpMid,
427 sdpMLineIndex: 2,
428 ufrag
429 }));
430 }, 'Invalid sdpMLineIndex should be ignored if valid sdpMid is provided');
431
432 promise_test(t => {
433 const pc = new RTCPeerConnection();
434
435 return pc.setRemoteDescription(sessionDesc)
Soares Chenb20cf6b2017-05-19 12:28:43436 .then(() => pc.addIceCandidate({
437 candidate: candidateStr2,
438 sdpMid: sdpMid2,
439 sdpMLineIndex: sdpMLineIndex2,
440 ufrag: null
441 }))
442 .then(() => {
443 assert_candidate_line_after(pc.remoteDescription.sdp,
444 mediaLine2, candidateLine2);
445 });
446 }, 'Add candidate for media stream 2 with null ufrag should succeed');
447
Soares Chen25c23902017-06-27 15:32:57448 /*
449 4.3.2. addIceCandidate
450 4.5. If candidate.ufrag is neither undefined nor null, and is not equal
451 to any ufrag present in the corresponding media description of an
452 applied remote description, reject p with a newly created
453 OperationError and abort these steps.
454 */
455 promise_test(t => {
456 const pc = new RTCPeerConnection();
457
458 return pc.setRemoteDescription(sessionDesc)
459 .then(() =>
460 promise_rejects(t, 'OperationError',
461 pc.addIceCandidate({
462 candidate: candidateStr1,
463 sdpMid, sdpMLineIndex,
464 ufrag: 'invalid'
465 })));
466 }, 'Add candidate with invalid ufrag should reject with OperationError');
467
468 /*
469 4.3.2. addIceCandidate
470 4.6.1. If candidate could not be successfully added the user agent MUST
471 queue a task that runs the following steps:
472 2. Reject p with a DOMException object whose name attribute has
473 the value OperationError and abort these steps.
474 */
475 promise_test(t => {
476 const pc = new RTCPeerConnection();
477
478 return pc.setRemoteDescription(sessionDesc)
479 .then(() =>
480 promise_rejects(t, 'OperationError',
481 pc.addIceCandidate({
482 candidate: invalidCandidateStr,
483 sdpMid, sdpMLineIndex, ufrag
484 })));
485 }, 'Add candidate with invalid candidate string should reject with OperationError');
486
Soares Chenb20cf6b2017-05-19 12:28:43487 promise_test(t => {
488 const pc = new RTCPeerConnection();
489
490 return pc.setRemoteDescription(sessionDesc)
491 .then(() =>
492 promise_rejects(t, 'OperationError',
493 pc.addIceCandidate({
494 candidate: candidateStr2,
495 sdpMid: sdpMid2,
496 sdpMLineIndex: sdpMLineIndex2,
497 ufrag: ufrag
498 })));
499 }, 'Add candidate with sdpMid belonging to different ufrag should reject with OperationError');
500
Soares Chen25c23902017-06-27 15:32:57501 /*
502 TODO
503 4.3.2. addIceCandidate
504 4.6. In parallel, add the ICE candidate candidate as described in [JSEP]
505 (section 4.1.17.). Use candidate.ufrag to identify the ICE generation;
Soares Chenb20cf6b2017-05-19 12:28:43506
Soares Chen25c23902017-06-27 15:32:57507 If the ufrag is null, process the candidate for the most recent ICE
508 generation.
509
510 - Call with candidate string containing partial malformed syntax, i.e. malformed IP.
511 Some browsers may ignore the syntax error and add it to the SDP regardless.
512
513 Non-Testable
514 4.3.2. addIceCandidate
515 4.6. (The steps are non-testable because the abort step in enqueue operation
516 steps in before they can reach here):
517 1. If candidate could not be successfully added the user agent MUST
518 queue a task that runs the following steps:
519 1. If connection's [[isClosed]] slot is true, then abort
520 these steps.
521
522 2. If candidate is applied successfully, the user agent MUST queue
523 a task that runs the following steps:
524 1. If connection's [[isClosed]] slot is true, then abort these steps.
525
526 Issues
527 w3c/webrtc-pc#1213
528 addIceCandidate end of candidates woes
529
530 w3c/webrtc-pc#1216
531 Clarify addIceCandidate behavior when adding candidate after end of candidate
532
533 w3c/webrtc-pc#1227
534 addIceCandidate may add ice candidate to the wrong remote description
535
536 w3c/webrtc-pc#1345
537 Make promise rejection/enqueing consistent
538
539 Coverage Report
540 Total: 23
541 Tested: 19
542 Not Tested: 2
543 Non-Testable: 2
544 */
Soares Chenb20cf6b2017-05-19 12:28:43545</script>