| <!doctype html> | |
| <meta charset=utf-8> | |
| <title>RTCPeerConnection.prototype.connectionState</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.htm | |
| // The following helper functions are called from RTCPeerConnection-helper.js: | |
| // exchangeIceCandidates | |
| // exchangeOfferAnswer | |
| /* | |
| 4.3.2. Interface Definition | |
| interface RTCPeerConnection : EventTarget { | |
| ... | |
| readonly attribute RTCPeerConnectionState connectionState; | |
| attribute EventHandler onconnectionstatechange; | |
| }; | |
| 4.4.3. RTCPeerConnectionState Enum | |
| enum RTCPeerConnectionState { | |
| "new", | |
| "connecting", | |
| "connected", | |
| "disconnected", | |
| "failed", | |
| "closed" | |
| }; | |
| 5.5. RTCDtlsTransport Interface | |
| interface RTCDtlsTransport { | |
| readonly attribute RTCIceTransport iceTransport; | |
| readonly attribute RTCDtlsTransportState state; | |
| ... | |
| }; | |
| enum RTCDtlsTransportState { | |
| "new", | |
| "connecting", | |
| "connected", | |
| "closed", | |
| "failed" | |
| }; | |
| 5.6. RTCIceTransport Interface | |
| interface RTCIceTransport { | |
| readonly attribute RTCIceTransportState state; | |
| ... | |
| }; | |
| enum RTCIceTransportState { | |
| "new", | |
| "checking", | |
| "connected", | |
| "completed", | |
| "failed", | |
| "disconnected", | |
| "closed" | |
| }; | |
| */ | |
| /* | |
| 4.4.3. RTCPeerConnectionState Enum | |
| new | |
| Any of the RTCIceTransports or RTCDtlsTransports are in the new | |
| state and none of the transports are in the connecting, checking, | |
| failed or disconnected state, or all transports are in the closed state. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| assert_equals(pc.connectionState, 'new'); | |
| }, 'Initial connectionState should be new'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| pc.close(); | |
| assert_equals(pc.connectionState, 'closed'); | |
| }, 'Closing the connection should set connectionState to closed'); | |
| /* | |
| 4.4.3. RTCPeerConnectionState Enum | |
| connected | |
| All RTCIceTransports and RTCDtlsTransports are in the connected, | |
| completed or closed state and at least of them is in the connected | |
| or completed state. | |
| 5.5. RTCDtlsTransportState | |
| connected | |
| DTLS has completed negotiation of a secure connection. | |
| 5.6. 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_connecting = false; | |
| const onConnectionStateChange = t.step_func(() => { | |
| const {connectionState} = pc1; | |
| if (connectionState === 'connecting') { | |
| had_connecting = true; | |
| } else if (connectionState === 'connected') { | |
| assert_true(had_connecting, "state should pass connecting before reaching connected"); | |
| t.done(); | |
| } | |
| }); | |
| pc1.createDataChannel('test'); | |
| pc1.addEventListener('connectionstatechange', onConnectionStateChange); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| }, 'connection with one data channel should eventually have connected connection state'); | |
| async_test(t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const onConnectionStateChange = t.step_func(() => { | |
| const {connectionState} = pc1; | |
| if (connectionState === 'connected') { | |
| const sctpTransport = pc1.sctp; | |
| const dtlsTransport = sctpTransport.transport; | |
| assert_equals(dtlsTransport.state, 'connected', | |
| 'Expect DTLS transport to be in connected state'); | |
| const iceTransport = dtlsTransport.iceTransport | |
| assert_true(iceTransport.state === 'connected' || | |
| iceTransport.state === 'completed', | |
| 'Expect ICE transport to be in connected or completed state'); | |
| t.done(); | |
| } | |
| }); | |
| pc1.createDataChannel('test'); | |
| pc1.addEventListener('connectionstatechange', onConnectionStateChange); | |
| exchangeIceCandidates(pc1, pc2); | |
| exchangeOfferAnswer(pc1, pc2); | |
| }, 'connection with one data channel should eventually have transports in connected state'); | |
| /* | |
| TODO | |
| 4.4.3. RTCPeerConnectionState Enum | |
| connecting | |
| Any of the RTCIceTransports or RTCDtlsTransports are in the | |
| connecting or checking state and none of them is in the failed state. | |
| disconnected | |
| Any of the RTCIceTransports or RTCDtlsTransports are in the disconnected | |
| state and none of them are in the failed or connecting or checking state. | |
| failed | |
| Any of the RTCIceTransports or RTCDtlsTransports are in a failed state. | |
| closed | |
| The RTCPeerConnection object's [[isClosed]] slot is true. | |
| 5.5. RTCDtlsTransportState | |
| new | |
| DTLS has not started negotiating yet. | |
| connecting | |
| DTLS is in the process of negotiating a secure connection. | |
| closed | |
| The transport has been closed. | |
| failed | |
| The transport has failed as the result of an error (such as a failure | |
| to validate the remote fingerprint). | |
| 5.6. RTCIceTransportState | |
| new | |
| The RTCIceTransport is gathering candidates and/or waiting for | |
| remote candidates to be supplied, and has not yet started checking. | |
| 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. | |
| 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. | |
| */ | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection(); | |
| t.add_cleanup(() => caller.close()); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| caller.addTrack(track, stream); | |
| await exchangeOfferAnswer(caller, callee); | |
| assert_equals(caller.iceConnectionState, 'new'); | |
| assert_equals(callee.iceConnectionState, 'new'); | |
| }, 'connectionState remains new when not adding remote ice candidates'); | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection(); | |
| t.add_cleanup(() => caller.close()); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| caller.addTrack(track, stream); | |
| const states = []; | |
| caller.addEventListener('connectionstatechange', () => states.push(caller.connectionState)); | |
| exchangeIceCandidates(caller, callee); | |
| await exchangeOfferAnswer(caller, callee); | |
| await listenToConnected(caller); | |
| assert_array_equals(states, ['connecting', 'connected']); | |
| }, 'connectionState transitions to connected via connecting'); | |
| // Make the callee act as if not bundle-aware | |
| async function exchangeOfferAnswerUnbundled(caller, callee) { | |
| const offer = await caller.createOffer(); | |
| const sdp = offer.sdp.replace('BUNDLE', 'SOMETHING') | |
| .replace(/rtp-hdrext:sdes/g, 'rtp-hdrext:something') | |
| .replace(/a=ssrc:/g, 'a=notssrc'); | |
| await caller.setLocalDescription(offer); | |
| await callee.setRemoteDescription({type: 'offer', sdp}); | |
| await exchangeAnswer(caller, callee); | |
| } | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection({bundlePolicy: 'max-compat'}); | |
| 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); | |
| exchangeOfferAnswerUnbundled(pc1, pc2); | |
| await listenToConnected(pc1); | |
| // https://github.com/w3c/webrtc-pc/issues/2678#issuecomment-948554126 | |
| let had_intermediary_connecting = false | |
| let channel; | |
| const onConnectionStateChange = t.step_func(() => { | |
| const {connectionState, iceConnectionState} = pc1; | |
| if (connectionState === 'connecting') { | |
| had_intermediary_connecting = true; | |
| } | |
| }); | |
| pc1.addEventListener('connectionstatechange', onConnectionStateChange); | |
| channel = pc1.createDataChannel('test'); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await listenToConnected(pc1); | |
| assert_true(had_intermediary_connecting, "state should re-pass connecting before reaching connected"); | |
| }, 'when adding a datachannel to an existing unbundled connected PC, it should go through a connecting state'); | |
| 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.onconnectionstatechange = t.unreached_func(); | |
| pc2.close(); | |
| assert_equals(pc2.connectionState, 'closed'); | |
| await new Promise(r => t.step_timeout(r, 100)); | |
| }, 'Closing a PeerConnection should not fire connectionstatechange event'); | |
| </script> |