Bug 1180539 - Correctly dispatch media-playback notifications when an AudioContext is closed/suspended/resumed; r=baku
authorEhsan Akhgari <ehsan@mozilla.com>
Sun, 05 Jul 2015 19:44:28 -0400
changeset 284932 feb02fb51745eaa089d91d196cde1b9d6f9926e6
parent 284931 551085502cbf5718a46538b3ff200fa0cbd258cc
child 284933 17f5cab3e712305af92161db0e7c71ccb9fcccbe
push id934
push userraliiev@mozilla.com
push dateMon, 26 Oct 2015 12:58:05 +0000
treeherdermozilla-release@05704e35c1d0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1180539, 1180535
milestone42.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 1180539 - Correctly dispatch media-playback notifications when an AudioContext is closed/suspended/resumed; r=baku Since navigating away from a page with an active AudioContext will close it internally, this patch fixes a similar issue to bug 1180535 for Web Audio too.
dom/base/test/file_webaudioLoop.html
dom/base/test/file_webaudioLoop2.html
dom/base/test/mochitest.ini
dom/base/test/test_webaudioNotification.html
dom/base/test/test_webaudioNotificationStopOnNavigation.html
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioDestinationNode.cpp
dom/media/webaudio/AudioDestinationNode.h
dom/media/webaudio/test/browser_mozAudioChannel.js
new file mode 100644
--- /dev/null
+++ b/dom/base/test/file_webaudioLoop.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script>
+var ac = new AudioContext();
+fetch("audio.ogg").then(response => {
+  return response.arrayBuffer();
+}).then(ab => {
+  return ac.decodeAudioData(ab);
+}).then(ab => {
+  var src = ac.createBufferSource();
+  src.buffer = ab;
+  src.loop = true;
+  src.start();
+  src.connect(ac.destination);
+  setTimeout(() => {
+    if (ac.state == "running") {
+      parent.runTest();
+    } else {
+      setTimeout(arguments.callee, 0);
+    }
+  });
+});
+
+var suspendPromise;
+function suspendAC() {
+  suspendPromise = ac.suspend();
+}
+
+var resumePromise;
+function resumeAC() {
+  suspendPromise.then(() => {
+    resumePromise = ac.resume();
+  });
+}
+
+function closeAC() {
+  resumePromise.then(() => {
+    ac.close();
+  });
+}
+</script>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/file_webaudioLoop2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script>
+var ac = new AudioContext();
+fetch("audio.ogg").then(response => {
+  return response.arrayBuffer();
+}).then(ab => {
+  return ac.decodeAudioData(ab);
+}).then(ab => {
+  var src = ac.createBufferSource();
+  src.buffer = ab;
+  src.loop = true;
+  src.start();
+  src.connect(ac.destination);
+});
+</script>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -232,16 +232,18 @@ support-files =
   variable_style_sheet.sjs
   viewport_helpers.js
   w3element_traversal.svg
   wholeTexty-helper.xml
   file_nonascii_blob_url.html
   referrerHelper.js
   test_performance_user_timing.js
   img_referrer_testserver.sjs
+  file_webaudioLoop.html
+  file_webaudioLoop2.html
 
 [test_anonymousContent_api.html]
 [test_anonymousContent_append_after_reflow.html]
 [test_anonymousContent_insert.html]
 [test_anonymousContent_manipulate_content.html]
 [test_appname_override.html]
 [test_audioWindowUtils.html]
 [test_audioNotification.html]
@@ -300,16 +302,20 @@ skip-if = e10s || buildapp == 'b2g'
 [test_url.html]
 [test_url_data.html]
 [test_url_empty_port.html]
 [test_url_malformedHost.html]
 [test_urlExceptions.html]
 [test_urlSearchParams.html]
 [test_urlSearchParams_utf8.html]
 [test_urlutils_stringify.html]
