445 lines
14 KiB
JavaScript
445 lines
14 KiB
JavaScript
/* 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";
|
|
}
|
|
}
|