Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

<!DOCTYPE HTML>
<html>
<head>
<script type="application/javascript" src="pc.js"></script>
<script type="application/javascript" src="iceTestUtils.js"></script>
<script type="application/javascript" src="helpers_from_wpt/sdp.js"></script></head>
<body>
<pre id="test">
<script type="application/javascript">
createHTML({
bug: "1898696",
title: "Corner cases for ICE candidate pair selection"
});
const tests = [
async function checkRelayPriorityWithLateTrickle() {
// Test that relay-based candidate pairs don't get prflx priority when
// trickle is late.
// Block host candidates; if we mess up and interpret relay as
// prflx, we won't have host candidates with a higher priority
// masking the problem.
await pushPrefs(
['media.peerconnection.ice.obfuscate_host_addresses', false],
['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
['media.peerconnection.nat_simulator.network_delay_ms', 50],
['media.peerconnection.ice.loopback', true],
// The above triggers warning about 5 ICE servers
['media.peerconnection.treat_warnings_as_errors', false],
['media.getusermedia.insecure.enabled', true]);
let turnServer = structuredClone(
iceServersArray.find(server => "username" in server));
// Disable TCP-based TURN; this goes through the NAT simulator much more
// quickly than UDP, and can result in TURN TCP establishment happening
// before srflx is even attempted.
turnServer.urls = turnServer.urls.filter(
u => u.indexOf("turns:") == -1 && u.indexOf("transport=t") == -1);
let stunServer = structuredClone(
iceServersArray.find(server => !("username" in server)));
// This is a somewhat contrived situation. What we're trying to do is
// cause the non-controlling side to learn about the controller's relay
// candidate from a STUN check, but learn about the srflx through
// trickle.
const pc1 = new RTCPeerConnection({iceServers: [turnServer]});
const pc2 = new RTCPeerConnection({iceServers: [stunServer]});
// Ensure that no host or relay candidates are trickled. Also, record all
// interfaces which are able to gather a srflx (ie; are able to reach the
// TURN server). Anything that cannot reach the TURN server and gather a
// srflx must be filtered out in both directions, otherwise pc2 will
// learn about those as prflx, and we want pc2 to only have prflx for
// pc1's relay candidates.
const ipAddrsWithSrflx = new Set();
pc1.onicecandidate = e => {
if (e.candidate && e.candidate.type == "srflx") {
ipAddrsWithSrflx.add(e.candidate.address);
}
// Add only srflx or the end-of-candidates signal
if (!e.candidate || e.candidate.type == "srflx") {
pc2.addIceCandidate(e.candidate);
}
};
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
// Wait for gathering to complete.
await new Promise(r => pc1.onicegatheringstatechange = () => {
if (pc1.iceGatheringState == "complete") {
r();
}
});
// Remove any candidates in the offer.
let mungedOffer = {
type: "offer",
sdp: pc1.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:")
};
await pc2.setRemoteDescription(mungedOffer);
ok(ipAddrsWithSrflx.size != 0, "PC1 was able to reach the TURN server with at least one address");
pc2.onicecandidate = e => {
if (!e.candidate || ipAddrsWithSrflx.has(e.candidate.address)) {
pc1.addIceCandidate(e.candidate);
}
};
await pc2.setLocalDescription();
let mungedAnswer = {
type: "answer",
sdp: pc2.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:")
};
await pc1.setRemoteDescription(mungedAnswer);
await Promise.all([iceConnected(pc1), iceConnected(pc2)]);
info("ICE connected");
const stats = await pc2.getStats();
info("Have all stats");
stats.forEach((value, key) => {
info(`${key} => ${JSON.stringify(value)}`);
});
function getRemoteCandidate(pair, stats) {
info(`Getting ${pair.remoteCandidateId} => ${JSON.stringify(stats.get(pair.remoteCandidateId))}`);
return stats.get(pair.remoteCandidateId);
}
// Convert the iterable to an array so we can use it more than once
const pairs = [...stats.values().filter(s => s.type == "candidate-pair")];
const srflxPriorities = pairs.filter(p => getRemoteCandidate(p, stats).candidateType == "srflx").map(p => p.priority);
// We obfuscate remote prflx candidates, so cannot match on port. The
// above code is intended to only allow prflx for the relay candidates.
const prflxPriorities = pairs.filter(p => getRemoteCandidate(p, stats).candidateType == "prflx").map(p => p.priority);
const minSrflxPriority = Math.min(...srflxPriorities);
const maxRelayPriority = Math.max(...prflxPriorities);
ok(maxRelayPriority < minSrflxPriority, `relay priorities should be less than srflx priorities (${maxRelayPriority} vs ${minSrflxPriority})`);
await SpecialPowers.popPrefEnv();
},
async function checkTurnTcpPriority() {
await pushPrefs(
['media.peerconnection.ice.obfuscate_host_addresses', false],
['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
['media.peerconnection.nat_simulator.network_delay_ms', 150],
['media.peerconnection.nat_simulator.block_udp', false],
['media.peerconnection.nat_simulator.block_tcp', false],
['media.peerconnection.nat_simulator.block_tls', false],
['media.peerconnection.ice.loopback', true],
// The above triggers warning about 5 ICE servers
['media.peerconnection.treat_warnings_as_errors', false],
['media.getusermedia.insecure.enabled', true]);
let turnServer = structuredClone(
iceServersArray.find(server => "username" in server));
turnServer.urls = turnServer.urls.filter(u => u.indexOf("turns:") == -1);
let stunServer = structuredClone(
iceServersArray.find(server => !("username" in server)));
const pc1 = new RTCPeerConnection(
{iceServers: [turnServer], iceTransportPolicy: "relay"});
const pc2 = new RTCPeerConnection({iceServers: [stunServer]});
// We will not allow the relay-only side (pc1) to trickle candidates. pc2
// will learn about those relay candidates as prflx, as long as it
// trickles its srflx.
pc2.onicecandidate = e => {
if (e.candidate && e.candidate.type == "srflx") {
pc1.addIceCandidate(e.candidate);
}
};
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const sender = pc1.addTrack(stream.getTracks()[0]);
await pc1.setLocalDescription();
// Ensure that the relay candidates are gathered and ready to go.
await new Promise(r => pc1.onicegatheringstatechange = () => {
if (pc1.iceGatheringState == "complete") {
r();
}
});
// Finish negotiation while removing any candidates in SDP
let mungedOffer = {
type: "offer",
sdp: pc1.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:")
};
await pc2.setRemoteDescription(mungedOffer);
await pc2.setLocalDescription();
let mungedAnswer = {
type: "answer",
sdp: pc2.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:")
};
await pc1.setRemoteDescription(mungedAnswer);
await Promise.all([iceConnected(pc1), iceConnected(pc2)]);
info("ICE connected");
const offererStats = await pc1.getStats();
const answererStats = await pc2.getStats();
info("Have all stats");
offererStats.forEach((value, key) => {
info(`${key} => ${JSON.stringify(value)}`);
});
answererStats.forEach((value, key) => {
info(`${key} => ${JSON.stringify(value)}`);
});
const turnUdpLocalCandidates = [...offererStats.values().filter(s => {
return s.type == "local-candidate" &&
s.candidateType == "relay" && s.relayProtocol == "udp";
})];
const turnTcpLocalCandidates = [...offererStats.values().filter(s => {
return s.type == "local-candidate" &&
s.candidateType == "relay" && s.relayProtocol == "tcp";
})];
// Remote candidates don't have relay protocol, but we can find them by
// matching ports.
const turnUdpPorts = [...turnUdpLocalCandidates.map(c => c.port)];
const turnTcpPorts = [...turnTcpLocalCandidates.map(c => c.port)];
// The relay candidates will be prflx, and we'll be able to tell which is
// which by port number.
const turnUdpRemoteCandidates = [...answererStats.values().filter(s => {
return s.type == "remote-candidate" &&
s.candidateType == "prflx" && turnUdpPorts.includes(s.port);
})];
const turnTcpRemoteCandidates = [...answererStats.values().filter(s => {
return s.type == "remote-candidate" &&
s.candidateType == "prflx" && turnTcpPorts.includes(s.port);
})];
ok(turnTcpLocalCandidates.length,
"There are local TURN TCP candidates");
ok(turnUdpLocalCandidates.length,
"There are local TURN UDP candidates");
ok(turnTcpRemoteCandidates.length,
"There are remote TURN TCP candidates");
ok(turnUdpRemoteCandidates.length,
"There are remote TURN UDP candidates");
const maxLocalTurnTcpPriority =
Math.max(...turnTcpLocalCandidates.map(c => c.priority));
const maxRemoteTurnTcpPriority =
Math.max(...turnTcpRemoteCandidates.map(c => c.priority));
const minLocalTurnUdpPriority =
Math.min(...turnUdpLocalCandidates.map(c => c.priority));
const minRemoteTurnUdpPriority =
Math.min(...turnUdpRemoteCandidates.map(c => c.priority));
ok(minLocalTurnUdpPriority > 2 * maxLocalTurnTcpPriority,
`Local TURN UDP candidates all have much higher priority than` +
` local TURN TCP candidates` +
` (${minLocalTurnUdpPriority} vs ${maxLocalTurnTcpPriority})`);
ok(minRemoteTurnUdpPriority > 2 * maxRemoteTurnTcpPriority,
`Remote TURN UDP candidates all have much higher priority than` +
` remote TURN TCP candidates` +
` (${minRemoteTurnUdpPriority} vs ${maxRemoteTurnTcpPriority})`);
await SpecialPowers.popPrefEnv();
},
];
if (!("mediaDevices" in navigator)) {
SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
() => location.reload());
} else {
runNetworkTest(async () => {
for (const test of tests) {
info(`Running test: ${test.name}`);
try {
await test();
} catch (e) {
ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`);
}
info(`Done running test: ${test.name}`);
// Make sure we don't build up a pile of GC work, and also get PCImpl to
// print their timecards.
await new Promise(r => SpecialPowers.exactGC(r));
}
await SpecialPowers.popPrefEnv();
}, { useIceServer: true });
}
</script>
</pre>
</body>
</html>