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 275869 44fff46837cc15843069c21f01c79389ca21ad53
parent 275868 a2c04ccd34c039f7091965991da6c970d4fc3fda
child 275870 1c0f1468c3d9d31b87a10496ed86be1d42f53be7
push id3246
push usergijskruitbosch@gmail.com
push dateTue, 07 Jul 2015 09:06:38 +0000
reviewersbaku
bugs1180539, 1180535
milestone42.0a1
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
new file mode 100644
--- /dev/null
+++ b/dom/base/test/file_webaudioLoop.html
@@ -0,0 +1,34 @@
+<!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);
+  parent.runTest();
+});
+
+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,18 @@ AudioContext::Close(ErrorResult& aRv)
 
   if (mAudioContextState == AudioContextState::Closed) {
     promise->MaybeResolve(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   mCloseCalled = true;
 
+  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;