Bug 1580944 - Require user gesture for getDisplayMedia(). r=pehrsons,tnguyen,ng
authorJan-Ivar Bruaroey <jib@mozilla.com>
Tue, 19 Nov 2019 15:24:42 +0000
changeset 502742 4d512fa7b07148a92a9f598ed004fb5bb116b3cb
parent 502741 f4a9e675a33f01ce97adce2296b9b21ce5e3111f
child 502743 6b9730121396474298cc365d65942ce3d3ce13c7
push id36823
push usermalexandru@mozilla.com
push dateWed, 20 Nov 2019 09:47:58 +0000
treeherdermozilla-central@79821df17239 [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
@@ -1,23 +1,20 @@
 [feature-policy.https.sub.html]
   [Default "display-capture" feature policy ["self"\] disallows cross-origin iframes.]
     expected: FAIL
 
 
 [feature-policy.https.html]
   [Default "display-capture" feature policy ["self"\] disallows cross-origin iframes.]
     expected:
-      if os == "android": PASS
+      if os == "android": FAIL
       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');