| <!doctype html> | |
| <meta charset=utf-8> | |
| <title>getDisplayMedia</title> | |
| <meta name="timeout" content="long"> | |
| <button id="button">User gesture</button> | |
| <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'; | |
| 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) { | |
| const p = new Promise(r => button.onclick = r); | |
| await test_driver.click(button); | |
| await p; | |
| 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`); | |
| [ | |
| {video: true}, | |
| {video: true, audio: false}, | |
| {video: {}}, | |
| {audio: false}, | |
| {}, | |
| 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}}}, | |
| ].forEach(constraints => promise_test(async t => { | |
| await test_driver.bless('getDisplayMedia()'); | |
| const p = navigator.mediaDevices.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}, | |
| {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`)); | |
| [ | |
| {width: {max: 0}}, | |
| {height: {max: 0}}, | |
| {frameRate: {max: 0}}, | |
| {width: {max: -1}}, | |
| {height: {max: -1}}, | |
| {frameRate: {max: -1}}, | |
| {width: {min: 100, max: 10}}, | |
| {height: {min: 100, max: 10}}, | |
| {frameRate: {min: 100, max: 10}}, | |
| ].forEach(constraints => promise_test(async t => { | |
| const stream = await getDisplayMedia(); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const [track] = stream.getTracks(); | |
| try { | |
| await track.applyConstraints(constraints); | |
| } catch (err) { | |
| assert_equals(err.name, 'OverconstrainedError', err.message); | |
| assert_equals(err.constraint, Object.keys(constraints)[0], "Constraint is set"); | |
| return; | |
| } | |
| assert_unreached('applyConstraints hould have failed'); | |
| }, `applyConstraints(${j(constraints)}) for display media 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'); | |
| { | |
| const displaySurfaces = ['monitor', 'window', 'browser']; | |
| displaySurfaces.forEach((displaySurface) => { | |
| promise_test(async t => { | |
| const stream = await getDisplayMedia({video: {displaySurface}}); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const settings = stream.getVideoTracks()[0].getSettings(); | |
| assert_equals(settings.displaySurface, displaySurface); | |
| assert_any(assert_equals, settings.logicalSurface, [true, false]); | |
| assert_any(assert_equals, settings.cursor, ['never', 'always', 'motion']); | |
| assert_false("suppressLocalAudioPlayback" in settings); | |
| }, `getDisplayMedia({"video":{"displaySurface":"${displaySurface}"}}) 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"}, | |
| {windowAudio: "exclude"}, | |
| {windowAudio: "window"}, | |
| {windowAudio: "system"}, | |
| ].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"}, | |
| {windowAudio: "invalid"}, | |
| {monitorTypeSurfaces: "invalid"}, | |
| ].forEach(constraints => promise_test(async t => { | |
| await test_driver.bless('getDisplayMedia()'); | |
| const p = navigator.mediaDevices.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`)); | |
| test(() => { | |
| const supportedConstraints = | |
| navigator.mediaDevices.getSupportedConstraints(); | |
| assert_true(supportedConstraints.suppressLocalAudioPlayback); | |
| }, "suppressLocalAudioPlayback is supported"); | |
| { | |
| const suppressLocalAudioPlaybacks = [true, false]; | |
| suppressLocalAudioPlaybacks.forEach((suppressLocalAudioPlayback) => { | |
| promise_test(async (t) => { | |
| const stream = await getDisplayMedia({ | |
| audio: { suppressLocalAudioPlayback }, | |
| }); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const [videoTrack] = stream.getVideoTracks(); | |
| assert_false("suppressLocalAudioPlayback" in videoTrack.getSettings()); | |
| const [audioTrack] = stream.getAudioTracks(); | |
| const audioTrackSettings = audioTrack.getSettings(); | |
| assert_true("suppressLocalAudioPlayback" in audioTrackSettings); | |
| assert_equals( | |
| audioTrackSettings.suppressLocalAudioPlayback, | |
| suppressLocalAudioPlayback | |
| ); | |
| await audioTrack.applyConstraints(); | |
| assert_true("suppressLocalAudioPlayback" in audioTrackSettings); | |
| assert_equals( | |
| audioTrackSettings.suppressLocalAudioPlayback, | |
| suppressLocalAudioPlayback | |
| ); | |
| }, `getDisplayMedia({"audio":{"suppressLocalAudioPlayback":${suppressLocalAudioPlayback}}}) with getSettings`); | |
| }); | |
| } | |
| promise_test(async t => { | |
| const stream = await getDisplayMedia({video: true}); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const capabilities = stream.getVideoTracks()[0].getCapabilities(); | |
| assert_any( | |
| assert_equals, capabilities.displaySurface, | |
| ['monitor', 'window', 'browser']); | |
| }, 'getDisplayMedia() with getCapabilities'); | |
| promise_test(async (t) => { | |
| const constraints = { | |
| video: { displaySurface: "monitor" }, | |
| monitorTypeSurfaces: "exclude", | |
| }; | |
| await test_driver.bless('getDisplayMedia()'); | |
| const p = navigator.mediaDevices.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({"video":{"displaySurface":"monitor"},"monitorTypeSurfaces":"exclude"}) rejects with TypeError`); | |
| promise_test(async (t) => { | |
| const stream = await getDisplayMedia({ | |
| video: { displaySurface: "monitor" }, | |
| monitorTypeSurfaces: "include", | |
| }); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const { displaySurface } = stream.getTracks()[0].getSettings(); | |
| assert_equals(displaySurface, "monitor"); | |
| }, `getDisplayMedia({"video":{"displaySurface":"monitor"},"monitorTypeSurfaces":"include"}) resolves with a monitor track`); | |
| promise_test(async (t) => { | |
| const stream = await getDisplayMedia({ | |
| monitorTypeSurfaces: "exclude", | |
| }); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const { displaySurface } = stream.getTracks()[0].getSettings(); | |
| assert_any(assert_equals, displaySurface, ["window", "browser"]); | |
| }, `getDisplayMedia({"monitorTypeSurfaces":"exclude"}) resolves with a non monitor track`); | |
| </script> |