Bug 1489278 - part2 : add test. r=padenot
authoralwu <alwu@mozilla.com>
Thu, 13 Sep 2018 11:39:14 +0000
changeset 436374 2477110ccfba50833934d4641ccd91a2e88d64a1
parent 436373 4cf5ee8d6112bce30a01cce856edf60ed09a3ccc
child 436375 650774fee0c12bafae60de9dca92c59990e1ee93
push id34643
push userbtara@mozilla.com
push dateFri, 14 Sep 2018 21:48:55 +0000
treeherdermozilla-central@750e71a8f79b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspadenot
bugs1489278
milestone64.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 1489278 - part2 : add test. r=padenot Differential Revision: https://phabricator.services.mozilla.com/D5611
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioContext.h
toolkit/content/tests/browser/browser.ini
toolkit/content/tests/browser/browser_autoplay_policy_web_audio.js
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -9,16 +9,17 @@
 #include "blink/PeriodicWave.h"
 
 #include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs.h"
 
 #include "mozilla/dom/AnalyserNode.h"
 #include "mozilla/dom/AnalyserNodeBinding.h"
 #include "mozilla/dom/AudioBufferSourceNodeBinding.h"
 #include "mozilla/dom/AudioContextBinding.h"
 #include "mozilla/dom/BaseAudioContextBinding.h"
 #include "mozilla/dom/BiquadFilterNodeBinding.h"
 #include "mozilla/dom/ChannelMergerNodeBinding.h"
@@ -205,16 +206,17 @@ AudioContext::EnsureAutoplayRequested()
                           self.get(),
                           request.get());
               self->ResumeInternal();
            },
            [self, request](nsresult aError) {
               AUTOPLAY_LOG("%p Autoplay request denied request=%p",
                           self.get(),
                           request.get());
+              self->DispatchBlockedEvent();
            });
 }
 
 nsresult
 AudioContext::Init()
 {
   if (!mIsOffline) {
     nsresult rv = mDestination->CreateAudioChannelAgent();
@@ -1074,17 +1076,20 @@ AudioContext::Resume(ErrorResult& aRv)
     return promise.forget();
   }
 
   mPendingResumePromises.AppendElement(promise);
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   if (isAllowedToPlay) {
     ResumeInternal();
+  } else {
+    DispatchBlockedEvent();
   }
+
   AUTOPLAY_LOG("Resume AudioContext %p, IsAllowedToPlay=%d",
     this, isAllowedToPlay);
   return promise.forget();
 }
 
 void
 AudioContext::ResumeInternal()
 {
@@ -1100,16 +1105,48 @@ AudioContext::ResumeInternal()
   }
   Graph()->ApplyAudioContextOperation(DestinationStream()->AsAudioNodeStream(),
                                       streams,
                                       AudioContextOperation::Resume,
                                       nullptr);
   mSuspendCalled = false;
 }
 
