Bug 1471485 - Hide autoplay permission doorhanger if user plays video. r?johannh draft
authorChris Pearce <cpearce@mozilla.com>
Tue, 03 Jul 2018 11:17:16 +1200
changeset 817319 1cd231e00abe900351eb52be88ce87c592188625
parent 817316 73121beb1e4381efd64a6208e80ff1641e36fde2
child 817320 5cfd44ff3abca6c1af9090072e49b49d9170dec3
push id116013
push userbmo:cpearce@mozilla.com
push dateThu, 12 Jul 2018 10:39:56 +0000
reviewersjohannh
bugs1471485
milestone63.0a1
Bug 1471485 - Hide autoplay permission doorhanger if user plays video. r?johannh If we're showing a permission UI prompt for "autoplay-media", the user can still actually play media without interacting with the doorhanger; if they click a "play" button in the document, they'll "gesture activate" the document and unblock autoplay and be able to start playback. It doesn't make sense to keep showing the permission doorhanger to approve autoplay when the page is already playing, as playback has already started, and if they clicked on "block" then the site would receive a promise reject on the promise returned on the first call to HTMLMediaElement.play() for which we were showing the permission prompt for, even though the media is actually playing. This will likely confuse JS video players. So we should hide the permission prompt when playback in the page starts. MozReview-Commit-ID: 1XU47AfT6vf
browser/modules/PermissionUI.jsm
toolkit/content/tests/browser/browser_autoplay_policy_request_permission.js
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -241,16 +241,22 @@ var PermissionPromptPrototype = {
    * If the prompt will be shown to the user, this callback will
    * be called just before. Subclasses may want to override this
    * in order to, for example, bump a counter Telemetry probe for
    * how often a particular permission request is seen.
    */
   onBeforeShow() {},
 
   /**
+   * If the prompt was be shown to the user, this callback will
+   * be called just after its been hidden.
+   */
+  onAfterShow() {},
+
+  /**
    * Will determine if a prompt should be shown to the user, and if so,
    * will show it.
    *
    * If a permissionKey is defined prompt() might automatically
    * allow or cancel itself based on the user's current
    * permission settings without displaying the prompt.
    *
    * If the permission is not already set and the <xul:browser> that the request
@@ -353,20 +359,29 @@ var PermissionPromptPrototype = {
     let options = this.popupOptions;
 
     if (!options.hasOwnProperty("displayURI") || options.displayURI) {
       options.displayURI = this.principal.URI;
     }
     // Permission prompts are always persistent; the close button is controlled by a pref.
     options.persistent = true;
     options.hideClose = !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton");
-    // When the docshell of the browser is aboout to be swapped to another one,
-    // the "swapping" event is called. Returning true causes the notification
-    // to be moved to the new browser.
-    options.eventCallback = topic => topic == "swapping";
+    options.eventCallback = (topic) => {
+      // When the docshell of the browser is aboout to be swapped to another one,
+      // the "swapping" event is called. Returning true causes the notification
+      // to be moved to the new browser.
+      if (topic == "swapping") {
+        return true;
+      }
+      // The prompt has been removed, notify the PermissionUI.
+      if (topic == "removed") {
+        this.onAfterShow();
+      }
+      return false;
+    };
 
     this.onBeforeShow();
     chromeWin.PopupNotifications.show(this.browser,
                                       this.notificationID,
                                       this.message,
                                       this.anchorID,
                                       mainAction,
                                       secondaryActions,
@@ -812,13 +827,33 @@ AutoplayPermissionPrompt.prototype = {
       },
       {
         label: gBrowserBundle.GetStringFromName("autoplay.DontAllow.label"),
         accessKey: gBrowserBundle.GetStringFromName("autoplay.DontAllow.accesskey"),
         action: Ci.nsIPermissionManager.DENY_ACTION,
     }];
   },
 
+  onAfterShow() {
+    // Remove the event listener to prevent any leaks.
+    this.browser.removeEventListener(
+      "DOMAudioPlaybackStarted", this.handlePlaybackStart);
+  },
+
   onBeforeShow() {
+    // Hide the prompt if the tab starts playing media.
+    this.handlePlaybackStart = () => {
+      let chromeWin = this.browser.ownerGlobal;
+      if (!chromeWin.PopupNotifications) {
+        return;
+      }
+      let notification = chromeWin.PopupNotifications.getNotification(
+        this.notificationID, this.browser);
+      if (notification) {
+        chromeWin.PopupNotifications.remove(notification);
+      }
+    };
+    this.browser.addEventListener(
+      "DOMAudioPlaybackStarted", this.handlePlaybackStart);
   },
 };
 
 PermissionUI.AutoplayPermissionPrompt = AutoplayPermissionPrompt;
--- a/toolkit/content/tests/browser/browser_autoplay_policy_request_permission.js
+++ b/toolkit/content/tests/browser/browser_autoplay_policy_request_permission.js
@@ -196,8 +196,67 @@ add_task(async () => {
   });
   await testAutoplayUnknownPermission({
     name: "Unknown permission click block call play",
     button: "block",
     shouldPlay: false,
     mode: "call play",
   });
 });
+
+// Test that if playback starts while the permission prompt is shown,
+// that the prompt is hidden.
+add_task(async () => {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: VIDEO_PAGE,
+  }, async (browser) => {
+    info("- Started test prompt hides upon play -");
+    let promptShowing = () =>
+      PopupNotifications.getNotification("autoplay-media", browser);
+
+    // Set this site to ask permission to autoplay.
+    SitePermissions.set(browser.currentURI, "autoplay-media", SitePermissions.UNKNOWN);
+    ok(!promptShowing(), "Should not be showing permission prompt");
+
+    let popupshown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+    await loadAutoplayVideo(browser, { mode: "call play" });
+
+    info("Awaiting popupshown");
+    await popupshown;
+    ok(promptShowing(), "Should now be showing permission prompt");
+
+    // Check that the video didn't start playing.
+    await ContentTask.spawn(browser, null,
+      async () => {
+        let video = content.document.getElementById("v1");
+        ok(video.paused && !video.didPlay, "Video should not be playing");
+      });
+
+    let popuphidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+
+    await ContentTask.spawn(browser, null,
+      async () => {
+        // Gesture activate the document, i.e. simulate a click in the document,
+        // to unblock autoplay,
+        content.document.notifyUserGestureActivation();
+        let video = content.document.getElementById("v1");
+        // Gesture activating in itself should not cause the previous pending
+        // play to proceed.
+        ok(video.paused && !video.didPlay, "Video should not have played yet");
+        // But trying to play again now that we're gesture activated will work...
+        let played = await video.play().then(() => true, () => false);
+        ok(played, "Should have played as now gesture activated");
+        // And because we started playing, the previous promise returned in the
+        // first call to play() above should also resolve too.
+        await video.didPlayPromise;
+        ok(video.didPlay, "Existing promise should resolve when media starts playing");
+      });
+
+    info("Awaiting popuphidden");
+    await popuphidden;
+    ok(!promptShowing(), "Permission prompt should have hidden when media started playing");
+
+    // Reset permission.
+    SitePermissions.remove(browser.currentURI, "autoplay-media");
+    info("- Finished test prompt hides upon play -");
+  });
+});