| <!doctype html> | |
| <meta charset=utf-8> | |
| <meta name="timeout" content="long"> | |
| <title>RTCPeerConnection.prototype.iceConnectionState</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| // Test is based on the following editor draft: | |
| // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html | |
| /* | |
| 4.3.2. Interface Definition | |
| interface RTCPeerConnection : EventTarget { | |
| ... | |
| readonly attribute RTCIceConnectionState iceConnectionState; | |
| attribute EventHandler oniceconnectionstatechange; | |
| }; | |
| 4.4.4 RTCIceConnectionState Enum | |
| enum RTCIceConnectionState { | |
| "new", | |
| "checking", | |
| "connected", | |
| "completed", | |
| "failed", | |
| "disconnected", | |
| "closed" | |
| }; | |
| 5.6. RTCIceTransport Interface | |
| interface RTCIceTransport { | |
| readonly attribute RTCIceTransportState state; | |
| attribute EventHandler onstatechange; | |
| ... | |
| }; | |
| enum RTCIceTransportState { | |
| "new", | |
| "checking", | |
| "connected", | |
| "completed", | |
| "failed", | |
| "disconnected", | |
| "closed" | |
| }; | |
| */ | |
| /* | |
| 4.4.4 RTCIceConnectionState Enum | |
| new | |
| Any of the RTCIceTransports are in the new state and none of them | |
| are in the checking, failed or disconnected state, or all | |
| RTCIceTransport s are in the closed state. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| assert_equals(pc.iceConnectionState, 'new'); | |
| }, 'Initial iceConnectionState should be new'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| pc.close(); | |
| assert_equals(pc.iceConnectionState, 'closed'); | |
| }, 'Closing the connection should set iceConnectionState to closed'); | |
| /* | |
| 4.4.4 RTCIceConnectionState Enum | |
| checking | |
| Any of the RTCIceTransport s are in the checking state and none of | |
| them are in the failed or disconnected state. | |
| connected | |
| All RTCIceTransport s are in the connected, completed or closed state | |
| and at least one of them is in the connected state. | |
| completed | |
| All RTCIceTransport s are in the completed or closed state and at least | |
| one of them is in the completed state. | |
| checking | |
| The RTCIceTransport has received at least one remote candidate and | |
| is checking candidate pairs and has either not yet found a connection | |
| or consent checks [RFC7675] have failed on all previously successful | |
| candidate pairs. In addition to checking, it may also still be gathering. | |
| 5.6. enum RTCIceTransportState | |
| connected | |
| The RTCIceTransport has found a usable connection, but is still | |
| checking other candidate pairs to see if there is a better connection. | |
| It may also still be gathering and/or waiting for additional remote | |
| candidates. If consent checks [RFC7675] fail on the connection in use, | |
| and there are no other successful candidate pairs available, then the | |
| state transitions to "checking" (if there are candidate pairs remaining | |
| to be checked) or "disconnected" (if there are no candidate pairs to | |
| check, but the peer is still gathering and/or waiting for additional | |
| remote candidates). | |
| completed | |
| The RTCIceTransport has finished gathering, received an indication that | |
| there are no more remote candidates, finished checking all candidate | |
| pairs and found a connection. If consent checks [RFC7675] subsequently | |
| fail on all successful candidate pairs, the state transitions to "failed". | |
| */ | |
| async_test(t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| let had_checking = false; | |
| const onIceConnectionStateChange = t.step_func(() => { | |
| const {iceConnectionState} = pc1; | |
| if (iceConnectionState === 'checking') { | |
| had_checking = true; | |
| } else if (iceConnectionState === 'connected' || | |
| iceConnectionState === 'completed') { | |
| assert_true(had_checking, 'state should pass checking before' + | |
| ' reaching connected or completed'); | |
| t.done(); | |
| } else if (iceConnectionState === 'failed') { | |
| assert_unreached("ICE should not fail"); | |
| } | |
| }); | |
| pc1.createDataChannel('test'); | |
| pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| }, 'connection with one data channel should eventually have connected or ' + | |
| 'completed connection state'); | |
| async_test(t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const onIceConnectionStateChange = t.step_func(() => { | |
| const { iceConnectionState } = pc1; | |
| if(iceConnectionState === 'checking') { | |
| const iceTransport = pc1.sctp.transport.iceTransport; | |
| assert_equals(iceTransport.state, 'checking', | |
| 'Expect ICE transport to be in checking state when' + | |
| ' iceConnectionState is checking'); | |
| } else if(iceConnectionState === 'connected') { | |
| const iceTransport = pc1.sctp.transport.iceTransport; | |
| assert_equals(iceTransport.state, 'connected', | |
| 'Expect ICE transport to be in connected state when' + | |
| ' iceConnectionState is connected'); | |
| t.done(); | |
| } else if(iceConnectionState === 'completed') { | |
| const iceTransport = pc1.sctp.transport.iceTransport; | |
| assert_equals(iceTransport.state, 'completed', | |
| 'Expect ICE transport to be in connected state when' + | |
| ' iceConnectionState is completed'); | |
| t.done(); | |
| } else if (iceConnectionState === 'failed') { | |
| assert_unreached("ICE should not fail"); | |
| } | |
| }); | |
| pc1.createDataChannel('test'); | |
| assert_equals(pc1.oniceconnectionstatechange, null, | |
| 'Expect connection to have iceconnectionstatechange event'); | |
| pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| }, 'connection with one data channel should eventually ' + | |
| 'have connected connection state'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| stream.getTracks().forEach(track => pc1.addTrack(track, stream)); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| await listenToIceConnected(pc1); | |
| }, 'connection with audio track should eventually ' + | |
| 'have connected connection state'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true, video:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| stream.getTracks().forEach(track => pc1.addTrack(track, stream)); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| await listenToIceConnected(pc1); | |
| }, 'connection with audio and video tracks should eventually ' + | |
| 'have connected connection state'); | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection(); | |
| t.add_cleanup(() => caller.close()); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| caller.addTransceiver('audio', {direction:'recvonly'}); | |
| const stream = await getNoiseStream({audio:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| callee.addTrack(track, stream); | |
| exchangeIceCandidates(caller, callee); | |
| await exchangeOfferAnswer(caller, callee); | |
| assert_equals(caller.getTransceivers().length, 1); | |
| const [transceiver] = caller.getTransceivers(); | |
| assert_equals(transceiver.currentDirection, 'recvonly'); | |
| await listenToIceConnected(caller); | |
| }, 'ICE can connect in a recvonly usecase'); | |
| /* | |
| TODO | |
| 4.4.4 RTCIceConnectionState Enum | |
| failed | |
| Any of the RTCIceTransport s are in the failed state. | |
| disconnected | |
| Any of the RTCIceTransport s are in the disconnected state and none of | |
| them are in the failed state. | |
| closed | |
| The RTCPeerConnection object's [[ isClosed]] slot is true. | |
| 5.6. enum RTCIceTransportState | |
| new | |
| The RTCIceTransport is gathering candidates and/or waiting for | |
| remote candidates to be supplied, and has not yet started checking. | |
| failed | |
| The RTCIceTransport has finished gathering, received an indication that | |
| there are no more remote candidates, finished checking all candidate pairs, | |
| and all pairs have either failed connectivity checks or have lost consent. | |
| disconnected | |
| The ICE Agent has determined that connectivity is currently lost for this | |
| RTCIceTransport . This is more aggressive than failed, and may trigger | |
| intermittently (and resolve itself without action) on a flaky network. | |
| The way this state is determined is implementation dependent. | |
| Examples include: | |
| Losing the network interface for the connection in use. | |
| Repeatedly failing to receive a response to STUN requests. | |
| Alternatively, the RTCIceTransport has finished checking all existing | |
| candidates pairs and failed to find a connection (or consent checks | |
| [RFC7675] once successful, have now failed), but it is still gathering | |
| and/or waiting for additional remote candidates. | |
| closed | |
| The RTCIceTransport has shut down and is no longer responding to STUN requests. | |
| */ | |
| for (let bundle_policy of ['balanced', 'max-bundle', 'max-compat']) { | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection({bundlePolicy: bundle_policy}); | |
| t.add_cleanup(() => caller.close()); | |
| const stream = await getNoiseStream( | |
| {audio: true, video:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track1, track2] = stream.getTracks(); | |
| const sender1 = caller.addTrack(track1); | |
| const sender2 = caller.addTrack(track2); | |
| caller.createDataChannel('datachannel'); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| exchangeIceCandidates(caller, callee); | |
| const offer = await caller.createOffer(); | |
| await caller.setLocalDescription(offer); | |
| const [caller_transceiver1, caller_transceiver2] = caller.getTransceivers(); | |
| assert_equals(sender1.transport, caller_transceiver1.sender.transport); | |
| await callee.setRemoteDescription(offer); | |
| const [callee_transceiver1, callee_transceiver2] = callee.getTransceivers(); | |
| const answer = await callee.createAnswer(); | |
| await callee.setLocalDescription(answer); | |
| await caller.setRemoteDescription(answer); | |
| // At this point, we should have a single ICE transport, and it | |
| // should eventually get to the "connected" state. | |
| await waitForState(caller_transceiver1.receiver.transport.iceTransport, | |
| 'connected'); | |
| // The PeerConnection's iceConnectionState should therefore be 'connected' | |
| assert_equals(caller.iceConnectionState, 'connected', | |
| 'PC.iceConnectionState:'); | |
| }, 'iceConnectionState changes at the right time, with bundle policy ' + | |
| bundle_policy); | |
| } | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); | |
| pc1.candidateBuffer = []; | |
| pc2.onicecandidate = e => { | |
| // Don't add candidate if candidate buffer is already used | |
| if (pc1.candidateBuffer) { | |
| pc1.candidateBuffer.push(e.candidate) | |
| } | |
| }; | |
| pc1.iceStates = [pc1.iceConnectionState]; | |
| pc2.iceStates = [pc2.iceConnectionState]; | |
| pc1.oniceconnectionstatechange = () => { | |
| pc1.iceStates.push(pc1.iceConnectionState); | |
| }; | |
| pc2.oniceconnectionstatechange = () => { | |
| pc2.iceStates.push(pc2.iceConnectionState); | |
| }; | |
| const localStream = await getNoiseStream({audio: true, video: true}); | |
| const localStream2 = await getNoiseStream({audio: true, video: true}); | |
| const remoteStream = await getNoiseStream({audio: true, video: true}); | |
| for (const stream of [localStream, localStream2, remoteStream]) { | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| } | |
| localStream.getTracks().forEach(t => pc1.addTrack(t, localStream)); | |
| localStream2.getTracks().forEach(t => pc1.addTrack(t, localStream2)); | |
| remoteStream.getTracks().forEach(t => pc2.addTrack(t, remoteStream)); | |
| const offer = await pc1.createOffer(); | |
| await pc2.setRemoteDescription(offer); | |
| await pc1.setLocalDescription(offer); | |
| const answer = await pc2.createAnswer(); | |
| await pc2.setLocalDescription(answer); | |
| await pc1.setRemoteDescription(answer); | |
| pc1.candidateBuffer.forEach(c => pc1.addIceCandidate(c)); | |
| delete pc1.candidateBuffer; | |
| await listenToIceConnected(pc1); | |
| await listenToIceConnected(pc2); | |
| // While we're waiting for pc2, pc1 may or may not have transitioned | |
| // to "completed" state, so allow for both cases. | |
| if (pc1.iceStates.length == 3) { | |
| assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected']); | |
| } else { | |
| assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected', | |
| 'completed']); | |
| } | |
| assert_array_equals(pc2.iceStates, ['new', 'checking', 'connected']); | |
| }, 'Responder ICE connection state behaves as expected'); | |
| /* | |
| Test case for step 11 of PeerConnection.close(). | |
| ... | |
| 11. Set connection's ICE connection state to "closed". This does not invoke | |
| the "update the ICE connection state" procedure, and does not fire any | |
| event. | |
| ... | |
| */ | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| stream.getTracks().forEach(track => pc1.addTrack(track, stream)); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| await listenToIceConnected(pc2); | |
| pc2.oniceconnectionstatechange = t.unreached_func(); | |
| pc2.close(); | |
| assert_equals(pc2.iceConnectionState, 'closed'); | |
| await new Promise(r => t.step_timeout(r, 100)); | |
| }, 'Closing a PeerConnection should not fire iceconnectionstatechange event'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| stream.getTracks().forEach(track => pc1.addTrack(track, stream)); | |
| // Only signal candidate from 1->2. | |
| pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); | |
| pc1.iceStates = [pc1.iceConnectionState]; | |
| pc1.oniceconnectionstatechange = () => { | |
| pc1.iceStates.push(pc1.iceConnectionState); | |
| }; | |
| exchangeOfferAnswer(pc1, pc2); | |
| await listenToIceConnected(pc2); | |
| assert_true(pc1.iceStates.length >= 2); | |
| assert_equals(pc1.iceStates[1], 'checking'); | |
| }, 'iceConnectionState can go to checking without explictly calling addIceCandidate'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| exchangeIceCandidates(pc1, pc2); | |
| const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' }); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await listenToIceConnected(pc1); | |
| expectNoMoreIceConnectionStateChanges(t, pc1); | |
| pc1.restartIce(); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await new Promise(r => t.step_timeout(r, 1000)); | |
| }, 'ICE restart does not result in a transition back to checking'); | |
| </script> |