diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/webrtc/simulcast | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webrtc/simulcast')
9 files changed, 1509 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..ed85131c2c --- /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 spatial layers'); +</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..36f096add3 --- /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, 100)); // 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, 100)); // 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, 100)); // 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..fb33529db6 --- /dev/null +++ b/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html @@ -0,0 +1,468 @@ +<!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> + +async function queueAWebrtcTask() { + const pc = new RTCPeerConnection(); + pc.addTransceiver('audio'); + await new Promise(r => pc.onnegotiationneeded = r); +} + +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..9d7786ea3b --- /dev/null +++ b/testing/web-platform/tests/webrtc/simulcast/simulcast.js @@ -0,0 +1,250 @@ +'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) { + 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) { + 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..3733682435 --- /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 spatial layers'); +</script> |