Skip to content

Commit

Permalink
Test cases for RTCIceTransport.
Browse files Browse the repository at this point in the history
Differential Revision: https://phabricator.services.mozilla.com/D193124

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1811912
gecko-commit: f743d6b74f6026f076f6519c6ca6ef8659a2d0ac
gecko-reviewers: jib
  • Loading branch information
docfaraday authored and moz-wptsync-bot committed Feb 22, 2024
1 parent eb8b11e commit 85108be
Show file tree
Hide file tree
Showing 9 changed files with 820 additions and 20 deletions.
35 changes: 27 additions & 8 deletions webrtc/RTCConfiguration-iceTransportPolicy.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@
t.add_cleanup(() => offerer.close());

offerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

offerer.addTransceiver('audio');
await offerer.setLocalDescription();
Expand All @@ -142,8 +147,13 @@
t.add_cleanup(() => offerer.close());
t.add_cleanup(() => answerer.close());

answerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
offerer.addEventListener('icecandidate',
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

offerer.addTransceiver('audio');
const offer = await offerer.createOffer();
Expand Down Expand Up @@ -175,9 +185,14 @@
offerer.setConfiguration({iceTransportPolicy: 'relay'});

offerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));

await Promise.all([
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

await Promise.all([
exchangeOfferAnswer(offerer, answerer),
waitForIceStateChange(offerer, ['failed']),
waitForIceStateChange(answerer, ['failed']),
Expand All @@ -201,9 +216,13 @@
exchangeIceCandidates(offerer, answerer);

const checkNoCandidate =
e => assert_equals(e.candidate, null, 'Should get no ICE candidates');
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
};

offerer.addEventListener('icecandidate', checkNoCandidate);
offerer.addEventListener('icecandidate', checkNoCandidate);

await Promise.all([
exchangeOfferAnswer(offerer, answerer),
Expand Down
282 changes: 280 additions & 2 deletions webrtc/RTCIceTransport.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>RTCIceTransport</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script src='RTCConfiguration-helper.js'></script>
<script>
'use strict';

Expand Down Expand Up @@ -58,7 +60,7 @@
assert_true(dtlsTransport instanceof RTCDtlsTransport,
'Expect sctp.transport to be an RTCDtlsTransport');

const iceTransport = dtlsTransport.iceTransport;
const {iceTransport} = dtlsTransport;
assert_true(iceTransport instanceof RTCIceTransport,
'Expect dtlsTransport.transport to be an RTCIceTransport');

Expand Down Expand Up @@ -162,7 +164,7 @@
assert_equals(iceTransport2.role, 'controlled',
`Expect answerer's iceTransport to take the controlled role`);
});
}, 'Two connected iceTransports should has matching local/remote candidates returned');
}, 'Two connected iceTransports should have matching local/remote candidates returned');

