Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!DOCTYPE html>
<html>
<head>
<title>Candidate exchange</title>
<meta name="variant" content="?interop-2026">
<meta name="variant" content="?rest">
<meta name=timeout content=long>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../RTCPeerConnection-helper.js"></script>
</head>
<body>
<script>
const rest = !new URLSearchParams(location.search).has("interop-2026");
const interop2026 = !new URLSearchParams(location.search).has("rest");
class StateLogger {
constructor(source, eventname, field) {
source.addEventListener(eventname, event => {
this.events.push(source[field]);
});
this.events = [source[field]];
}
}
class IceStateLogger extends StateLogger {
constructor(source) {
super(source, 'iceconnectionstatechange', 'iceConnectionState');
}
}
// Asserts that |cand| looks like a host candidate gathered locally over
// UDP, with fields populated from parsing the candidate-attribute string.
function assertHostLocalUdp(cand) {
assert_not_equals(cand.candidate, '', 'local candidate string is non-empty');
assert_equals(cand.type, 'host', 'local type');
assert_not_equals(cand.foundation, null, 'local foundation');
assert_equals(cand.component, 'rtp', 'local component');
assert_not_equals(cand.priority, null, 'local priority');
assert_not_equals(cand.address, null, 'local address');
assert_equals(cand.protocol, 'udp', 'local protocol');
assert_not_equals(cand.port, null, 'local port');
assert_equals(cand.tcpType, null, 'local tcpType');
assert_equals(cand.relatedAddress, null, 'local relatedAddress');
assert_equals(cand.relatedPort, null, 'local relatedPort');
}
// Asserts that |cand| represents a peer-reflexive remote candidate whose
// address has not been previously surfaced via addIceCandidate. Per
// webrtc-pc, the candidate-attribute string is empty for prflx remote, and
// the address/port stay null by default for any exposed remote candidate
// whose address has not been learned via addIceCandidate. The other
// parse-derived attributes (foundation, component, priority, protocol,
// type, tcpType) reflect what the UA learned about the pair.
function assertPrflxRemoteHiddenAddress(cand) {
assert_equals(cand.candidate, '', 'remote candidate string is empty');
assert_equals(cand.type, 'prflx', 'remote type');
assert_equals(cand.address, null, 'remote address hidden');
assert_not_equals(cand.port, null, 'remote port not hidden');
assert_not_equals(cand.foundation, null, 'remote foundation');
assert_equals(cand.component, 'rtp', 'remote component');
assert_not_equals(cand.priority, null, 'remote priority');
assert_equals(cand.protocol, 'udp', 'remote protocol');
assert_equals(cand.tcpType, null, 'remote tcpType');
assert_equals(cand.relatedAddress, null, 'remote relatedAddress');
assert_not_equals(cand.relatedPort, null, 'remote relatedPort');
}
if (rest) {
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('datachannel');
pc1IceStates = new IceStateLogger(pc1);
pc2IceStates = new IceStateLogger(pc1);
exchangeIceCandidates(pc1, pc2);
await exchangeOfferAnswer(pc1, pc2);
// Note - it's been claimed that this state sometimes jumps straight
// to "completed". If so, this test should be flaky.
await waitForIceStateChange(pc1, ['connected']);
assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
}, 'Two way ICE exchange works');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('datachannel');
pc1.onicecandidate = e => {
pc2.addIceCandidate(e.candidate);
}
// Candidates from |pc1| are not delivered to |pc2|, either via
// addIceCandidate or setRemoteDescription. |pc2| will use peer-reflexive
// candidates.
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
const noCandidatesAnswer =
pc2.localDescription.sdp.replace(/^a=candidate:/mg, "a=unknownattr:");
await pc1.setRemoteDescription({type: "answer", sdp: noCandidatesAnswer});
await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
waitForIceStateChange(pc2, ['connected', 'completed'])]);
const candidate_pair = pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
assertHostLocalUdp(candidate_pair.local);
assertPrflxRemoteHiddenAddress(candidate_pair.remote);
}, 'Selected candidate pair exposes local host fields and hides remote prflx fields');
}
if (interop2026) {
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1IceStates = new IceStateLogger(pc1);
pc2IceStates = new IceStateLogger(pc1);
let candidates = [];
pc1.createDataChannel('datachannel');
pc1.onicecandidate = e => {
candidates.push(e.candidate);
}
// Candidates from PC2 are not delivered to pc1, so pc1 will use
// peer-reflexive candidates.
await exchangeOfferAnswer(pc1, pc2);
const waiter = waitForIceGatheringState(pc1, ['complete']);
await waiter;
for (const candidate of candidates) {
if (candidate) {
pc2.addIceCandidate(candidate);
}
}
await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
waitForIceStateChange(pc2, ['connected', 'completed'])]);
const candidate_pair = pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
assert_equals(candidate_pair.local.type, 'host');
assert_equals(candidate_pair.remote.type, 'prflx');
assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
}, 'Adding only caller -> callee candidates gives a connection');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1IceStates = new IceStateLogger(pc1);
pc2IceStates = new IceStateLogger(pc1);
let candidates = [];
pc1.createDataChannel('datachannel');
pc2.onicecandidate = e => {
candidates.push(e.candidate);
}
// Candidates from pc1 are not delivered to pc2. so pc2 will use
// peer-reflexive candidates.
await exchangeOfferAnswer(pc1, pc2);
const waiter = waitForIceGatheringState(pc2, ['complete']);
await waiter;
for (const candidate of candidates) {
if (candidate) {
pc1.addIceCandidate(candidate);
}
}
await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
waitForIceStateChange(pc2, ['connected', 'completed'])]);
const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
assert_equals(candidate_pair.local.type, 'host');
assert_equals(candidate_pair.remote.type, 'prflx');
assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
}, 'Adding only callee -> caller candidates gives a connection');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1IceStates = new IceStateLogger(pc1);
pc2IceStates = new IceStateLogger(pc1);
let pc2ToPc1Candidates = [];
pc1.createDataChannel('datachannel');
pc2.onicecandidate = e => {
pc2ToPc1Candidates.push(e.candidate);
// This particular test verifies that candidates work
// properly if added from the pc2 onicecandidate event.
if (!e.candidate) {
for (const candidate of pc2ToPc1Candidates) {
if (candidate) {
pc1.addIceCandidate(candidate);
}
}
}
}
// Candidates from |pc1| are not delivered to |pc2|. |pc2| will use
// peer-reflexive candidates.
await exchangeOfferAnswer(pc1, pc2);
await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
waitForIceStateChange(pc2, ['connected', 'completed'])]);
const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
assert_equals(candidate_pair.local.type, 'host');
assert_equals(candidate_pair.remote.type, 'prflx');
assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
}, 'Adding callee -> caller candidates from end-of-candidates gives a connection');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1IceStates = new IceStateLogger(pc1);
pc2IceStates = new IceStateLogger(pc1);
let pc1ToPc2Candidates = [];
let pc2ToPc1Candidates = [];
pc1.createDataChannel('datachannel');
pc1.onicecandidate = e => {
pc1ToPc2Candidates.push(e.candidate);
}
pc2.onicecandidate = e => {
pc2ToPc1Candidates.push(e.candidate);
}
const offer = await pc1.createOffer();
await Promise.all([pc1.setLocalDescription(offer),
pc2.setRemoteDescription(offer)]);
const answer = await pc2.createAnswer();
await waitForIceGatheringState(pc1, ['complete']);
await pc2.setLocalDescription(answer).then(() => {
for (const candidate of pc1ToPc2Candidates) {
if (candidate) {
pc2.addIceCandidate(candidate);
}
}
});
await waitForIceGatheringState(pc2, ['complete']);
pc1.setRemoteDescription(answer).then(async () => {
for (const candidate of pc2ToPc1Candidates) {
if (candidate) {
await pc1.addIceCandidate(candidate);
}
}
});
await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
waitForIceStateChange(pc2, ['connected', 'completed'])]);
const candidate_pair =
pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
assert_equals(candidate_pair.local.type, 'host');
// When we supply remote candidates, we expect a jump to the 'host' candidate,
// but it might also remain as 'prflx'.
assert_true(candidate_pair.remote.type == 'host' ||
candidate_pair.remote.type == 'prflx');
assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
}, 'Explicit offer/answer exchange gives a connection');
}
if (rest) {
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
pc1.createDataChannel('datachannel');
pc1.onicecandidate = assert_unreached;
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
await new Promise(resolve => {
pc1.onicecandidate = resolve;
});
}, 'Candidates always arrive after setLocalDescription(offer) resolves');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('datachannel');
pc2.onicecandidate = assert_unreached;
const offer = await pc1.createOffer();
await pc2.setRemoteDescription(offer);
await pc2.setLocalDescription(await pc2.createAnswer());
await new Promise(resolve => {
pc2.onicecandidate = resolve;
});
}, 'Candidates always arrive after setLocalDescription(answer) resolves');
}
</script>
</body>
</html>