Bug 1187778 - Block playback of media elements until their window has been in the foreground (preffed off).
authorChris Pearce <cpearce@mozilla.com>
Thu, 17 Sep 2015 18:26:05 +1200
changeset 586988 386b8539f55dfbfd5cafe9276acc4f6c04cb1bce
parent 586987 7e443dea8b3669d843edf5b93f2ab9cc24105b22
child 586989 1c46e94b763eacc5891a754ddb94c39f21fdeaf4
push id88774
push usercpearce@mozilla.com
push dateThu, 17 Sep 2015 06:27:07 +0000
treeherdertry@e21208fdfdfe [default view] [failures only]
bugs1187778
milestone43.0a1
Bug 1187778 - Block playback of media elements until their window has been in the foreground (preffed off).
dom/audiochannel/AudioChannelAgent.cpp
dom/audiochannel/AudioChannelAgent.h
dom/audiochannel/AudioChannelService.cpp
dom/audiochannel/AudioChannelService.h
dom/audiochannel/nsIAudioChannelAgent.idl
dom/base/nsGlobalWindow.cpp
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/media/webaudio/AudioDestinationNode.cpp
dom/media/webspeech/synth/nsSpeechTask.cpp
dom/plugins/base/nsNPAPIPluginInstance.cpp
dom/telephony/Telephony.cpp
modules/libpref/init/all.js
--- a/dom/audiochannel/AudioChannelAgent.cpp
+++ b/dom/audiochannel/AudioChannelAgent.cpp
@@ -32,29 +32,37 @@ NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(AudioChannelAgent)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(AudioChannelAgent)
 
 AudioChannelAgent::AudioChannelAgent()
   : mAudioChannelType(AUDIO_AGENT_CHANNEL_ERROR)
   , mInnerWindowID(0)
   , mIsRegToService(false)
+  , mIsPlaying(false)
 {
 }
 
 AudioChannelAgent::~AudioChannelAgent()
 {
   Shutdown();
 }
 
 void
 AudioChannelAgent::Shutdown()
 {
+  if (mIsPlaying) {
+    NotifyStoppedPlaying(nsIAudioChannelAgent::AUDIO_AGENT_NOTIFY);
+  }
   if (mIsRegToService) {
-    NotifyStoppedPlaying(nsIAudioChannelAgent::AUDIO_AGENT_NOTIFY);
+    nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
+    if (service) {
+      service->UnregisterAudioChannelAgent(this);
+    }
+    mIsRegToService = false;
   }
 }
 
 NS_IMETHODIMP AudioChannelAgent::GetAudioChannelType(int32_t *aAudioChannelType)
 {
   *aAudioChannelType = mAudioChannelType;
   return NS_OK;
 }
