Bug 1478208 - Implement HTMLMediaElement.allowedToPlay. r=bz,r=alwu draft
authorChris Pearce <cpearce@mozilla.com>
Wed, 25 Jul 2018 14:25:17 +1200
changeset 825585 970d9d6113ab4ef922b83665631795281860f556
parent 825434 89374090a8377286a2025e15c8063a0285920304
push id118142
push userbmo:cpearce@mozilla.com
push dateThu, 02 Aug 2018 02:22:23 +0000
reviewersbz, alwu
bugs1478208
milestone63.0a1
Bug 1478208 - Implement HTMLMediaElement.allowedToPlay. r=bz,r=alwu Various web authors have expressed desire to know in advance whether autoplay will work. They want this in order to avoid paying the price for downloading media that won't play. Or they want to take other action such as showing a poster image instead. This is of particular interest to Firefox, as we're planning on showing a prompt to ask the user whether they would like a site to play. If sites want to determine whether they can autoplay but avoid the prompt showing, they won't be able to just call play() in Firefox and see whether it works, as that would likely show the prompt if the user doesn't already have a stored permission. We've been working out a spec here: https://github.com/whatwg/html/issues/3617#issuecomment-398613484 This implements what is the consensus to date there; HTMLMediaElement.allowedToPlay, which returns true when a play() call would not be blocked with NotAllowedError by autoplay blocking policies. MozReview-Commit-ID: AkBu0G7uCJ0
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/media/test/AutoplayTestUtils.js
dom/media/test/test_autoplay_policy_activation.html
dom/media/test/test_autoplay_policy_permission.html
dom/webidl/HTMLMediaElement.webidl
modules/libpref/init/all.js
testing/profiles/common/user.js
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -2517,16 +2517,22 @@ HTMLMediaElement::ResumeLoad(PreloadActi
     // We were loading from a child <source> element. Try to resume the
     // load of that child, and if that fails, try the next child.
     if (NS_FAILED(LoadResource())) {
       LoadFromSourceChildren();
     }
   }
 }
 
+bool
+HTMLMediaElement::AllowedToPlay() const
+{
+  return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
+}
+
 void
 HTMLMediaElement::UpdatePreloadAction()
 {
   PreloadAction nextAction = PRELOAD_UNDEFINED;
   // If autoplay is set, or we're playing, we should always preload data,
   // as we'll need it to play.
   if ((AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED &&
        HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay)) ||
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -636,16 +636,22 @@ public:
     return mIsCasting;
   }
 
   void SetMozIsCasting(bool aShow)
   {
     mIsCasting = aShow;
   }
 
+  // Returns whether a call to Play() would be rejected with NotAllowedError.
+  // This assumes "worst case" for unknowns. So if prompting for permission is
+  // enabled and no permission is stored, this behaves as if the user would
+  // opt to block.
+  bool AllowedToPlay() const;
+
   already_AddRefed<MediaSource> GetMozMediaSourceObject() const;
   // Returns a string describing the state of the media player internal
   // data. Used for debugging purposes.
   void GetMozDebugReaderData(nsAString& aString);
 
   // Returns a promise which will be resolved after collecting debugging
   // data from decoder/reader/MDSM. Used for debugging purposes.
   already_AddRefed<Promise> MozRequestDebugInfo(ErrorResult& aRv);
--- a/dom/media/test/AutoplayTestUtils.js
+++ b/dom/media/test/AutoplayTestUtils.js
@@ -1,22 +1,22 @@
 function playAndPostResult(muted, parent_window) {
   let element = document.createElement("video");
   element.preload = "auto";
   element.muted = muted;
   element.src = "short.mp4";
   element.id = "video";
   document.body.appendChild(element);
-
+  let allowedToPlay = element.allowedToPlay;
   element.play().then(
       () => {
-        parent_window.postMessage({played: true}, "*");
+        parent_window.postMessage({played: true, allowedToPlay}, "*");
       },
       () => {
-        parent_window.postMessage({played: false}, "*");
+        parent_window.postMessage({played: false, allowedToPlay}, "*");
       }
     );
 }
 
 function nextWindowMessage() {
   return nextEvent(window, "message");
 }
 
