332 lines
12 KiB
HTML
332 lines
12 KiB
HTML
<!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>
|