@@ -125,66 +133,102 @@ AudioChannelAgent::InitInternal(nsIDOMWi
   mAudioChannelType = aChannelType;
 
   if (aUseWeakRef) {
     mWeakCallback = do_GetWeakReference(aCallback);
   } else {
     mCallback = aCallback;
   }
 
+  // Register, so that we get callbacks if blocked status changes.
+  nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
+  if (service == nullptr || mIsRegToService) {
+    return NS_ERROR_FAILURE;
+  }
+  service->RegisterAudioChannelAgent(this);
+  mIsRegToService = true;
+
+  return NS_OK;
+}
+
+bool AudioChannelAgent::IsPlaybackBlocked()
+{
+  if (!mWindow) {
+    return true;
+  }
+  nsCOMPtr<nsIDOMWindow> topWindow;
+  mWindow->GetScriptableTop(getter_AddRefs(topWindow));
+  if (!topWindow) {
+    return true;
+  }
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(topWindow);
+  if (!window) {
+    return true;
+  }
+  window = window->GetOuterWindow();
+  return !window->EverInForeground();
+}
+
+NS_IMETHODIMP AudioChannelAgent::IsPlaybackBlocked(bool* aBlocked)
+{
+  MOZ_ASSERT(aBlocked);
+
+  *aBlocked = IsPlaybackBlocked();
   return NS_OK;
 }
 
 NS_IMETHODIMP AudioChannelAgent::NotifyStartedPlaying(uint32_t aNotifyPlayback,
                                                       float *aVolume,
                                                       bool* aMuted)
 {
   MOZ_ASSERT(aVolume);
   MOZ_ASSERT(aMuted);
+  MOZ_ASSERT(mIsRegToService);
 
   // Window-less AudioChannelAgents are muted by default.
   if (!mWindow) {
     *aVolume = 0;
     *aMuted = true;
     return NS_OK;
   }
 
   nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   if (mAudioChannelType == AUDIO_AGENT_CHANNEL_ERROR ||
-      service == nullptr || mIsRegToService) {
+      service == nullptr || mIsPlaying) {
     return NS_ERROR_FAILURE;
   }
 
-  service->RegisterAudioChannelAgent(this, aNotifyPlayback,
+  service->NotifyPlaybackStarted(this, aNotifyPlayback,
     static_cast<AudioChannel>(mAudioChannelType));
 
   service->GetState(mWindow, mAudioChannelType, aVolume, aMuted);
 
-  mIsRegToService = true;
+  mIsPlaying = true;
+
   return NS_OK;
 }
 
 NS_IMETHODIMP AudioChannelAgent::NotifyStoppedPlaying(uint32_t aNotifyPlayback)
 {
   if (mAudioChannelType == AUDIO_AGENT_CHANNEL_ERROR ||
-      !mIsRegToService) {
+    !mIsPlaying) {
     return NS_ERROR_FAILURE;
   }
 
   nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
-  service->UnregisterAudioChannelAgent(this, aNotifyPlayback);
-  mIsRegToService = false;
+  service->NotifyPlaybackStopped(this, aNotifyPlayback);
+  mIsPlaying = false;
   return NS_OK;
 }
 
 already_AddRefed<nsIAudioChannelAgentCallback>
 AudioChannelAgent::GetCallback()
 {
   nsCOMPtr<nsIAudioChannelAgentCallback> callback = mCallback;
-  if (!callback) {
+  if (!callback && mWeakCallback) {
     callback = do_QueryReferent(mWeakCallback);
   }
   return callback.forget();
 }
 
 void
 AudioChannelAgent::WindowVolumeChanged()
 {
@@ -197,16 +241,27 @@ AudioChannelAgent::WindowVolumeChanged()
   bool muted = false;
 
   nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   service->GetState(mWindow, mAudioChannelType, &volume, &muted);
 
   callback->WindowVolumeChanged(volume, muted);
 }
 
+void
+AudioChannelAgent::WindowPlaybackBlockedChanged()
+{
+  nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback();
+  if (!callback) {
+    return;
+  }
+
+  callback->WindowPlaybackBlockedChanged();
+}
+
 uint64_t
 AudioChannelAgent::WindowID() const
 {
   return mWindow ? mWindow->WindowID() : 0;
 }
 
 void
 AudioChannelAgent::WindowAudioCaptureChanged(uint64_t aInnerWindowID)
--- a/dom/audiochannel/AudioChannelAgent.h
+++ b/dom/audiochannel/AudioChannelAgent.h
@@ -28,18 +28,20 @@ class AudioChannelAgent : public nsIAudi
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIAUDIOCHANNELAGENT
 
   NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgent)
 
   AudioChannelAgent();
 
+  bool IsPlaybackBlocked();
   void WindowVolumeChanged();
   void WindowAudioCaptureChanged(uint64_t aInnerWindowID);
+  void WindowPlaybackBlockedChanged();
 
   nsPIDOMWindow* Window() const
   {
     return mWindow;
   }
 
   uint64_t WindowID() const;
 
@@ -59,15 +61,16 @@ private:
   nsCOMPtr<nsPIDOMWindow> mWindow;
   nsCOMPtr<nsIAudioChannelAgentCallback> mCallback;
 
   nsWeakPtr mWeakCallback;
 
   int32_t mAudioChannelType;
   uint64_t mInnerWindowID;
   bool mIsRegToService;
+  bool mIsPlaying;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 
 #endif
--- a/dom/audiochannel/AudioChannelService.cpp
+++ b/dom/audiochannel/AudioChannelService.cpp
@@ -233,51 +233,75 @@ AudioChannelService::AudioChannelService
                                "dom.audiochannel.mutedByDefault");
 }
 
 AudioChannelService::~AudioChannelService()
 {
 }
 
 void
-AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
-                                               uint32_t aNotifyPlayback,
-                                               AudioChannel aChannel)
+AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent)
 {
   uint64_t windowID = aAgent->WindowID();
   AudioChannelWindow* winData = GetWindowData(windowID);
   if (!winData) {
     winData = new AudioChannelWindow(windowID);
     mWindows.AppendElement(winData);
   }
 
   MOZ_ASSERT(!winData->mAgents.Contains(aAgent));
   winData->mAgents.AppendElement(aAgent);
+}
+
+void
+AudioChannelService::UnregisterAudioChannelAgent(AudioChannelAgent* aAgent)
+{
+  AudioChannelWindow* winData = GetWindowData(aAgent->WindowID());
+  if (!winData) {
+    return;
+  }
+
+  if (winData->mAgents.Contains(aAgent)) {
+    // aAgent can be freed after this call.
+    winData->mAgents.RemoveElement(aAgent);
+  }
+
+}
+
+void
+AudioChannelService::NotifyPlaybackStarted(AudioChannelAgent* aAgent,
+                                           uint32_t aNotifyPlayback,
+                                           AudioChannel aChannel)
+{
+  uint64_t windowID = aAgent->WindowID();
+  AudioChannelWindow* winData = GetWindowData(windowID);
+  MOZ_ASSERT(winData); // Should already be registered.
+  MOZ_ASSERT(winData->mAgents.Contains(aAgent));
 
   ++winData->mChannels[(uint32_t)aChannel].mNumberOfAgents;
 
   // The first one, we must inform the BrowserElementAudioChannel.
   if (winData->mChannels[(uint32_t)aChannel].mNumberOfAgents == 1) {
     NotifyChannelActive(aAgent->WindowID(), aChannel, true);
   }
 
   // If this is the first agent for this window, we must notify the observers.
   if (aNotifyPlayback == nsIAudioChannelAgent::AUDIO_AGENT_NOTIFY &&
-      winData->mAgents.Length() == 1) {
+    (++winData->mNotifyCount) == 1) {
     nsRefPtr<MediaPlaybackRunnable> runnable =
       new MediaPlaybackRunnable(aAgent->Window(), true /* active */);
     NS_DispatchToCurrentThread(runnable);
   }
 
   MaybeSendStatusUpdate();
 }
 
 void
-AudioChannelService::UnregisterAudioChannelAgent(AudioChannelAgent* aAgent,
-                                                 uint32_t aNotifyPlayback)
+AudioChannelService::NotifyPlaybackStopped(AudioChannelAgent* aAgent,
+                                           uint32_t aNotifyPlayback)
 {
   AudioChannelWindow* winData = GetWindowData(aAgent->WindowID());
   if (!winData) {
     return;
   }
 
   if (winData->mAgents.Contains(aAgent)) {
     int32_t channel = aAgent->AudioChannelType();
@@ -300,17 +324,17 @@ AudioChannelService::UnregisterAudioChan
   bool active = AnyAudioChannelIsActive();
   for (uint32_t i = 0; i < mSpeakerManager.Length(); i++) {
     mSpeakerManager[i]->SetAudioChannelActive(active);
   }
 #endif
 
   // If this is the last agent for this window, we must notify the observers.
   if (aNotifyPlayback == nsIAudioChannelAgent::AUDIO_AGENT_NOTIFY &&
-      winData->mAgents.IsEmpty()) {
+      (--winData->mNotifyCount) == 0) {
     nsRefPtr<MediaPlaybackRunnable> runnable =
       new MediaPlaybackRunnable(aAgent->Window(), false /* active */);
     NS_DispatchToCurrentThread(runnable);
   }
 
   MaybeSendStatusUpdate();
 }
 
@@ -559,16 +583,24 @@ AudioChannelService::RefreshAgents(nsPID
   nsTObserverArray<AudioChannelAgent*>::ForwardIterator
     iter(winData->mAgents);
   while (iter.HasMore()) {
     aFunc(iter.GetNext());
   }
 }
 
 void
+AudioChannelService::RefreshAgentsPlaybackBlocked(nsPIDOMWindow* aWindow)
+{
+  RefreshAgents(aWindow, [aWindow](AudioChannelAgent* agent) {
+    agent->WindowPlaybackBlockedChanged();
+  });
+}
+
+void
 AudioChannelService::RefreshAgentsVolume(nsPIDOMWindow* aWindow)
 {
   RefreshAgents(aWindow, [aWindow](AudioChannelAgent* agent) {
     agent->WindowVolumeChanged();
   });
 }
 
 void
--- a/dom/audiochannel/AudioChannelService.h
+++ b/dom/audiochannel/AudioChannelService.h
@@ -42,29 +42,36 @@ public:
    * If AudioChannelServce is not exist, create and return new one.
    * Only to be called from main thread.
    */
   static already_AddRefed<AudioChannelService> GetOrCreate();
 
   static bool IsAudioChannelMutedByDefault();
 
   /**
-   * Any audio channel agent that starts playing should register itself to
-   * this service, sharing the AudioChannel.
+   * Any AudioChannelAgent to use the service must register for callbacks
+   * before notifying that it's starting/stopping playback, and unregister
+   * after.
    */
-  void RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
-                                 uint32_t aNotifyPlayback,
-                                 AudioChannel aChannel);
+  void RegisterAudioChannelAgent(AudioChannelAgent* aAgent);
+  void UnregisterAudioChannelAgent(AudioChannelAgent* aAgent);
 
   /**
-   * Any audio channel agent that stops playing should unregister itself to
-   * this service.
+   * Any audio channel agent that starts playing should notify the service,
+   * sharing the AudioChannel.
    */
