summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/simulcast
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webrtc/simulcast
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webrtc/simulcast')
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/basic.https.html23
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/getStats.https.html34
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/h264.https.html31
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html534
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html39
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html104
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html462
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/simulcast.js254
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp8.https.html26
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html35
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp9.https.html26
11 files changed, 1568 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/simulcast/basic.https.html b/testing/web-platform/tests/webrtc/simulcast/basic.https.html
new file mode 100644
index 0000000000..f7b9def762
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/basic.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+}, 'Basic simulcast setup with two spatial layers');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/getStats.https.html b/testing/web-platform/tests/webrtc/simulcast/getStats.https.html
new file mode 100644
index 0000000000..b5a9e6eb28
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/getStats.https.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - getStats</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ const rids = [0, 1, 2];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ const outboundStats = [];
+ const senderStats = await pc1.getSenders()[0].getStats();
+ senderStats.forEach(stat => {
+ if (stat.type === 'outbound-rtp') {
+ outboundStats.push(stat);
+ }
+ });
+ assert_equals(outboundStats.length, 3, "getStats result should contain three layers");
+ const statsRids = outboundStats.map(stat => parseInt(stat.rid, 10));
+ assert_array_equals(rids, statsRids.sort(), "getStats result should match the rids provided");
+}, 'Simulcast getStats results');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/h264.https.html b/testing/web-platform/tests/webrtc/simulcast/h264.https.html
new file mode 100644
index 0000000000..038449aa6e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/h264.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+/*
+ * Chromium note: this requires build bots with H264 support. See
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=840659
+ * for details on how to enable support.
+ */
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/H264'), 'H264 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/H264'});
+}, 'H264 simulcast setup with two streams');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html b/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html
new file mode 100644
index 0000000000..c16e2674b0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html
@@ -0,0 +1,534 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - negotiation/encodings</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // pc1 is unicast right now
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'addTrack, then sRD(simulcast recv offer) results in simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // pc1 is unicast right now
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'simulcast is not supported for audio');
+
+// We do not have a test case for sRD(offer) narrowing a simulcast envelope
+// from addTransceiver, since that transceiver cannot be paired up with a remote
+// offer m-section
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope specified by addTransceiver');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope from a previous negotiation');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"], "[[SendEncodings]] is not updated in have-remote-offer for reoffers");
+
+ await doAnswerToSendSimulcast(pc2, pc1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope from a previous negotiation');
+
+// https://github.com/w3c/webrtc-pc/issues/2780
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar", "foo"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv foo;bar;foo"), "Duplicate rids should be present in offer");
+ assert_false(pc1.localDescription.sdp.includes("a=simulcast:send foo;bar;foo"), "Duplicate rids should not be present in answer");
+ assert_true(pc1.localDescription.sdp.includes("a=simulcast:send foo;bar"), "Answer should use the correct rids");
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Duplicate rids in sRD(offer) are ignored');
+
+// https://github.com/w3c/webrtc-pc/issues/2769
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo,bar", "1,2"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "1"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv foo,bar;1,2"), "Choices of rids should be present in offer");
+ assert_true(pc1.localDescription.sdp.includes("a=simulcast:send foo;1\r\n"), "Choices of rids should not be present in answer");
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "1"]);
+}, 'Choices in rids in sRD(offer) are ignored');
+
+// https://github.com/w3c/webrtc-pc/issues/2764
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without a rid');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream.getTracks()[0]);
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ assert_equals(pc1.getTransceivers().length, 1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'sRD(simulcast offer), addTrack, then rollback brings us back to having a single encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, ["bar", "foo"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Answer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(answer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, ["bar", "foo"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Answer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(reanswer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar", "foo"]);
+ await doAnswerToSendSimulcast(pc2, pc1);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Reoffer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(reoffer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ // Keep the second encoding!
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Rollback of sRD(reoffer) with a single rid results in all previous encodings');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope specified by addTransceiver by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope from a previous negotiation by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"], "[[SendEncodings]] is not updated in have-remote-offer for reoffers");
+
+ await doAnswerToSendSimulcast(pc2, pc1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope from a previous negotiation by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc1.getTransceivers()[0].direction = "inactive";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'sender renegotiation to inactive does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc1.getTransceivers()[0].direction = "recvonly";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'sender renegotiation to recvonly does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc2.getTransceivers()[0].direction = "inactive";
+ pc2.getTransceivers()[1].direction = "inactive";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'receiver renegotiation to inactive does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc2.getTransceivers()[0].direction = "sendonly";
+ pc2.getTransceivers()[1].direction = "sendonly";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'receiver renegotiation to sendonly does not disable simulcast');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html b/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html
new file mode 100644
index 0000000000..a88506305a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - RID manipulation</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const rids = [0, 1, 2];
+ pc1.addTransceiver("video", {sendEncodings: rids.map(rid => ({rid}))});
+ const [{sender}] = pc1.getTransceivers();
+
+ const negotiateSfuAnswer = async asimulcast => {
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ offer.sdp = swapRidAndMidExtensionsInSimulcastOffer(offer, rids);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ answer.sdp = swapRidAndMidExtensionsInSimulcastAnswer(answer,pc1.localDescription, rids);
+ answer.sdp = answer.sdp.replace('a=simulcast:recv 0;1;2', asimulcast);
+ return answer;
+ };
+ await pc1.setRemoteDescription(await negotiateSfuAnswer('a=simulcast:recv foo;1;2'));
+ await pc1.setRemoteDescription(await negotiateSfuAnswer('a=simulcast:recv foo;bar;2'));
+}, 'Remote reanswer altering rids does not throw an exception.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html b/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html
new file mode 100644
index 0000000000..dbe162c610
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html
@@ -0,0 +1,104 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - setParameters/active</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+async function queryReceiverStats(pc) {
+ const inboundStats = [];
+ await Promise.all(pc.getReceivers().map(async receiver => {
+ const receiverStats = await receiver.getStats();
+ receiverStats.forEach(stat => {
+ if (stat.type === 'inbound-rtp') {
+ inboundStats.push(stat);
+ }
+ });
+ }));
+ return inboundStats.map(s => s.framesDecoded);
+}
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate first sender.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings[0].active = false;
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received on the first encoding.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ assert_equals(subsequentStats[0], initialStats[0]);
+ assert_greater_than(subsequentStats[1], initialStats[1]);
+}, 'Simulcast setParameters active=false on first encoding stops sending frames for that encoding');
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate second sender.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings[1].active = false;
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received on the second encoding.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ assert_equals(subsequentStats[1], initialStats[1]);
+ assert_greater_than(subsequentStats[0], initialStats[0]);
+}, 'Simulcast setParameters active=false on second encoding stops sending frames for that encoding');
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate all senders.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings.forEach(e => {
+ e.active = false;
+ });
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ subsequentStats.forEach((framesDecoded, idx) => {
+ assert_equals(framesDecoded, initialStats[idx]);
+ });
+}, 'Simulcast setParameters active=false stops sending frames');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html b/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html
new file mode 100644
index 0000000000..ac04ca55fb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html
@@ -0,0 +1,462 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - setParameters/encodings</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'sRD(simulcast answer) can narrow the simulcast envelope when interrupted by a setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ const reoffer = await pc2.createOffer();
+ const simulcastSdp = midToRid(reoffer, pc1.localDescription, ["foo"]);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastSdp});
+ await sender.setParameters(parameters);
+ await reofferDone;
+ await pc1.setLocalDescription();
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope when interrupted by a setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ await sender.setParameters(parameters);
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, []);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'a simulcast setParameters followed by a sRD(unicast answer) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcast(pc1, pc2);
+
+ await pc2.setLocalDescription();
+ const unicastAnswer = midToRid(pc2.localDescription, pc1.localDescription, []);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: unicastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'sRD(unicast answer) interrupted by setParameters(simulcast) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ const reoffer = await pc2.createOffer();
+ const unicastSdp = midToRid(reoffer, pc1.localDescription, []);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: unicastSdp});
+ await sender.setParameters(parameters);
+ await reofferDone;
+ await pc1.setLocalDescription();
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'sRD(unicast reoffer) interrupted by setParameters(simulcast) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_equals(encodings[0].scaleResolutionDownBy, 3.3);
+}, 'sRD(simulcast answer) interrupted by a setParameters does not result in losing modifications from the setParameters to the encodings that remain');
+
+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
+`;
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ 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);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
+}, 'addTrack, then a unicast setParameters, then sRD(simulcast offer) results in simulcast without the settings from setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ 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);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
+}, 'addTrack, then sRD(simulcast offer) interrupted by a unicast setParameters results in simulcast without the settings from setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ // Race simulcast setParameters against sLD(unicast reanswer)
+ const answer = await pc1.createAnswer();
+ const aTask = queueAWebrtcTask();
+ // This also queues a task to clear [[LastReturnedParameters]]
+ const parameters = sender.getParameters();
+ // This might or might not queue a task right away (it might do some
+ // microtask stuff first), but it doesn't really matter.
+ const sLDDone = pc1.setLocalDescription(answer);
+ await aTask;
+ // Task queue should now have the task that clears
+ // [[LastReturnedParameters]], _then_ the success task for sLD.
+ // setParameters should succeed because [[LastReturnedParameters]] has not
+ // yet been cleared, and the steps in the success task for sLD have not run
+ // either.
+ await sender.setParameters(parameters);
+ await sLDDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'getParameters, then sLD(unicast answer) interrupted by a simulcast setParameters results in unicast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ const answer = await pc1.createAnswer();
+
+ // The timing on this is very difficult. We want to ensure that our
+ // getParameters call happens after the initial steps in sLD, but
+ // before the queued task that sLD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sLDDone = pc1.setLocalDescription(answer);
+ // We now have a queued task (aTask). We might also have the success task for
+ // sLD, but maybe not. Allowing aTask to finish gives us our best chance that
+ // the success task for sLD is queued, but not run yet.
+ await aTask;
+ const parameters = sender.getParameters();
+ // Hopefully we now have the success task for sLD, followed by the
+ // success task for getParameters.
+ await sLDDone;
+ // Success task for getParameters should not have run yet.
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setLocalDescription(answer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await pc2.setLocalDescription();
+ const simulcastOffer = midToRid(
+ pc2.localDescription,
+ pc1.localDescription,
+ []
+ );
+
+ // The timing on this is very difficult. We need to ensure that our
+ // getParameters call happens after the initial steps in sRD, but
+ // before the queued task that sRD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sRDDone = pc1.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
+
+ await aTask;
+ const parameters = sender.getParameters();
+ await sRDDone;
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setRemoteDescription(offer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(
+ pc2.localDescription,
+ pc1.localDescription,
+ []
+ );
+
+ // The timing on this is very difficult. We need to ensure that our
+ // getParameters call happens after the initial steps in sRD, but
+ // before the queued task that sRD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sRDDone = pc1.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
+ await aTask;
+
+ const parameters = sender.getParameters();
+ await sRDDone;
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setRemoteDescription(answer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
+}, 'addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without any previously set parameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ await sender.setParameters(parameters);
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 3);
+ assert_equals(parameters.encodings[1].scaleResolutionDownBy, 5);
+}, 'rollback of a remote offer that disabled a previously negotiated simulcast should restore simulcast along with any previously set parameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ const aTask = queueAWebrtcTask();
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+
+ const rollbackDone = pc1.setRemoteDescription({sdp: "", type: "rollback"});
+ await aTask;
+ await sender.setParameters(parameters);
+ await rollbackDone;
+
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
+}, 'rollback of sRD(simulcast offer) interrupted by setParameters(simulcast) brings us back to having a single encoding without any previously set parameters');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/simulcast.js b/testing/web-platform/tests/webrtc/simulcast/simulcast.js
new file mode 100644
index 0000000000..4682729233
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/simulcast.js
@@ -0,0 +1,254 @@
+'use strict';
+/* Helper functions to munge SDP and split the sending track into
+ * separate tracks on the receiving end. This can be done in a number
+ * of ways, the one used here uses the fact that the MID and RID header
+ * extensions which are used for packet routing share the same wire
+ * format. The receiver interprets the rids from the sender as mids
+ * which allows receiving the different spatial resolutions on separate
+ * m-lines and tracks.
+ */
+
+const ridExtensions = [
+ "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
+ "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
+];
+
+function ridToMid(description, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip mid extension; we are replacing it with the rid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid"
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid";
+ }
+ }
+
+ // Filter rtx as we have no way to (re)interpret rrid.
+ // Not doing this makes probing use RTX, it's not understood and ramp-up is slower.
+ rtpParameters.codecs = rtpParameters.codecs.filter(c => c.name.toUpperCase() !== 'RTX');
+
+ if (!rids) {
+ rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map(r => r[1]);
+ }
+
+ let sdp = SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ 'a=group:BUNDLE ' + rids.join(' ') + '\r\n';
+ const baseRtpDescription = SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
+ for (const rid of rids) {
+ sdp += baseRtpDescription +
+ 'a=mid:' + rid + '\r\n' +
+ 'a=msid:rid-' + rid + ' rid-' + rid + '\r\n';
+ sdp += directionValue + "\r\n";
+ }
+ return sdp;
+}
+
+function midToRid(description, localDescription, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip rid extensions; we are replacing them with the mid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => !ridExtensions.includes(ext.uri)
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
+ }
+ }
+
+ const localMid = localDescription ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) : "0";
+
+ if (!rids) {
+ rids = [];
+ for (let i = 1; i < sections.length; i++) {
+ rids.push(SDPUtils.getMid(sections[i]));
+ }
+ }
+
+ let sdp = SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ 'a=group:BUNDLE ' + localMid + '\r\n';
+ sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
+ // Although we are converting mids to rids, we still need a mid.
+ // The first one will be consistent with trickle ICE candidates.
+ sdp += 'a=mid:' + localMid + '\r\n';
+ sdp += directionValue + "\r\n";
+
+ for (const rid of rids) {
+ const stringrid = String(rid); // allow integers
+ const choices = stringrid.split(",");
+ choices.forEach(choice => {
+ sdp += 'a=rid:' + choice + ' recv\r\n';
+ });
+ }
+ if (rids.length) {
+ sdp += 'a=simulcast:recv ' + rids.join(';') + '\r\n';
+ }
+
+ return sdp;
+}
+
+async function doOfferToSendSimulcast(offerer, answerer) {
+ await offerer.setLocalDescription();
+
+ // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids,
+ // even if some rids have been removed or reordered.
+ let mids = [];
+ if (answerer.localDescription) {
+ // Renegotiation. Mids must be the same as before, because renegotiation
+ // can never remove or reorder mids, nor can it expand the simulcast
+ // envelope.
+ mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+ } else {
+ // First negotiation; the mids will be exactly the same as the rids
+ const simulcastAttr = offerer.localDescription.sdp.match(
+ /a=simulcast:send (.*)/
+ );
+ if (simulcastAttr) {
+ mids = simulcastAttr[1].split(";");
+ }
+ }
+
+ const nonSimulcastOffer = ridToMid(offerer.localDescription, mids);
+ await answerer.setRemoteDescription({
+ type: "offer",
+ sdp: nonSimulcastOffer,
+ });
+}
+
+async function doAnswerToRecvSimulcast(offerer, answerer, rids) {
+ await answerer.setLocalDescription();
+ const simulcastAnswer = midToRid(
+ answerer.localDescription,
+ offerer.localDescription,
+ rids
+ );
+ await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
+}
+
+async function doOfferToRecvSimulcast(offerer, answerer, rids) {
+ await offerer.setLocalDescription();
+ const simulcastOffer = midToRid(
+ offerer.localDescription,
+ answerer.localDescription,
+ rids
+ );
+ await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
+}
+
+async function doAnswerToSendSimulcast(offerer, answerer) {
+ await answerer.setLocalDescription();
+
+ // See which mids the offerer had; it will barf if we remove or reorder them
+ const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+
+ const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids);
+ await offerer.setRemoteDescription({
+ type: "answer",
+ sdp: nonSimulcastAnswer,
+ });
+}
+
+async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToSendSimulcast(offerer, answerer);
+ await doAnswerToRecvSimulcast(offerer, answerer, rids);
+}
+
+async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToRecvSimulcast(offerer, answerer, rids);
+ await doAnswerToSendSimulcast(offerer, answerer);
+}
+
+function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) {
+ return ridToMid(offer, rids);
+}
+
+function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) {
+ return midToRid(answer, localDescription, rids);
+}
+
+async function negotiateSimulcastAndWaitForVideo(
+ t, rids, pc1, pc2, codec, scalabilityMode = undefined) {
+ exchangeIceCandidates(pc1, pc2);
+
+ const metadataToBeLoaded = [];
+ pc2.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ metadataToBeLoaded.push(new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ }));
+ };
+
+ const sendEncodings = rids.map(rid => ({rid}));
+ // Use a 2X downscale factor between each layer. To improve ramp-up time, the
+ // top layer is scaled down by a factor 2. Smaller layer comes first. For
+ // example if MediaStreamTrack is 720p and we want to send three layers we'll
+ // get {90p, 180p, 360p}.
+ let scaleResolutionDownBy = 2;
+ for (let i = sendEncodings.length - 1; i >= 0; --i) {
+ if (scalabilityMode) {
+ sendEncodings[i].scalabilityMode = scalabilityMode;
+ }
+ sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy;
+ scaleResolutionDownBy *= 2;
+ }
+
+ // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up.
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
+ streams: [stream],
+ sendEncodings: sendEncodings,
+ });
+ if (codec) {
+ preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine);
+ }
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer),
+ await pc2.setRemoteDescription({
+ type: 'offer',
+ sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids),
+ });
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription({
+ type: 'answer',
+ sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids),
+ });
+ assert_equals(metadataToBeLoaded.length, rids.length);
+ return Promise.all(metadataToBeLoaded);
+}
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp8.https.html b/testing/web-platform/tests/webrtc/simulcast/vp8.https.html
new file mode 100644
index 0000000000..3d04bc7172
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp8.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP8'), 'VP8 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/VP8'});
+}, 'VP8 simulcast setup with two streams');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html b/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html
new file mode 100644
index 0000000000..9dc8a3103d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP9'), 'VP9 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // This is not a scalability mode test (see wpt/webrtc-svc/ for those) but a
+ // VP9 simulcast test. Setting `scalabilityMode` should not be needed, however
+ // many browsers interprets multiple VP9 encodings to mean multiple spatial
+ // layers by default. During a transition period, Chromium-based browsers
+ // requires explicitly specifying the scalability mode as a way to opt-in to
+ // spec-compliant simulcast. See also wpt/webrtc/simulcast/vp9.https.html for
+ // a version of this test that does not set the scalability mode.
+ const scalabilityMode = 'L1T2';
+ return negotiateSimulcastAndWaitForVideo(
+ t, rids, pc1, pc2, {mimeType: 'video/VP9'}, scalabilityMode);
+}, 'VP9 simulcast setup with two streams and L1T2 set');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp9.https.html b/testing/web-platform/tests/webrtc/simulcast/vp9.https.html
new file mode 100644
index 0000000000..a033dab477
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp9.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP9'), 'VP9 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/VP9'});
+}, 'VP9 simulcast setup with two streams');
+</script>