| <!doctype html> |
| <meta charset=utf-8> |
| <title>getDisplayMedia</title> |
| <meta name="timeout" content="long"> |
| <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> |
| <video id="display"></video> |
| <script> |
| 'use strict'; |
| test(() => { |
| assert_idl_attribute(navigator.mediaDevices, 'getDisplayMedia'); |
| }, "getDisplayMedia in navigator.mediaDevices"); |
| |
| const stopTracks = stream => stream.getTracks().forEach(track => track.stop()); |
| const j = obj => JSON.stringify(obj); |
| |
| async function getDisplayMedia(constraints) { |
| await test_driver.bless("getDisplayMedia", () => navigator.mediaDevices.getDisplayMedia(constraints)); |
| return navigator.mediaDevices.getDisplayMedia(constraints); |
| } |
| |
| promise_test(t => { |
| const p = navigator.mediaDevices.getDisplayMedia({video: true}); |
| t.add_cleanup(async () => { |
| try { stopTracks(await p) } catch {} |
| }); |
| // Race a settled promise to check that the returned promise is already |
| // rejected. |
| return promise_rejects_dom( |
| t, 'InvalidStateError', Promise.race([p, Promise.resolve()]), |
| 'getDisplayMedia should have returned an already-rejected promise.'); |
| }, `getDisplayMedia() must require user activation`); |
| |
| promise_test( async t => { |
| const v = document.getElementById('display'); |
| v.autoplay = true; |
| // work around firefox bug 1586505, orthogonal to what's being tested |
| const frames = () => v.getVideoPlaybackQuality()?.totalVideoFrames || v.mozPaintedFrames; |
| const target_rate = 5; |
| const stream = await getDisplayMedia({video: {width:160, frameRate: target_rate}}); |
| t.add_cleanup(() => stopTracks(stream)); |
| v.srcObject = stream; |
| const intitial_time = v.currentTime; |
| const initial_frame_count = frames(); |
| await new Promise(r => t.step_timeout(r, 10000)); |
| const total_elapsed_frames = frames() - initial_frame_count; |
| const total_elapsed_time = v.currentTime - intitial_time; |
| const average_fps = total_elapsed_frames / total_elapsed_time; |
| assert_greater_than_equal(average_fps, target_rate * 0.50); |
| assert_less_than_equal(average_fps, target_rate * 1.75); |
| }, "getDisplayMedia() must adhere to frameRate if set"); |
| |
| [ |
| {video: true}, |
| {video: true, audio: false}, |
| {audio: false}, |
| {audio: true}, |
| {}, |
| undefined |
| ].forEach(constraints => promise_test(async t => { |
| const stream = await getDisplayMedia(constraints); |
| t.add_cleanup(() => stopTracks(stream)); |
| assert_equals(stream.getTracks().length, 1); |
| assert_equals(stream.getVideoTracks().length, 1); |
| assert_equals(stream.getAudioTracks().length, 0); |
| }, `getDisplayMedia(${j(constraints)}) must succeed with video`)); |
| |
| [ |
| {video: false}, |
| {video: {advanced: [{width: 320}]}}, |
| {video: {width: {min: 320}}}, |
| {video: {width: {exact: 320}}}, |
| {video: {height: {min: 240}}}, |
| {video: {height: {exact: 240}}}, |
| {video: {frameRate: {min: 4}}}, |
| {video: {frameRate: {exact: 4}}}, |
| {video: false, audio: true}, |
| ].forEach(constraints => promise_test(async t => { |
| const p = getDisplayMedia(constraints); |
| t.add_cleanup(async () => { |
| try { stopTracks(await p) } catch {} |
| }); |
| await promise_rejects_js( |
| t, TypeError, Promise.race([p, Promise.resolve()]), |
| 'getDisplayMedia should have returned an already-rejected promise.'); |
| }, `getDisplayMedia(${j(constraints)}) must fail with TypeError`)); |
| |
| [ |
| {video: true, audio: true}, |
| ].forEach(constraints => promise_test(async t => { |
| const stream = await getDisplayMedia(constraints); |
| t.add_cleanup(() => stopTracks(stream)); |
| assert_greater_than_equal(stream.getTracks().length, 1); |
| assert_less_than_equal(stream.getTracks().length, 2); |
| assert_equals(stream.getVideoTracks().length, 1); |
| assert_less_than_equal(stream.getAudioTracks().length, 1); |
| }, `getDisplayMedia(${j(constraints)}) must succeed with video maybe audio`)); |
| |
| [ |
| {width: {max: 360}}, |
| {height: {max: 240}}, |
| {width: {max: 360}, height: {max: 240}}, |
| {frameRate: {max: 4}}, |
| {frameRate: {max: 4}, width: {max: 360}}, |
| {frameRate: {max: 4}, height: {max: 240}}, |
| {frameRate: {max: 4}, width: {max: 360}, height: {max: 240}}, |
| ].forEach(constraints => promise_test(async t => { |
| const stream = await getDisplayMedia({video: constraints}); |
| t.add_cleanup(() => stopTracks(stream)); |
| const {width, height, frameRate} = stream.getTracks()[0].getSettings(); |
| assert_greater_than_equal(width, 1); |
| assert_greater_than_equal(height, 1); |
| assert_greater_than_equal(frameRate, 1); |
| if (constraints.width) { |
| assert_less_than_equal(width, constraints.width.max); |
| } |
| if (constraints.height) { |
| assert_less_than_equal(height, constraints.height.max); |
| } |
| if (constraints.frameRate) { |
| assert_less_than_equal(frameRate, constraints.frameRate.max); |
| } |
| }, `getDisplayMedia({video: ${j(constraints)}}) must be constrained`)); |
| |
| const someSizes = [ |
| {width: 160}, |
| {height: 120}, |
| {width: 80}, |
| {height: 60}, |
| {width: 158}, |
| {height: 118}, |
| ]; |
| |
| someSizes.forEach(constraints => promise_test(async t => { |
| const stream = await getDisplayMedia({video: constraints}); |
| t.add_cleanup(() => stopTracks(stream)); |
| const {width, height, frameRate} = stream.getTracks()[0].getSettings(); |
| if (constraints.width) { |
| assert_equals(width, constraints.width); |
| } else { |
| assert_equals(height, constraints.height); |
| } |
| assert_greater_than_equal(frameRate, 1); |
| }, `getDisplayMedia({video: ${j(constraints)}}) must be downscaled precisely`)); |
| |
| promise_test(async t => { |
| const video = {height: 240}; |
| const stream = await getDisplayMedia({video}); |
| t.add_cleanup(() => stopTracks(stream)); |
| const [track] = stream.getVideoTracks(); |
| const {height} = track.getSettings(); |
| assert_equals(height, video.height); |
| for (const constraints of someSizes) { |
| await track.applyConstraints(constraints); |
| const {width, height} = track.getSettings(); |
| if (constraints.width) { |
| assert_equals(width, constraints.width); |
| } else { |
| assert_equals(height, constraints.height); |
| } |
| } |
| }, `applyConstraints(width or height) must downscale precisely`); |
| |
| [ |
| {video: {width: {max: 0}}}, |
| {video: {height: {max: 0}}}, |
| {video: {frameRate: {max: 0}}}, |
| {video: {width: {max: -1}}}, |
| {video: {height: {max: -1}}}, |
| {video: {frameRate: {max: -1}}}, |
| ].forEach(constraints => promise_test(async t => { |
| try { |
| stopTracks(await getDisplayMedia(constraints)); |
| } catch (err) { |
| assert_equals(err.name, 'OverconstrainedError', err.message); |
| return; |
| } |
| assert_unreached('getDisplayMedia should have failed'); |
| }, `getDisplayMedia(${j(constraints)}) must fail with OverconstrainedError`)); |
| |
| // Content shell picks a fake desktop device by default. |
| promise_test(async t => { |
| const stream = await getDisplayMedia({video: true}); |
| t.add_cleanup(() => stopTracks(stream)); |
| assert_equals(stream.getVideoTracks().length, 1); |
| const track = stream.getVideoTracks()[0]; |
| assert_equals(track.kind, "video"); |
| assert_equals(track.enabled, true); |
| assert_equals(track.readyState, "live"); |
| track.stop(); |
| assert_equals(track.readyState, "ended"); |
| }, 'getDisplayMedia() resolves with stream with video track'); |
| |
| promise_test(async t => { |
| const stream = await getDisplayMedia({video: true}); |
| t.add_cleanup(() => stopTracks(stream)); |
| const settings = stream.getVideoTracks()[0].getSettings(); |
| assert_any( |
| assert_equals, settings.displaySurface, |
| ['monitor', 'window', 'browser']); |
| assert_any(assert_equals, settings.logicalSurface, [true, false]); |
| assert_any(assert_equals, settings.cursor, ['never', 'always', 'motion']); |
| }, 'getDisplayMedia() with getSettings'); |
| |
| { |
| const properties = ["displaySurface"]; |
| properties.forEach((property) => { |
| test(() => { |
| const supportedConstraints = |
| navigator.mediaDevices.getSupportedConstraints(); |
| assert_true(supportedConstraints[property]); |
| }, property + " is supported"); |
| }); |
| } |
| |
| [ |
| {video: {displaySurface: "monitor"}}, |
| {video: {displaySurface: "window"}}, |
| {video: {displaySurface: "browser"}}, |
| {selfBrowserSurface: "include"}, |
| {selfBrowserSurface: "exclude"}, |
| {surfaceSwitching: "include"}, |
| {surfaceSwitching: "exclude"}, |
| {systemAudio: "include"}, |
| {systemAudio: "exclude"}, |
| ].forEach(constraints => promise_test(async t => { |
| const stream = await getDisplayMedia(constraints); |
| t.add_cleanup(() => stopTracks(stream)); |
| }, `getDisplayMedia(${j(constraints)}) must succeed`)); |
| |
| [ |
| {selfBrowserSurface: "invalid"}, |
| {surfaceSwitching: "invalid"}, |
| {systemAudio: "invalid"}, |
| ].forEach(constraints => promise_test(async t => { |
| const p = getDisplayMedia(constraints); |
| t.add_cleanup(async () => { |
| try { stopTracks(await p) } catch {} |
| }); |
| await promise_rejects_js( |
| t, TypeError, Promise.race([p, Promise.resolve()]), |
| 'getDisplayMedia should have returned an already-rejected promise.'); |
| }, `getDisplayMedia(${j(constraints)}) must fail with TypeError`)); |
| |
| </script> |