-  void UnregisterAudioChannelAgent(AudioChannelAgent* aAgent,
-                                   uint32_t aNotifyPlayback);
+  void NotifyPlaybackStarted(AudioChannelAgent* aAgent,
+                             uint32_t aNotifyPlayback,
+                             AudioChannel aChannel);
+
+  /**
+   * Any audio channel agent that stops playing should notify the service.
+   */
+  void NotifyPlaybackStopped(AudioChannelAgent* aAgent,
+                             uint32_t aNotifyPlayback);
 
   /**
    * Return the state to indicate this audioChannel for his window should keep
    * playing/muted.
    */
   void GetState(nsPIDOMWindow* aWindow, uint32_t aChannel,
                 float* aVolume, bool* aMuted);
 
@@ -108,16 +115,17 @@ public:
 
   // This method needs to know the inner window that wants to capture audio. We
   // group agents per top outer window, but we can have multiple innerWindow per
   // top outerWindow (subiframes, etc.) and we have to identify all the agents
   // just for a particular innerWindow.
   void RefreshAgentsCapture(nsPIDOMWindow* aWindow,
                             uint64_t aInnerWindowID);
 
+  void RefreshAgentsPlaybackBlocked(nsPIDOMWindow* aWindow);
 
 #ifdef MOZ_WIDGET_GONK
   void RegisterSpeakerManager(SpeakerManagerService* aSpeakerManager)
   {
     if (!mSpeakerManager.Contains(aSpeakerManager)) {
       mSpeakerManager.AppendElement(aSpeakerManager);
     }
   }
@@ -174,23 +182,27 @@ private:
 
     uint32_t mNumberOfAgents;
   };
 
   struct AudioChannelWindow final
   {
     explicit AudioChannelWindow(uint64_t aWindowID)
       : mWindowID(aWindowID)
+      , mNotifyCount(0)
     {}
 
     uint64_t mWindowID;
     AudioChannelConfig mChannels[NUMBER_OF_AUDIO_CHANNELS];
 
     // Raw pointer because the AudioChannelAgent must unregister itself.
     nsTObserverArray<AudioChannelAgent*> mAgents;
+
+    // Count of notifying agents that are playing.
+    int32_t mNotifyCount;
   };
 
   AudioChannelWindow*
   GetOrCreateWindowData(nsPIDOMWindow* aWindow);
 
   AudioChannelWindow*
   GetWindowData(uint64_t aWindowID) const;
 
--- a/dom/audiochannel/nsIAudioChannelAgent.idl
+++ b/dom/audiochannel/nsIAudioChannelAgent.idl
@@ -1,45 +1,50 @@
 /* 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 "nsISupports.idl"
 
 interface nsIDOMWindow;
 
-[uuid(5fe83b24-38b9-4901-a4a1-d1bd57d3fe18)]
+[uuid(0e8fe51b-9377-4f8f-9c71-1dec11bcc4a2)]
 interface nsIAudioChannelAgentCallback : nsISupports
 {
   /**
    * Notified when the window volume/mute is changed
    */
   void windowVolumeChanged(in float aVolume, in bool aMuted);
 
   /**
    * Notified when the capture state is changed.
    */
   void windowAudioCaptureChanged();
+  
+  /**
+   * Notified when the playback blocked state is changed.
+   */
+  void windowPlaybackBlockedChanged();
 };
 
 /**
  * This interface provides an agent for gecko components to participate
  * in the audio channel service. Gecko components are responsible for
  *   1. Indicating what channel type they are using (via the init() member
  *      function).
  *   2. Before playing, checking the playable status of the channel.
  *   3. Notifying the agent when they start/stop using this channel.
  *   4. Notifying the agent of changes to the visibility of the component using
  *      this channel.
  *
  * The agent will invoke a callback to notify Gecko components of
  *   1. Changes to the playable status of this channel.
  */
 
