Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!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>