MediaStreamTrack-resizeMode.https.html (16226B)
1 <!doctype html> 2 <title>MediaStreamTrack video resizeMode. Assumes Mozilla's fake camera source with 480p and 720p capabilities.</title> 3 <meta name="timeout" content="long"> 4 <p class="instructions">When prompted, accept to share your video stream.</p> 5 <script src=/resources/testharness.js></script> 6 <script src=/resources/testharnessreport.js></script> 7 <script src=/resources/testdriver.js></script> 8 <script src=/resources/testdriver-vendor.js></script> 9 <script src=settings-helper.js></script> 10 <script src=video-test-helper.js></script> 11 <script> 12 "use strict" 13 14 // Native capabilities supported by the fake camera. 15 const nativeLow = {width: 640, height: 480, frameRate: 30, resizeMode: "none"}; 16 const nativeHigh = {width: 1280, height: 720, frameRate: 10, resizeMode: "none"}; 17 18 19 [ 20 [{resizeMode: "none", width: 500}, nativeLow], 21 [{resizeMode: "none", height: 500}, nativeLow], 22 [{resizeMode: "none", width: 500, height: 500}, nativeLow], 23 [{resizeMode: "none", frameRate: 50}, nativeLow], 24 [{resizeMode: "none", width: 500, height: 500, frameRate: 50}, nativeLow], 25 [{resizeMode: "none", width: 1000}, nativeHigh], 26 [{resizeMode: "none", height: 1000}, nativeHigh], 27 [{resizeMode: "none", width: 1000, height: 1000}, nativeHigh], 28 [{resizeMode: "none", frameRate: 1}, nativeHigh, [3, 12]], 29 [{resizeMode: "none", width: 1000, height: 1000, frameRate: 1}, nativeHigh], 30 [ 31 {resizeMode: "crop-and-scale"}, 32 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 33 ], 34 [ 35 {resizeMode: "crop-and-scale", height: 500}, 36 {resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10} 37 ], 38 [ 39 {resizeMode: "crop-and-scale", width: {min: 500}, height: {max: 200}}, 40 {resizeMode: "crop-and-scale", width: 500, height: 200, frameRate: 30} 41 ], 42 [ 43 {resizeMode: "crop-and-scale", frameRate: 50}, 44 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 45 ], 46 [ 47 {resizeMode: "crop-and-scale", width: 10000, frameRate: {min: 30}}, 48 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 49 ], 50 [ 51 {resizeMode: "crop-and-scale", frameRate: {exact: 5}}, 52 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5}, 53 [2, 7] 54 ], 55 ].forEach(([video, expected, testFramerate]) => promise_test(async t => { 56 const stream = await navigator.mediaDevices.getUserMedia({video}); 57 const [track] = stream.getTracks(); 58 t.add_cleanup(() => track.stop()); 59 const settings = track.getSettings(); 60 for (const key of Object.keys(expected)) { 61 assert_equals(settings[key], expected[key], key); 62 } 63 if (testFramerate) { 64 const [low, high] = testFramerate; 65 await test_framerate_between_exclusive(t, track, low, high); 66 } 67 }, `gUM gets ${JSON.stringify(expected)} mode by ${JSON.stringify(video)}`)); 68 69 promise_test(async t => { 70 const stream = await navigator.mediaDevices.getUserMedia({video: {resizeMode: "none", height: 720}}); 71 const [track] = stream.getTracks(); 72 const stream2 = await navigator.mediaDevices.getUserMedia({video: {resizeMode: "crop-and-scale", height: 300}}); 73 const [track2] = stream2.getTracks(); 74 t.add_cleanup(() => { 75 track.stop(); 76 track2.stop(); 77 }); 78 const settings = track.getSettings(); 79 assert_equals(settings.resizeMode, "none", "track resizeMode"); 80 assert_equals(settings.width, 1280, "track width"); 81 assert_equals(settings.height, 720, "track height"); 82 assert_equals(settings.frameRate, 10, "track framerate"); 83 const settings2 = track2.getSettings(); 84 assert_equals(settings2.resizeMode, "crop-and-scale", "track2 resizeMode"); 85 assert_equals(settings2.width, 400, "track2 width"); 86 assert_equals(settings2.height, 300, "track2 height"); 87 // Bug 1988466: source is configured at 10fps so we cannot completely 88 // masquerade the selected capability. 89 assert_equals(settings2.frameRate, 30, "track2 framerate"); 90 }, `gUM gets expected downscaling with competing capabilities`); 91 92 promise_test(async t => { 93 try { 94 const stream = await navigator.mediaDevices.getUserMedia( 95 {video: {resizeMode: "none", width: {min: 2000}}} 96 ); 97 const [track] = stream.getTracks(); 98 t.add_cleanup(() => track.stop()); 99 } catch(e) { 100 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 101 return; 102 } 103 assert_unreached("gUM is rejected with impossible width"); 104 }, "gUM is rejected by resizeMode none and impossible min-width"); 105 106 promise_test(async t => { 107 try { 108 const stream = await navigator.mediaDevices.getUserMedia( 109 {video: {resizeMode: "none", width: {max: 200}}} 110 ); 111 const [track] = stream.getTracks(); 112 t.add_cleanup(() => track.stop()); 113 } catch(e) { 114 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 115 return; 116 } 117 assert_unreached("gUM is rejected with impossible width"); 118 }, "gUM is rejected by resizeMode none and impossible max-width"); 119 120 promise_test(async t => { 121 try { 122 const stream = await navigator.mediaDevices.getUserMedia( 123 {video: {resizeMode: "crop-and-scale", width: {min: 2000}}} 124 ); 125 const [track] = stream.getTracks(); 126 t.add_cleanup(() => track.stop()); 127 } catch(e) { 128 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 129 return; 130 } 131 assert_unreached("gUM is rejected with impossible width"); 132 }, "gUM is rejected by resizeMode crop-and-scale and impossible width"); 133 134 promise_test(async t => { 135 try { 136 const stream = await navigator.mediaDevices.getUserMedia( 137 {video: {resizeMode: "crop-and-scale", frameRate: {min: 50}}} 138 ); 139 const [track] = stream.getTracks(); 140 t.add_cleanup(() => track.stop()); 141 } catch(e) { 142 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 143 return; 144 } 145 assert_unreached("gUM is rejected with impossible fps"); 146 }, "gUM is rejected by resizeMode crop-and-scale and impossible fps"); 147 148 149 [ 150 [{resizeMode: "none", width: 500}, nativeLow], 151 [{resizeMode: "none", height: 500}, nativeLow], 152 [{resizeMode: "none", width: 500, height: 500}, nativeLow], 153 [{resizeMode: "none", frameRate: 50}, nativeLow], 154 [{resizeMode: "none", width: 500, height: 500, frameRate: 50}, nativeLow], 155 [{resizeMode: "none", width: 1000}, nativeHigh], 156 [{resizeMode: "none", height: 1000}, nativeHigh], 157 [{resizeMode: "none", width: 1000, height: 1000}, nativeHigh], 158 [{resizeMode: "none", frameRate: 1}, nativeHigh, [3, 12]], 159 [{resizeMode: "none", width: 1000, height: 1000, frameRate: 1}, nativeHigh], 160 [ 161 {resizeMode: "crop-and-scale"}, 162 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 163 ], 164 [ 165 {resizeMode: "crop-and-scale", height: 400}, 166 {resizeMode: "crop-and-scale", width: 533, height: 400, frameRate: 30} 167 ], 168 [ 169 {resizeMode: "crop-and-scale", height: 500}, 170 {resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10} 171 ], 172 [ 173 {resizeMode: "crop-and-scale", height: {exact: 500}}, 174 {resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10} 175 ], 176 [ 177 {resizeMode: "crop-and-scale", width: {min: 500}, height: {max: 200}}, 178 {resizeMode: "crop-and-scale", width: 500, height: 200, frameRate: 30} 179 ], 180 [ 181 {resizeMode: "crop-and-scale", frameRate: 50}, 182 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 183 ], 184 [ 185 {resizeMode: "crop-and-scale", width: 10000, frameRate: {min: 30}}, 186 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 30} 187 ], 188 [ 189 {resizeMode: "crop-and-scale", frameRate: {exact: 5}}, 190 {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5}, 191 [2, 7] 192 ], 193 ].forEach(([video, expected, testFramerate]) => promise_test(async t => { 194 const stream = await navigator.mediaDevices.getUserMedia({video: true}); 195 const [track] = stream.getTracks(); 196 t.add_cleanup(() => track.stop()); 197 await track.applyConstraints(video); 198 const settings = track.getSettings(); 199 for (const key of Object.keys(expected)) { 200 assert_equals(settings[key], expected[key], key); 201 } 202 if (testFramerate) { 203 const [low, high] = testFramerate; 204 await test_framerate_between_exclusive(t, track, low, high); 205 } 206 }, `applyConstraints gets ${JSON.stringify(expected)} mode by ${JSON.stringify(video)}`)); 207 208 promise_test(async t => { 209 const stream = await navigator.mediaDevices.getUserMedia({video: true}); 210 const [track] = stream.getTracks(); 211 t.add_cleanup(() => track.stop()); 212 try { 213 await track.applyConstraints({resizeMode: "none", width: {min: 2000}}) 214 } catch(e) { 215 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 216 return; 217 } 218 assert_unreached("applyConstraints is rejected with impossible width"); 219 }, "applyConstraints is rejected by resizeMode none and impossible min-width"); 220 221 promise_test(async t => { 222 const stream = await navigator.mediaDevices.getUserMedia({video: true}); 223 const [track] = stream.getTracks(); 224 t.add_cleanup(() => track.stop()); 225 try { 226 await track.applyConstraints({resizeMode: "none", width: {max: 200}}) 227 } catch(e) { 228 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 229 return; 230 } 231 assert_unreached("applyConstraints is rejected with impossible width"); 232 }, "applyConstraints is rejected by resizeMode none and impossible max-width"); 233 234 promise_test(async t => { 235 const stream = await navigator.mediaDevices.getUserMedia({video: true}); 236 const [track] = stream.getTracks(); 237 t.add_cleanup(() => track.stop()); 238 try { 239 await track.applyConstraints({resizeMode: "crop-and-scale", width: {min: 2000}}) 240 } catch(e) { 241 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 242 return; 243 } 244 assert_unreached("applyConstraints is rejected with impossible width"); 245 }, "applyConstraints is rejected by resizeMode crop-and-scale and impossible width"); 246 247 promise_test(async t => { 248 const stream = await navigator.mediaDevices.getUserMedia({video: true}); 249 const [track] = stream.getTracks(); 250 t.add_cleanup(() => track.stop()); 251 try { 252 await track.applyConstraints({resizeMode: "crop-and-scale", frameRate: {min: 50}}); 253 } catch(e) { 254 assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); 255 return; 256 } 257 assert_unreached("applyConstraints is rejected with impossible fps"); 258 }, "applyConstraints is rejected by resizeMode crop-and-scale impossible fps"); 259 260 261 // Note these gDM tests will fail if our own window is on a screen different 262 // than the system's first screen. They're functions in case the browser 263 // window needs to be moved to the first screen during the test in order to 264 // pass. 265 function screenPixelRatio() { return SpecialPowers.wrap(window).desktopToDeviceScale; } 266 function screenWidth() { return window.screen.width * window.devicePixelRatio; } 267 function screenHeight() { return window.screen.height * window.devicePixelRatio; } 268 function desktopWidth() { 269 // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. 270 // return screenWidth() / screenPixelRatio(); 271 return screenWidth(); 272 } 273 function desktopHeight() { 274 // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. 275 // return screenHeight() / screenPixelRatio(); 276 return screenHeight(); 277 } 278 279 promise_test(async t => { 280 await test_driver.bless('getDisplayMedia()'); 281 const stream = await navigator.mediaDevices.getDisplayMedia( 282 {video: {resizeMode: "none", width: 100}} 283 ); 284 const [track] = stream.getTracks(); 285 t.add_cleanup(() => track.stop()); 286 assert_equals(track.getSettings().width, screenWidth(), "width"); 287 assert_equals(track.getSettings().height, screenHeight(), "height"); 288 assert_equals(track.getSettings().frameRate, 60, "framerate"); 289 assert_equals(track.getSettings().resizeMode, "none", "resizeMode"); 290 }, "gDM gets full screen resolution by width"); 291 292 // TODO: By default we shouldn't be multiplying with window.devicePixelRatio (bug 1703991). 293 function defaultScreen() { 294 return { 295 resizeMode: "crop-and-scale", 296 width: screenWidth(), 297 height: screenHeight(), 298 frameRate: 30, 299 }; 300 } 301 // TODO: Should get the source's real refresh rate for frameRate (bug 1984363). 302 function nativeScreen() { 303 return { 304 resizeMode: "none", 305 width: screenWidth(), 306 height: screenHeight(), 307 frameRate: 60 308 }; 309 } 310 311 [ 312 [{resizeMode: "none", width: 100}, nativeScreen], 313 [{resizeMode: "none", frameRate: 50}, nativeScreen], 314 [{resizeMode: "crop-and-scale"}, defaultScreen], 315 [{resizeMode: "crop-and-scale", height: 100}, () => ({ 316 resizeMode: "crop-and-scale", 317 width: Math.round(screenWidth() / screenHeight() * 100), 318 height: 100, 319 frameRate: 30 320 })], 321 [{resizeMode: "crop-and-scale", frameRate: 5}, () => { 322 const { width, height } = defaultScreen(); 323 return { width, height, frameRate: 5}; 324 }, [2, 7]], 325 [{resizeMode: "crop-and-scale", frameRate: 50}, () => { 326 const { width, height } = defaultScreen(); 327 return { width, height, frameRate: 50}; 328 }], 329 [{resizeMode: "crop-and-scale", frameRate: 5000}, () => { 330 const { width, height } = defaultScreen(); 331 return { width, height, frameRate: 120}; 332 }], 333 ].forEach(([video, expectedFunc, testFramerate]) => { 334 let expected; 335 promise_test(async t => { 336 expected = expectedFunc(); 337 await test_driver.bless('getDisplayMedia()'); 338 const stream = await navigator.mediaDevices.getDisplayMedia({video}); 339 const [track] = stream.getTracks(); 340 t.add_cleanup(() => track.stop()); 341 const settings = track.getSettings(); 342 for (const key of Object.keys(expected)) { 343 assert_equals(settings[key], expected[key], key); 344 } 345 if (testFramerate) { 346 const [low, high] = testFramerate; 347 await test_framerate_between_exclusive(t, track, low, high); 348 } 349 }, `gDM gets expected mode by ${JSON.stringify(video)}`); 350 }); 351 352 promise_test(async t => { 353 await test_driver.bless('getDisplayMedia()'); 354 const stream = await navigator.mediaDevices.getDisplayMedia( 355 {video: { 356 resizeMode: "crop-and-scale", 357 width: 400, 358 height: 400 359 }} 360 ); 361 const [track] = stream.getTracks(); 362 t.add_cleanup(() => track.stop()); 363 const expected = findFittestResolutionSetting( 364 screenWidth(), 365 screenHeight(), 366 track.getConstraints() 367 ); 368 assert_equals(track.getSettings().width, expected.width, "width"); 369 assert_equals(track.getSettings().height, expected.height, "height"); 370 assert_approx_equals( 371 track.getSettings().width / track.getSettings().height, 372 desktopWidth() / desktopHeight(), 373 0.01, 374 "aspect ratio" 375 ); 376 assert_equals(track.getSettings().resizeMode, "crop-and-scale", "resizeMode"); 377 }, "gDM doesn't crop with only ideal dimensions"); 378 379 promise_test(async t => { 380 await test_driver.bless('getDisplayMedia()'); 381 const stream = await navigator.mediaDevices.getDisplayMedia( 382 {video: { 383 resizeMode: "crop-and-scale", 384 width: {max: 400}, 385 height: {ideal: 400} 386 }} 387 ); 388 const [track] = stream.getTracks(); 389 t.add_cleanup(() => track.stop()); 390 assert_equals(track.getSettings().width, 400, "width"); 391 assert_equals( 392 track.getSettings().height, 393 Math.round(screenHeight() / screenWidth() * 400), 394 "height" 395 ); 396 assert_equals(track.getSettings().resizeMode, "crop-and-scale", "resizeMode"); 397 }, "gDM doesn't crop with ideal and max dimensions"); 398 </script>