| <!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`)); |
| |
| // 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"}, |
| ].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"}, |
| {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> |