+[test_webaudioNotification.html]
+skip-if = buildapp == 'mulet'
+[test_webaudioNotificationStopOnNavigation.html]
+skip-if = buildapp == 'mulet'
 [test_window_constructor.html]
 [test_window_cross_origin_props.html]
 [test_window_define_symbol.html]
 [test_window_enumeration.html]
 [test_window_extensible.html]
 [test_window_indexing.html]
 [test_window_named_frame_enumeration.html]
 [test_writable-replaceable.html]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_webaudioNotification.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for audio controller in windows</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+</pre>
+<iframe></iframe>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var expectedNotification = null;
+var iframe = null;
+
+var observer = {
+  observe: function(subject, topic, data) {
+    is(topic, "media-playback", "media-playback received");
+    is(data, expectedNotification, "This is the right notification");
+    SimpleTest.executeSoon(runTest);
+  }
+};
+
+var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+                                   .getService(SpecialPowers.Ci.nsIObserverService);
+
+var tests = [
+  function() {
+    iframe = document.querySelector("iframe");
+    SpecialPowers.pushPrefEnv({"set": [["media.useAudioChannelService", true]]}, runTest);
+  },
+
+  function() {
+    iframe.src = "file_webaudioLoop.html";
+  },
+
+  function() {
+    observerService.addObserver(observer, "media-playback", false);
+    ok(true, "Observer set");
+    runTest();
+  },
+
+  function() {
+    expectedNotification = 'inactive';
+    iframe.contentWindow.suspendAC();
+  },
+
+  function() {
+    expectedNotification = 'active';
+    iframe.contentWindow.resumeAC();
+  },
+
+  function() {
+    expectedNotification = 'inactive';
+    iframe.contentWindow.closeAC();
+  },
+
+  function() {
+    observerService.removeObserver(observer, "media-playback");
+    ok(true, "Observer removed");
+    runTest();
+  }
+];
+
+function runTest() {
+  if (!tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests.shift();
+  test();
+}
+
+onload = runTest;
+
+</script>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_webaudioNotificationStopOnNavigation.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for audio controller in windows</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+</pre>
+<iframe></iframe>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var expectedNotification = null;
+var iframe = null;
+
+var observer = {
+  observe: function(subject, topic, data) {
+    is(topic, "media-playback", "media-playback received");
+    is(data, expectedNotification, "This is the right notification");
+    runTest();
+  }
+};
+
+var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+                                   .getService(SpecialPowers.Ci.nsIObserverService);
+
+var tests = [
+  function() {
+    iframe = document.querySelector("iframe");
+    SpecialPowers.pushPrefEnv({"set": [["media.useAudioChannelService", true]]}, runTest);
+  },
+
+  function() {
+    observerService.addObserver(observer, "media-playback", false);
+    ok(true, "Observer set");
+    runTest();
+  },
+
+  function() {
+    expectedNotification = 'active';
+    iframe.src = "file_webaudioLoop2.html";
+  },
+
+  function() {
+    expectedNotification = 'inactive';
+    iframe.src = "data:text/html,page without audio";
+  },
+
+  function() {
+    observerService.removeObserver(observer, "media-playback");
+    ok(true, "Observer removed");
+    runTest();
+  }
+];
+
+function runTest() {
+  if (!tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests.shift();
+  test();
+}
+
+onload = runTest;
+
+</script>
+</body>
+</html>
+
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -846,16 +846,18 @@ AudioContext::Suspend(ErrorResult& aRv)
     return promise.forget();
   }
 
   if (mAudioContextState == AudioContextState::Suspended) {
     promise->MaybeResolve(JS::UndefinedHandleValue);
     return promise.forget();
   }
 
+  Destination()->DestroyAudioChannelAgent();
+
   MediaStream* ds = DestinationStream();
   if (ds) {
     ds->BlockStreamIfNeeded();
   }
 
   mPromiseGripArray.AppendElement(promise);
   Graph()->ApplyAudioContextOperation(DestinationStream()->AsAudioNodeStream(),
                                       AudioContextOperation::Suspend, promise);
@@ -884,16 +886,18 @@ AudioContext::Resume(ErrorResult& aRv)
     return promise.forget();
   }
 
   if (mAudioContextState == AudioContextState::Running) {
     promise->MaybeResolve(JS::UndefinedHandleValue);
     return promise.forget();
   }
 
