Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// This is mostly so test_peerConnection_gatherWithStun300.html and
// test_peerConnection_gatherWithStun300IPv6 can share this code. I would have
// put the ipv6 test code in the same file, but our ipv6 tester support is
// inconsistent enough that we need to be able to track the ipv6 test
// separately.
async function findStatsRelayCandidates(pc, protocol) {
const stats = await pc.getStats();
return [...stats.values()].filter(
v =>
v.type == "local-candidate" &&
v.candidateType == "relay" &&
v.relayProtocol == protocol
);
}
// Trickles candidates if pcDst is set, and resolves the candidate list
async function trickleIce(pc, pcDst) {
const candidates = [],
addCandidatePromises = [];
while (true) {
const { candidate } = await new Promise(r =>
pc.addEventListener("icecandidate", r, { once: true })
);
if (!candidate) {
break;
}
candidates.push(candidate);
if (pcDst) {
addCandidatePromises.push(pcDst.addIceCandidate(candidate));
}
}
await Promise.all(addCandidatePromises);
return candidates;
}
async function gather(pc) {
if (pc.signalingState == "stable") {
await pc.setLocalDescription(
await pc.createOffer({ offerToReceiveAudio: true })
);
} else if (pc.signalingState == "have-remote-offer") {
await pc.setLocalDescription();
}
return trickleIce(pc);
}
async function gatherWithTimeout(pc, timeout, context) {
const throwOnTimeout = async () => {
await wait(timeout);
throw new Error(
`Gathering did not complete within ${timeout} ms with ${context}`
);
};
return Promise.race([gather(pc), throwOnTimeout()]);
}
async function iceConnected(pc) {
return new Promise((resolve, reject) => {
pc.addEventListener("iceconnectionstatechange", () => {
if (["connected", "completed"].includes(pc.iceConnectionState)) {
resolve();
} else if (pc.iceConnectionState == "failed") {
reject(new Error(`ICE failed`));
}
});
});
}
async function dtlsConnected(pc) {
return new Promise((resolve, reject) => {
pc.addEventListener("connectionstatechange", () => {
if (["connected", "completed"].includes(pc.connectionState)) {
resolve();
} else if (pc.connectionState == "failed") {
reject(new Error(`Connection failed`));
}
});
});
}
// Set up trickle, but does not wait for it to complete. Can be used by itself
// in cases where we do not expect any new candidates, but want to still set up
// the signal handling in case new candidates _do_ show up.
async function connectNoTrickleWait(offerer, answerer, timeout, context) {
return connect(offerer, answerer, timeout, context, true);
}
async function connect(
offerer,
answerer,
timeout,
context,
noTrickleWait = false,
waitForDtls = false
) {
const trickle1 = trickleIce(offerer, answerer);
const trickle2 = trickleIce(answerer, offerer);
try {
const offer = await offerer.createOffer({ offerToReceiveAudio: true });
await offerer.setLocalDescription(offer);
await answerer.setRemoteDescription(offer);
const answer = await answerer.createAnswer();
await Promise.all([
offerer.setRemoteDescription(answer),
answerer.setLocalDescription(answer),
]);
const throwOnTimeout = async () => {
if (timeout) {
await wait(timeout);
throw new Error(
`ICE did not complete within ${timeout} ms with ${context}`
);
}
};
const connectionPromises = waitForDtls
? [dtlsConnected(offerer), dtlsConnected(answerer)]
: [iceConnected(offerer), iceConnected(answerer)];
await Promise.race([
Promise.all(connectionPromises),
throwOnTimeout(timeout, context),
]);
} finally {
if (!noTrickleWait) {
// TODO(bug 1751509): For now, we need to let gathering finish before we
// proceed, because there are races in ICE restart wrt gathering state.
await Promise.all([trickle1, trickle2]);
}
}
}
function isV6HostCandidate(candidate) {
const fields = candidate.candidate.split(" ");
const type = fields[7];
const ipAddress = fields[4];
return type == "host" && ipAddress.includes(":");
}
async function ipv6Supported() {
const pc = new RTCPeerConnection();
const candidates = await gatherWithTimeout(pc, 8000);
info(`baseline candidates: ${JSON.stringify(candidates)}`);
pc.close();
return candidates.some(isV6HostCandidate);
}
function makeContextString(iceServers) {
const currentRedirectAddress = SpecialPowers.getCharPref(
"media.peerconnection.nat_simulator.redirect_address",
""
);
const currentRedirectTargets = SpecialPowers.getCharPref(
"media.peerconnection.nat_simulator.redirect_targets",
""
);
return `redirect rule: ${currentRedirectAddress}=>${currentRedirectTargets} iceServers: ${JSON.stringify(
iceServers
)}`;
}
async function checkSrflx(iceServers) {
const context = makeContextString(iceServers);
info(`checkSrflx ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
info(`candidates: ${JSON.stringify(srflxCandidates)}`);
// TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
// result in a single srflx candidate
is(
srflxCandidates.length,
2,
`Should have two srflx candidates with ${context}`
);
pc.close();
}
async function checkNoSrflx(iceServers) {
const context = makeContextString(iceServers);
info(`checkNoSrflx ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
info(`candidates: ${JSON.stringify(srflxCandidates)}`);
is(
srflxCandidates.length,
0,
`Should have no srflx candidates with ${context}`
);
pc.close();
}
async function checkRelayUdp(iceServers) {
const context = makeContextString(iceServers);
info(`checkRelayUdp ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
info(`candidates: ${JSON.stringify(relayCandidates)}`);
// TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
// result in a single relay candidate
is(
relayCandidates.length,
2,
`Should have two relay candidates with ${context}`
);
// It would be nice if RTCIceCandidate had a field telling us what the
// "related protocol" is (similar to relatedAddress and relatedPort).
// Because there is no such thing, we need to go through the stats API,
// which _does_ have that information.
is(
(await findStatsRelayCandidates(pc, "tcp")).length,
0,
`No TCP relay candidates should be present with ${context}`
);
pc.close();
}
async function checkRelayTcp(iceServers) {
const context = makeContextString(iceServers);
info(`checkRelayTcp ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
info(`candidates: ${JSON.stringify(relayCandidates)}`);
// TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
// result in a single relay candidate
is(
relayCandidates.length,
2,
`Should have two relay candidates with ${context}`
);
// It would be nice if RTCIceCandidate had a field telling us what the
// "related protocol" is (similar to relatedAddress and relatedPort).
// Because there is no such thing, we need to go through the stats API,
// which _does_ have that information.
is(
(await findStatsRelayCandidates(pc, "udp")).length,
0,
`No UDP relay candidates should be present with ${context}`
);
pc.close();
}
async function checkRelayUdpTcp(iceServers) {
const context = makeContextString(iceServers);
info(`checkRelayUdpTcp ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
info(`candidates: ${JSON.stringify(relayCandidates)}`);
// TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
// result in a single relay candidate each for UDP and TCP
is(
relayCandidates.length,
4,
`Should have two relay candidates for each protocol with ${context}`
);
// It would be nice if RTCIceCandidate had a field telling us what the
// "related protocol" is (similar to relatedAddress and relatedPort).
// Because there is no such thing, we need to go through the stats API,
// which _does_ have that information.
is(
(await findStatsRelayCandidates(pc, "udp")).length,
2,
`Two UDP relay candidates should be present with ${context}`
);
// TODO(bug 1705563): This is 1 because of bug 1705563
is(
(await findStatsRelayCandidates(pc, "tcp")).length,
1,
`One TCP relay candidates should be present with ${context}`
);
pc.close();
}
async function checkNoRelay(iceServers) {
const context = makeContextString(iceServers);
info(`checkNoRelay ${context}`);
const pc = new RTCPeerConnection({
iceServers,
bundlePolicy: "max-bundle", // Avoids extra candidates
});
const candidates = await gatherWithTimeout(pc, 8000, context);
const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
info(`candidates: ${JSON.stringify(relayCandidates)}`);
is(
relayCandidates.length,
0,
`Should have no relay candidates with ${context}`
);
pc.close();
}
function gatheringStateReached(object, state) {
if (object instanceof RTCIceTransport) {
return new Promise(r =>
object.addEventListener("gatheringstatechange", function listener() {
if (object.gatheringState == state) {
object.removeEventListener("gatheringstatechange", listener);
r(state);
}
})
);
} else if (object instanceof RTCPeerConnection) {
return new Promise(r =>
object.addEventListener("icegatheringstatechange", function listener() {
if (object.iceGatheringState == state) {
object.removeEventListener("icegatheringstatechange", listener);
r(state);
}
})
);
} else {
throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
}
}
function nextGatheringState(object) {
if (object instanceof RTCIceTransport) {
return new Promise(resolve =>
object.addEventListener(
"gatheringstatechange",
() => resolve(object.gatheringState),
{ once: true }
)
);
} else if (object instanceof RTCPeerConnection) {
return new Promise(resolve =>
object.addEventListener(
"icegatheringstatechange",
() => resolve(object.iceGatheringState),
{ once: true }
)
);
} else {
throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
}
}
function emptyCandidate(pc) {
return new Promise(r =>
pc.addEventListener("icecandidate", function listener(e) {
if (e.candidate && e.candidate.candidate == "") {
pc.removeEventListener("icecandidate", listener);
r(e);
}
})
);
}
function nullCandidate(pc) {
return new Promise(r =>
pc.addEventListener("icecandidate", function listener(e) {
if (!e.candidate) {
pc.removeEventListener("icecandidate", listener);
r(e);
}
})
);
}
function connectionStateReached(object, state) {
if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
return new Promise(resolve =>
object.addEventListener("statechange", function listener() {
if (object.state == state) {
object.removeEventListener("statechange", listener);
resolve(state);
}
})
);
} else if (object instanceof RTCPeerConnection) {
return new Promise(resolve =>
object.addEventListener("connectionstatechange", function listener() {
if (object.connectionState == state) {
object.removeEventListener("connectionstatechange", listener);
resolve(state);
}
})
);
} else {
throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
}
}
function nextConnectionState(object) {
if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
return new Promise(resolve =>
object.addEventListener("statechange", () => resolve(object.state), {
once: true,
})
);
} else if (object instanceof RTCPeerConnection) {
return new Promise(resolve =>
object.addEventListener(
"connectionstatechange",
() => resolve(object.connectionState),
{ once: true }
)
);
} else {
throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
}
}
function nextIceConnectionState(pc) {
if (pc instanceof RTCPeerConnection) {
return new Promise(resolve =>
pc.addEventListener(
"iceconnectionstatechange",
() => resolve(pc.iceConnectionState),
{ once: true }
)
);
} else {
throw "First parameter is not an RTCPeerConnection";
}
}