Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
- /mediacapture-streams/MediaStreamTrack-resizeMode.https.html - WPT Dashboard Interop Dashboard
<!doctype html>
<title>MediaStreamTrack video resizeMode. 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>
"use strict"
async function test_framerate_between_exclusive(t, track, lower, upper) {
const video = document.createElement("video");
document.body.appendChild(video);
t.add_cleanup(async () => document.body.removeChild(video));
video.srcObject = new MediaStream([track]);
await video.play();
const numSeconds = 2;
await new Promise(r => setTimeout(r, numSeconds * 1000));
const totalVideoFrames = video.mozPaintedFrames;
assert_between_exclusive(totalVideoFrames / numSeconds, lower, upper, "totalVideoFrames");
}
function createSettingsDicts(width, height, step = 1) {
const settingsDicts = [], aspect = width / height;
do {
settingsDicts.push({ width, height });
if (width > height) {
height = Math.round((width - step) / aspect);
width -= step;
} else {
width = Math.round((height - step) * aspect);
height -= step;
}
} while (width > 2 && height > 2);
return settingsDicts;
}
function integerFitness(actual, ideal) {
if (actual == ideal) {
return 0;
}
return Math.abs(actual - ideal) / Math.max(Math.abs(actual), Math.abs(ideal));
}
function findFittestResolutionSetting(width, height, constraints) {
const widthIsNumber = typeof constraints.width == "number";
const heightIsNumber = typeof constraints.height == "number";
const c = {
width: {
ideal: widthIsNumber ? constraints.width : constraints?.width?.ideal,
max: constraints?.width?.max ?? 1000000,
},
height: {
ideal: heightIsNumber ? constraints.height : constraints?.height?.ideal,
max: constraints?.height?.max ?? 1000000,
},
};
const dicts = createSettingsDicts(width, height)
.filter(s => s.width <= c.width.max && s.height <= c.height.max);
for (const dict of dicts) {
dict.distance =
integerFitness(dict.width, c.width.ideal) +
integerFitness(dict.height, c.height.ideal);
}
const filteredDicts = dicts.filter(s => {
return (!c.width.ideal || s.width <= c.width.ideal) &&
(!c.height.ideal || s.height <= c.height.ideal);
});
return filteredDicts.reduce(
(a, b) => (a.distance < b.distance ? a : b),
filteredDicts[0],
);
}
// 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: 500}, nativeLow],
[{resizeMode: "none", height: 500}, nativeLow],
[{resizeMode: "none", width: 500, height: 500}, nativeLow],
[{resizeMode: "none", frameRate: 50}, nativeLow],
[{resizeMode: "none", width: 500, height: 500, frameRate: 50}, nativeLow],
[{resizeMode: "none", width: 1000}, nativeHigh],
[{resizeMode: "none", height: 1000}, nativeHigh],
[{resizeMode: "none", width: 1000, height: 1000}, nativeHigh],
[{resizeMode: "none", frameRate: 1}, nativeHigh, [3, 12]],
[{resizeMode: "none", width: 1000, height: 1000, frameRate: 1}, nativeHigh],
[
{resizeMode: "crop-and-scale"},
{resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30}
],
[
{resizeMode: "crop-and-scale", height: 500},
{resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10}
],
[
{resizeMode: "crop-and-scale", width: {min: 500}, height: {max: 200}},
{resizeMode: "crop-and-scale", width: 500, height: 200, frameRate: 30}
],
[
{resizeMode: "crop-and-scale", frameRate: {exact: 5}},
{resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5},
[2, 7]
],
].forEach(([video, expected, testFramerate]) => promise_test(async t => {
const stream = await navigator.mediaDevices.getUserMedia({video});
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
const settings = track.getSettings();
for (const key of Object.keys(expected)) {
assert_equals(settings[key], expected[key], key);
}
if (testFramerate) {
const [low, high] = testFramerate;
await test_framerate_between_exclusive(t, track, low, high);
}
}, `gUM gets ${JSON.stringify(expected)} mode by ${JSON.stringify(video)}`));
promise_test(async t => {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{video: {resizeMode: "none", width: {min: 2000}}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
} catch(e) {
assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`);
return;
}
assert_unreached("gUM is rejected with impossible width");
}, "gUM is rejected by resizeMode none and impossible min-width");
promise_test(async t => {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{video: {resizeMode: "none", width: {max: 200}}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
} catch(e) {
assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`);
return;
}
assert_unreached("gUM is rejected with impossible width");
}, "gUM is rejected by resizeMode none and impossible max-width");
promise_test(async t => {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{video: {resizeMode: "crop-and-scale", width: {min: 2000}}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
} catch(e) {
assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`);
return;
}
assert_unreached("gUM is rejected with impossible width");
}, "gUM is rejected by resizeMode crop-and-scale and impossible width");
promise_test(async t => {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{video: {resizeMode: "crop-and-scale", frameRate: {min: 50}}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
} catch(e) {
assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`);
return;
}
assert_unreached("gUM is rejected with impossible fps");
}, "gUM is rejected by resizeMode crop-and-scale and impossible fps");
[
[{resizeMode: "none", width: 500}, nativeLow],
[{resizeMode: "none", height: 500}, nativeLow],
[{resizeMode: "none", width: 500, height: 500}, nativeLow],
[{resizeMode: "none", frameRate: 50}, nativeLow],
[{resizeMode: "none", width: 500, height: 500, frameRate: 50}, nativeLow],
[{resizeMode: "none", width: 1000}, nativeHigh],
[{resizeMode: "none", height: 1000}, nativeHigh],
[{resizeMode: "none", width: 1000, height: 1000}, nativeHigh],
[{resizeMode: "none", frameRate: 1}, nativeHigh, [3, 12]],
[{resizeMode: "none", width: 1000, height: 1000, frameRate: 1}, nativeHigh],
[
{resizeMode: "crop-and-scale"},
{resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30}
],
[
{resizeMode: "crop-and-scale", height: 400},
{resizeMode: "crop-and-scale", width: 533, height: 400, frameRate: 30}
],
[
{resizeMode: "crop-and-scale", height: 500},
{resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10}
],
[
{resizeMode: "crop-and-scale", height: {exact: 500}},
{resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10}
],
[
{resizeMode: "crop-and-scale", width: {min: 500}, height: {max: 200}},
{resizeMode: "crop-and-scale", width: 500, height: 200, frameRate: 30}
],
[
{resizeMode: "crop-and-scale", frameRate: {exact: 5}},
{resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5},
[2, 7]
],
].forEach(([video, expected, testFramerate]) => promise_test(async t => {
const stream = await navigator.mediaDevices.getUserMedia({video: true});
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
await track.applyConstraints(video);
const settings = track.getSettings();
for (const key of Object.keys(expected)) {
assert_equals(settings[key], expected[key], key);
}
if (testFramerate) {
const [low, high] = testFramerate;
await test_framerate_between_exclusive(t, track, low, high);
}
}, `applyConstraints gets ${JSON.stringify(expected)} mode by ${JSON.stringify(video)}`));
promise_test(async t => {
const stream = await navigator.mediaDevices.getUserMedia({video: true});
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
try {
await track.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 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();
t.add_cleanup(() => track.stop());
try {
await track.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 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();
t.add_cleanup(() => track.stop());
try {
await track.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 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();
t.add_cleanup(() => track.stop());
try {
await track.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 is rejected by resizeMode crop-and-scale impossible fps");
// 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() {
// return screenWidth() / screenPixelRatio();
return screenWidth();
}
function desktopHeight() {
// return screenHeight() / screenPixelRatio();
return screenHeight();
}
promise_test(async t => {
await test_driver.bless('getDisplayMedia()');
const stream = await navigator.mediaDevices.getDisplayMedia(
{video: {resizeMode: "none", width: 100}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
assert_equals(track.getSettings().width, screenWidth(), "width");
assert_equals(track.getSettings().height, screenHeight(), "height");
assert_equals(track.getSettings().frameRate, 60, "framerate");
assert_equals(track.getSettings().resizeMode, "none", "resizeMode");
}, "gDM gets full screen resolution by width");
function defaultScreen() {
return {
resizeMode: "crop-and-scale",
width: screenWidth(),
height: screenHeight(),
frameRate: 30,
};
}
function nativeScreen() {
return {
resizeMode: "none",
width: screenWidth(),
height: screenHeight(),
frameRate: 60
};
}
[
[{resizeMode: "none", width: 100}, nativeScreen],
[{resizeMode: "none", frameRate: 50}, nativeScreen],
[{resizeMode: "crop-and-scale"}, defaultScreen],
[{resizeMode: "crop-and-scale", height: 100}, () => ({
resizeMode: "crop-and-scale",
width: Math.round(screenWidth() / screenHeight() * 100),
height: 100,
frameRate: 30
})],
[{resizeMode: "crop-and-scale", frameRate: 5}, () => {
const { width, height } = defaultScreen();
return { width, height, frameRate: 5};
}, [2, 7]],
].forEach(([video, expectedFunc, testFramerate]) => {
let expected;
promise_test(async t => {
expected = expectedFunc();
await test_driver.bless('getDisplayMedia()');
const stream = await navigator.mediaDevices.getDisplayMedia({video});
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
const settings = track.getSettings();
for (const key of Object.keys(expected)) {
assert_equals(settings[key], expected[key], key);
}
if (testFramerate) {
const [low, high] = testFramerate;
await test_framerate_between_exclusive(t, track, low, high);
}
}, `gDM gets expected mode by ${JSON.stringify(video)}`);
});
promise_test(async t => {
await test_driver.bless('getDisplayMedia()');
const stream = await navigator.mediaDevices.getDisplayMedia(
{video: {
resizeMode: "crop-and-scale",
width: 400,
height: 400
}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
const expected = findFittestResolutionSetting(
screenWidth(),
screenHeight(),
track.getConstraints()
);
assert_equals(track.getSettings().width, expected.width, "width");
assert_equals(track.getSettings().height, expected.height, "height");
assert_approx_equals(
track.getSettings().width / track.getSettings().height,
desktopWidth() / desktopHeight(),
0.01,
"aspect ratio"
);
assert_equals(track.getSettings().resizeMode, "crop-and-scale", "resizeMode");
}, "gDM doesn't crop with only ideal dimensions");
promise_test(async t => {
await test_driver.bless('getDisplayMedia()');
const stream = await navigator.mediaDevices.getDisplayMedia(
{video: {
resizeMode: "crop-and-scale",
width: {max: 400},
height: {ideal: 400}
}}
);
const [track] = stream.getTracks();
t.add_cleanup(() => track.stop());
assert_equals(track.getSettings().width, 400, "width");
assert_equals(
track.getSettings().height,
Math.round(screenHeight() / screenWidth() * 400),
"height"
);
assert_equals(track.getSettings().resizeMode, "crop-and-scale", "resizeMode");
}, "gDM doesn't crop with ideal and max dimensions");
</script>