Bug 1580944 - Require user gesture for getDisplayMedia(). r=pehrsons,tnguyen,ng
☠☠ backed out by bdbdb1a54bfd ☠ ☠
authorJan-Ivar Bruaroey <jib@mozilla.com>
Mon, 18 Nov 2019 23:20:13 +0000
changeset 502556 1f4bac75290ebf1aed98980395180250e19c01a1
parent 502555 b5c5ba07d3dbd0d07b66fa42a103f4df2c27d3a2
child 502557 3ebb341e344ae8acc32bb118f45905f5e236d47a
push id100906
push userjbruaroey@mozilla.com
push dateTue, 19 Nov 2019 05:50:11 +0000
treeherderautoland@1f4bac75290e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspehrsons, tnguyen, ng
bugs1580944
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1580944 - Require user gesture for getDisplayMedia(). r=pehrsons,tnguyen,ng Differential Revision: https://phabricator.services.mozilla.com/D53091
dom/media/MediaManager.cpp
testing/web-platform/meta/screen-capture/feature-policy.https.html.ini
testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini
testing/web-platform/tests/screen-capture/feature-policy.https.html
testing/web-platform/tests/screen-capture/getdisplaymedia.https.html
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -2818,16 +2818,31 @@ RefPtr<MediaManager::StreamPromise> Medi
 };
 
 RefPtr<MediaManager::StreamPromise> MediaManager::GetDisplayMedia(
     nsPIDOMWindowInner* aWindow,
     const DisplayMediaStreamConstraints& aConstraintsPassedIn,
     CallerType aCallerType) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aWindow);
+  Document* doc = aWindow->GetExtantDoc();
+  if (NS_WARN_IF(!doc)) {
+    return StreamPromise::CreateAndReject(
+        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+        __func__);
+  }
+
+  if (!doc->HasBeenUserGestureActivated()) {
+    return StreamPromise::CreateAndReject(
+        MakeRefPtr<MediaMgrError>(
+            MediaMgrError::Name::InvalidStateError,
+            NS_LITERAL_STRING(
+                "getDisplayMedia must be called from a user gesture handler.")),
+        __func__);
+  }
 
   if (!IsOn(aConstraintsPassedIn.mVideo)) {
     return StreamPromise::CreateAndReject(
         MakeRefPtr<MediaMgrError>(MediaMgrError::Name::TypeError,
                                   NS_LITERAL_STRING("video is required")),
         __func__);
   }
 
--- a/testing/web-platform/meta/screen-capture/feature-policy.https.html.ini
+++ b/testing/web-platform/meta/screen-capture/feature-policy.https.html.ini
@@ -9,15 +9,12 @@
       if os == "android": PASS
       FAIL
 
   [Default "display-capture" feature policy ["self"\] allows the top-level document.]
     expected:
       if os == "android": FAIL
 
   [Feature policy "display-capture" can be enabled in cross-origin iframes using "allow" attribute.]
-    expected:
-      if os == "android": FAIL
+    disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1596585
 
   [Default "display-capture" feature policy ["self"\] allows same-origin iframes.]
-    expected:
-      if os == "android": FAIL
-
+    disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1596585
--- a/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini
+++ b/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini
@@ -1,13 +1,10 @@
 [getdisplaymedia.https.html]
   disabled:
     if (os == "android") and e10s: bug 1550895 (frequently fails on geckoview)
   expected:
     if (os == "linux") and webrender and not debug: ["OK", "TIMEOUT"]
+
   [getDisplayMedia() with getSettings]
     expected:
       if (os == "linux") and webrender and not debug: ["FAIL", "TIMEOUT"]
       FAIL
-
-  [getDisplayMedia({"audio":true}) must fail with TypeError]
-    expected: FAIL
-
--- a/testing/web-platform/tests/screen-capture/feature-policy.https.html
+++ b/testing/web-platform/tests/screen-capture/feature-policy.https.html
@@ -1,22 +1,32 @@
 <!DOCTYPE html>
 <meta charset=utf-8>
 <body>