+  Destination()->CreateAudioChannelAgent();
+
   MediaStream* ds = DestinationStream();
   if (ds) {
     ds->UnblockStreamIfNeeded();
   }
 
   mPromiseGripArray.AppendElement(promise);
   Graph()->ApplyAudioContextOperation(DestinationStream()->AsAudioNodeStream(),
                                       AudioContextOperation::Resume, promise);
@@ -918,16 +922,20 @@ AudioContext::Close(ErrorResult& aRv)
 
   if (mAudioContextState == AudioContextState::Closed) {
     promise->MaybeResolve(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   mCloseCalled = true;
 
+  if (Destination()) {
+    Destination()->DestroyAudioChannelAgent();
+  }
+
   mPromiseGripArray.AppendElement(promise);
 
   // This can be called when freeing a document, and the streams are dead at
   // this point, so we need extra null-checks.
   MediaStream* ds = DestinationStream();
   if (ds) {
     Graph()->ApplyAudioContextOperation(ds->AsAudioNodeStream(),
                                         AudioContextOperation::Close, promise);
--- a/dom/media/webaudio/AudioDestinationNode.cpp
+++ b/dom/media/webaudio/AudioDestinationNode.cpp
@@ -398,29 +398,35 @@ AudioDestinationNode::SizeOfExcludingThi
 
 size_t
 AudioDestinationNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
 {
   return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
 }
 
 void
-AudioDestinationNode::DestroyMediaStream()
+AudioDestinationNode::DestroyAudioChannelAgent()
 {
   if (mAudioChannelAgent && !Context()->IsOffline()) {
     mAudioChannelAgent->StopPlaying();
     mAudioChannelAgent = nullptr;
 
     nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(GetOwner());
     NS_ENSURE_TRUE_VOID(target);
 
     target->RemoveSystemEventListener(NS_LITERAL_STRING("visibilitychange"),
                                       mEventProxyHelper,
                                       /* useCapture = */ true);
   }
+}
+
+void
+AudioDestinationNode::DestroyMediaStream()
+{
+  DestroyAudioChannelAgent();
 
   if (!mStream)
     return;
 
   mStream->RemoveMainThreadListener(this);
   MediaStreamGraph* graph = mStream->Graph();
   if (graph->IsNonRealtime()) {
     MediaStreamGraph::DestroyNonRealtimeInstance(graph);
--- a/dom/media/webaudio/AudioDestinationNode.h
+++ b/dom/media/webaudio/AudioDestinationNode.h
@@ -73,16 +73,17 @@ public:
   // An amount that should be added to the MediaStream's current time to
   // get the AudioContext.currentTime.
   double ExtraCurrentTime();
 
   // When aIsOnlyNode is true, this is the only node for the AudioContext.
   void SetIsOnlyNodeForContext(bool aIsOnlyNode);
 
   void CreateAudioChannelAgent();
+  void DestroyAudioChannelAgent();
 
   virtual const char* NodeType() const override
   {
     return "AudioDestinationNode";
   }
 
   virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
   virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
--- a/dom/media/webaudio/test/browser_mozAudioChannel.js
+++ b/dom/media/webaudio/test/browser_mozAudioChannel.js
@@ -56,18 +56,20 @@ function test() {
                 tab1.linkedBrowser.contentWindow.removeEventListener('testmozchannel', eventListener);
                 gBrowser.removeTab(tab1);
                 finish();
               }
 
               info("Unloading a tab...");
               whenBrowserUnloaded(tab2.linkedBrowser, function() { info("Tab unloaded."); });
 
-              gBrowser.removeTab(tab2);
-              gBrowser.selectedTab = tab1;
+              executeSoon(function() {
+                gBrowser.removeTab(tab2);
+                gBrowser.selectedTab = tab1;
+              });
             }
 
             let tab2 = gBrowser.addTab(testURL);
             gBrowser.selectedTab = tab2;
 
             info("Loading the tab...");
             whenBrowserLoaded(tab2.linkedBrowser, function() { info("Tab restored."); });
           }