| <!doctype html> |
| <meta charset=utf-8> |
| <meta name="timeout" content="long"> |
| <title>RTCPeerConnection.prototype.iceGatheringState</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 |
| |
| // The following helper functions are called from RTCPeerConnection-helper.js: |
| // exchangeAnswer |
| // exchangeIceCandidates |
| // generateAudioReceiveOnlyOffer |
| |
| /* |
| 4.3.2. Interface Definition |
| interface RTCPeerConnection : EventTarget { |
| ... |
| readonly attribute RTCIceGatheringState iceGatheringState; |
| attribute EventHandler onicegatheringstatechange; |
| }; |
| |
| 4.4.2. RTCIceGatheringState Enum |
| enum RTCIceGatheringState { |
| "new", |
| "gathering", |
| "complete" |
| }; |
| |
| 5.6. RTCIceTransport Interface |
| interface RTCIceTransport { |
| readonly attribute RTCIceGathererState gatheringState; |
| ... |
| }; |
| |
| enum RTCIceGathererState { |
| "new", |
| "gathering", |
| "complete" |
| }; |
| */ |
| |
| /* |
| 4.4.2. RTCIceGatheringState Enum |
| new |
| Any of the RTCIceTransport s are in the new gathering state and |
| none of the transports are in the gathering state, or there are |
| no transports. |
| */ |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| assert_equals(pc.iceGatheringState, 'new'); |
| }, 'Initial iceGatheringState should be new'); |
| |
| async_test(t => { |
| const pc = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc.close()); |
| |
| let reachedGathering = false; |
| const onIceGatheringStateChange = t.step_func(() => { |
| const { iceGatheringState } = pc; |
| |
| if(iceGatheringState === 'gathering') { |
| reachedGathering = true; |
| } else if(iceGatheringState === 'complete') { |
| assert_true(reachedGathering, 'iceGatheringState should reach gathering before complete'); |
| t.done(); |
| } |
| }); |
| |
| assert_equals(pc.onicegatheringstatechange, null, |
| 'Expect connection to have icegatheringstatechange event'); |
| assert_equals(pc.iceGatheringState, 'new'); |
| |
| pc.addEventListener('icegatheringstatechange', onIceGatheringStateChange); |
| |
| generateAudioReceiveOnlyOffer(pc) |
| .then(offer => pc.setLocalDescription(offer)) |
| .then(err => t.step_func(err => |
| assert_unreached(`Unhandled rejection ${err.name}: ${err.message}`))); |
| }, 'iceGatheringState should eventually become complete after setLocalDescription'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| await initialOfferAnswerWithIceGatheringStateTransitions( |
| pc1, pc2); |
| |
| expectNoMoreGatheringStateChanges(t, pc1); |
| expectNoMoreGatheringStateChanges(t, pc2); |
| |
| await pc1.setLocalDescription(await pc1.createOffer()); |
| await pc2.setLocalDescription(await pc2.createOffer()); |
| |
| await new Promise(r => t.step_timeout(r, 500)); |
| }, 'setLocalDescription(reoffer) with no new transports should not cause iceGatheringState to change'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| |
| expectNoMoreGatheringStateChanges(t, pc1); |
| |
| await pc1.setLocalDescription(await pc1.createOffer()); |
| |
| await new Promise(r => t.step_timeout(r, 500)); |
| }, 'setLocalDescription() with no transports should not cause iceGatheringState to change'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| await pc1.setLocalDescription(); |
| await iceGatheringStateTransitions(pc1, 'gathering', 'complete'); |
| assert_true(pc1.localDescription.sdp.includes('a=end-of-candidates')); |
| }, 'local description should have a=end-of-candidates when gathering completes'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| await pc1.setLocalDescription(); |
| const iceTransport = transceiver.sender.transport.iceTransport; |
| |
| // This test code assumes that https://github.com/w3c/webrtc-pc/pull/2894 |
| // will be merged. The spec will say to dispatch two tasks; one that fires |
| // the empty candidate, and another that fires |
| // RTCIceTransport.gatheringstatechange, then |
| // RTCPeerConnection.icegatheringstatechange, then the global null |
| // candidate. |
| while (true) { |
| const {candidate} = await new Promise(r => pc1.onicecandidate = r); |
| assert_not_equals(candidate, null, 'Global null candidate event should not fire yet'); |
| if (candidate.candidate == '') { |
| break; |
| } |
| } |
| assert_equals(iceTransport.gatheringState, 'gathering'); |
| assert_equals(pc1.iceGatheringState, 'gathering'); |
| |
| // Now, we test the stuff that happens in the second queued task. |
| const events = []; |
| await new Promise(r => { |
| iceTransport.ongatheringstatechange = () => { |
| assert_equals(iceTransport.gatheringState, 'complete'); |
| assert_equals(pc1.iceGatheringState, 'complete'); |
| events.push('gatheringstatechange'); |
| }; |
| pc1.onicegatheringstatechange = () => { |
| assert_equals(iceTransport.gatheringState, 'complete'); |
| assert_equals(pc1.iceGatheringState, 'complete'); |
| events.push('icegatheringstatechange'); |
| } |
| pc1.onicecandidate = e => { |
| assert_equals(e.candidate, null); |
| assert_equals(iceTransport.gatheringState, 'complete'); |
| assert_equals(pc1.iceGatheringState, 'complete'); |
| events.push('icecandidate'); |
| r(); |
| }; |
| }); |
| |
| assert_array_equals(events, [ |
| 'gatheringstatechange', |
| 'icegatheringstatechange', |
| 'icecandidate' |
| ], 'events must be fired on the same task in this order'); |
| }, 'gathering state and candidate callbacks should fire in the correct order'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| await initialOfferAnswerWithIceGatheringStateTransitions( |
| pc1, pc2); |
| await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true})); |
| await iceGatheringStateTransitions(pc1, 'gathering', 'complete'); |
| }, 'setLocalDescription(reoffer) with a restarted transport should cause iceGatheringState to go to "gathering" and then "complete"'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| pc1.addTransceiver('video', { direction: 'recvonly' }); |
| exchangeIceCandidates(pc1, pc2); |
| await pc1.setLocalDescription(); |
| const firstGather = Promise.all([ |
| iceGatheringStateTransitions(pc1, 'gathering', 'complete'), |
| iceGatheringStateTransitions(pc2, 'gathering', 'complete')]); |
| const mungedOffer = {type: 'offer', sdp: pc1.localDescription.sdp.replace('BUNDLE', 'BUNGLE')}; |
| await pc2.setRemoteDescription(mungedOffer); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| // Let gathering finish, so we don't have two generations gathering at once |
| // This can cause errors depending on timing. |
| await firstGather; |
| await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true})); |
| // We only do this so we don't get errors in addCandidate. We don't want |
| // to wait for it, because we might miss the gathering transitions. |
| pc2.setRemoteDescription(pc1.localDescription); |
| await iceGatheringStateTransitions(pc1, 'gathering', 'complete'); |
| }, 'setLocalDescription(reoffer) with two restarted transports should cause iceGatheringState to go to "gathering" and then "complete"'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| expectNoMoreGatheringStateChanges(t, pc2); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| const offer = await pc1.createOffer(); |
| await pc2.setRemoteDescription(offer); |
| await pc2.setRemoteDescription(offer); |
| await pc2.setRemoteDescription({type: 'rollback'}); |
| await pc2.setRemoteDescription(offer); |
| }, 'sRD does not cause ICE gathering state changes'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); |
| await initialOfferAnswerWithIceGatheringStateTransitions( |
| pc1, pc2); |
| |
| const pc1waiter = iceGatheringStateTransitions(pc1, 'new'); |
| const pc2waiter = iceGatheringStateTransitions(pc2, 'new'); |
| pc1.getTransceivers()[0].stop(); |
| await pc1.setLocalDescription(await pc1.createOffer()); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(await pc2.createAnswer()); |
| assert_equals(pc2.getTransceivers().length, 0, |
| 'PC2 transceivers should be invisible after negotiation'); |
| assert_equals(pc2.iceGatheringState, 'new'); |
| await pc2waiter; |
| await pc1.setRemoteDescription(pc2.localDescription); |
| assert_equals(pc1.getTransceivers().length, 0, |
| 'PC1 transceivers should be invisible after negotiation'); |
| assert_equals(pc1.iceGatheringState, 'new'); |
| await pc1waiter; |
| }, 'renegotiation that closes all transports should result in ICE gathering state "new"'); |
| |
| /* |
| 4.3.2. RTCIceGatheringState Enum |
| new |
| Any of the RTCIceTransports are in the "new" gathering state and none |
| of the transports are in the "gathering" state, or there are no |
| transports. |
| |
| gathering |
| Any of the RTCIceTransport s are in the gathering state. |
| |
| complete |
| At least one RTCIceTransport exists, and all RTCIceTransports are |
| in the completed gathering state. |
| |
| 5.6. RTCIceGathererState |
| gathering |
| The RTCIceTransport is in the process of gathering candidates. |
| |
| complete |
| The RTCIceTransport has completed gathering and the end-of-candidates |
| indication for this transport has been sent. It will not gather candidates |
| again until an ICE restart causes it to restart. |
| */ |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc2.close()); |
| |
| const onIceGatheringStateChange = t.step_func(() => { |
| const { iceGatheringState } = pc2; |
| |
| if(iceGatheringState === 'gathering') { |
| const iceTransport = pc2.sctp.transport.iceTransport; |
| |
| assert_equals(iceTransport.gatheringState, 'gathering', |
| 'Expect ICE transport to be in gathering gatheringState when iceGatheringState is gathering'); |
| |
| } else if(iceGatheringState === 'complete') { |
| const iceTransport = pc2.sctp.transport.iceTransport; |
| |
| assert_equals(iceTransport.gatheringState, 'complete', |
| 'Expect ICE transport to be in complete gatheringState when iceGatheringState is complete'); |
| |
| t.done(); |
| } |
| }); |
| |
| pc1.createDataChannel('test'); |
| |
| // Spec bug w3c/webrtc-pc#1382 |
| // Because sctp is only defined when answer is set, we listen |
| // to pc2 so that we can be confident that sctp is defined |
| // when icegatheringstatechange event is fired. |
| pc2.addEventListener('icegatheringstatechange', onIceGatheringStateChange); |
| |
| |
| exchangeIceCandidates(pc1, pc2); |
| |
| await pc1.setLocalDescription(); |
| assert_equals(pc1.sctp.transport.iceTransport.gatheringState, 'new'); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| |
| await exchangeAnswer(pc1, pc2); |
| }, 'connection with one data channel should eventually have connected connection state'); |
| |
| /* |
| TODO |
| 5.6. RTCIceTransport Interface |
| new |
| The RTCIceTransport was just created, and has not started gathering |
| candidates yet. |
| */ |
| </script> |