promise_test(t => {
const pc1 = new RTCPeerConnection();
Expand Down Expand Up @@ -190,4 +192,280 @@
});
}, 'Unconnected iceTransport should have empty remote candidates and selected pair');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(iceTransport.state, 'new');
assert_equals(iceTransport.gatheringState, 'new');
}, 'RTCIceTransport should be in state "new" initially');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
assert_equals(await nextGatheringState(iceTransport), 'complete');
}, 'RTCIceTransport should transition to "gathering" then "complete", after sLD');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
pc1.close();
assert_equals(iceTransport.gatheringState, 'gathering');
const result = await Promise.race([
gatheringStateReached(iceTransport, 'complete'),
new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange after PC.close(), but got one. state is "${result}"`);
}, 'PC.close() should not cause the RTCIceTransport gathering state to transition to "complete"');

promise_test(async t => {
const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
// TODO: If the spec settles on exposing the sctp transport in
// have-local-offer, we won't need this audio transceiver hack.
// See https://github.com/w3c/webrtc-pc/issues/2898 and
// https://github.com/w3c/webrtc-pc/issues/2899
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
assert_equals(await nextGatheringState(iceTransport), 'complete');
// TODO: Maybe, maybe not.
assert_not_equals(pc1.sctp, null, 'pc1.sctp should be set after sLD');
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(pc1.sctp.transport.iceTransport, transceiver.sender.transport.iceTransport);
}, 'RTCIceTransport should transition to "gathering", then "complete" after sLD (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());

const {sender} = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
// Copy the SDP before it has candidate attrs
const offer = pc1.localDescription;
const checkingReached = connectionStateReached(sender.transport.iceTransport, 'checking');

let result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange right after sLD(offer), but got one. state is "${result}"`);

await pc2.setRemoteDescription(offer);

const firstPc2CandidatePromise =
new Promise(r => pc2.onicecandidate = e => r(e.candidate));

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange callback after sRD(answer), but got one. state is "${result}"`);

const candidate = await firstPc2CandidatePromise;
pc1.addIceCandidate(candidate);

await checkingReached;
}, 'RTCIceTransport should not transition to "checking" until after the answer is set _and_ the first remote candidate is received');


promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(await nextConnectionState(sender.transport.iceTransport), 'checking');
assert_equals(await nextConnectionState(sender.transport.iceTransport), 'connected');
await gatheringDone;
pc2.close();
await connectionStateReached(sender.transport.iceTransport, 'disconnected');
}, 'RTCIceTransport should transition to "disconnected" if packets stop flowing');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {sctp} = pc1;
assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'checking');
assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'connected');
await gatheringDone;
pc2.close();
await connectionStateReached(sctp.transport.iceTransport, 'disconnected');
}, 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

pc1.restartIce();

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Local ICE restart should not result in a different ICE transport');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {iceTransport} = pc1.sctp.transport;

pc1.restartIce();

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Local ICE restart should not result in a different ICE transport (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');

await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

pc2.restartIce();

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc1.localDescription);

assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Remote ICE restart should not result in a different ICE transport');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {iceTransport} = pc1.sctp.transport;

pc2.restartIce();

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc1.localDescription);

assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Remote ICE restart should not result in a different ICE transport (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
// Add two transceivers, one audio and one video. The default bundlePolicy
// ("balanced") will result in each being offered with its own transport,
// but allowing the answerer to bundle the second transceiver on the
// transport of the first, which the answerer will do by default.
const audioTransceiver = pc1.addTransceiver('audio');
const videoTransceiver = pc1.addTransceiver('video');
pc1.createDataChannel('test');

await pc1.setLocalDescription();
const audioIceTransport = audioTransceiver.sender.transport.iceTransport;
const videoIceTransport = videoTransceiver.sender.transport.iceTransport;

assert_not_equals(audioIceTransport, videoIceTransport, 'audio and video should start out with different transports');

await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const sctpIceTransport = pc1.sctp.transport.iceTransport;

assert_equals(videoTransceiver.sender.transport.iceTransport, audioIceTransport, 'After negotiation, the video sender should use the bundle ICE transport from the audio sender');
assert_equals(pc1.sctp.transport.iceTransport, audioIceTransport, 'After negotiation, the datachannel should use the bundle ICE transport from the audio sender');
assert_not_equals(videoIceTransport.state, 'closed', 'Completion of offer/answer should not close the unused ICE transport immediately');

await connectionStateReached(videoIceTransport, 'closed');
}, 'RTCIceTransport should transition to "closed" if the underlying transport is closed because the answer used bundle');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(await nextConnectionState(iceTransport), 'checking');
assert_equals(await nextConnectionState(iceTransport), 'connected');
await gatheringDone;

const closedEvent = connectionStateReached(iceTransport, 'closed');
pc1.close();
assert_equals(sender.transport.iceTransport, iceTransport, 'PC.close() should not unset the sender transport');
assert_equals(iceTransport.state, 'closed', 'pc.close() should close the sender transport synchronously');
const result = await Promise.race([closedEvent, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, 'statechange event should not fire when transitioning to closed due to PC.close()');
}, 'RTCIceTransport should synchronously transition to "closed" with no event if the underlying transport is closed due to PC.close()');

</script>
Loading

0 comments on commit 85108be

Please sign in to comment.