Bug 1413098 - part1 : add policy to decide whether allow audio context to start. r=padenot
authorAlastor Wu <alwu@mozilla.com>
Wed, 13 Dec 2017 14:05:35 -0600
changeset 421276 d2f35c824530616482b8577f392ad9a86936543c
parent 421136 5b15326286d466b5cf4889160cc09b59bdde08fd
child 421277 f99e6152df75ce0443a6d9a912264ae133eeb735
push id34091
push userbtara@mozilla.com
push dateTue, 05 Jun 2018 13:52:34 +0000
treeherdermozilla-central@752465b44c79 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspadenot
bugs1413098
milestone62.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 1413098 - part1 : add policy to decide whether allow audio context to start. r=padenot Audio context would be allowed to start if (1) its document has been activated by user gesture (2) it's a offline audio context, because it won't directly output sound to audio devices In addition, all resume promises would be pending until audio context has been allowed and user calls resume() again. MozReview-Commit-ID: G6RV8dDM6vQ
dom/media/AutoplayPolicy.cpp
dom/media/AutoplayPolicy.h
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioContext.h
dom/media/webaudio/AudioDestinationNode.cpp
dom/media/webaudio/AudioDestinationNode.h
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AutoplayPolicy.h"
 
 #include "mozilla/EventStateManager.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/dom/AudioContext.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "nsContentUtils.h"
 #include "nsIDocument.h"
 #include "MediaManager.h"
 
 namespace mozilla {
 namespace dom {
@@ -63,10 +64,47 @@ AutoplayPolicy::IsMediaElementAllowedToP
   // Activated by user gesture.
   if (aElement->OwnerDoc()->HasBeenUserActivated()) {
     return true;
   }
 
   return false;
 }
 
+/* static */ bool
+AutoplayPolicy::IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext)
+{
+  if (Preferences::GetBool("media.autoplay.enabled")) {
+    return true;
+  }
+
+  if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) {
+    return true;
+  }
+
+  // Offline context won't directly output sound to audio devices.
+  if (aContext->IsOffline()) {
+    return true;
+  }
+
+  nsPIDOMWindowInner* window = aContext->GetOwner();
+  if (!window) {
+    return false;
+  }
+
+   nsCOMPtr<nsIPrincipal> principal = aContext->GetParentObject()->AsGlobal()->PrincipalOrNull();
+
+  // Whitelisted.
+  if (principal &&
+      nsContentUtils::IsExactSitePermAllow(principal, "autoplay-media")) {
+    return true;
+  }
+
+  // Activated by user gesture.
+  if (window->GetExtantDoc()->HasBeenUserActivated()) {
+    return true;
+  }
+
+  return false;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/media/AutoplayPolicy.h
+++ b/dom/media/AutoplayPolicy.h
@@ -10,16 +10,17 @@
 #include "mozilla/NotNull.h"
 
 class nsIDocument;
 
 namespace mozilla {
 namespace dom {
 
 class HTMLMediaElement;
+class AudioContext;
 
 /**
  * AutoplayPolicy is used to manage autoplay logic for all kinds of media,
  * including MediaElement, Web Audio and Web Speech.
  *
  * Autoplay could be disable by turn off the pref "media.autoplay.enabled".
  * Once user disable autoplay, media could only be played if one of following
  * conditions is true.
@@ -27,14 +28,17 @@ class HTMLMediaElement;
  *    We restrict user gestures to "mouse click", "keyboard press" and "touch".
  * 2) Muted media content or video without audio content.
  * 3) Document's origin has the "autoplay-media" permission.
  */
 class AutoplayPolicy
 {
 public:
   static bool IsMediaElementAllowedToPlay(NotNull<HTMLMediaElement*> aElement);
+  static bool IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext);
+private:
+  static bool IsDocumentAllowedToPlay(nsIDocument* aDoc);
 };
 
 } // namespace dom
 } // namespace mozilla
 
-#endif
\ No newline at end of file
+#endif
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AudioContext.h"
 
 #include "blink/PeriodicWave.h"
 
 #include "mozilla/ErrorResult.h"
+#include "mozilla/NotNull.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Preferences.h"
 
 #include "mozilla/dom/AnalyserNode.h"
 #include "mozilla/dom/AnalyserNodeBinding.h"
 #include "mozilla/dom/AudioBufferSourceNodeBinding.h"
 #include "mozilla/dom/AudioContextBinding.h"
@@ -39,16 +40,17 @@
 
 #include "AudioBuffer.h"
 #include "AudioBufferSourceNode.h"
 #include "AudioChannelService.h"
 #include "AudioDestinationNode.h"
 #include "AudioListener.h"
 #include "AudioNodeStream.h"
 #include "AudioStream.h"
