RTCPeerConnection-iceConnectionState.https.html (16487B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCPeerConnection.prototype.iceConnectionState</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="RTCPeerConnection-helper.js"></script> 8 <script> 9 'use strict'; 10 11 // Test is based on the following editor draft: 12 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 13 14 /* 15 4.3.2. Interface Definition 16 interface RTCPeerConnection : EventTarget { 17 ... 18 readonly attribute RTCIceConnectionState iceConnectionState; 19 attribute EventHandler oniceconnectionstatechange; 20 }; 21 22 4.4.4 RTCIceConnectionState Enum 23 enum RTCIceConnectionState { 24 "new", 25 "checking", 26 "connected", 27 "completed", 28 "failed", 29 "disconnected", 30 "closed" 31 }; 32 33 5.6. RTCIceTransport Interface 34 interface RTCIceTransport { 35 readonly attribute RTCIceTransportState state; 36 attribute EventHandler onstatechange; 37 38 ... 39 }; 40 41 enum RTCIceTransportState { 42 "new", 43 "checking", 44 "connected", 45 "completed", 46 "failed", 47 "disconnected", 48 "closed" 49 }; 50 */ 51 52 /* 53 4.4.4 RTCIceConnectionState Enum 54 new 55 Any of the RTCIceTransports are in the new state and none of them 56 are in the checking, failed or disconnected state, or all 57 RTCIceTransport s are in the closed state. 58 */ 59 test(t => { 60 const pc = new RTCPeerConnection(); 61 assert_equals(pc.iceConnectionState, 'new'); 62 }, 'Initial iceConnectionState should be new'); 63 64 test(t => { 65 const pc = new RTCPeerConnection(); 66 pc.close(); 67 assert_equals(pc.iceConnectionState, 'closed'); 68 }, 'Closing the connection should set iceConnectionState to closed'); 69 70 /* 71 4.4.4 RTCIceConnectionState Enum 72 checking 73 Any of the RTCIceTransport s are in the checking state and none of 74 them are in the failed or disconnected state. 75 76 connected 77 All RTCIceTransport s are in the connected, completed or closed state 78 and at least one of them is in the connected state. 79 80 completed 81 All RTCIceTransport s are in the completed or closed state and at least 82 one of them is in the completed state. 83 84 checking 85 The RTCIceTransport has received at least one remote candidate and 86 is checking candidate pairs and has either not yet found a connection 87 or consent checks [RFC7675] have failed on all previously successful 88 candidate pairs. In addition to checking, it may also still be gathering. 89 90 5.6. enum RTCIceTransportState 91 connected 92 The RTCIceTransport has found a usable connection, but is still 93 checking other candidate pairs to see if there is a better connection. 94 It may also still be gathering and/or waiting for additional remote 95 candidates. If consent checks [RFC7675] fail on the connection in use, 96 and there are no other successful candidate pairs available, then the 97 state transitions to "checking" (if there are candidate pairs remaining 98 to be checked) or "disconnected" (if there are no candidate pairs to 99 check, but the peer is still gathering and/or waiting for additional 100 remote candidates). 101 102 completed 103 The RTCIceTransport has finished gathering, received an indication that 104 there are no more remote candidates, finished checking all candidate 105 pairs and found a connection. If consent checks [RFC7675] subsequently 106 fail on all successful candidate pairs, the state transitions to "failed". 107 */ 108 async_test(t => { 109 const pc1 = new RTCPeerConnection(); 110 t.add_cleanup(() => pc1.close()); 111 const pc2 = new RTCPeerConnection(); 112 t.add_cleanup(() => pc2.close()); 113 114 let had_checking = false; 115 116 const onIceConnectionStateChange = t.step_func(() => { 117 const {iceConnectionState} = pc1; 118 if (iceConnectionState === 'checking') { 119 had_checking = true; 120 } else if (iceConnectionState === 'connected' || 121 iceConnectionState === 'completed') { 122 assert_true(had_checking, 'state should pass checking before' + 123 ' reaching connected or completed'); 124 t.done(); 125 } else if (iceConnectionState === 'failed') { 126 assert_unreached("ICE should not fail"); 127 } 128 }); 129 130 pc1.createDataChannel('test'); 131 132 pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange); 133 134 exchangeIceCandidates(pc1, pc2); 135 exchangeOfferAnswer(pc1, pc2); 136 }, 'connection with one data channel should eventually have connected or ' + 137 'completed connection state'); 138 139 async_test(t => { 140 const pc1 = new RTCPeerConnection(); 141 t.add_cleanup(() => pc1.close()); 142 const pc2 = new RTCPeerConnection(); 143 144 t.add_cleanup(() => pc2.close()); 145 146 const onIceConnectionStateChange = t.step_func(() => { 147 const { iceConnectionState } = pc1; 148 149 if(iceConnectionState === 'checking') { 150 const iceTransport = pc1.sctp.transport.iceTransport; 151 152 assert_equals(iceTransport.state, 'checking', 153 'Expect ICE transport to be in checking state when' + 154 ' iceConnectionState is checking'); 155 156 } else if(iceConnectionState === 'connected') { 157 const iceTransport = pc1.sctp.transport.iceTransport; 158 159 assert_equals(iceTransport.state, 'connected', 160 'Expect ICE transport to be in connected state when' + 161 ' iceConnectionState is connected'); 162 t.done(); 163 } else if(iceConnectionState === 'completed') { 164 const iceTransport = pc1.sctp.transport.iceTransport; 165 166 assert_equals(iceTransport.state, 'completed', 167 'Expect ICE transport to be in connected state when' + 168 ' iceConnectionState is completed'); 169 t.done(); 170 } else if (iceConnectionState === 'failed') { 171 assert_unreached("ICE should not fail"); 172 } 173 }); 174 175 pc1.createDataChannel('test'); 176 177 assert_equals(pc1.oniceconnectionstatechange, null, 178 'Expect connection to have iceconnectionstatechange event'); 179 180 pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange); 181 182 exchangeIceCandidates(pc1, pc2); 183 exchangeOfferAnswer(pc1, pc2); 184 }, 'connection with one data channel should eventually ' + 185 'have connected connection state'); 186 187 promise_test(async t => { 188 const pc1 = new RTCPeerConnection(); 189 t.add_cleanup(() => pc1.close()); 190 const pc2 = new RTCPeerConnection(); 191 t.add_cleanup(() => pc2.close()); 192 193 const stream = await getNoiseStream({audio: true}); 194 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 195 stream.getTracks().forEach(track => pc1.addTrack(track, stream)); 196 197 exchangeIceCandidates(pc1, pc2); 198 exchangeOfferAnswer(pc1, pc2); 199 await listenToIceConnected(pc1); 200 }, 'connection with audio track should eventually ' + 201 'have connected connection state'); 202 203 promise_test(async t => { 204 const pc1 = new RTCPeerConnection(); 205 t.add_cleanup(() => pc1.close()); 206 const pc2 = new RTCPeerConnection(); 207 t.add_cleanup(() => pc2.close()); 208 209 const stream = await getNoiseStream({audio: true, video:true}); 210 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 211 stream.getTracks().forEach(track => pc1.addTrack(track, stream)); 212 213 exchangeIceCandidates(pc1, pc2); 214 exchangeOfferAnswer(pc1, pc2); 215 await listenToIceConnected(pc1); 216 }, 'connection with audio and video tracks should eventually ' + 217 'have connected connection state'); 218 219 promise_test(async t => { 220 const caller = new RTCPeerConnection(); 221 t.add_cleanup(() => caller.close()); 222 const callee = new RTCPeerConnection(); 223 t.add_cleanup(() => callee.close()); 224 225 caller.addTransceiver('audio', {direction:'recvonly'}); 226 const stream = await getNoiseStream({audio:true}); 227 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 228 const [track] = stream.getTracks(); 229 callee.addTrack(track, stream); 230 exchangeIceCandidates(caller, callee); 231 await exchangeOfferAnswer(caller, callee); 232 233 assert_equals(caller.getTransceivers().length, 1); 234 const [transceiver] = caller.getTransceivers(); 235 assert_equals(transceiver.currentDirection, 'recvonly'); 236 237 await listenToIceConnected(caller); 238 }, 'ICE can connect in a recvonly usecase'); 239 240 /* 241 TODO 242 4.4.4 RTCIceConnectionState Enum 243 failed 244 Any of the RTCIceTransport s are in the failed state. 245 246 disconnected 247 Any of the RTCIceTransport s are in the disconnected state and none of 248 them are in the failed state. 249 250 closed 251 The RTCPeerConnection object's [[ isClosed]] slot is true. 252 253 5.6. enum RTCIceTransportState 254 new 255 The RTCIceTransport is gathering candidates and/or waiting for 256 remote candidates to be supplied, and has not yet started checking. 257 258 failed 259 The RTCIceTransport has finished gathering, received an indication that 260 there are no more remote candidates, finished checking all candidate pairs, 261 and all pairs have either failed connectivity checks or have lost consent. 262 263 disconnected 264 The ICE Agent has determined that connectivity is currently lost for this 265 RTCIceTransport . This is more aggressive than failed, and may trigger 266 intermittently (and resolve itself without action) on a flaky network. 267 The way this state is determined is implementation dependent. 268 269 Examples include: 270 Losing the network interface for the connection in use. 271 Repeatedly failing to receive a response to STUN requests. 272 273 Alternatively, the RTCIceTransport has finished checking all existing 274 candidates pairs and failed to find a connection (or consent checks 275 [RFC7675] once successful, have now failed), but it is still gathering 276 and/or waiting for additional remote candidates. 277 278 closed 279 The RTCIceTransport has shut down and is no longer responding to STUN requests. 280 */ 281 282 for (let bundle_policy of ['balanced', 'max-bundle', 'max-compat']) { 283 promise_test(async t => { 284 const caller = new RTCPeerConnection({bundlePolicy: bundle_policy}); 285 t.add_cleanup(() => caller.close()); 286 const stream = await getNoiseStream( 287 {audio: true, video:true}); 288 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 289 const [track1, track2] = stream.getTracks(); 290 const sender1 = caller.addTrack(track1); 291 const sender2 = caller.addTrack(track2); 292 caller.createDataChannel('datachannel'); 293 const callee = new RTCPeerConnection(); 294 t.add_cleanup(() => callee.close()); 295 exchangeIceCandidates(caller, callee); 296 const offer = await caller.createOffer(); 297 await caller.setLocalDescription(offer); 298 const [caller_transceiver1, caller_transceiver2] = caller.getTransceivers(); 299 assert_equals(sender1.transport, caller_transceiver1.sender.transport); 300 await callee.setRemoteDescription(offer); 301 const [callee_transceiver1, callee_transceiver2] = callee.getTransceivers(); 302 const answer = await callee.createAnswer(); 303 await callee.setLocalDescription(answer); 304 await caller.setRemoteDescription(answer); 305 // At this point, we should have a single ICE transport, and it 306 // should eventually get to the "connected" state. 307 await waitForState(caller_transceiver1.receiver.transport.iceTransport, 308 'connected'); 309 // The PeerConnection's iceConnectionState should therefore be 'connected' 310 assert_equals(caller.iceConnectionState, 'connected', 311 'PC.iceConnectionState:'); 312 }, 'iceConnectionState changes at the right time, with bundle policy ' + 313 bundle_policy); 314 } 315 316 promise_test(async t => { 317 const pc1 = new RTCPeerConnection(); 318 t.add_cleanup(() => pc1.close()); 319 const pc2 = new RTCPeerConnection(); 320 t.add_cleanup(() => pc2.close()); 321 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 322 pc1.candidateBuffer = []; 323 pc2.onicecandidate = e => { 324 // Don't add candidate if candidate buffer is already used 325 if (pc1.candidateBuffer) { 326 pc1.candidateBuffer.push(e.candidate) 327 } 328 }; 329 pc1.iceStates = [pc1.iceConnectionState]; 330 pc2.iceStates = [pc2.iceConnectionState]; 331 pc1.oniceconnectionstatechange = () => { 332 pc1.iceStates.push(pc1.iceConnectionState); 333 }; 334 pc2.oniceconnectionstatechange = () => { 335 pc2.iceStates.push(pc2.iceConnectionState); 336 }; 337 338 const localStream = await getNoiseStream({audio: true, video: true}); 339 const localStream2 = await getNoiseStream({audio: true, video: true}); 340 const remoteStream = await getNoiseStream({audio: true, video: true}); 341 for (const stream of [localStream, localStream2, remoteStream]) { 342 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 343 } 344 localStream.getTracks().forEach(t => pc1.addTrack(t, localStream)); 345 localStream2.getTracks().forEach(t => pc1.addTrack(t, localStream2)); 346 remoteStream.getTracks().forEach(t => pc2.addTrack(t, remoteStream)); 347 const offer = await pc1.createOffer(); 348 await pc2.setRemoteDescription(offer); 349 await pc1.setLocalDescription(offer); 350 const answer = await pc2.createAnswer(); 351 await pc2.setLocalDescription(answer); 352 await pc1.setRemoteDescription(answer); 353 pc1.candidateBuffer.forEach(c => pc1.addIceCandidate(c)); 354 delete pc1.candidateBuffer; 355 await listenToIceConnected(pc1); 356 await listenToIceConnected(pc2); 357 // While we're waiting for pc2, pc1 may or may not have transitioned 358 // to "completed" state, so allow for both cases. 359 if (pc1.iceStates.length == 3) { 360 assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected']); 361 } else { 362 assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected', 363 'completed']); 364 } 365 assert_array_equals(pc2.iceStates, ['new', 'checking', 'connected']); 366 }, 'Responder ICE connection state behaves as expected'); 367 368 /* 369 Test case for step 11 of PeerConnection.close(). 370 ... 371 11. Set connection's ICE connection state to "closed". This does not invoke 372 the "update the ICE connection state" procedure, and does not fire any 373 event. 374 ... 375 */ 376 promise_test(async t => { 377 const pc1 = new RTCPeerConnection(); 378 t.add_cleanup(() => pc1.close()); 379 const pc2 = new RTCPeerConnection(); 380 const stream = await getNoiseStream({ audio: true }); 381 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 382 383 stream.getTracks().forEach(track => pc1.addTrack(track, stream)); 384 exchangeIceCandidates(pc1, pc2); 385 exchangeOfferAnswer(pc1, pc2); 386 await listenToIceConnected(pc2); 387 388 pc2.oniceconnectionstatechange = t.unreached_func(); 389 pc2.close(); 390 assert_equals(pc2.iceConnectionState, 'closed'); 391 await new Promise(r => t.step_timeout(r, 100)); 392 }, 'Closing a PeerConnection should not fire iceconnectionstatechange event'); 393 394 promise_test(async t => { 395 const pc1 = new RTCPeerConnection(); 396 t.add_cleanup(() => pc1.close()); 397 const pc2 = new RTCPeerConnection(); 398 const stream = await getNoiseStream({ audio: true }); 399 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 400 401 stream.getTracks().forEach(track => pc1.addTrack(track, stream)); 402 // Only signal candidate from 1->2. 403 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 404 pc1.iceStates = [pc1.iceConnectionState]; 405 pc1.oniceconnectionstatechange = () => { 406 pc1.iceStates.push(pc1.iceConnectionState); 407 }; 408 exchangeOfferAnswer(pc1, pc2); 409 await listenToIceConnected(pc2); 410 411 assert_true(pc1.iceStates.length >= 2); 412 assert_equals(pc1.iceStates[1], 'checking'); 413 }, 'iceConnectionState can go to checking without explictly calling addIceCandidate'); 414 415 promise_test(async t => { 416 const pc1 = new RTCPeerConnection(); 417 const pc2 = new RTCPeerConnection(); 418 exchangeIceCandidates(pc1, pc2); 419 const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' }); 420 await exchangeOfferAnswer(pc1, pc2); 421 await listenToIceConnected(pc1); 422 expectNoMoreIceConnectionStateChanges(t, pc1); 423 pc1.restartIce(); 424 await exchangeOfferAnswer(pc1, pc2); 425 await new Promise(r => t.step_timeout(r, 1000)); 426 }, 'ICE restart does not result in a transition back to checking'); 427 428 </script>