470 lines
18 KiB
HTML
470 lines
18 KiB
HTML
<!DOCTYPE HTML>
|
|
<html>
|
|
<head>
|
|
<script type="application/javascript" src="pc.js"></script>
|
|
</head>
|
|
<body>
|
|
<pre id="test">
|
|
<script type="application/javascript">
|
|
createHTML({
|
|
bug: "1230184",
|
|
title: "Set parameters on sender",
|
|
visible: true
|
|
});
|
|
|
|
const simulcastOffer = `v=0
|
|
o=- 3840232462471583827 0 IN IP4 127.0.0.1
|
|
s=-
|
|
t=0 0
|
|
a=group:BUNDLE 0
|
|
a=msid-semantic: WMS
|
|
m=video 9 UDP/TLS/RTP/SAVPF 96
|
|
c=IN IP4 0.0.0.0
|
|
a=rtcp:9 IN IP4 0.0.0.0
|
|
a=ice-ufrag:Li6+
|
|
a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
|
|
a=ice-options:trickle
|
|
a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
|
|
a=setup:actpass
|
|
a=mid:0
|
|
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
|
|
a=recvonly
|
|
a=rtcp-mux
|
|
a=rtpmap:96 VP8/90000
|
|
a=rtcp-fb:96 goog-remb
|
|
a=rtcp-fb:96 transport-cc
|
|
a=rtcp-fb:96 ccm fir
|
|
a=rid:foo recv
|
|
a=rid:bar recv
|
|
a=simulcast:recv foo;bar
|
|
`;
|
|
|
|
function buildMaximumSendEncodings() {
|
|
const sendEncodings = [];
|
|
while (true) {
|
|
// isDeeply does not see identical string primitives and String objects
|
|
// as the same, so we make this a string primitive.
|
|
sendEncodings.push({rid: `${sendEncodings.length}`});
|
|
const pc = new RTCPeerConnection();
|
|
const {sender} = pc.addTransceiver('video', {sendEncodings});
|
|
const {encodings} = sender.getParameters();
|
|
if (encodings.length < sendEncodings.length) {
|
|
sendEncodings.pop();
|
|
return sendEncodings;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function queueAWebrtcTask() {
|
|
const pc = new RTCPeerConnection();
|
|
pc.addTransceiver('audio');
|
|
await new Promise(r => pc.onnegotiationneeded = r);
|
|
pc.close();
|
|
}
|
|
|
|
// setParameters is mostly tested in wpt, but we test a few
|
|
// implementation-specific things here. Other mochitests check whether the
|
|
// set parameters actually have the desired effect on the media streams.
|
|
const tests = [
|
|
|
|
// wpt currently does not assume support for 3 encodings, which limits the
|
|
// effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
|
|
async function checkScaleResolutionDownByAutoFillPowersOf2() {
|
|
const pc = new RTCPeerConnection();
|
|
const {sender} = pc.addTransceiver('video', {
|
|
sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
|
|
});
|
|
const {encodings} = sender.getParameters();
|
|
const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
|
|
isDeeply(scaleValues, [4, 2, 1]);
|
|
},
|
|
|
|
// wpt currently does not assume support for 3 encodings, which limits the
|
|
// effectiveness of its fill-with-1 test
|
|
async function checkScaleResolutionDownByAutoFillWith1() {
|
|
const pc = new RTCPeerConnection();
|
|
const {sender} = pc.addTransceiver('video', {
|
|
sendEncodings: [
|
|
{rid: "0"},{rid: "1", scaleResolutionDownBy: 3},{rid: "2"}
|
|
]
|
|
});
|
|
const {encodings} = sender.getParameters();
|
|
const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
|
|
isDeeply(scaleValues, [1, 3, 1]);
|
|
},
|
|
|
|
async function checkVideoEncodingLimit() {
|
|
const pc = new RTCPeerConnection();
|
|
const maxSendEncodings = buildMaximumSendEncodings();
|
|
const sendEncodings = maxSendEncodings.concat({rid: "a"});
|
|
const {sender} = pc.addTransceiver('video', {sendEncodings});
|
|
const {encodings} = sender.getParameters();
|
|
|
|
const rids = encodings.map(({rid}) => rid);
|
|
const expectedRids = maxSendEncodings.map(({rid}) => rid);
|
|
isDeeply(rids, expectedRids);
|
|
|
|
const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
|
|
const expectedScaleValues = [];
|
|
let scale = 1;
|
|
while (expectedScaleValues.length < maxSendEncodings.length) {
|
|
expectedScaleValues.push(scale);
|
|
scale *= 2;
|
|
}
|
|
isDeeply(scaleValues, expectedScaleValues.reverse());
|
|
},
|
|
|
|
async function checkScaleDownByInTrimmedEncoding() {
|
|
const pc = new RTCPeerConnection();
|
|
const maxSendEncodings = buildMaximumSendEncodings();
|
|
const sendEncodings = maxSendEncodings.concat({rid: "a", scaleResolutionDownBy: 3});
|
|
const {sender} = pc.addTransceiver('video', {sendEncodings});
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
const expectedRids = maxSendEncodings.map(({rid}) => rid);
|
|
isDeeply(rids, expectedRids);
|
|
const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
|
|
const expectedScaleValues = maxSendEncodings.map(() => 1);
|
|
isDeeply(scaleValues, expectedScaleValues);
|
|
},
|
|
|
|
async function checkLibwebrtcRidLengthLimit() {
|
|
const pc = new RTCPeerConnection();
|
|
try {
|
|
pc.addTransceiver('video', {
|
|
sendEncodings: [{rid: "wibblywobblyjeremybearimy"}]}
|
|
);
|
|
ok(false, "Rid should be too long for libwebrtc!");
|
|
} catch (e) {
|
|
is(e.name, "TypeError",
|
|
"Rid that is too long for libwebrtc should result in a TypeError");
|
|
}
|
|
},
|
|
|
|
async function checkErrorsInTrimmedEncodings() {
|
|
const pc = new RTCPeerConnection();
|
|
const maxSendEncodings = buildMaximumSendEncodings();
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat({rid: "foo-bar"});
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw due to invalid rid characters");
|
|
} catch (e) {
|
|
is(e.name, "TypeError")
|
|
}
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat({rid: "wibblywobblyjeremybearimy"});
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw because rid too long");
|
|
} catch (e) {
|
|
is(e.name, "TypeError")
|
|
}
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat({scaleResolutionDownBy: 2});
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw due to missing rid");
|
|
} catch (e) {
|
|
is(e.name, "TypeError")
|
|
}
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat(maxSendEncodings[0]);
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw due to duplicate rid");
|
|
} catch (e) {
|
|
is(e.name, "TypeError")
|
|
}
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, scaleResolutionDownBy: 0});
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw due to invalid scaleResolutionDownBy");
|
|
} catch (e) {
|
|
is(e.name, "RangeError")
|
|
}
|
|
try {
|
|
const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, maxFramerate: -1});
|
|
pc.addTransceiver('video', { sendEncodings });
|
|
ok(false, "Should throw due to invalid maxFramerate");
|
|
} catch (e) {
|
|
is(e.name, "RangeError")
|
|
}
|
|
},
|
|
|
|
async function checkCompatModeUnicastSetParametersAllowsSimulcastOffer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].scaleResolutionDownBy = 3.0;
|
|
await sender.setParameters(parameters);
|
|
|
|
await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
is(encodings[0].scaleResolutionDownBy, 2.0);
|
|
is(encodings[1].scaleResolutionDownBy, 1.0);
|
|
},
|
|
|
|
async function checkCompatModeUnicastSetParametersInterruptAllowsSimulcastOffer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].scaleResolutionDownBy = 3.0;
|
|
|
|
const offerDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
|
|
await sender.setParameters(parameters);
|
|
await offerDone;
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
is(encodings[0].scaleResolutionDownBy, 2.0);
|
|
is(encodings[1].scaleResolutionDownBy, 1.0);
|
|
},
|
|
|
|
async function checkCompatModeSimulcastSetParametersSetsSimulcastEnvelope() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].rid = "1";
|
|
parameters.encodings.push({rid: "2"});
|
|
await sender.setParameters(parameters);
|
|
|
|
await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
// No overlap in rids -> unicast
|
|
isDeeply(rids, [undefined]);
|
|
},
|
|
|
|
async function checkCompatModeSimulcastSetParametersRacesLocalUnicastOffer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
// unicast offer
|
|
const offer = await pc1.createOffer();
|
|
const aTask = queueAWebrtcTask();
|
|
const sldPromise = pc1.setLocalDescription(offer);
|
|
|
|
// Right now, we have aTask queued. The success task for sLD is not queued
|
|
// yet, because Firefox performs the initial steps on the microtask queue,
|
|
// which we have not allowed to run yet. Awaiting aTask will first clear
|
|
// the microtask queue, then run the task queue until aTask is finished.
|
|
// That _should_ result in the success task for sLD(offer) being queued.
|
|
await aTask;
|
|
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].rid = "foo";
|
|
parameters.encodings.push({rid: "bar"});
|
|
// simulcast setParameters; the task to update [[SendEncodings]] should be
|
|
// queued after the success task for sLD(offer)
|
|
await sender.setParameters(parameters);
|
|
await sldPromise;
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
// Compat mode lets this slide, but won't try to negotiate it since we've
|
|
// already applied a unicast local offer.
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
|
|
// Let negotiation finish, so we can generate a new offer
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
|
|
const reoffer = await pc1.createOffer();
|
|
ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
|
|
},
|
|
|
|
async function checkCompatModeSimulcastSetParametersRacesRemoteOffer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].rid = "foo";
|
|
parameters.encodings.push({rid: "bar"});
|
|
const p = sender.setParameters(parameters);
|
|
await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
|
|
await p;
|
|
const answer = await pc1.createAnswer();
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
ok(answer.sdp.includes("a=simulcast:send foo;bar"), "answer should be simulcast");
|
|
},
|
|
|
|
async function checkCompatModeSimulcastSetParametersRacesLocalAnswer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
// We do an initial negotiation, and while the local answer is pending,
|
|
// perform a setParameters on a not-yet-negotiated video sender. The intent
|
|
// here is to have the success task for sLD(answer) run while the
|
|
// setParameters is pending.
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
|
|
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
// We use this later on, but set it up now so we don't inadvertently
|
|
// crank the event loop more than we intend below.
|
|
const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
pc2.addTrack(audioStream.getTracks()[0]);
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
const answer = await pc1.createAnswer();
|
|
const aTask = queueAWebrtcTask();
|
|
const sldPromise = pc1.setLocalDescription(answer);
|
|
|
|
// Right now, we have aTask queued. The success task for sLD is not queued
|
|
// yet, because Firefox performs the initial steps on the microtask queue,
|
|
// which we have not allowed to run yet. Awaiting aTask will first clear
|
|
// the microtask queue, then run the task queue until aTask is finished.
|
|
// That _should_ result in the success task for sLD(answer) being queued.
|
|
await aTask;
|
|
|
|
// The success task for sLD(answer) should be queued now. Don't relinquish
|
|
// the event loop!
|
|
|
|
// New sender that has nothing to do with the negotiation in progress.
|
|
const sender = pc1.addTrack(videoStream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].rid = "foo";
|
|
parameters.encodings.push({rid: "bar"});
|
|
|
|
// We have not relinquished the event loop, so the sLD(answer) should still
|
|
// be queued. The task that updates [[SendEncodings]] (from setParameters)
|
|
// should be queued behind it. Let them both run.
|
|
await sender.setParameters(parameters);
|
|
await sldPromise;
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
|
|
},
|
|
|
|
async function checkCompatModeSimulcastSetParametersRacesRemoteAnswer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
// We do an initial negotiation, and while the remote answer is pending,
|
|
// perform a setParameters on a not-yet-negotiated video sender. The intent
|
|
// here is to have the success task for sRD(answer) run while the
|
|
// setParameters is pending.
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
|
|
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
// We use this later on, but set it up now so we don't inadvertently
|
|
// crank the event loop more than we intend below.
|
|
const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
pc1.addTrack(audioStream.getTracks()[0]);
|
|
await pc1.setLocalDescription();
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
await pc2.setLocalDescription();
|
|
const aTask = queueAWebrtcTask();
|
|
const srdPromise = pc1.setRemoteDescription(pc2.localDescription);
|
|
|
|
// Right now, we have aTask queued. The success task for sRD is not queued
|
|
// yet, because Firefox performs the initial steps on the microtask queue,
|
|
// which we have not allowed to run yet. Awaiting aTask will first clear
|
|
// the microtask queue, then run the task queue until aTask is finished.
|
|
// That _should_ result in the success task for sRD(answer) being queued.
|
|
await aTask;
|
|
|
|
// The success task for sRD(answer) should be queued now. Don't relinquish
|
|
// the event loop!
|
|
|
|
const sender = pc1.addTrack(videoStream.getTracks()[0]);
|
|
const parameters = sender.getParameters();
|
|
parameters.encodings[0].rid = "foo";
|
|
parameters.encodings.push({rid: "bar"});
|
|
|
|
// We have not relinquished the event loop, so the sRD(answer) should still
|
|
// be queued. The task that updates [[SendEncodings]] (from setParameters)
|
|
// should be queued behind it. Let them both run.
|
|
await sender.setParameters(parameters);
|
|
await srdPromise;
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
isDeeply(rids, ["foo", "bar"]);
|
|
ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
|
|
},
|
|
|
|
async function checkCompatModeSimulcastRidlessSetParametersRacesLocalOffer() {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", true]);
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
|
const sender = pc1.addTrack(stream.getTracks()[0]);
|
|
// unicast offer
|
|
const aTask = queueAWebrtcTask();
|
|
const sldPromise = pc1.setLocalDescription();
|
|
|
|
// Right now, we have aTask queued. The success task for sLD is not queued
|
|
// yet, because Firefox performs the initial steps on the microtask queue,
|
|
// which we have not allowed to run yet. Awaiting aTask will first clear
|
|
// the microtask queue, then run the task queue until aTask is finished.
|
|
// That _should_ result in the success task for sLD(offer) being queued.
|
|
await aTask;
|
|
|
|
// simulcast setParameters; the task to update [[SendEncodings]] should be
|
|
// queued after the success task for sLD(offer)
|
|
try {
|
|
await sender.setParameters({"encodings": [{}, {}]});
|
|
ok(false, "setParameters with two ridless encodings should fail");
|
|
} catch (e) {
|
|
ok(true, "setParameters with two ridless encodings should fail");
|
|
}
|
|
await sldPromise;
|
|
|
|
const {encodings} = sender.getParameters();
|
|
const rids = encodings.map(({rid}) => rid);
|
|
// Compat mode lets this slide, but won't try to negotiate it since we've
|
|
// already applied a unicast local offer.
|
|
isDeeply(rids, [undefined]);
|
|
|
|
// Let negotiation finish, so we can generate a new offer
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
|
|
const reoffer = await pc1.createOffer();
|
|
ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
|
|
},
|
|
|
|
];
|
|
|
|
runNetworkTest(async () => {
|
|
await pushPrefs(
|
|
["media.peerconnection.allow_old_setParameters", false]);
|
|
for (const test of tests) {
|
|
info(`Running test: ${test.name}`);
|
|
await test();
|
|
info(`Done running test: ${test.name}`);
|
|
}
|
|
});
|
|
|
|
</script>
|
|
</pre>
|
|
</body>
|
|
</html>
|