+void
+AudioContext::DispatchBlockedEvent()
+{
+  if (!StaticPrefs::MediaBlockEventEnabled()) {
+    return;
+  }
+
+  RefPtr<AudioContext> self = this;
+  RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+    "AudioContext::AutoplayBlocked",
+    [self] () {
+      nsPIDOMWindowInner* parent = self->GetParentObject();
+      if (!parent) {
+        return;
+      }
+
+      nsIDocument* doc = parent->GetExtantDoc();
+      if (!doc) {
+        return;
+      }
+
+      AUTOPLAY_LOG("Dispatch `blocked` event for AudioContext %p", self.get());
+      nsContentUtils::DispatchTrustedEvent(
+        doc,
+        static_cast<DOMEventTargetHelper*>(self),
+        NS_LITERAL_STRING("blocked"),
+        CanBubble::eNo,
+        Cancelable::eNo);
+  });
+  Dispatch(r.forget());
+}
+
 already_AddRefed<Promise>
 AudioContext::Close(ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject());
   RefPtr<Promise> promise;
   promise = Promise::Create(parentObject, aRv);
   if (aRv.Failed()) {
     return nullptr;
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -342,16 +342,19 @@ private:
   nsTArray<MediaStream*> GetAllStreams() const;
 
   // Request the prompt to ask for user's approval for autoplay.
   void EnsureAutoplayRequested();
 
   void ResumeInternal();
   void SuspendInternal(void* aPromise);
 
+  // This event is used for testing only.
+  void DispatchBlockedEvent();
+
 private:
   // Each AudioContext has an id, that is passed down the MediaStreams that
   // back the AudioNodes, so we can easily compute the set of all the
   // MediaStreams for a given context, on the MediasStreamGraph side.
   const AudioContextId mId;
   // Note that it's important for mSampleRate to be initialized before
   // mDestination, as mDestination's constructor needs to access it!
   const float mSampleRate;
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -46,16 +46,19 @@ support-files =
 [browser_autoplay_policy_request_permission.js]
 support-files =
   file_empty.html
   gizmo.mp4
 [browser_autoplay_policy_user_gestures.js]
 support-files =
   gizmo.mp4
   file_video.html
+[browser_autoplay_policy_web_audio.js]
+support-files =
+  file_empty.html
 [browser_autoplay_videoDocument.js]
 [browser_autoscroll_disabled.js]
 skip-if = true # Bug 1312652
 [browser_block_autoplay_media.js]
 tags = audiochannel
 [browser_block_autoplay_media_pausedAfterPlay.js]
 tags = audiochannel
 [browser_block_autoplay_playAfterTabVisible.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_autoplay_policy_web_audio.js
@@ -0,0 +1,201 @@
+/**
+ * This test is used for testing whether WebAudio can be started correctly in
+ * different scenarios, such as
+ * 1) site has existing 'autoplay-media' permission for allowing autoplay
+ * 2) site has existing 'autoplay-media' permission for blocking autoplay
+ * 3) site doesn't have permission, user clicks 'allow' button on the doorhanger
+ * 4) site doesn't have permission, user clicks 'deny' button on the doorhanger
+ * 5) site doesn't have permission, user ignores the doorhanger
+ */
+"use strict";
+
+ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_empty.html";
+
+function setup_test_preference() {
+  return SpecialPowers.pushPrefEnv({"set": [
+    ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.PROMPT],
+    ["media.autoplay.enabled.user-gestures-needed", true],
+    ["media.autoplay.ask-permission", true],
+    ["media.autoplay.block-webaudio", true],
+    ["media.autoplay.block-event.enabled", true],
+  ]});
+}
+
+function createAudioContext() {
+  content.ac = new content.AudioContext();
+  const ac = content.ac;
+
+  ac.allowedToStart = new Promise(resolve => {
+    ac.addEventListener("statechange", function() {
+      if (ac.state === "running") {
+        resolve();
+      }
+    }, {once: true});
+  });
+
+  ac.notAllowedToStart = new Promise(resolve => {
+    ac.addEventListener("blocked", function() {
+      resolve();
+    }, {once: true});
+  });
+}
+
+async function checkIfAudioContextIsAllowedToStart(isAllowedToStart) {
+  const ac = content.ac;
+  if (isAllowedToStart) {
+    await ac.allowedToStart;
+    ok(true, `AudioContext is running.`);
+  } else {
+    await ac.notAllowedToStart;
+    ok(true, `AudioContext is not started yet.`);
+  }
+}
+
+async function resumeAudioContext(isAllowedToStart) {
+  const ac = content.ac;
+  const resumePromise = ac.resume();
+  const blockedPromise = new Promise(resolve => {
+    ac.addEventListener("blocked", function() {
+      resolve();
+    }, {once: true});
+  });
+
+  if (isAllowedToStart) {
+    await resumePromise;
+    ok(ac.state === "running", `AudioContext is running.`);
+  } else {
+    await blockedPromise;
+    ok(ac.state === "suspended", `AudioContext is suspended.`);
+  }
+}
+
+function checkAudioContextState(state) {
+  ok(content.ac.state === state,
+     `AudioContext state is ${content.ac.state}, expected state is ${state}`);
+}
+
+function connectAudibleNodeToContext() {
+  info(`- connect audible node to context graph -`);
+  const ac = content.ac;
+  const dest = ac.destination;
+  const osc = ac.createOscillator();
+  osc.connect(dest);
+  osc.start();
+}
+
+async function testAutoplayExistingPermission(args) {
+  info(`- starting \"${args.name}\" -`);
+  const tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, PAGE);
+  const browser = tab.linkedBrowser;
+
+  info(`- set the permission -`);
+  const promptShow = () =>
+    PopupNotifications.getNotification("autoplay-media", browser);
+  SitePermissions.set(browser.currentURI, "autoplay-media", args.permission);
+  ok(!promptShow(), `should not be showing permission prompt yet`);
+
+  info(`- create audio context -`);
+  // We want the same audio context to be used across different content
+  // tasks, so it needs to be loaded by a frame script.
+  const mm = tab.linkedBrowser.messageManager;
+  mm.loadFrameScript("data:,(" + createAudioContext.toString() + ")();", false);
+
+  info(`- check AudioContext status -`);
+  const isAllowedToStart = args.permission === SitePermissions.ALLOW;
+  await ContentTask.spawn(browser, isAllowedToStart,
+                          checkIfAudioContextIsAllowedToStart);
+  await ContentTask.spawn(browser, isAllowedToStart,
+                          resumeAudioContext);
+
+  info(`- remove tab -`);
+  SitePermissions.remove(browser.currentURI, "autoplay-media");
+  await BrowserTestUtils.removeTab(tab);
+}
+
+async function testAutoplayUnknownPermission(args) {
+  info(`- starting \"${args.name}\" -`);
+  const tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, PAGE);
+  const browser = tab.linkedBrowser;
+
+  info(`- set the 'autoplay-media' permission -`);
+  const promptShow = () =>
+    PopupNotifications.getNotification("autoplay-media", browser);
+  SitePermissions.set(browser.currentURI, "autoplay-media", SitePermissions.UNKNOWN);
+  ok(!promptShow(), `should not be showing permission prompt yet`);
+
+  info(`- create audio context -`);
+  const popupShow = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+  // We want the same audio context to be used across different content
+  // tasks, so it needs to be loaded by a frame script.
+  const mm = tab.linkedBrowser.messageManager;
+  mm.loadFrameScript("data:,(" + createAudioContext.toString() + ")();", false);
+  await popupShow;
+  ok(promptShow(), `should now be showing permission prompt`);
+
+  info(`- AudioContext should not be started before user responds to doorhanger -`);
+  await ContentTask.spawn(browser, "suspended",
+                          checkAudioContextState);
+
+  if (args.ignoreDoorhanger) {
+    const popupHide = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+    await ContentTask.spawn(browser, null, () => {
+      info(`- user ingores the doorhanger and interacts with page directly -`);
+      content.document.notifyUserGestureActivation();
+    });
+
+    await ContentTask.spawn(browser, true,
+                            resumeAudioContext);
+    ok(promptShow(), `doorhanger would only be dismissed when audible media starts`);
+    await ContentTask.spawn(browser, null,
+                            connectAudibleNodeToContext);
+    await popupHide;
+    ok(true, `doorhanger should dismiss after AudioContext starts audible`);
+  } else {
+    info(`- simulate clicking button on doorhanger-`);
+    if (args.button == "allow") {
+      PopupNotifications.panel.firstElementChild.button.click();
+    } else if (args.button == "block") {
+      PopupNotifications.panel.firstChild.secondaryButton.click();
+    } else {
+      ok(false, `Invalid button field`);
+    }
+
+    info(`- check AudioContext status -`);
+    const isAllowedToStart = args.button === "allow";
+    await ContentTask.spawn(browser, isAllowedToStart,
+                            checkIfAudioContextIsAllowedToStart);
+    await ContentTask.spawn(browser, isAllowedToStart,
+                            resumeAudioContext);
+  }
+
+  info(`- remove tab -`);
+  SitePermissions.remove(browser.currentURI, "autoplay-media");
+  await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_test() {
+  info("- setup test preference -");
+  await setup_test_preference();
+
+  await testAutoplayExistingPermission({
+    name: "Prexisting allow permission",
+    permission: SitePermissions.ALLOW,
+  });
+  await testAutoplayExistingPermission({
+    name: "Prexisting block permission",
+    permission: SitePermissions.BLOCK,
+  });
+  await testAutoplayUnknownPermission({
+    name: "Unknown permission and click allow button on doorhanger",
+    button: "allow",
+  });
+  await testAutoplayUnknownPermission({
+    name: "Unknown permission and click block button on doorhanger",
+    button: "block",
+  });
+  await testAutoplayUnknownPermission({
+    name: "Unknown permission and ignore doorhanger",
+    ignoreDoorhanger: true,
+  });
+});