-[uuid(62e0037c-9786-4b79-b986-27111f6e553b)]
+[uuid(94dbee0c-912c-4a09-85fc-4c39ec90528c)]
 interface nsIAudioChannelAgent : nsISupports
 {
   const long AUDIO_AGENT_CHANNEL_NORMAL             = 0;
   const long AUDIO_AGENT_CHANNEL_CONTENT            = 1;
   const long AUDIO_AGENT_CHANNEL_NOTIFICATION       = 2;
   const long AUDIO_AGENT_CHANNEL_ALARM              = 3;
   const long AUDIO_AGENT_CHANNEL_TELEPHONY          = 4;
   const long AUDIO_AGENT_CHANNEL_RINGER             = 5;
@@ -123,9 +128,11 @@ interface nsIAudioChannelAgent : nsISupp
    *   or AUDIO_CHANNEL_DONT_NOTIFY.
    *
    * Note : even if notifyStartedPlaying() returned false, the agent would
    * still be registered with the audio channel service and receive callbacks
    * for status changes. So notifyStoppedPlaying must still eventually be
    * called to unregister the agent with the channel service.
    */
   void notifyStoppedPlaying(in unsigned long notifyPlayback);
+  
+  void isPlaybackBlocked(out bool blocked);
 };
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -3771,18 +3771,18 @@ nsPIDOMWindow::SetAudioCapture(bool aCap
 
 void
 nsPIDOMWindow::SetIsBackground(bool aIsBackground)
 {
   MOZ_ASSERT(IsOuterWindow());
   mIsBackground = aIsBackground;
   if (!aIsBackground && !mEverInForeground) {
     mEverInForeground = true;
-    // TODO: Later patch in this queue; refresh AudioChannelService's agents
-    // so they can notify blocked media elements.
+    nsRefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
+    service->RefreshAgentsPlaybackBlocked(GetOuterWindow());
   }
 }
 
 // nsISpeechSynthesisGetter
 
 #ifdef MOZ_WEBSPEECH
 SpeechSynthesis*
 nsGlobalWindow::GetSpeechSynthesis(ErrorResult& aError)
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -123,21 +123,21 @@ class MOZ_STACK_CLASS AutoNotifyAudioCha
 public:
   AutoNotifyAudioChannelAgent(mozilla::dom::HTMLMediaElement* aElement,
                               bool aNotify
                               MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
     : mElement(aElement)
     , mShouldNotify(aNotify)
   {
     MOZ_GUARD_OBJECT_NOTIFIER_INIT;
-    if (mShouldNotify) {
-      // The audio channel agent may not exist now.
-      if (mElement->MaybeCreateAudioChannelAgent()) {
-        mElement->NotifyAudioChannelAgent(false);
-      }
+    // Note: *Always* create the AudioChannelAgent, in case playback start
+    // is waiting for the window to unblock and send a notification via
+    // the AudioChannelAgent to begin.
+    if (mElement->MaybeCreateAudioChannelAgent() && mShouldNotify) {
+      mElement->NotifyAudioChannelAgent(false);
     }
   }
   ~AutoNotifyAudioChannelAgent()
   {
     if (mShouldNotify) {
       // The audio channel agent is destroyed at this point.
       if (mElement->MaybeCreateAudioChannelAgent()) {
         mElement->NotifyAudioChannelAgent(true);
@@ -2243,17 +2243,17 @@ HTMLMediaElement::Play(ErrorResult& aRv)
   }
 
   // Even if we just did Load() or ResumeLoad(), we could already have a decoder
   // here if we managed to clone an existing decoder.
   if (mDecoder) {
     if (mDecoder->IsEndedOrShutdown()) {
       SetCurrentTime(0);
     }
-    if (!mPausedForInactiveDocumentOrChannel) {
+    if (!mPausedForInactiveDocumentOrChannel && !IsPlaybackBlockedByAudioChannelAgent()) {
       aRv = mDecoder->Play();
       if (aRv.Failed()) {
         return;
       }
     }
   }
 
   if (mCurrentPlayRangeStart == -1.0) {
@@ -2866,17 +2866,18 @@ nsresult HTMLMediaElement::FinishDecoder
   AddMediaElementToURITable();
 
   // We may want to suspend the new stream now.
   // This will also do an AddRemoveSelfReference.
   NotifyOwnerDocumentActivityChangedInternal();
 
   if (!mPaused) {
     SetPlayedOrSeeked(true);
-    if (!mPausedForInactiveDocumentOrChannel) {
+    if (!mPausedForInactiveDocumentOrChannel &&
+        !IsPlaybackBlockedByAudioChannelAgent()) {
       rv = mDecoder->Play();
     }
   }
 
   if (NS_FAILED(rv)) {
     ShutdownDecoder();
   }
 
@@ -3763,17 +3764,18 @@ void HTMLMediaElement::ChangeNetworkStat
 
 bool HTMLMediaElement::CanActivateAutoplay()
 {
   // For stream inputs, we activate autoplay on HAVE_NOTHING because
   // this element itself might be blocking the stream from making progress by
   // being paused. We also activate autoplay when playing a media source since
   // the data download is controlled by the script and there is no way to
   // evaluate MediaDecoder::CanPlayThrough().
-  return !mPausedForInactiveDocumentOrChannel &&
+  return !IsPlaybackBlockedByAudioChannelAgent() &&
+         !mPausedForInactiveDocumentOrChannel &&
          mAutoplaying &&
          mPaused &&
          ((mDecoder && mReadyState >= nsIDOMHTMLMediaElement::HAVE_ENOUGH_DATA) ||
           mSrcStream || mMediaSource) &&
          HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay) &&
          mAutoplayEnabled &&
          !IsEditable();
 }
@@ -3991,37 +3993,42 @@ void HTMLMediaElement::SuspendOrResumeEl
       if (mMediaSource) {
         ReportMSETelemetry();
 #ifdef MOZ_EME
         ReportEMETelemetry();
 #endif
       }
 
 #ifdef MOZ_EME
-      // For EME content, force destruction of the CDM client (and CDM
-      // instance if this is the last client for that CDM instance) and
-      // the CDM's decoder. This ensures the CDM gets reliable and prompt
-      // shutdown notifications, as it may have book-keeping it needs
-      // to do on shutdown.
-      if (mMediaKeys) {
-        mMediaKeys->Shutdown();
-        mMediaKeys = nullptr;
-        if (mDecoder) {
-          ShutdownDecoder();
+      // TODO: Unsure whether IsActive() is right here.
+      if (!OwnerDoc()->IsActive()) {
+        // For EME content, force destruction of the CDM client (and CDM
+        // instance if this is the last client for that CDM instance) and
+        // the CDM's decoder. This ensures the CDM gets reliable and prompt
+        // shutdown notifications, as it may have book-keeping it needs
+        // to do on shutdown.
+        if (mMediaKeys) {
+          mMediaKeys->Shutdown();
+          mMediaKeys = nullptr;
+          if (mDecoder) {
+            ShutdownDecoder();
+          }
         }
       }
 #endif
       if (mDecoder) {
         mDecoder->Pause();
         mDecoder->Suspend();
       }
       mEventDeliveryPaused = aSuspendEvents;
     } else {
 #ifdef MOZ_EME
-      MOZ_ASSERT(!mMediaKeys);
+      // CDM client should have been torn down if document became inactive,
+      // otherwise it can stay alive.
+      MOZ_ASSERT(!mMediaKeys || OwnerDoc()->IsActive());
 #endif
       if (mDecoder) {
         mDecoder->Resume(false);
         if (!mPaused && !mDecoder->IsEndedOrShutdown()) {
           mDecoder->Play();
         }
       }
       if (mEventDeliveryPaused) {
@@ -4073,25 +4080,26 @@ HTMLMediaElement::NotifyOwnerDocumentAct
 {
   nsIDocument* ownerDoc = OwnerDoc();
   if (mDecoder && !IsBeingDestroyed()) {
     mDecoder->SetElementVisibility(!ownerDoc->Hidden());
     mDecoder->NotifyOwnerActivityChanged();
   }
 
   bool pauseElement = !IsActive();
+  if (mAudioChannelAgent) {
 #ifdef PAUSE_MEDIA_ELEMENT_FROM_AUDIOCHANNEL
-  // Only pause the element when we start playing. If we pause without playing
-  // audio, the resource loading would be affected unexpectedly. For example,
-  // the media element is muted by default, but we don't want this behavior
-  // interrupting the loading process.
-  if (mAudioChannelAgent) {
+    // Only pause the element when we start playing. If we pause without playing
+    // audio, the resource loading would be affected unexpectedly. For example,
+    // the media element is muted by default, but we don't want this behavior
+    // interrupting the loading process.
     pauseElement |= ComputedMuted();
-  }
 #endif
+    pauseElement |= IsPlaybackBlockedByAudioChannelAgent();
+  }
 
   SuspendOrResumeElement(pauseElement, !IsActive());
 
   AddRemoveSelfReference();
 
   return pauseElement;
 }
 
@@ -4517,41 +4525,76 @@ nsresult HTMLMediaElement::UpdateChannel
 #endif
   return NS_OK;
 }
 
 bool
 HTMLMediaElement::MaybeCreateAudioChannelAgent()
 {
   if (!mAudioChannelAgent) {
+    if (!OwnerDoc()->GetInnerWindow()) {
+      // TODO: Justify this...
+      return false;
+    }
     nsresult rv;
-    mAudioChannelAgent = do_CreateInstance("@mozilla.org/audiochannelagent;1", &rv);
+    nsCOMPtr<nsIAudioChannelAgent> agent;
+    agent = do_CreateInstance("@mozilla.org/audiochannelagent;1", &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return false;
+    }
+    MOZ_ASSERT(agent);
+    rv = agent->InitWithWeakCallback(OwnerDoc()->GetInnerWindow(),
+                                     static_cast<int32_t>(mAudioChannel),
+                                     this);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return false;
     }
-    MOZ_ASSERT(mAudioChannelAgent);
-    mAudioChannelAgent->InitWithWeakCallback(OwnerDoc()->GetInnerWindow(),
-                                             static_cast<int32_t>(mAudioChannel),
-                                             this);
-  }
+    mAudioChannelAgent = agent;
+  }
+  MOZ_ASSERT(((AudioChannelAgent*)mAudioChannelAgent.get())->WindowID() != 0);
   return true;
 }
 
+bool
+HTMLMediaElement::IsPlaybackBlockedByAudioChannelAgent()
+{
+  if (!Preferences::GetBool("media.block-play-until-in-foreground", false)) {
+    return false;
+  }
+  if (!MaybeCreateAudioChannelAgent()) {
+    return false;
+  }
+  bool blocked = false;
+  mAudioChannelAgent->IsPlaybackBlocked(&blocked);
+  return blocked;
+}
+
 void
 HTMLMediaElement::UpdateAudioChannelPlayingState()
 {
   bool playingThroughTheAudioChannel =
      (!mPaused &&
       !Muted() &&
       std::fabs(Volume()) > 1e-7 &&
+      !IsPlaybackBlockedByAudioChannelAgent() &&
       (HasAttr(kNameSpaceID_None, nsGkAtoms::loop) ||
        (mReadyState >= nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA &&
         !IsPlaybackEnded()) ||
        mPlayingThroughTheAudioChannelBeforeSeek));
+
+  LOG(LogLevel::Debug, ("%p UpdateAudioChannelPlayingState paused=%d muted=%d "
+                        "vol=%d loop=%d readyState=%d ended=%d playb4seek=%d blocked=%d",
+                        this, !!mPaused, Muted(), (std::fabs(Volume()) > 1e-7),
+                        HasAttr(kNameSpaceID_None, nsGkAtoms::loop),
+                        mReadyState.Ref(), IsPlaybackEnded(), mPlayingThroughTheAudioChannelBeforeSeek,
+                        IsPlaybackBlockedByAudioChannelAgent()));
+
   if (playingThroughTheAudioChannel != mPlayingThroughTheAudioChannel) {
+    LOG(LogLevel::Debug, ("%p UpdateAudioChannelPlayingState mPlayingThroughTheAudioChannel=%d",
+        this, playingThroughTheAudioChannel));
     mPlayingThroughTheAudioChannel = playingThroughTheAudioChannel;
 
     // If we are not playing, we don't need to create a new audioChannelAgent.
     if (!mAudioChannelAgent && !mPlayingThroughTheAudioChannel) {
        return;
     }
 
     if (MaybeCreateAudioChannelAgent()) {
@@ -4582,16 +4625,23 @@ HTMLMediaElement::NotifyAudioChannelAgen
     mAudioChannelAgent->NotifyStartedPlaying(notify, &volume, &muted);
     WindowVolumeChanged(volume, muted);
   } else {
     mAudioChannelAgent->NotifyStoppedPlaying(notify);
     mAudioChannelAgent = nullptr;
   }
 }
 
+NS_IMETHODIMP HTMLMediaElement::WindowPlaybackBlockedChanged()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  NotifyOwnerDocumentActivityChanged();
+  return NS_OK;
+}
+
 NS_IMETHODIMP HTMLMediaElement::WindowVolumeChanged(float aVolume, bool aMuted)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   UpdateChannelMuteState(aVolume, aMuted);
 
 #ifdef PAUSE_MEDIA_ELEMENT_FROM_AUDIOCHANNEL
   mPaused.SetCanPlay(!aMuted);
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -1028,16 +1028,18 @@ protected:
   void ReportMSETelemetry();
 
   // Check the permissions for audiochannel.
   bool CheckAudioChannelPermissions(const nsAString& aType);
 
   // This method does the check for muting/nmuting the audio channel.
   nsresult UpdateChannelMuteState(float aVolume, bool aMuted);
 
+  bool IsPlaybackBlockedByAudioChannelAgent();
+
   // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the
   // seek target, or PrevSyncPoint if a quicker but less precise seek is
   // desired, and we'll seek to the sync point (keyframe and/or start of the
   // next block of audio samples) preceeding seek target.
   void Seek(double aTime, SeekTarget::Type aSeekType, ErrorResult& aRv);
 
   // Update the audio channel playing state
   void UpdateAudioChannelPlayingState();
--- a/dom/media/webaudio/AudioDestinationNode.cpp
+++ b/dom/media/webaudio/AudioDestinationNode.cpp
@@ -493,16 +493,22 @@ AudioDestinationNode::SetCanPlay(float a
   if (!mStream) {
     return;
   }
 
   mStream->SetTrackEnabled(AudioNodeStream::AUDIO_TRACK, !aMuted);
   mStream->SetAudioOutputVolume(&gWebAudioOutputKey, aVolume);
 }
 
+NS_IMETHODIMP AudioDestinationNode::WindowPlaybackBlockedChanged()
+{
+  // This is not supported yet.
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 AudioDestinationNode::WindowVolumeChanged(float aVolume, bool aMuted)
 {
   if (aMuted != mAudioChannelAgentPlaying) {
     mAudioChannelAgentPlaying = aMuted;
 
     if (UseAudioChannelAPI()) {
       Context()->DispatchTrustedEvent(
--- a/dom/media/webspeech/synth/nsSpeechTask.cpp
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -689,16 +689,23 @@ nsSpeechTask::DestroyAudioChannelAgent()
 NS_IMETHODIMP
 nsSpeechTask::WindowVolumeChanged(float aVolume, bool aMuted)
 {
   SetAudioOutputVolume(aMuted ? 0.0 : mVolume * aVolume);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsSpeechTask::WindowPlaybackBlockedChanged()
+{
+  // This is not supported yet.
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsSpeechTask::WindowAudioCaptureChanged()
 {
   // This is not supported yet.
   return NS_OK;
 }
 
 void
 nsSpeechTask::SetAudioOutputVolume(float aVolume)
--- a/dom/plugins/base/nsNPAPIPluginInstance.cpp
+++ b/dom/plugins/base/nsNPAPIPluginInstance.cpp
@@ -1814,16 +1814,23 @@ nsNPAPIPluginInstance::GetOrCreateAudioC
   }
 
   nsCOMPtr<nsIAudioChannelAgent> agent = mAudioChannelAgent;
   agent.forget(aAgent);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsNPAPIPluginInstance::WindowPlaybackBlockedChanged()
+{
+  // This is not supported yet.
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsNPAPIPluginInstance::WindowVolumeChanged(float aVolume, bool aMuted)
 {
   // We just support mute/unmute
   nsresult rv = SetMuted(aMuted);
   NS_WARN_IF(NS_FAILED(rv));
   return rv;
 }
 
--- a/dom/telephony/Telephony.cpp
+++ b/dom/telephony/Telephony.cpp
@@ -713,16 +713,23 @@ Telephony::WindowVolumeChanged(float aVo
 
 NS_IMETHODIMP
 Telephony::WindowAudioCaptureChanged()
 {
   // Do nothing, it's useless for the telephony object.
   return NS_OK;
 }
 
+NS_IMETHODIMP
+Telephony::WindowPlaybackBlockedChanged()
+{
+  // This is not supported yet.
+  return NS_OK;
+}
+
 // nsITelephonyListener
 
 NS_IMETHODIMP
 Telephony::CallStateChanged(uint32_t aLength, nsITelephonyCallInfo** aAllInfo)
 {
   nsresult rv;
   for (uint32_t i = 0; i < aLength; ++i) {
     rv = HandleCallInfo(aAllInfo[i]);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -294,18 +294,19 @@ pref("media.volume_scale", "1.0");
 // Timeout for wakelock release
 pref("media.wakelock_timeout", 2000);
 
 // Whether we should play videos opened in a "video document", i.e. videos
 // opened as top-level documents, as opposed to inside a media element.
 pref("media.play-stand-alone", true);
 
 // Whether we should delay actioning a "play()" JS function call and autoplay
-// attribute until the media element's owner document is visible.
-pref("media.block-play-until-visible", false);
+// attribute until the window in which media element's resides has been in
+// the foreground once or more.
+pref("media.block-play-until-in-foreground", false);
 
 pref("media.hardware-video-decoding.enabled", true);
 
 pref("media.decoder.heuristic.dormant.enabled", true);
 pref("media.decoder.heuristic.dormant.timeout", 60000);
 
 #ifdef MOZ_WMF
 pref("media.wmf.decoder.thread-count", -1);