--- a/dom/media/test/test_autoplay_policy_activation.html
+++ b/dom/media/test/test_autoplay_policy_activation.html
@@ -138,17 +138,18 @@
         async function runTest() {
           for (test_case of test_cases) {
             // Run each test in a new window, to ensure its user gesture
             // activation state isn't tainted by preceeding tests.
             let child = window.open(child_url, "", "width=500,height=500");
             await once(child, "load");
             child.postMessage(test_case, window.origin);
             let result = await nextWindowMessage();
-            SimpleTest.is(result.data.played, test_case.should_play, test_case.name);
+            SimpleTest.is(result.data.allowedToPlay, test_case.should_play, "allowed - " + test_case.name);
+            SimpleTest.is(result.data.played, test_case.should_play, "played - " + test_case.name);
             child.close();
           }
           SimpleTest.finish();
         }
 
         SimpleTest.waitForExplicitFinish();
 
       </script>
--- a/dom/media/test/test_autoplay_policy_permission.html
+++ b/dom/media/test/test_autoplay_policy_permission.html
@@ -29,17 +29,18 @@
           // Run test in a new window, to ensure its user gesture
           // activation state isn't tainted by preceeding tests.
           let url = testCase.origin + "/tests/dom/media/test/file_autoplay_policy_activation_frame.html";
           let child = window.open(url, "", "width=500,height=500");
           is((await nextWindowMessage()).data, "ready", "Expected a 'ready' message");
           child.postMessage("play-audible", testCase.origin);
           // Wait for the window to tell us whether it could play video.
           let result = await nextWindowMessage();
-          is(result.data.played, testCase.shouldPlay, testCase.message);
+          is(result.data.allowedToPlay, testCase.shouldPlay, "allowedToPlay - " + testCase.message);
+          is(result.data.played, testCase.shouldPlay, "played - " + testCase.message);
           child.close();
         }
 
         async function runTest() {
           // First verify that we can't play in a document unwhitelisted.
           is(window.origin, "http://mochi.test:8888", "Origin should be as we assume, otherwise the rest of the test is bogus!");
 
           await testPlayInOrigin({
--- a/dom/webidl/HTMLMediaElement.webidl
+++ b/dom/webidl/HTMLMediaElement.webidl
@@ -214,8 +214,18 @@ partial interface HTMLMediaElement {
  */
 partial interface HTMLMediaElement {
   [Pref="media.test.video-suspend"]
   void setVisible(boolean aVisible);
 
   [Pref="media.test.video-suspend"]
   boolean hasSuspendTaint();
 };
+
+/*
+ * API that exposes whether a call to HTMLMediaElement.play() would be
+ * blocked by autoplay policies; whether the promise returned by play()
+ * would be rejected with NotAllowedError.
+ */
+partial interface HTMLMediaElement {
+  [Pref="media.allowed-to-play.enabled"]
+  readonly attribute boolean allowedToPlay;
+};
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -584,16 +584,24 @@ pref("media.autoplay.allow-muted", true)
 // If "media.autoplay.default" is not ALLOWED, and this pref is true,
 // then audible media would only be allowed to autoplay after website has
 // been activated by specific user gestures, but non-audible
 // media won't be restricted.
 #ifdef NIGHTLY_BUILD
 pref("media.autoplay.enabled.user-gestures-needed", false);
 #endif
 
+// HTMLMediaElement.allowedToPlay should be exposed to web content when
+// block autoplay rides the trains to release. Until then, Nightly only.
+#ifdef NIGHTLY_BUILD
+pref("media.allowed-to-play.enabled", true);
+#else
+pref("media.allowed-to-play.enabled", false);
+#endif
+
 // The default number of decoded video frames that are enqueued in
 // MediaDecoderReader's mVideoQueue.
 pref("media.video-queue.default-size", 10);
 
 // The maximum number of queued frames to send to the compositor.
 // By default, send all of them.
 pref("media.video-queue.send-to-compositor-size", 9999);
 
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -41,8 +41,9 @@ user_pref("security.turn_off_all_securit
 user_pref("xpinstall.signatures.required", false);
 // Prevent Remote Settings to issue non local connections.
 user_pref("services.settings.server", "http://localhost/remote-settings-dummy/v1");
 // Ensure autoplay is enabled for all platforms.
 user_pref("media.autoplay.default", 0); // 0=Allowed, 1=Blocked, 2=Prompt
 user_pref("media.autoplay.enabled.user-gestures-needed", true);
 user_pref("media.autoplay.ask-permission", false);
 user_pref("media.autoplay.block-webaudio", false);
+user_pref("media.allowed-to-play.enabled", true);