+#include "AutoplayPolicy.h"
 #include "BiquadFilterNode.h"
 #include "ChannelMergerNode.h"
 #include "ChannelSplitterNode.h"
 #include "ConstantSourceNode.h"
 #include "ConvolverNode.h"
 #include "DelayNode.h"
 #include "DynamicsCompressorNode.h"
 #include "GainNode.h"
@@ -78,16 +80,17 @@ namespace dom {
 static dom::AudioContext::AudioContextId gAudioContextId = 1;
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(AudioContext)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioContext)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDestination)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseGripArray)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingResumePromises)
   if (!tmp->mIsStarted) {
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveNodes)
   }
   // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed explicitly.
   // mAllNodes is an array of weak pointers, ignore it here.
   // mPannerNodes is an array of weak pointers, ignore it here.
   // mBasicWaveFormCache cannot participate in cycles, ignore it here.
 
@@ -96,16 +99,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Au
   tmp->DisconnectFromWindow();
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioContext,
                                                   DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDestination)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromiseGripArray)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingResumePromises)
   if (!tmp->mIsStarted) {
     MOZ_ASSERT(tmp->mIsOffline,
                "Online AudioContexts should always be started");
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveNodes)
   }
   // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed explicitly.
   // mAllNodes is an array of weak pointers, ignore it here.
   // mPannerNodes is an array of weak pointers, ignore it here.
@@ -144,23 +148,37 @@ AudioContext::AudioContext(nsPIDOMWindow
   , mCloseCalled(false)
   , mSuspendCalled(false)
   , mIsDisconnecting(false)
 {
   bool mute = aWindow->AddAudioContext(this);
 
   // Note: AudioDestinationNode needs an AudioContext that must already be
   // bound to the window.
+  bool allowToStart = AutoplayPolicy::IsAudioContextAllowedToPlay(WrapNotNull(this));
   mDestination = new AudioDestinationNode(this, aIsOffline,
-                                          aNumberOfChannels, aLength, aSampleRate);
+                                          aNumberOfChannels,
+                                          aLength,
+                                          aSampleRate,
+                                          allowToStart);
 
   // The context can't be muted until it has a destination.
   if (mute) {
     Mute();
   }
+
+  // If we won't allow audio context to start, we need to suspend all its stream
+  // in order to delay the state changing from 'suspend' to 'start'.
+  if (!allowToStart) {
+    ErrorResult rv;
+    RefPtr<Promise> dummy = Suspend(rv);
+    MOZ_ASSERT(!rv.Failed(), "can't create promise");
+    MOZ_ASSERT(dummy->State() != Promise::PromiseState::Rejected,
+               "suspend failed");
+  }
 }
 
 nsresult
 AudioContext::Init()
 {
   if (!mIsOffline) {
     nsresult rv = mDestination->CreateAudioChannelAgent();
     if (NS_WARN_IF(NS_FAILED(rv))) {
@@ -721,16 +739,21 @@ AudioContext::Shutdown()
       RefPtr<Promise> ignored = Close(IgnoreErrors());
     }
 
     for (auto p : mPromiseGripArray) {
       p->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     }
 
     mPromiseGripArray.Clear();
+
+    for (const auto& p : mPendingResumePromises) {
+      p->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+    }
+    mPendingResumePromises.Clear();
   }
 
   // Release references to active nodes.
   // Active AudioNodes don't unregister in destructors, at which point the
   // Node is already unregistered.
   mActiveNodes.Clear();
 
   // For offline contexts, we can destroy the MediaStreamGraph at this point.
@@ -900,16 +923,26 @@ AudioContext::OnStateChanged(void* aProm
     // already freed memory.
     if (mPromiseGripArray.Contains(promise)) {
       promise->MaybeResolveWithUndefined();
       DebugOnly<bool> rv = mPromiseGripArray.RemoveElement(promise);
       MOZ_ASSERT(rv, "Promise wasn't in the grip array?");
     }
   }
 
+  // Resolve all pending promises once the audio context has been allowed to
+  // start.
+  if (mAudioContextState == AudioContextState::Suspended &&
+      aNewState == AudioContextState::Running) {
+    for (const auto& p : mPendingResumePromises) {
+      p->MaybeResolveWithUndefined();
+    }
+    mPendingResumePromises.Clear();
+  }
+
   if (mAudioContextState != aNewState) {
     RefPtr<OnStateChangeTask> task = new OnStateChangeTask(this);
     Dispatch(task.forget());
   }
 
   mAudioContextState = aNewState;
 }
 
@@ -983,32 +1016,34 @@ AudioContext::Resume(ErrorResult& aRv)
   }
 
   if (mAudioContextState == AudioContextState::Closed ||
       mCloseCalled) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
