tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 27a4ecbdec5bf60ce5ece91e531445d0fcc2c864
parent 4e1d102fe9f1c370ccaa98d7bb29b2b69823c9cc
Author: Andreas Pehrson <apehrson@mozilla.com>
Date:   Thu, 23 Oct 2025 14:11:18 +0000

Bug 1771789 - Test video capture cloning independence. r=jib

Differential Revision: https://phabricator.services.mozilla.com/D266398

Diffstat:
Atesting/web-platform/mozilla/meta/mediacapture-streams/MediaStreamTrack-independent-clones.https.html.ini | 2++
Atesting/web-platform/mozilla/meta/screen-capture/MediaStreamTrack-independent-clones.https.html.ini | 3+++
Atesting/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-independent-clones.https.html | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/mozilla/tests/mediacapture-streams/video-test-helper.js | 22++++++++++++++++++++++
Atesting/web-platform/mozilla/tests/screen-capture/MediaStreamTrack-independent-clones.https.html | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 421 insertions(+), 0 deletions(-)

diff --git a/testing/web-platform/mozilla/meta/mediacapture-streams/MediaStreamTrack-independent-clones.https.html.ini b/testing/web-platform/mozilla/meta/mediacapture-streams/MediaStreamTrack-independent-clones.https.html.ini @@ -0,0 +1,2 @@ +[MediaStreamTrack-independent-clones.https.html] + prefs: [media.getusermedia.camera.fake.force:true, media.navigator.streams.fake:false] diff --git a/testing/web-platform/mozilla/meta/screen-capture/MediaStreamTrack-independent-clones.https.html.ini b/testing/web-platform/mozilla/meta/screen-capture/MediaStreamTrack-independent-clones.https.html.ini @@ -0,0 +1,3 @@ +[MediaStreamTrack-independent-clones.https.html] + expected: + if os == "android": ERROR diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-independent-clones.https.html b/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-independent-clones.https.html @@ -0,0 +1,162 @@ +<!doctype html> +<title>MediaStreamTrack clones have independent settings. Assumes Mozilla's fake camera source with 480p and 720p capabilities.</title> +<meta name="timeout" content="long"> +<p class="instructions">When prompted, accept to share your video stream.</p> +<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=video-test-helper.js></script> +<script> + "use strict" + + // Native capabilities supported by the fake camera. + const nativeLow = {width: 640, height: 480, frameRate: 30, resizeMode: "none"}; + const nativeHigh = {width: 1280, height: 720, frameRate: 10, resizeMode: "none"}; + + [ + [ + [{resizeMode: "none", width: 1000}, {resizeMode: "none", width: 500}], + [nativeHigh, nativeLow], + ], + [ + [{resizeMode: "none", height: 500}, {resizeMode: "none", width: 1000}], + [nativeLow, nativeHigh], + ], + [ + [{resizeMode: "none", width: 500, height: 500}, {resizeMode: "crop-and-scale", width: 1000, height: 750}], + [nativeLow, {resizeMode: "crop-and-scale", width: 1000, height: 720, frameRate: 10}], + ], + [ + [{resizeMode: "crop-and-scale", width: 800, height: 600}, {resizeMode: "none"}], + [{resizeMode: "crop-and-scale", width: 800, height: 600, frameRate: 10}, nativeLow], + ], + [ + [{resizeMode: "crop-and-scale", height: 500}, {resizeMode: "crop-and-scale", height: 300}], + [ + {resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10}, + {resizeMode: "crop-and-scale", width: 400, height: 300, frameRate: 30} + ], + ], + [ + [ + {resizeMode: "crop-and-scale", frameRate: {exact: 5}}, + {resizeMode: "crop-and-scale", frameRate: {exact: 1}}, + ], + [ + {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5}, + {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 1}, + ], + [[2, 7], [0.5, 3]], + ], + ].forEach( + ([ + [video, cloneVideo], + [expected, cloneExpected], + [testFramerate, cloneTestFramerate] = [null, null] + ]) => promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + let settings = track.getSettings(); + let cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original: ${key}`); + assert_equals(cloneSettings[key], expected[key], `clone: ${key}`); + } + await clone.applyConstraints(cloneVideo); + settings = track.getSettings(); + cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original-post: ${key}`); + } + for (const key of Object.keys(cloneExpected)) { + assert_equals(cloneSettings[key], cloneExpected[key], `clone-post: ${key}`); + } + await test_resolution_equals(t, track, settings.width, settings.height); + await test_resolution_equals(t, clone, cloneSettings.width, cloneSettings.height); + if (testFramerate) { + const [low, high] = testFramerate; + await test_framerate_between_exclusive(t, track, low, high); + } + if (cloneTestFramerate) { + const [low, high] = cloneTestFramerate; + await test_framerate_between_exclusive(t, clone, low, high); + } + }, `gUM gets ${JSON.stringify(expected)} + clone ` + + `${JSON.stringify(cloneExpected)} by ${JSON.stringify(video)} ` + + `and ${JSON.stringify(cloneVideo)}`)); + + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "none", width: {min: 2000}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode none and impossible min-width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "none", width: {max: 200}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode none and impossible max-width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "crop-and-scale", width: {min: 2000}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode crop-and-scale and impossible width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "crop-and-scale", frameRate: {min: 50}}); + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible fps"); + }, "applyConstraints on gUM clone is rejected by resizeMode crop-and-scale impossible fps"); +</script> diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/video-test-helper.js b/testing/web-platform/mozilla/tests/mediacapture-streams/video-test-helper.js @@ -1,5 +1,27 @@ // Helper functions checking video flow using HTMLVideoElement. +async function test_resolution_equals(t, track, width, height) { + const video = document.createElement("video"); + const timeout = new Promise(r => t.step_timeout(r, 1000)); + const waitForResize = () => Promise.race([ + new Promise(r => video.onresize = r), + timeout.then(() => { throw new Error("Timeout waiting for resize"); }), + ]); + video.srcObject = new MediaStream([track]); + video.play(); + // Wait for the first frame. + await waitForResize(); + // There's a potential race with applyConstraints, where a frame of the old + // resolution is in flight and renders after applyConstraints has resolved. + // In that case, wait for another frame. + if (video.videoWidth != width || video.videoHeight != height) { + await waitForResize(); + } + + assert_equals(video.videoWidth, width, "videoWidth"); + assert_equals(video.videoHeight, height, "videoHeight"); +} + async function test_framerate_between_exclusive(t, track, lower, upper) { const video = document.createElement("video"); document.body.appendChild(video); diff --git a/testing/web-platform/mozilla/tests/screen-capture/MediaStreamTrack-independent-clones.https.html b/testing/web-platform/mozilla/tests/screen-capture/MediaStreamTrack-independent-clones.https.html @@ -0,0 +1,232 @@ +<!doctype html> +<title>MediaStreamTrack clones have independent settings. Assumes Mozilla's fake camera source with 480p and 720p capabilities.</title> +<meta name="timeout" content="long"> +<p class="instructions">When prompted, accept to share your video stream.</p> +<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/settings-helper.js></script> +<script src=../mediacapture-streams/video-test-helper.js></script> +<script> + "use strict" + + setup(() => assert_implements("getDisplayMedia" in navigator.mediaDevices, "getDisplayMedia() not supported")); + + // Note these gDM tests will fail if our own window is on a screen different + // than the system's first screen. They're functions in case the browser + // window needs to be moved to the first screen during the test in order to + // pass. + function screenPixelRatio() { return SpecialPowers.wrap(window).desktopToDeviceScale; } + function screenWidth() { return window.screen.width * window.devicePixelRatio; } + function screenHeight() { return window.screen.height * window.devicePixelRatio; } + function desktopWidth() { + // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. + // return screenWidth() / screenPixelRatio(); + return screenWidth(); + } + function desktopHeight() { + // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. + // return screenHeight() / screenPixelRatio(); + return screenHeight(); + } + + // TODO: By default we shouldn't be multiplying with window.devicePixelRatio (bug 1703991). + function defaultScreen() { + return { + resizeMode: "crop-and-scale", + width: screenWidth(), + height: screenHeight(), + frameRate: 30, + }; + } + // TODO: Should get the source's real refresh rate for frameRate (bug 1984363). + function nativeScreen() { + return { + resizeMode: "none", + width: screenWidth(), + height: screenHeight(), + frameRate: 60 + }; + } + + [ + [ + [{resizeMode: "none", width: 100}, {resizeMode: "none", height: 500}], + [nativeScreen, nativeScreen], + ], + [ + [{resizeMode: "none", frameRate: 50}, {resizeMode: "none", frameRate: 1}], + [nativeScreen, nativeScreen], + ], + [ + [{resizeMode: "none"}, {resizeMode: "crop-and-scale"}], + [nativeScreen, defaultScreen], + ], + [ + [{resizeMode: "crop-and-scale"}, {resizeMode: "crop-and-scale", height: 100}], + [ + defaultScreen, + () => ({ + resizeMode: "crop-and-scale", + width: Math.round(screenWidth() / screenHeight() * 100), + height: 100, + frameRate: 30 + }), + ], + ], + [ + [{resizeMode: "crop-and-scale", height: 100}, {resizeMode: "crop-and-scale"}], + [ + () => ({ + resizeMode: "crop-and-scale", + width: Math.round(screenWidth() / screenHeight() * 100), + height: 100, + frameRate: 30 + }), + defaultScreen, + ], + ], + [ + [{resizeMode: "crop-and-scale", frameRate: 5}, {resizeMode: "crop-and-scale", frameRate: 50}], + [ + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 5}; + }, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 50}; + }, + ], + [[2, 7], [2, 50]], + ], + [ + [{resizeMode: "crop-and-scale", frameRate: 50}, {resizeMode: "crop-and-scale", frameRate: 5}], + [ + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 50}; + }, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 5}; + }, + [[2, 50], [2, 7]] + ], + ], + [ + [{resizeMode: "none"}, {resizeMode: "crop-and-scale", frameRate: 5000}, {}], + [ + nativeScreen, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 120}; + }, + ] + ], + ].forEach( + ([ + [video, cloneVideo], + [expectedFunc, cloneExpectedFunc], + [testFramerate, cloneTestFramerate] = [null, null] + ]) => { + let expected, cloneExpected; + promise_test(async t => { + expected = expectedFunc(); + cloneExpected = cloneExpectedFunc(); + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + let settings = track.getSettings(); + let cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original: ${key}`); + assert_equals(cloneSettings[key], expected[key], `clone: ${key}`); + } + await clone.applyConstraints(cloneVideo); + settings = track.getSettings(); + cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original-post: ${key}`); + } + for (const key of Object.keys(cloneExpected)) { + assert_equals(cloneSettings[key], cloneExpected[key], `clone-post: ${key}`); + } + await test_resolution_equals(t, track, settings.width, settings.height); + await test_resolution_equals(t, clone, cloneSettings.width, cloneSettings.height); + if (testFramerate) { + const [low, high] = testFramerate; + await test_framerate_between_exclusive(t, track, low, high); + } + if (cloneTestFramerate) { + const [low, high] = cloneTestFramerate; + await test_framerate_between_exclusive(t, clone, low, high); + } + }, `gDM gets expected modes by ${JSON.stringify(video)} + (cloned) ${JSON.stringify(cloneVideo)}`); + }); + + promise_test(async t => { + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video: {resizeMode: "none"}}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + await clone.applyConstraints( + { + resizeMode: "crop-and-scale", + width: 400, + height: 400 + } + ); + const expected = findFittestResolutionSetting( + screenWidth(), + screenHeight(), + clone.getConstraints() + ); + assert_equals(clone.getSettings().width, expected.width, "width"); + assert_equals(clone.getSettings().height, expected.height, "height"); + await test_resolution_equals(t, clone, clone.getSettings().width, clone.getSettings().height); + assert_approx_equals( + clone.getSettings().width / clone.getSettings().height, + desktopWidth() / desktopHeight(), + 0.01, + "aspect ratio" + ); + assert_equals(clone.getSettings().resizeMode, "crop-and-scale", "resizeMode"); + }, "applyConstraints on gDM clone doesn't crop with only ideal dimensions"); + + promise_test(async t => { + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video: {resizeMode: "none"}}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + await clone.applyConstraints( + { + resizeMode: "crop-and-scale", + width: {max: 400}, + height: {ideal: 400} + } + ); + assert_equals(clone.getSettings().width, 400, "width"); + assert_equals( + clone.getSettings().height, + Math.round(screenHeight() / screenWidth() * 400), + "height" + ); + await test_resolution_equals(t, clone, clone.getSettings().width, clone.getSettings().height); + assert_equals(clone.getSettings().resizeMode, "crop-and-scale", "resizeMode"); + }, "applyConstraints on gDM clone doesn't crop with ideal and max dimensions"); +</script>