Bug 1491475 - part1 : postpone starting AudioContext until calling AudioContext.resume() or AudioScheduledSourceNode.start(). r=padenot
authoralwu <alwu@mozilla.com>
Tue, 20 Nov 2018 22:58:22 +0000
changeset 447505 3d997ec4174de4296a6bd30641d42309999703b0
parent 447504 929c91b8afc67ab6b89df4f242dcfbb5fbf21019
child 447506 3b5ff99abb5ee601d7798e194d587dc56758139e
push id35081
push userccoroiu@mozilla.com
push dateWed, 21 Nov 2018 21:41:51 +0000
treeherdermozilla-central@e52cc9776809 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspadenot
bugs1491475
milestone65.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 1491475 - part1 : postpone starting AudioContext until calling AudioContext.resume() or AudioScheduledSourceNode.start(). r=padenot If AudioContext is not allowed to start, we would postpone its state transition from `suspended` to `running` until site explicitly calls AudioContext.resume() or AudioScheduledSourceNode.start(). Differential Revision: https://phabricator.services.mozilla.com/D12082
dom/media/webaudio/AudioBufferSourceNode.cpp
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioContext.h
dom/media/webaudio/ConstantSourceNode.cpp
dom/media/webaudio/OscillatorNode.cpp
--- a/dom/media/webaudio/AudioBufferSourceNode.cpp
+++ b/dom/media/webaudio/AudioBufferSourceNode.cpp
@@ -699,16 +699,18 @@ void AudioBufferSourceNode::Start(double
   if (mBuffer) {
     SendOffsetAndDurationParametersToStream(ns);
   }
 
   // Don't set parameter unnecessarily
   if (aWhen > 0.0) {
     ns->SetDoubleParameter(START, aWhen);
   }
+
+  Context()->NotifyScheduledSourceNodeStarted();
 }
 
 void AudioBufferSourceNode::Start(double aWhen, ErrorResult& aRv) {
   Start(aWhen, 0 /* offset */, Optional<double>(), aRv);
 }
 
 void AudioBufferSourceNode::SendBufferParameterToStream(JSContext* aCx) {
   AudioNodeStream* ns = mStream;
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -59,16 +59,17 @@
 #include "DynamicsCompressorNode.h"
 #include "GainNode.h"
 #include "IIRFilterNode.h"
 #include "MediaElementAudioSourceNode.h"
 #include "MediaStreamAudioDestinationNode.h"
 #include "MediaStreamAudioSourceNode.h"
 #include "MediaStreamGraph.h"
 #include "nsContentUtils.h"
+#include "nsIScriptError.h"
 #include "nsNetCID.h"
 #include "nsNetUtil.h"
 #include "nsPIDOMWindow.h"
 #include "nsPrintfCString.h"
 #include "nsRFPService.h"
 #include "OscillatorNode.h"
 #include "PannerNode.h"
 #include "PeriodicWave.h"
@@ -146,63 +147,96 @@ AudioContext::AudioContext(nsPIDOMWindow
       mSampleRate(GetSampleRateForAudioContext(aIsOffline, aSampleRate)),
       mAudioContextState(AudioContextState::Suspended),
       mNumberOfChannels(aNumberOfChannels),
       mIsOffline(aIsOffline),
       mIsStarted(!aIsOffline),
       mIsShutDown(false),
       mCloseCalled(false),
       mSuspendCalled(false),
-      mIsDisconnecting(false) {
+      mIsDisconnecting(false),
+      mWasAllowedToStart(true) {
   bool mute = aWindow->AddAudioContext(this);
 
   // Note: AudioDestinationNode needs an AudioContext that must already be
   // bound to the window.
-  bool allowedToStart = AutoplayPolicy::IsAllowedToPlay(*this);
+  const bool allowedToStart = AutoplayPolicy::IsAllowedToPlay(*this);
   mDestination = new AudioDestinationNode(this, aIsOffline, allowedToStart,
                                           aNumberOfChannels, aLength);
 
   // The context can't be muted until it has a destination.
   if (mute) {
     Mute();
   }
 
+  // If an AudioContext is not allowed to start, we would postpone its state
+  // transition from `suspended` to `running` until sites explicitly call
+  // AudioContext.resume() or AudioScheduledSourceNode.start().
   if (!allowedToStart) {
-    // Not allowed to start, delay the transition from `suspended` to `running`.
+    AUTOPLAY_LOG("AudioContext %p is not allowed to start", this);
+    mWasAllowedToStart = false;
     SuspendInternal(nullptr);
-    EnsureAutoplayRequested();
+    DispatchBlockedEvent();
   }
 
   FFTBlock::MainThreadInit();
 }
 
+void AudioContext::NotifyScheduledSourceNodeStarted() {
+  MOZ_ASSERT(NS_IsMainThread());
+  // Only try to start AudioContext when AudioContext was not allowed to start.
+  if (mWasAllowedToStart) {
+    return;
+  }
+
+  const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
+  AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d",
+               this, isAllowedToPlay);
+  if (isAllowedToPlay) {
+    ResumeInternal();
+  } else {
+    EnsureAutoplayRequested();
+  }
+}
+
 void AudioContext::EnsureAutoplayRequested() {
   nsPIDOMWindowInner* parent = GetParentObject();
   if (!parent || !parent->AsGlobal()) {
     return;
   }
 
   RefPtr<AutoplayPermissionManager> request =
       AutoplayPolicy::RequestFor(*(parent->GetExtantDoc()));
   if (!request) {
     return;
   }
 
+  AUTOPLAY_LOG("AudioContext %p EnsureAutoplayRequested %p",
+               this, request.get());
   RefPtr<AudioContext> self = this;
   request->RequestWithPrompt()->Then(
       parent->AsGlobal()->AbstractMainThreadFor(TaskCategory::Other), __func__,
       [self, request](bool aApproved) {
         AUTOPLAY_LOG("%p Autoplay request approved request=%p", self.get(),
                      request.get());
+        self->mWasAllowedToStart = true;
         self->ResumeInternal();
       },
       [self, request](nsresult aError) {
         AUTOPLAY_LOG("%p Autoplay request denied request=%p", self.get(),
                      request.get());
+        self->mWasAllowedToStart = false;
         self->DispatchBlockedEvent();
+        nsIDocument* doc = self->GetParentObject() ?
+            self->GetParentObject()->GetExtantDoc() : nullptr;
+        nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                        NS_LITERAL_CSTRING("Media"),
+                                        doc,
+                                        nsContentUtils::eDOM_PROPERTIES,
+                                        "BlockAutoplayError");
       });
 }
 
 nsresult AudioContext::Init() {
   if (!mIsOffline) {
     nsresult rv = mDestination->CreateAudioChannelAgent();
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
@@ -938,28 +972,30 @@ already_AddRefed<Promise> AudioContext::
   if (mAudioContextState == AudioContextState::Closed || mCloseCalled) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   mPendingResumePromises.AppendElement(promise);
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
+  AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d",
+               this, isAllowedToPlay);
   if (isAllowedToPlay) {
+    mWasAllowedToStart = true;
     ResumeInternal();
-  } else {
-    DispatchBlockedEvent();
+  } else if (!isAllowedToPlay && !mWasAllowedToStart) {
+    EnsureAutoplayRequested();
   }
 
-  AUTOPLAY_LOG("Resume AudioContext %p, IsAllowedToPlay=%d", this,
-               isAllowedToPlay);
   return promise.forget();
 }
 
 void AudioContext::ResumeInternal() {
+  AUTOPLAY_LOG("Allow to resume AudioContext %p", this);
   Destination()->Resume();
 
   nsTArray<MediaStream*> streams;
   // If mSuspendCalled is false then we already resumed all our streams,
   // so don't resume them again (since suspend(); resume(); resume(); should
   // be OK). But we still need to do ApplyAudioContextOperation
   // to ensure our new promise is resolved.
   if (mSuspendCalled) {
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -178,16 +178,20 @@ class AudioContext final : public DOMEve
   AudioListener* Listener();
 
   AudioContextState State() const { return mAudioContextState; }
 
   Worklet* GetAudioWorklet(ErrorResult& aRv);
 
   bool IsRunning() const;
 
+  // Called when an AudioScheduledSourceNode started, this method might resume
+  // the AudioContext if it was not allowed to start.
+  void NotifyScheduledSourceNodeStarted();
+
   // Those three methods return a promise to content, that is resolved when an
   // (possibly long) operation is completed on the MSG (and possibly other)
   // thread(s). To avoid having to match the calls and asychronous result when
   // the operation is completed, we keep a reference to the promises on the main
   // thread, and then send the promises pointers down the MSG thread, as a void*
   // (to make it very clear that the pointer is to merely be treated as an ID).
   // When back on the main thread, we can resolve or reject the promise, by
   // casting it back to a `Promise*` while asserting we're back on the main
@@ -357,16 +361,18 @@ class AudioContext final : public DOMEve
   bool mIsOffline;
   bool mIsStarted;
   bool mIsShutDown;
   // Close has been called, reject suspend and resume call.
   bool mCloseCalled;
   // Suspend has been called with no following resume.
   bool mSuspendCalled;
   bool mIsDisconnecting;
+  // This flag stores the value of previous status of `allowed-to-start`.
+  bool mWasAllowedToStart;
 };
 
 static const dom::AudioContext::AudioContextId NO_AUDIO_CONTEXT = 0;
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
--- a/dom/media/webaudio/ConstantSourceNode.cpp
+++ b/dom/media/webaudio/ConstantSourceNode.cpp
@@ -219,16 +219,17 @@ void ConstantSourceNode::Start(double aW
   if (!mStream) {
     return;
   }
 
   mStream->SetStreamTimeParameter(ConstantSourceNodeEngine::START, Context(),
                                   aWhen);
 
   MarkActive();
+  Context()->NotifyScheduledSourceNodeStarted();
 }
 
 void ConstantSourceNode::Stop(double aWhen, ErrorResult& aRv) {
   if (!WebAudioUtils::IsTimeValid(aWhen)) {
     aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>(NS_LITERAL_STRING("stop time"));
     return;
   }
 
--- a/dom/media/webaudio/OscillatorNode.cpp
+++ b/dom/media/webaudio/OscillatorNode.cpp
@@ -483,16 +483,17 @@ void OscillatorNode::Start(double aWhen,
     return;
   }
 
   // TODO: Perhaps we need to do more here.
   mStream->SetStreamTimeParameter(OscillatorNodeEngine::START, Context(),
                                   aWhen);
 
   MarkActive();
+  Context()->NotifyScheduledSourceNodeStarted();
 }
 
 void OscillatorNode::Stop(double aWhen, ErrorResult& aRv) {
   if (!WebAudioUtils::IsTimeValid(aWhen)) {
     aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return;
   }