-  Destination()->Resume();
+  mPendingResumePromises.AppendElement(promise);
+
+  if (AutoplayPolicy::IsAudioContextAllowedToPlay(WrapNotNull(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) {
-    streams = GetAllStreams();
+    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) {
+      streams = GetAllStreams();
+    }
+    Graph()->ApplyAudioContextOperation(DestinationStream()->AsAudioNodeStream(),
+                                        streams,
+                                        AudioContextOperation::Resume, promise);
+    mSuspendCalled = false;
   }
-  mPromiseGripArray.AppendElement(promise);
-  Graph()->ApplyAudioContextOperation(DestinationStream()->AsAudioNodeStream(),
-                                      streams,
-                                      AudioContextOperation::Resume, promise);
-
-  mSuspendCalled = false;
 
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 AudioContext::Close(ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject());
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -348,19 +348,24 @@ private:
   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;
   AudioContextState mAudioContextState;
   RefPtr<AudioDestinationNode> mDestination;
   RefPtr<AudioListener> mListener;
   nsTArray<UniquePtr<WebAudioDecodeJob> > mDecodeJobs;
-  // This array is used to keep the suspend/resume/close promises alive until
+  // This array is used to keep the suspend/close promises alive until
   // they are resolved, so we can safely pass them accross threads.
   nsTArray<RefPtr<Promise>> mPromiseGripArray;
+  // This array is used to onlly keep the resume promises alive until they are
+  // resolved, so we can safely pass them accross threads. If the audio context
+  // is not allowed to play, the promise would be pending in this array and be
+  // resolved until audio context has been allowed and user call resume() again.
+  nsTArray<RefPtr<Promise>> mPendingResumePromises;
   // See RegisterActiveNode.  These will keep the AudioContext alive while it
   // is rendering and the window remains alive.
   nsTHashtable<nsRefPtrHashKey<AudioNode> > mActiveNodes;
   // Raw (non-owning) references to all AudioNodes for this AudioContext.
   nsTHashtable<nsPtrHashKey<AudioNode> > mAllNodes;
   // Hashsets containing all the PannerNodes, to compute the doppler shift.
   // These are weak pointers.
   nsTHashtable<nsPtrHashKey<PannerNode> > mPannerNodes;
--- a/dom/media/webaudio/AudioDestinationNode.cpp
+++ b/dom/media/webaudio/AudioDestinationNode.cpp
@@ -319,17 +319,19 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
 NS_INTERFACE_MAP_END_INHERITING(AudioNode)
 
 NS_IMPL_ADDREF_INHERITED(AudioDestinationNode, AudioNode)
 NS_IMPL_RELEASE_INHERITED(AudioDestinationNode, AudioNode)
 
 AudioDestinationNode::AudioDestinationNode(AudioContext* aContext,
                                            bool aIsOffline,
                                            uint32_t aNumberOfChannels,
-                                           uint32_t aLength, float aSampleRate)
+                                           uint32_t aLength,
+                                           float aSampleRate,
+                                           bool aAllowToStart)
   : AudioNode(aContext, aNumberOfChannels,
               ChannelCountMode::Explicit, ChannelInterpretation::Speakers)
   , mFramesToProduce(aLength)
   , mIsOffline(aIsOffline)
   , mAudioChannelSuspended(false)
   , mCaptured(false)
   , mAudible(AudioChannelService::AudibleState::eAudible)
 {
@@ -347,17 +349,17 @@ AudioDestinationNode::AudioDestinationNo
   AudioNodeStream::Flags flags =
     AudioNodeStream::NEED_MAIN_THREAD_CURRENT_TIME |
     AudioNodeStream::NEED_MAIN_THREAD_FINISHED |
     AudioNodeStream::EXTERNAL_OUTPUT;
   mStream = AudioNodeStream::Create(aContext, engine, flags, graph);
   mStream->AddMainThreadListener(this);
   mStream->AddAudioOutput(&gWebAudioOutputKey);
 
-  if (!aIsOffline) {
+  if (!aIsOffline && aAllowToStart) {
     graph->NotifyWhenGraphStarted(mStream);
   }
 }
 
 AudioDestinationNode::~AudioDestinationNode()
 {
 }
 
--- a/dom/media/webaudio/AudioDestinationNode.h
+++ b/dom/media/webaudio/AudioDestinationNode.h
@@ -22,17 +22,18 @@ class AudioDestinationNode final : publi
 {
 public:
   // This node type knows what MediaStreamGraph to use based on
   // whether it's in offline mode.
   AudioDestinationNode(AudioContext* aContext,
                        bool aIsOffline,
                        uint32_t aNumberOfChannels = 0,
                        uint32_t aLength = 0,
-                       float aSampleRate = 0.0f);
+                       float aSampleRate = 0.0f,
+                       bool aAllowToStart = true);
 
   void DestroyMediaStream() override;
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioDestinationNode, AudioNode)
   NS_DECL_NSIAUDIOCHANNELAGENTCALLBACK
 
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;