RTCIceTransport.html (22666B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCIceTransport</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="RTCPeerConnection-helper.js"></script> 8 <script src='RTCConfiguration-helper.js'></script> 9 <script> 10 'use strict'; 11 12 // Test is based on the following editor draft: 13 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 14 15 // The following helper functions are called from RTCPeerConnection-helper.js: 16 // createDataChannelPair 17 // awaitMessage 18 19 /* 20 5.6. RTCIceTransport Interface 21 interface RTCIceTransport { 22 readonly attribute RTCIceRole role; 23 readonly attribute RTCIceComponent component; 24 readonly attribute RTCIceTransportState state; 25 readonly attribute RTCIceGathererState gatheringState; 26 sequence<RTCIceCandidate> getLocalCandidates(); 27 sequence<RTCIceCandidate> getRemoteCandidates(); 28 RTCIceCandidatePair? getSelectedCandidatePair(); 29 RTCIceParameters? getLocalParameters(); 30 RTCIceParameters? getRemoteParameters(); 31 ... 32 }; 33 34 getLocalCandidates 35 Returns a sequence describing the local ICE candidates gathered for this 36 RTCIceTransport and sent in onicecandidate 37 38 getRemoteCandidates 39 Returns a sequence describing the remote ICE candidates received by this 40 RTCIceTransport via addIceCandidate() 41 42 getSelectedCandidatePair 43 Returns the selected candidate pair on which packets are sent, or null if 44 there is no such pair. 45 46 getLocalParameters 47 Returns the local ICE parameters received by this RTCIceTransport via 48 setLocalDescription , or null if the parameters have not yet been received. 49 50 getRemoteParameters 51 Returns the remote ICE parameters received by this RTCIceTransport via 52 setRemoteDescription or null if the parameters have not yet been received. 53 */ 54 function getIceTransportFromSctp(pc) { 55 const sctpTransport = pc.sctp; 56 assert_true(sctpTransport instanceof RTCSctpTransport, 57 'Expect pc.sctp to be instantiated from RTCSctpTransport'); 58 59 const dtlsTransport = sctpTransport.transport; 60 assert_true(dtlsTransport instanceof RTCDtlsTransport, 61 'Expect sctp.transport to be an RTCDtlsTransport'); 62 63 const {iceTransport} = dtlsTransport; 64 assert_true(iceTransport instanceof RTCIceTransport, 65 'Expect dtlsTransport.transport to be an RTCIceTransport'); 66 67 return iceTransport; 68 } 69 70 function validateCandidates(candidates) { 71 assert_greater_than(candidates.length, 0, 72 'Expect at least one ICE candidate returned from get*Candidates()'); 73 74 for(const candidate of candidates) { 75 assert_true(candidate instanceof RTCIceCandidate, 76 'Expect candidate elements to be instance of RTCIceCandidate'); 77 } 78 } 79 80 function validateCandidateParameter(param) { 81 assert_not_equals(param, null, 82 'Expect candidate parameter to be non-null after data channels are connected'); 83 84 assert_equals(typeof param.usernameFragment, 'string', 85 'Expect param.usernameFragment to be set with string value'); 86 assert_equals(typeof param.password, 'string', 87 'Expect param.password to be set with string value'); 88 } 89 90 function validateConnectedIceTransport(iceTransport) { 91 const { state, gatheringState, role, component } = iceTransport; 92 93 assert_true(role === 'controlling' || role === 'controlled', 94 'Expect RTCIceRole to be either controlling or controlled, found ' + role); 95 96 assert_true(component === 'rtp' || component === 'rtcp', 97 'Expect RTCIceComponent to be either rtp or rtcp'); 98 99 assert_true(state === 'connected' || state === 'completed', 100 'Expect ICE transport to be in connected or completed state after data channels are connected'); 101 102 assert_true(gatheringState === 'gathering' || gatheringState === 'completed', 103 'Expect ICE transport to be in gathering or completed gatheringState after data channels are connected'); 104 105 validateCandidates(iceTransport.getLocalCandidates()); 106 validateCandidates(iceTransport.getRemoteCandidates()); 107 108 const candidatePair = iceTransport.getSelectedCandidatePair(); 109 assert_not_equals(candidatePair, null, 110 'Expect selected candidate pair to be non-null after ICE transport is connected'); 111 112 assert_true(candidatePair.local instanceof RTCIceCandidate, 113 'Expect candidatePair.local to be instance of RTCIceCandidate'); 114 115 assert_true(candidatePair.remote instanceof RTCIceCandidate, 116 'Expect candidatePair.remote to be instance of RTCIceCandidate'); 117 118 validateCandidateParameter(iceTransport.getLocalParameters()); 119 validateCandidateParameter(iceTransport.getRemoteParameters()); 120 } 121 122 promise_test(t => { 123 const pc1 = new RTCPeerConnection(); 124 t.add_cleanup(() => pc1.close()); 125 const pc2 = new RTCPeerConnection(); 126 t.add_cleanup(() => pc2.close()); 127 128 const pc1Candidates = new Set(); 129 const pc2Candidates = new Set(); 130 pc1.addEventListener('icecandidate', e => { if (e.candidate) pc1Candidates.add(e.candidate.candidate); }); 131 pc2.addEventListener('icecandidate', e => { if (e.candidate) pc2Candidates.add(e.candidate.candidate); }); 132 133 return createDataChannelPair(t, {}, pc1, pc2) 134 .then(([channel1, channel2]) => { 135 // Send a ping message and wait for it just to make sure 136 // that the connection is fully working before testing 137 channel1.send('ping'); 138 return awaitMessage(channel2); 139 }) 140 .then(() => { 141 const iceTransport1 = getIceTransportFromSctp(pc1); 142 const iceTransport2 = getIceTransportFromSctp(pc2); 143 144 test(() => { 145 const candidatePair1 = iceTransport1.getSelectedCandidatePair(); 146 const candidatePair2 = iceTransport2.getSelectedCandidatePair(); 147 148 assert_equals(candidatePair1.local.candidate, candidatePair2.remote.candidate, 149 'Expect selected local candidate of one pc is the selected remote candidate or another'); 150 151 assert_equals(candidatePair1.remote.candidate, candidatePair2.local.candidate, 152 'Expect selected local candidate of one pc is the selected remote candidate or another'); 153 154 assert_true(pc1Candidates.has(candidatePair1.local.candidate), "pc1 selected local candidate in pc1 candidates"); 155 assert_true(pc1Candidates.has(candidatePair2.remote.candidate), "pc2 selected remote candidate in pc1 candidates"); 156 assert_true(pc2Candidates.has(candidatePair2.local.candidate), "pc2 selected local candidate in pc2 candidates"); 157 assert_true(pc2Candidates.has(candidatePair1.remote.candidate), "pc1 selected remote candidate in pc2 candidates"); 158 }, "Validate selected candidate pair"); 159 160 validateConnectedIceTransport(iceTransport1); 161 validateConnectedIceTransport(iceTransport2); 162 163 assert_equals( 164 iceTransport1.getLocalCandidates().length, 165 iceTransport2.getRemoteCandidates().length, 166 `Expect iceTransport1 to have same number of local candidate as iceTransport2's remote candidates`); 167 168 assert_equals( 169 iceTransport1.getRemoteCandidates().length, 170 iceTransport2.getLocalCandidates().length, 171 `Expect iceTransport1 to have same number of remote candidate as iceTransport2's local candidates`); 172 173 assert_equals(iceTransport1.role, 'controlling', 174 `Expect offerer's iceTransport to take the controlling role`); 175 176 assert_equals(iceTransport2.role, 'controlled', 177 `Expect answerer's iceTransport to take the controlled role`); 178 }); 179 }, 'Two connected iceTransports should have matching local/remote candidates returned'); 180 181 promise_test(t => { 182 const pc1 = new RTCPeerConnection(); 183 t.add_cleanup(() => pc1.close()); 184 const pc2 = new RTCPeerConnection(); 185 t.add_cleanup(() => pc2.close()); 186 pc1.createDataChannel(''); 187 188 // setRemoteDescription(answer) without the other peer 189 // setting answer it's localDescription 190 return pc1.createOffer() 191 .then(offer => 192 pc1.setLocalDescription(offer) 193 .then(() => pc2.setRemoteDescription(offer)) 194 .then(() => pc2.createAnswer())) 195 .then(answer => pc1.setRemoteDescription(answer)) 196 .then(() => { 197 const iceTransport = getIceTransportFromSctp(pc1); 198 199 assert_array_equals(iceTransport.getRemoteCandidates(), [], 200 'Expect iceTransport to not have any remote candidate'); 201 202 assert_equals(iceTransport.getSelectedCandidatePair(), null, 203 'Expect selectedCandidatePair to be null'); 204 }); 205 }, 'Unconnected iceTransport should have empty remote candidates and selected pair'); 206 207 promise_test(async t => { 208 const pc1 = new RTCPeerConnection(); 209 t.add_cleanup(() => pc1.close()); 210 const transceiver = pc1.addTransceiver('audio'); 211 await pc1.setLocalDescription(); 212 const {iceTransport} = transceiver.sender.transport; 213 assert_equals(iceTransport.state, 'new'); 214 assert_equals(iceTransport.gatheringState, 'new'); 215 }, 'RTCIceTransport should be in state "new" initially'); 216 217 promise_test(async t => { 218 const pc1 = new RTCPeerConnection(); 219 t.add_cleanup(() => pc1.close()); 220 const transceiver = pc1.addTransceiver('audio'); 221 await pc1.setLocalDescription(); 222 const {iceTransport} = transceiver.sender.transport; 223 assert_equals(await nextGatheringState(iceTransport), 'gathering'); 224 assert_equals(await nextGatheringState(iceTransport), 'complete'); 225 }, 'RTCIceTransport should transition to "gathering" then "complete", after sLD'); 226 227 promise_test(async t => { 228 const pc1 = new RTCPeerConnection(); 229 t.add_cleanup(() => pc1.close()); 230 const transceiver = pc1.addTransceiver('audio'); 231 await pc1.setLocalDescription(); 232 const {iceTransport} = transceiver.sender.transport; 233 assert_equals(await nextGatheringState(iceTransport), 'gathering'); 234 pc1.close(); 235 assert_equals(iceTransport.gatheringState, 'gathering'); 236 const result = await Promise.race([ 237 gatheringStateReached(iceTransport, 'complete'), 238 new Promise(r => t.step_timeout(r, 1000))]); 239 assert_equals(result, undefined, `Did not expect a statechange after PC.close(), but got one. state is "${result}"`); 240 }, 'PC.close() should not cause the RTCIceTransport gathering state to transition to "complete"'); 241 242 promise_test(async t => { 243 const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'}); 244 t.add_cleanup(() => pc1.close()); 245 const pc2 = new RTCPeerConnection(); 246 t.add_cleanup(() => pc2.close()); 247 pc1.createDataChannel('test'); 248 // TODO: If the spec settles on exposing the sctp transport in 249 // have-local-offer, we won't need this audio transceiver hack. 250 // See https://github.com/w3c/webrtc-pc/issues/2898 and 251 // https://github.com/w3c/webrtc-pc/issues/2899 252 const transceiver = pc1.addTransceiver('audio'); 253 await pc1.setLocalDescription(); 254 const {iceTransport} = transceiver.sender.transport; 255 assert_equals(await nextGatheringState(iceTransport), 'gathering'); 256 assert_equals(await nextGatheringState(iceTransport), 'complete'); 257 // TODO: Maybe, maybe not. 258 assert_not_equals(pc1.sctp, null, 'pc1.sctp should be set after sLD'); 259 await pc2.setRemoteDescription(pc1.localDescription); 260 await pc2.setLocalDescription(); 261 await pc1.setRemoteDescription(pc2.localDescription); 262 assert_equals(pc1.sctp.transport.iceTransport, transceiver.sender.transport.iceTransport); 263 }, 'RTCIceTransport should transition to "gathering", then "complete" after sLD (DataChannel case)'); 264 265 promise_test(async t => { 266 const pc1 = new RTCPeerConnection(); 267 t.add_cleanup(() => pc1.close()); 268 const pc2 = new RTCPeerConnection(); 269 t.add_cleanup(() => pc2.close()); 270 271 const {sender} = pc1.addTransceiver('audio'); 272 await pc1.setLocalDescription(); 273 // Copy the SDP before it has candidate attrs 274 const offer = pc1.localDescription; 275 const checkingReached = connectionStateReached(sender.transport.iceTransport, 'checking'); 276 277 let result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]); 278 assert_equals(result, undefined, `Did not expect a statechange right after sLD(offer), but got one. state is "${result}"`); 279 280 await pc2.setRemoteDescription(offer); 281 282 const firstPc2CandidatePromise = 283 new Promise(r => pc2.onicecandidate = e => r(e.candidate)); 284 285 await pc2.setLocalDescription(); 286 await pc1.setRemoteDescription(pc2.localDescription); 287 288 result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]); 289 assert_equals(result, undefined, `Did not expect a statechange callback after sRD(answer), but got one. state is "${result}"`); 290 291 const candidate = await firstPc2CandidatePromise; 292 pc1.addIceCandidate(candidate); 293 294 await checkingReached; 295 }, 'RTCIceTransport should not transition to "checking" until after the answer is set _and_ the first remote candidate is received'); 296 297 298 promise_test(async t => { 299 const pc1 = new RTCPeerConnection(); 300 t.add_cleanup(() => pc1.close()); 301 const pc2 = new RTCPeerConnection(); 302 t.add_cleanup(() => pc2.close()); 303 const {sender} = pc1.addTransceiver('audio'); 304 exchangeIceCandidates(pc1, pc2); 305 const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]); 306 await pc1.setLocalDescription(); 307 await pc2.setRemoteDescription(pc1.localDescription); 308 await pc2.setLocalDescription(); 309 await pc1.setRemoteDescription(pc2.localDescription); 310 assert_equals(await nextConnectionState(sender.transport.iceTransport), 'checking'); 311 assert_equals(await nextConnectionState(sender.transport.iceTransport), 'connected'); 312 await gatheringDone; 313 pc2.close(); 314 await connectionStateReached(sender.transport.iceTransport, 'disconnected'); 315 }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing'); 316 317 promise_test(async t => { 318 const pc1 = new RTCPeerConnection(); 319 t.add_cleanup(() => pc1.close()); 320 const pc2 = new RTCPeerConnection(); 321 t.add_cleanup(() => pc2.close()); 322 pc1.createDataChannel('test'); 323 exchangeIceCandidates(pc1, pc2); 324 const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]); 325 await pc1.setLocalDescription(); 326 await pc2.setRemoteDescription(pc1.localDescription); 327 await pc2.setLocalDescription(); 328 await pc1.setRemoteDescription(pc2.localDescription); 329 const {sctp} = pc1; 330 assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'checking'); 331 assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'connected'); 332 await gatheringDone; 333 pc2.close(); 334 await connectionStateReached(sctp.transport.iceTransport, 'disconnected'); 335 }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)'); 336 337 promise_test(async t => { 338 const pc1 = new RTCPeerConnection(); 339 t.add_cleanup(() => pc1.close()); 340 const pc2 = new RTCPeerConnection(); 341 t.add_cleanup(() => pc2.close()); 342 const {sender} = pc1.addTransceiver('audio'); 343 await pc1.setLocalDescription(); 344 const {iceTransport} = sender.transport; 345 await pc2.setRemoteDescription(pc1.localDescription); 346 await pc2.setLocalDescription(); 347 await pc1.setRemoteDescription(pc2.localDescription); 348 349 pc1.restartIce(); 350 351 await pc1.setLocalDescription(); 352 await pc2.setRemoteDescription(pc1.localDescription); 353 await pc2.setLocalDescription(); 354 await pc1.setRemoteDescription(pc2.localDescription); 355 356 assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport'); 357 }, 'Local ICE restart should not result in a different ICE transport'); 358 359 promise_test(async t => { 360 const pc1 = new RTCPeerConnection(); 361 t.add_cleanup(() => pc1.close()); 362 const pc2 = new RTCPeerConnection(); 363 t.add_cleanup(() => pc2.close()); 364 pc1.createDataChannel('test'); 365 await pc1.setLocalDescription(); 366 await pc2.setRemoteDescription(pc1.localDescription); 367 await pc2.setLocalDescription(); 368 await pc1.setRemoteDescription(pc2.localDescription); 369 const {iceTransport} = pc1.sctp.transport; 370 371 pc1.restartIce(); 372 373 await pc1.setLocalDescription(); 374 await pc2.setRemoteDescription(pc1.localDescription); 375 await pc2.setLocalDescription(); 376 await pc1.setRemoteDescription(pc2.localDescription); 377 378 assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport'); 379 }, 'Local ICE restart should not result in a different ICE transport (DataChannel case)'); 380 381 promise_test(async t => { 382 const pc1 = new RTCPeerConnection(); 383 t.add_cleanup(() => pc1.close()); 384 const pc2 = new RTCPeerConnection(); 385 t.add_cleanup(() => pc2.close()); 386 const {sender} = pc1.addTransceiver('audio'); 387 388 await pc1.setLocalDescription(); 389 const {iceTransport} = sender.transport; 390 await pc2.setRemoteDescription(pc1.localDescription); 391 await pc2.setLocalDescription(); 392 await pc1.setRemoteDescription(pc2.localDescription); 393 394 pc2.restartIce(); 395 396 await pc2.setLocalDescription(); 397 await pc1.setRemoteDescription(pc2.localDescription); 398 await pc2.setLocalDescription(); 399 await pc1.setRemoteDescription(pc1.localDescription); 400 401 assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport'); 402 }, 'Remote ICE restart should not result in a different ICE transport'); 403 404 promise_test(async t => { 405 const pc1 = new RTCPeerConnection(); 406 t.add_cleanup(() => pc1.close()); 407 const pc2 = new RTCPeerConnection(); 408 t.add_cleanup(() => pc2.close()); 409 pc1.createDataChannel('test'); 410 411 await pc1.setLocalDescription(); 412 await pc2.setRemoteDescription(pc1.localDescription); 413 await pc2.setLocalDescription(); 414 await pc1.setRemoteDescription(pc2.localDescription); 415 const {iceTransport} = pc1.sctp.transport; 416 417 pc2.restartIce(); 418 419 await pc2.setLocalDescription(); 420 await pc1.setRemoteDescription(pc2.localDescription); 421 await pc2.setLocalDescription(); 422 await pc1.setRemoteDescription(pc1.localDescription); 423 424 assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport'); 425 }, 'Remote ICE restart should not result in a different ICE transport (DataChannel case)'); 426 427 promise_test(async t => { 428 const pc1 = new RTCPeerConnection(); 429 t.add_cleanup(() => pc1.close()); 430 const pc2 = new RTCPeerConnection(); 431 t.add_cleanup(() => pc2.close()); 432 // Add two transceivers, one audio and one video. The default bundlePolicy 433 // ("balanced") will result in each being offered with its own transport, 434 // but allowing the answerer to bundle the second transceiver on the 435 // transport of the first, which the answerer will do by default. 436 const audioTransceiver = pc1.addTransceiver('audio'); 437 const videoTransceiver = pc1.addTransceiver('video'); 438 pc1.createDataChannel('test'); 439 440 await pc1.setLocalDescription(); 441 const audioIceTransport = audioTransceiver.sender.transport.iceTransport; 442 const videoIceTransport = videoTransceiver.sender.transport.iceTransport; 443 444 assert_not_equals(audioIceTransport, videoIceTransport, 'audio and video should start out with different transports'); 445 446 await pc2.setRemoteDescription(pc1.localDescription); 447 await pc2.setLocalDescription(); 448 await pc1.setRemoteDescription(pc2.localDescription); 449 const sctpIceTransport = pc1.sctp.transport.iceTransport; 450 451 assert_equals(videoTransceiver.sender.transport.iceTransport, audioIceTransport, 'After negotiation, the video sender should use the bundle ICE transport from the audio sender'); 452 assert_equals(pc1.sctp.transport.iceTransport, audioIceTransport, 'After negotiation, the datachannel should use the bundle ICE transport from the audio sender'); 453 assert_not_equals(videoIceTransport.state, 'closed', 'Completion of offer/answer should not close the unused ICE transport immediately'); 454 455 await connectionStateReached(videoIceTransport, 'closed'); 456 }, 'RTCIceTransport should transition to "closed" if the underlying transport is closed because the answer used bundle'); 457 458 promise_test(async t => { 459 const pc1 = new RTCPeerConnection(); 460 t.add_cleanup(() => pc1.close()); 461 const pc2 = new RTCPeerConnection(); 462 t.add_cleanup(() => pc2.close()); 463 const {sender} = pc1.addTransceiver('audio'); 464 exchangeIceCandidates(pc1, pc2); 465 const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]); 466 await pc1.setLocalDescription(); 467 const {iceTransport} = sender.transport; 468 await pc2.setRemoteDescription(pc1.localDescription); 469 await pc2.setLocalDescription(); 470 await pc1.setRemoteDescription(pc2.localDescription); 471 assert_equals(await nextConnectionState(iceTransport), 'checking'); 472 assert_equals(await nextConnectionState(iceTransport), 'connected'); 473 await gatheringDone; 474 475 const closedEvent = connectionStateReached(iceTransport, 'closed'); 476 pc1.close(); 477 assert_equals(sender.transport.iceTransport, iceTransport, 'PC.close() should not unset the sender transport'); 478 assert_equals(iceTransport.state, 'closed', 'pc.close() should close the sender transport synchronously'); 479 const result = await Promise.race([closedEvent, new Promise(r => t.step_timeout(r, 1000))]); 480 assert_equals(result, undefined, 'statechange event should not fire when transitioning to closed due to PC.close()'); 481 }, 'RTCIceTransport should synchronously transition to "closed" with no event if the underlying transport is closed due to PC.close()'); 482 483 promise_test(async t => { 484 const pc1 = new RTCPeerConnection(); 485 t.add_cleanup(() => pc1.close()); 486 const pc2 = new RTCPeerConnection(); 487 t.add_cleanup(() => pc2.close()); 488 const {sender} = pc1.addTransceiver('audio'); 489 490 // Candidates only go in one direction, like in webrtc-stats/getStats-remote-candidate-address.html 491 // This means the remote candidates are peer-reflexive candidates. 492 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 493 exchangeOfferAnswer(pc1, pc2); 494 await listenToConnected(pc1); 495 496 assert_equals(sender.transport.iceTransport.getRemoteCandidates().length, 0); 497 }, 'RTCIceTransport does not expose remote peer-reflexive candidates.'); 498 499 </script>