-  <script src=/resources/testharness.js></script>
-  <script src=/resources/testharnessreport.js></script>
-  <script src=/common/get-host-info.sub.js></script>
-  <script src=/feature-policy/resources/featurepolicy.js></script>
+  <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 src="/common/get-host-info.sub.js"></script>
+  <script src="/feature-policy/resources/featurepolicy.js"></script>
   <script>
   'use strict';
 
-  async function gDM({audio, video}) {
+  async function getDisplayMedia(constraints) {
+    const p = new Promise(r => button.onclick = r);
+    await test_driver.click(button);
+    await p;
+    return navigator.mediaDevices.getDisplayMedia(constraints);
+  }
+
+  async function testGDM({audio, video}) {
     let stream;
     try {
-      stream = await navigator.mediaDevices.getDisplayMedia({audio, video});
+      stream = await getDisplayMedia({audio, video});
       if (stream.getVideoTracks().length == 0) {
         throw {name: `requested video track must be present with ` +
                      `audio ${audio} and video ${video}, or fail`};
       }
     } finally {
       if (stream) {
         stream.getTracks().forEach(track => track.stop());
       }
@@ -24,15 +34,15 @@
   }
 
   const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN;
   run_all_fp_tests_allow_self(
     cross_domain,
     'display-capture',
     'NotAllowedError',
     async () => {
-      await gDM({video: true});
-      await gDM({audio: true, video: true});
-      await gDM({audio: true});
+      await testGDM({video: true});
+      await testGDM({audio: true, video: true});
+      await testGDM({audio: true});
     }
   );
   </script>
 </body>
--- a/testing/web-platform/tests/screen-capture/getdisplaymedia.https.html
+++ b/testing/web-platform/tests/screen-capture/getdisplaymedia.https.html
@@ -1,78 +1,98 @@
 <!doctype html>
 <meta charset=utf-8>
 <title>getDisplayMedia</title>
+<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(async t => {
+  try {
+    stopTracks(await navigator.mediaDevices.getDisplayMedia({video: true}));
+  } catch (err) {
+    assert_equals(err.name, 'InvalidStateError', err.message);
+    return;
+  }
+  assert_unreached('getDisplayMedia should have failed');
+}, `getDisplayMedia() must require user activation`);
+
 [
  {video: true},
  {video: true, audio: false},
  {audio: false},
+ {audio: true},
  {},
  undefined
 ].forEach(constraints => promise_test(async t => {
-  const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
+  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}}},
- {audio: true},
  {video: false, audio: true},
 ].forEach(constraints => promise_test(async t => {
   try {
-    stopTracks(await navigator.mediaDevices.getDisplayMedia(constraints));
+    stopTracks(await getDisplayMedia(constraints));
   } catch (err) {
     assert_equals(err.name, 'TypeError', err.message);
     return;
   }
   assert_unreached('getDisplayMedia should have failed');
 }, `getDisplayMedia(${j(constraints)}) must fail with TypeError`));
 
 [
  {video: true, audio: true},
 ].forEach(constraints => promise_test(async t => {
-  const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
+  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`));
 
 [
  {video: {width: {max: 360}}},
  {video: {height: {max: 240}}},
  {video: {width: {max: 360}, height: {max: 240}}},
  {video: {frameRate: {max: 4}}},
  {video: {frameRate: {max: 4}, width: {max: 360}}},
  {video: {frameRate: {max: 4}, height: {max: 240}}},
  {video: {frameRate: {max: 4}, width: {max: 360}, height: {max: 240}}},
 ].forEach(constraints => promise_test(async t => {
-  const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
+  const stream = await getDisplayMedia(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);
   }
@@ -88,27 +108,39 @@ const j = obj => JSON.stringify(obj);
  {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 navigator.mediaDevices.getDisplayMedia(constraints));
+    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 navigator.mediaDevices.getDisplayMedia({video: true});
+  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', 'application', 'browser']);
   assert_any(assert_equals, settings.logicalSurface, [true, false]);
   assert_any(assert_equals, settings.cursor, ['never', 'always', 'motion']);
 }, 'getDisplayMedia() with getSettings');