Bug 1347758 - Let outer window decides when need to resume the tab. r=ehsan, a=gchang
authorAlastor Wu <alwu@mozilla.com>
Mon, 27 Mar 2017 11:19:17 +0800
changeset 395709 76b7351b7c2ef1e9528543389ea7778f65959098
parent 395708 47c3ec42917e04c125fbf90522d80a93f6ba198c
child 395710 8da6e9aa2b0f4c7327e4a51e6c444a95e289e8fb
push id1468
push userasasaki@mozilla.com
push dateMon, 05 Jun 2017 19:31:07 +0000
treeherdermozilla-release@0641fc6ee9d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, gchang
bugs1347758
milestone54.0a2
Bug 1347758 - Let outer window decides when need to resume the tab. r=ehsan, a=gchang
dom/audiochannel/AudioChannelAgent.cpp
dom/audiochannel/AudioChannelService.cpp
dom/audiochannel/AudioChannelService.h
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsGlobalWindow.cpp
dom/base/nsIDocument.h
dom/base/nsPIDOMWindow.h
dom/html/HTMLMediaElement.cpp
dom/media/webaudio/AudioDestinationNode.cpp
dom/plugins/base/nsNPAPIPluginInstance.cpp
toolkit/content/tests/browser/browser.ini
toolkit/content/tests/browser/browser_block_notInTreeAudio.js
toolkit/content/tests/browser/browser_block_plugIn.js
toolkit/content/tests/browser/browser_block_webAudio.js
toolkit/content/tests/browser/file_nonAutoplayAudio.html
toolkit/content/tests/browser/file_plugIn.html
toolkit/content/tests/browser/file_webAudio.html
toolkit/content/tests/browser/head.js
--- a/dom/audiochannel/AudioChannelAgent.cpp
+++ b/dom/audiochannel/AudioChannelAgent.cpp
@@ -215,18 +215,20 @@ AudioChannelAgent::NotifyStartedPlaying(
   service->RegisterAudioChannelAgent(this,
     static_cast<AudioChannelService::AudibleState>(aAudible));
 
   AudioPlaybackConfig config = service->GetMediaConfig(mWindow,
                                                        mAudioChannelType);
 
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelAgent, NotifyStartedPlaying, this = %p, "
-          "audible = %d, mute = %d, volume = %f, suspend = %d\n", this,
-          aAudible, config.mMuted, config.mVolume, config.mSuspend));
+          "audible = %s, mute = %s, volume = %f, suspend = %s\n", this,
+          AudibleStateToStr(static_cast<AudioChannelService::AudibleState>(aAudible)),
+          config.mMuted ? "true" : "false", config.mVolume,
+          SuspendTypeToStr(config.mSuspend)));
 
   aConfig->SetConfig(config.mVolume, config.mMuted, config.mSuspend);
   mIsRegToService = true;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AudioChannelAgent::NotifyStoppedPlaying()
@@ -248,17 +250,19 @@ AudioChannelAgent::NotifyStoppedPlaying(
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AudioChannelAgent::NotifyStartedAudible(uint8_t aAudible, uint32_t aReason)
 {
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelAgent, NotifyStartedAudible, this = %p, "
-          "audible = %d, reason = %d\n", this, aAudible, aReason));
+          "audible = %s, reason = %s\n", this,
+          AudibleStateToStr(static_cast<AudioChannelService::AudibleState>(aAudible)),
+          AudibleChangedReasonToStr(static_cast<AudioChannelService::AudibleChangedReasons>(aReason))));
 
   RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   if (NS_WARN_IF(!service)) {
     return NS_ERROR_FAILURE;
   }
 
   service->AudioAudibleChanged(
     this,
@@ -282,18 +286,19 @@ AudioChannelAgent::WindowVolumeChanged()
 {
   nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback();
   if (!callback) {
     return;
   }
 
   AudioPlaybackConfig config = GetMediaConfig();
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
-         ("AudioChannelAgent, WindowVolumeChanged, this = %p, mute = %d, "
-          "volume = %f\n", this, config.mMuted, config.mVolume));
+         ("AudioChannelAgent, WindowVolumeChanged, this = %p, mute = %s, "
+          "volume = %f\n",
+          this, config.mMuted ? "true" : "false", config.mVolume));
 
   callback->WindowVolumeChanged(config.mVolume, config.mMuted);
 }
 
 void
 AudioChannelAgent::WindowSuspendChanged(nsSuspendedTypes aSuspend)
 {
   nsCOMPtr<nsIAudioChannelAgentCallback> callback = GetCallback();
@@ -302,17 +307,17 @@ AudioChannelAgent::WindowSuspendChanged(
   }
 
   if (!IsDisposableSuspend(aSuspend)) {
     aSuspend = GetMediaConfig().mSuspend;
   }
 
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelAgent, WindowSuspendChanged, this = %p, "
-          "suspended = %d\n", this, aSuspend));
+          "suspended = %s\n", this, SuspendTypeToStr(aSuspend)));
 
   callback->WindowSuspendChanged(aSuspend);
 }
 
 AudioPlaybackConfig
 AudioChannelAgent::GetMediaConfig()
 {
   RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
--- a/dom/audiochannel/AudioChannelService.cpp
+++ b/dom/audiochannel/AudioChannelService.cpp
@@ -86,18 +86,18 @@ public:
     // TODO : remove b2g related event in bug1299390.
     observerService->NotifyObservers(wrapper,
                                      "media-playback",
                                      mActive
                                        ? u"active"
                                        : u"inactive");
 
     MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
-           ("NotifyChannelActiveRunnable, type = %" PRIu32 ", active = %d\n",
-            static_cast<uint32_t>(mAudioChannel), mActive));
+           ("NotifyChannelActiveRunnable, type = %" PRIu32 ", active = %s\n",
+            static_cast<uint32_t>(mAudioChannel), mActive ? "true" : "false"));
 
     return NS_OK;
   }
 
 private:
   const uint64_t mWindowID;
   const AudioChannel mAudioChannel;
   const bool mActive;
@@ -130,18 +130,18 @@ public:
     nsAutoString state;
     GetActiveState(state);
 
     observerService->NotifyObservers(ToSupports(mWindow),
                                      "audio-playback",
                                      state.get());
 
     MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
-           ("AudioPlaybackRunnable, active = %d, reason = %d\n",
-            mActive, mReason));
+           ("AudioPlaybackRunnable, active = %s, reason = %s\n",
+            mActive ? "true" : "false", AudibleChangedReasonToStr(mReason)));
 
     return NS_OK;
   }
 
 private:
   void GetActiveState(nsAString& astate)
   {
     if (mActive) {
@@ -173,16 +173,82 @@ IsEnableAudioCompetingForAllAgents()
   return true;
 #else
   return sAudioChannelCompetingAllAgents;
 #endif
 }
 
 } // anonymous namespace
 
+namespace mozilla {
+namespace dom {
+
+const char*
+SuspendTypeToStr(const nsSuspendedTypes& aSuspend)
+{
+  MOZ_ASSERT(aSuspend == nsISuspendedTypes::NONE_SUSPENDED ||
+             aSuspend == nsISuspendedTypes::SUSPENDED_PAUSE ||
+             aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK ||
+             aSuspend == nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE ||
+             aSuspend == nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE);
+
+  switch (aSuspend) {
+    case nsISuspendedTypes::NONE_SUSPENDED:
+      return "none";
+    case nsISuspendedTypes::SUSPENDED_PAUSE:
+      return "pause";
+    case nsISuspendedTypes::SUSPENDED_BLOCK:
+      return "block";
+    case nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE:
+      return "disposable-pause";
+    case nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE:
+      return "disposable-stop";
+    default:
+      return "unknown";
+  }
+}
+
+const char*
+AudibleStateToStr(const AudioChannelService::AudibleState& aAudible)
+{
+  MOZ_ASSERT(aAudible == AudioChannelService::AudibleState::eNotAudible ||
+             aAudible == AudioChannelService::AudibleState::eMaybeAudible ||
+             aAudible == AudioChannelService::AudibleState::eAudible);
+
+  switch (aAudible) {
+    case AudioChannelService::AudibleState::eNotAudible :
+      return "not-audible";
+    case AudioChannelService::AudibleState::eMaybeAudible :
+      return "maybe-audible";
+    case AudioChannelService::AudibleState::eAudible :
+      return "audible";
+    default:
+      return "unknown";
+  }
+}
+
+const char*
+AudibleChangedReasonToStr(const AudioChannelService::AudibleChangedReasons& aReason)
+{
+  MOZ_ASSERT(aReason == AudioChannelService::AudibleChangedReasons::eVolumeChanged ||
+             aReason == AudioChannelService::AudibleChangedReasons::eDataAudibleChanged ||
+             aReason == AudioChannelService::AudibleChangedReasons::ePauseStateChanged);
+
+  switch (aReason) {
+    case AudioChannelService::AudibleChangedReasons::eVolumeChanged :
+      return "volume";
+    case AudioChannelService::AudibleChangedReasons::eDataAudibleChanged :
+      return "data-audible";
+    case AudioChannelService::AudibleChangedReasons::ePauseStateChanged :
+      return "pause-state";
+    default:
+      return "unknown";
+  }
+}
+
 StaticRefPtr<AudioChannelService> gAudioChannelService;
 
 // Mappings from 'mozaudiochannel' attribute strings to an enumeration.
 static const nsAttrValue::EnumTable kMozAudioChannelAttributeTable[] = {
   { "normal",             (int16_t)AudioChannel::Normal },
   { "content",            (int16_t)AudioChannel::Content },
   { "notification",       (int16_t)AudioChannel::Notification },
   { "alarm",              (int16_t)AudioChannel::Alarm },
@@ -900,17 +966,18 @@ AudioChannelService::SetAudioChannelMute
                                           bool aMuted)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aWindow);
   MOZ_ASSERT(aWindow->IsOuterWindow());
 
   MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelService, SetAudioChannelMuted, window = %p, type = %" PRIu32 ", "
-          "mute = %d\n", aWindow, static_cast<uint32_t>(aAudioChannel), aMuted));
+          "mute = %s\n", aWindow, static_cast<uint32_t>(aAudioChannel),
+          aMuted ? "true" : "false"));
 
   if (aAudioChannel == AudioChannel::System) {
     // Workaround for bug1183033, system channel type can always playback.
     return;
   }
 
   AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
   winData->mChannels[(uint32_t)aAudioChannel].mMuted = aMuted;
@@ -1141,18 +1208,18 @@ AudioChannelService::AudioChannelWindow:
   // One exception is if the pref "media.block-autoplay-until-in-foreground"
   // is on and the background page is the non-visited before. Because the media
   // in that page would be blocked until the page is going to foreground.
   mOwningAudioFocus = (!(aAgent->Window()->IsBackground()) ||
                        aAgent->Window()->GetMediaSuspend() == nsISuspendedTypes::SUSPENDED_BLOCK) ;
 
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelWindow, RequestAudioFocus, this = %p, "
-          "agent = %p, owning audio focus = %d\n",
-          this, aAgent, mOwningAudioFocus));
+          "agent = %p, owning audio focus = %s\n",
+          this, aAgent, mOwningAudioFocus ? "true" : "false"));
 }
 
 void
 AudioChannelService::AudioChannelWindow::NotifyAudioCompetingChanged(AudioChannelAgent* aAgent)
 {
   // This function may be called after RemoveAgentAndReduceAgentsNum(), so the
   // agent may be not contained in mAgent. In addition, the agent would still
   // be alive because we have kungFuDeathGrip in UnregisterAudioChannelAgent().
@@ -1243,17 +1310,17 @@ AudioChannelService::AudioChannelWindow:
           agent->WindowSuspendChanged(type);
           break;
       }
     }
   }
 
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelWindow, AudioFocusChanged, this = %p, "
-          "OwningAudioFocus = %d\n", this, mOwningAudioFocus));
+          "OwningAudioFocus = %s\n", this, mOwningAudioFocus ? "true" : "false"));
 }
 
 bool
 AudioChannelService::AudioChannelWindow::IsContainingPlayingAgent(AudioChannelAgent* aAgent) const
 {
   return (aAgent->WindowID() == mWindowID);
 }
 
@@ -1271,18 +1338,19 @@ AudioChannelService::AudioChannelWindow:
   // TODO : add other competing cases for MediaSession API
   if (presentChannelType == int32_t(AudioChannel::Normal) &&
       aIncomingChannelType == int32_t(AudioChannel::Normal)) {
     competingBehavior = nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE;
   }
 
   MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
          ("AudioChannelWindow, GetCompetingBehavior, this = %p, "
-          "present type = %d, incoming channel = %d, behavior = %d\n",
-          this, presentChannelType, aIncomingChannelType, competingBehavior));
+          "present type = %d, incoming channel = %d, behavior = %s\n",
+          this, presentChannelType, aIncomingChannelType,
+          SuspendTypeToStr(competingBehavior)));
 
   return competingBehavior;
 }
 
 /* static */ bool
 AudioChannelService::IsAudioChannelMutedByDefault()
 {
   CreateServiceIfNeeded();
@@ -1393,23 +1461,22 @@ void
 AudioChannelService::AudioChannelWindow::AudioAudibleChanged(AudioChannelAgent* aAgent,
                                                              AudibleState aAudible,
                                                              AudibleChangedReasons aReason)
 {
   MOZ_ASSERT(aAgent);
 
   if (aAudible == AudibleState::eAudible) {
     AppendAudibleAgentIfNotContained(aAgent, aReason);
+    NotifyAudioCompetingChanged(aAgent);
   } else {
     RemoveAudibleAgentIfContained(aAgent, aReason);
   }
 
-  if (aAudible == AudibleState::eAudible) {
-    NotifyAudioCompetingChanged(aAgent);
-  } else if (aAudible != AudibleState::eNotAudible) {
+  if (aAudible != AudibleState::eNotAudible) {
     MaybeNotifyMediaBlockStart(aAgent);
   }
 }
 
 void
 AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained(AudioChannelAgent* aAgent,
                                                                           AudibleChangedReasons aReason)
 {
@@ -1516,8 +1583,12 @@ AudioChannelService::AudioChannelWindow:
 
         observerService->NotifyObservers(ToSupports(window),
                                          "audio-playback",
                                          u"blockStart");
       })
     );
   }
 }
+
+} // namespace dom
+} // namespace mozilla
+
--- a/dom/audiochannel/AudioChannelService.h
+++ b/dom/audiochannel/AudioChannelService.h
@@ -363,12 +363,16 @@ private:
   bool mAnyChannel;
 
   // This is needed for IPC comunication between
   // AudioChannelServiceChild and this class.
   friend class ContentParent;
   friend class ContentChild;
 };
 
+const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend);
+const char* AudibleStateToStr(const AudioChannelService::AudibleState& aAudible);
+const char* AudibleChangedReasonToStr(const AudioChannelService::AudibleChangedReasons& aReason);
+
 } // namespace dom
 } // namespace mozilla
 
 #endif
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1332,17 +1332,16 @@ nsIDocument::nsIDocument()
     mHasHadDefaultView(false),
     mStyleSheetChangeEventsEnabled(false),
     mIsSrcdocDocument(false),
     mDidDocumentOpen(false),
     mHasDisplayDocument(false),
     mFontFaceSetDirty(true),
     mGetUserFontSetCalled(false),
     mPostedFlushUserFontSet(false),
-    mEverInForeground(false),
     mDidFireDOMContentLoaded(true),
     mHasScrollLinkedEffect(false),
     mFrameRequestCallbacksScheduled(false),
     mCompatMode(eCompatibility_FullStandards),
     mReadyState(ReadyState::READYSTATE_UNINITIALIZED),
     mStyleBackendType(StyleBackendType::None),
 #ifdef MOZILLA_INTERNAL_API
     mVisibilityState(dom::VisibilityState::Hidden),
@@ -1434,18 +1433,16 @@ nsDocument::nsDocument(const char* aCont
     MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug,
            ("DOCUMENT %p created", this));
 
   // Start out mLastStyleSheetSet as null, per spec
   SetDOMStringToNull(mLastStyleSheetSet);
 
   // void state used to differentiate an empty source from an unselected source
   mPreloadPictureFoundSource.SetIsVoid(true);
-
-  mEverInForeground = false;
 }
 
 void
 nsDocument::ClearAllBoxObjects()
 {
   if (mBoxObjectTable) {
     for (auto iter = mBoxObjectTable->Iter(); !iter.Done(); iter.Next()) {
       nsPIBoxObject* boxObject = iter.UserData();
@@ -10261,32 +10258,16 @@ nsDocument::AddResponsiveContent(nsICont
 void
 nsDocument::RemoveResponsiveContent(nsIContent* aContent)
 {
   MOZ_ASSERT(aContent);
   mResponsiveContent.RemoveEntry(aContent);
 }
 
 void
-nsDocument::AddMediaContent(nsIContent* aContent)
-{
-  MOZ_ASSERT(aContent);
-  MOZ_ASSERT(aContent->IsHTMLElement(nsGkAtoms::video) ||
-             aContent->IsHTMLElement(nsGkAtoms::audio));
-  mMediaContent.PutEntry(aContent);
-}
-
-void
-nsDocument::RemoveMediaContent(nsIContent* aContent)
-{
-  MOZ_ASSERT(aContent);
-  mMediaContent.RemoveEntry(aContent);
-}
-
-void
 nsDocument::NotifyMediaFeatureValuesChanged()
 {
   for (auto iter = mResponsiveContent.ConstIter(); !iter.Done();
        iter.Next()) {
     nsCOMPtr<nsIContent> content = iter.Get()->GetKey();
     if (content->IsHTMLElement(nsGkAtoms::img)) {
       auto* imageElement = static_cast<HTMLImageElement*>(content.get());
       imageElement->MediaFeatureValuesChanged();
@@ -12069,29 +12050,20 @@ nsDocument::PostVisibilityUpdateEvent()
   nsCOMPtr<nsIRunnable> event =
     NewRunnableMethod(this, &nsDocument::UpdateVisibilityState);
   Dispatch("nsDocument::UpdateVisibilityState", TaskCategory::Other, event.forget());
 }
 
 void
 nsDocument::MaybeActiveMediaComponents()
 {
-  if (mEverInForeground) {
-    return;
-  }
-
   if (!mWindow) {
     return;
   }
 
-  if (mMediaContent.IsEmpty()) {
-    return;
-  }
-
-  mEverInForeground = true;
   GetWindow()->MaybeActiveMediaComponents();
 }
 
 NS_IMETHODIMP
 nsDocument::GetHidden(bool* aHidden)
 {
   *aHidden = Hidden();
   return NS_OK;
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1007,23 +1007,16 @@ public:
   virtual nsresult AddResponsiveContent(nsIContent* aContent) override;
   // Removes an element from mResponsiveContent when the element is
   // removed from the tree.
   virtual void RemoveResponsiveContent(nsIContent* aContent) override;
   // Notifies any responsive content added by AddResponsiveContent upon media
   // features values changing.
   virtual void NotifyMediaFeatureValuesChanged() override;
 
-  // Adds an element to mMediaContent when the element is added to the tree.
-  virtual void AddMediaContent(nsIContent* aContent) override;
-
-  // Removes an element from mMediaContent when the element is removed from
-  // the tree.
-  virtual void RemoveMediaContent(nsIContent* aContent) override;
-
   virtual nsresult GetStateObject(nsIVariant** aResult) override;
 
   virtual nsDOMNavigationTiming* GetNavigationTiming() const override;
   virtual nsresult SetNavigationTiming(nsDOMNavigationTiming* aTiming) override;
 
   virtual Element* FindImageMap(const nsAString& aNormalizedMapName) override;
 
   virtual nsTArray<Element*> GetFullscreenStack() const override;
@@ -1559,19 +1552,16 @@ private:
   // state so we can provide useful assertions to consumers of ForgetLink and
   // AddStyleRelevantLink.
   bool mStyledLinksCleared;
 #endif
 
   // A set of responsive images keyed by address pointer.
   nsTHashtable< nsPtrHashKey<nsIContent> > mResponsiveContent;
 
-  // A set of media elements keyed by address pointer.
-  nsTHashtable<nsPtrHashKey<nsIContent>> mMediaContent;
-
   // Member to store out last-selected stylesheet set.
   nsString mLastStyleSheetSet;
 
   nsTArray<RefPtr<nsFrameLoader> > mInitializableFrameLoaders;
   nsTArray<nsCOMPtr<nsIRunnable> > mFrameLoaderFinalizers;
   RefPtr<nsRunnableMethod<nsDocument> > mFrameLoaderRunner;
 
   nsCOMPtr<nsIRunnable> mMaybeEndOutermostXBLUpdateRunner;
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -903,17 +903,18 @@ nsPIDOMWindow<T>::nsPIDOMWindow(nsPIDOMW
   mMediaSuspend(Preferences::GetBool("media.block-autoplay-until-in-foreground", true) ?
     nsISuspendedTypes::SUSPENDED_BLOCK : nsISuspendedTypes::NONE_SUSPENDED),
   mAudioMuted(false), mAudioVolume(1.0), mAudioCaptured(false),
   mDesktopModeViewport(false), mIsRootOuterWindow(false), mInnerWindow(nullptr),
   mOuterWindow(aOuterWindow),
   // Make sure no actual window ends up with mWindowID == 0
   mWindowID(NextWindowID()), mHasNotifiedGlobalCreated(false),
   mMarkedCCGeneration(0), mServiceWorkersTestingEnabled(false),
-  mLargeAllocStatus(LargeAllocStatus::NONE)
+  mLargeAllocStatus(LargeAllocStatus::NONE),
+  mShouldResumeOnFirstActiveMediaComponent(false)
 {
   if (aOuterWindow) {
     mTimeoutManager =
       MakeUnique<mozilla::dom::TimeoutManager>(*nsGlobalWindow::Cast(AsInner()));
   }
 }
 
 template<class T>
@@ -4233,49 +4234,57 @@ bool
 nsPIDOMWindowInner::IsRunningTimeout()
 {
   return TimeoutManager().IsRunningTimeout();
 }
 
 void
 nsPIDOMWindowOuter::NotifyCreatedNewMediaComponent()
 {
-  if (mMediaSuspend != nsISuspendedTypes::SUSPENDED_BLOCK) {
-    return;
-  }
+  // We would only active media component when there is any alive one.
+  mShouldResumeOnFirstActiveMediaComponent = true;
 
   // If the document is already on the foreground but the suspend state is still
   // suspend-block, that means the media component was created after calling
   // MaybeActiveMediaComponents, so the window's suspend state doesn't be
   // changed yet. Therefore, we need to call it again, because the state is only
   // changed after there exists alive media within the window.
   MaybeActiveMediaComponents();
 }
 
 void
 nsPIDOMWindowOuter::MaybeActiveMediaComponents()
 {
   if (IsInnerWindow()) {
     return mOuterWindow->MaybeActiveMediaComponents();
   }
 
+  // Resume the media when the tab was blocked and the tab already has
+  // alive media components.
+  if (!mShouldResumeOnFirstActiveMediaComponent ||
+      mMediaSuspend != nsISuspendedTypes::SUSPENDED_BLOCK) {
+    return;
+  }
+
   nsCOMPtr<nsPIDOMWindowInner> inner = GetCurrentInnerWindow();
   if (!inner) {
     return;
   }
 
+  // If the document is not visible, don't need to resume it.
   nsCOMPtr<nsIDocument> doc = inner->GetExtantDoc();
-  if (!doc) {
-    return;
-  }
-
-  if (!doc->Hidden() &&
-      mMediaSuspend == nsISuspendedTypes::SUSPENDED_BLOCK) {
-    SetMediaSuspend(nsISuspendedTypes::NONE_SUSPENDED);
-  }
+  if (!doc || doc->Hidden()) {
+    return;
+  }
+
+  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+         ("nsPIDOMWindowOuter, MaybeActiveMediaComponents, "
+          "resume the window from blocked, this = %p\n", this));
+
+  SetMediaSuspend(nsISuspendedTypes::NONE_SUSPENDED);
 }
 
 SuspendTypes
 nsPIDOMWindowOuter::GetMediaSuspend() const
 {
   if (IsInnerWindow()) {
     return mOuterWindow->GetMediaSuspend();
   }
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2428,19 +2428,16 @@ public:
   virtual nsresult AddPlugin(nsIObjectLoadingContent* aPlugin) = 0;
   virtual void RemovePlugin(nsIObjectLoadingContent* aPlugin) = 0;
   virtual void GetPlugins(nsTArray<nsIObjectLoadingContent*>& aPlugins) = 0;
 
   virtual nsresult AddResponsiveContent(nsIContent* aContent) = 0;
   virtual void RemoveResponsiveContent(nsIContent* aContent) = 0;
   virtual void NotifyMediaFeatureValuesChanged() = 0;
 
-  virtual void AddMediaContent(nsIContent* aContent) = 0;
-  virtual void RemoveMediaContent(nsIContent* aContent) = 0;
-
   virtual nsresult GetStateObject(nsIVariant** aResult) = 0;
 
   virtual nsDOMNavigationTiming* GetNavigationTiming() const = 0;
 
   virtual nsresult SetNavigationTiming(nsDOMNavigationTiming* aTiming) = 0;
 
   virtual Element* FindImageMap(const nsAString& aNormalizedMapName) = 0;
 
@@ -3178,19 +3175,16 @@ protected:
   bool mFontFaceSetDirty : 1;
 
   // Has GetUserFontSet() been called?
   bool mGetUserFontSetCalled : 1;
 
   // Do we currently have an event posted to call FlushUserFontSet?
   bool mPostedFlushUserFontSet : 1;
 
-  // True is document has ever been in a foreground window.
-  bool mEverInForeground : 1;
-
   // True if we have fired the DOMContentLoaded event, or don't plan to fire one
   // (e.g. we're not being parsed at all).
   bool mDidFireDOMContentLoaded : 1;
 
   // True if ReportHasScrollLinkedEffect() has been called.
   bool mHasScrollLinkedEffect : 1;
 
   // True if we have frame request callbacks scheduled with the refresh driver.
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -732,16 +732,20 @@ protected:
 
   uint32_t mMarkedCCGeneration;
 
   // Let the service workers plumbing know that some feature are enabled while
   // testing.
   bool mServiceWorkersTestingEnabled;
 
   mozilla::dom::LargeAllocStatus mLargeAllocStatus; // Outer window only
+
+  // When there is any created alive media component, we can consider to resume
+  // the media content in the window.
+  bool mShouldResumeOnFirstActiveMediaComponent;
 };
 
 #define NS_PIDOMWINDOWINNER_IID \
 { 0x775dabc9, 0x8f43, 0x4277, \
   { 0x9a, 0xdb, 0xf1, 0x99, 0x0d, 0x77, 0xcf, 0xfb } }
 
 #define NS_PIDOMWINDOWOUTER_IID \
   { 0x769693d4, 0xb009, 0x4fe2, \
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -686,17 +686,18 @@ public:
 
   NS_IMETHODIMP
   WindowVolumeChanged(float aVolume, bool aMuted) override
   {
     MOZ_ASSERT(mAudioChannelAgent);
 
     MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
            ("HTMLMediaElement::AudioChannelAgentCallback, WindowVolumeChanged, "
-            "this = %p, aVolume = %f, aMuted = %d\n", this, aVolume, aMuted));
+            "this = %p, aVolume = %f, aMuted = %s\n",
+            this, aVolume, aMuted ? "true" : "false"));
 
     if (mAudioChannelVolume != aVolume) {
       mAudioChannelVolume = aVolume;
       mOwner->SetVolumeInternal();
     }
 
     const uint32_t muted = mOwner->mMuted;
     if (aMuted && !mOwner->ComputedMuted()) {
@@ -710,17 +711,17 @@ public:
 
   NS_IMETHODIMP
   WindowSuspendChanged(SuspendTypes aSuspend) override
   {
     MOZ_ASSERT(mAudioChannelAgent);
 
     MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
            ("HTMLMediaElement::AudioChannelAgentCallback, WindowSuspendChanged, "
-            "this = %p, aSuspend = %d\n", this, aSuspend));
+            "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend)));
 
     switch (aSuspend) {
       case nsISuspendedTypes::NONE_SUSPENDED:
         Resume();
         break;
       case nsISuspendedTypes::SUSPENDED_PAUSE:
       case nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE:
       case nsISuspendedTypes::SUSPENDED_BLOCK:
@@ -877,17 +878,17 @@ private:
     if (mSuspended == aSuspend) {
       return;
     }
 
     MaybeNotifyMediaResumed(aSuspend);
     mSuspended = aSuspend;
     MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
            ("HTMLMediaElement::AudioChannelAgentCallback, SetAudioChannelSuspended, "
-            "this = %p, aSuspend = %d\n", this, aSuspend));
+            "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend)));
   }
 
   void
   Resume()
   {
     if (!IsSuspended()) {
       MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
              ("HTMLMediaElement::AudioChannelAgentCallback, ResumeFromAudioChannel, "
@@ -4274,17 +4275,16 @@ nsresult HTMLMediaElement::BindToTree(ns
 
   if (aDocument) {
     mAutoplayEnabled =
       IsAutoplayEnabled() && (!aDocument || !aDocument->IsStaticDocument()) &&
       !IsEditable();
     // The preload action depends on the value of the autoplay attribute.
     // It's value may have changed, so update it.
     UpdatePreloadAction();
-    aDocument->AddMediaContent(this);
   }
 
   if (mDecoder) {
     // When the MediaElement is binding to tree, the dormant status is
     // aligned to document's hidden status.
     mDecoder->NotifyOwnerActivityChanged(!IsHidden());
   }
 
@@ -4510,19 +4510,16 @@ HTMLMediaElement::ReportTelemetry()
     }
   }
 }
 
 void HTMLMediaElement::UnbindFromTree(bool aDeep,
                                       bool aNullParent)
 {
   mUnboundFromTree = true;
-  if (OwnerDoc()) {
-    OwnerDoc()->RemoveMediaContent(this);
-  }
 
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 
   if (mDecoder) {
     MOZ_ASSERT(IsHidden());
     mDecoder->NotifyOwnerActivityChanged(false);
   }
 
--- a/dom/media/webaudio/AudioDestinationNode.cpp
+++ b/dom/media/webaudio/AudioDestinationNode.cpp
@@ -502,16 +502,21 @@ AudioDestinationNode::StartRendering(Pro
 
 NS_IMETHODIMP
 AudioDestinationNode::WindowVolumeChanged(float aVolume, bool aMuted)
 {
   if (!mStream) {
     return NS_OK;
   }
 
+  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+         ("AudioDestinationNode, WindowVolumeChanged, "
+          "this = %p, aVolume = %f, aMuted = %s\n",
+          this, aVolume, aMuted ? "true" : "false"));
+
   float volume = aMuted ? 0.0 : aVolume;
   mStream->SetAudioOutputVolume(&gWebAudioOutputKey, volume);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend)
 {
@@ -519,16 +524,20 @@ AudioDestinationNode::WindowSuspendChang
     return NS_OK;
   }
 
   bool suspended = (aSuspend != nsISuspendedTypes::NONE_SUSPENDED);
   if (mAudioChannelSuspended == suspended) {
     return NS_OK;
   }
 
+  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+         ("AudioDestinationNode, WindowSuspendChanged, "
+          "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend)));
+
   mAudioChannelSuspended = suspended;
   Context()->DispatchTrustedEvent(!suspended ?
     NS_LITERAL_STRING("mozinterruptend") :
     NS_LITERAL_STRING("mozinterruptbegin"));
 
   DisabledTrackMode disabledMode = suspended ? DisabledTrackMode::SILENCE_BLACK
                                              : DisabledTrackMode::ENABLED;
   mStream->SetTrackEnabled(AudioNodeStream::AUDIO_TRACK, disabledMode);
--- a/dom/plugins/base/nsNPAPIPluginInstance.cpp
+++ b/dom/plugins/base/nsNPAPIPluginInstance.cpp
@@ -1733,16 +1733,21 @@ nsNPAPIPluginInstance::GetOrCreateAudioC
   nsCOMPtr<nsIAudioChannelAgent> agent = mAudioChannelAgent;
   agent.forget(aAgent);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNPAPIPluginInstance::WindowVolumeChanged(float aVolume, bool aMuted)
 {
+  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+         ("nsNPAPIPluginInstance, WindowVolumeChanged, "
+          "this = %p, aVolume = %f, aMuted = %s\n",
+          this, aVolume, aMuted ? "true" : "false"));
+
   // We just support mute/unmute
   nsresult rv = SetMuted(aMuted);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetMuted failed");
   if (mMuted != aMuted) {
     mMuted = aMuted;
     if (mAudioChannelAgent) {
       AudioChannelService::AudibleState audible = aMuted ?
         AudioChannelService::AudibleState::eNotAudible :
@@ -1752,16 +1757,20 @@ nsNPAPIPluginInstance::WindowVolumeChang
     }
   }
   return rv;
 }
 
 NS_IMETHODIMP
 nsNPAPIPluginInstance::WindowSuspendChanged(nsSuspendedTypes aSuspend)
 {
+  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+         ("nsNPAPIPluginInstance, WindowSuspendChanged, "
+          "this = %p, aSuspend = %s\n", this, SuspendTypeToStr(aSuspend)));
+
   // It doesn't support suspended, so we just do something like mute/unmute.
   WindowVolumeChanged(1.0, /* useless */
                       aSuspend != nsISuspendedTypes::NONE_SUSPENDED);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNPAPIPluginInstance::WindowAudioCaptureChanged(bool aCapture)
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -1,78 +1,83 @@
 [DEFAULT]
 support-files =
-  head.js
+  audio.ogg
+  empty.png
+  file_blockMedia_shouldNotPlay.html
   file_blockMedia_shouldPlay.html
-  file_blockMedia_shouldNotPlay.html
   file_contentTitle.html
   file_mediaPlayback.html
   file_mediaPlayback2.html
   file_mediaPlaybackFrame.html
   file_mediaPlaybackFrame2.html
   file_multipleAudio.html
   file_multiplePlayingAudio.html
   file_nonAutoplayAudio.html
+  file_plugIn.html
+  file_redirect.html
+  file_redirect_to.html
   file_silentAudioTrack.html
-  audio.ogg
+  file_webAudio.html
+  head.js
+  image.jpg
+  image_page.html
   silentAudioTrack.webm
 
 [browser_audioCompeting.js]
 tags = audiochannel
 [browser_audioCompeting_onlyForActiveAgent.js]
 tags = audiochannel
 [browser_autoscroll_disabled.js]
 [browser_block_autoplay_media.js]
 tags = audiochannel
-[browser_block_silentAudioTrack_media.js]
-tags = audiochannel
 [browser_block_autoplay_media_pausedAfterPlay.js]
  tags = audiochannel
+[browser_block_autoplay_playAfterTabVisible.js]
+tags = audiochannel
+[browser_block_notInTreeAudio.js]
+tags = audiochannel
 [browser_block_playMediaInMuteTab.js]
 tags = audiochannel
-[browser_block_autoplay_playAfterTabVisible.js]
+[browser_block_plugIn.js]
+tags = audiochannel
+[browser_block_silentAudioTrack_media.js]
 tags = audiochannel
+[browser_block_webAudio.js]
+tags = audiochannel
+[browser_bug1170531.js]
+[browser_bug1198465.js]
 [browser_bug295977_autoscroll_overflow.js]
 [browser_bug451286.js]
 skip-if = !e10s
 [browser_bug594509.js]
 [browser_bug982298.js]
-[browser_bug1198465.js]
+[browser_content_url_annotation.js]
+skip-if = !e10s || !crashreporter
 [browser_contentTitle.js]
 [browser_crash_previous_frameloader.js]
 run-if = e10s && crashreporter
 [browser_default_image_filename.js]
 [browser_f7_caret_browsing.js]
 [browser_findbar.js]
-[browser_label_textlink.js]
 [browser_isSynthetic.js]
-support-files =
-  empty.png
 [browser_keyevents_during_autoscrolling.js]
-[browser_save_resend_postdata.js]
-support-files =
-  common/mockTransfer.js
-  data/post_form_inner.sjs
-  data/post_form_outer.sjs
-skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();)
-[browser_content_url_annotation.js]
-skip-if = !e10s || !crashreporter
-support-files =
-  file_redirect.html
-  file_redirect_to.html
-[browser_bug1170531.js]
+[browser_label_textlink.js]
 [browser_mediaPlayback.js]
 tags = audiochannel
 [browser_mediaPlayback_mute.js]
 tags = audiochannel
 [browser_mediaPlayback_suspended.js]
 tags = audiochannel
 [browser_mediaPlayback_suspended_multipleAudio.js]
 tags = audiochannel
 [browser_mute.js]
 tags = audiochannel
 [browser_mute2.js]
 tags = audiochannel
 [browser_quickfind_editable.js]
-[browser_saveImageURL.js]
+[browser_save_resend_postdata.js]
 support-files =
-  image.jpg
-  image_page.html
+  common/mockTransfer.js
+  data/post_form_inner.sjs
+  data/post_form_outer.sjs
+skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();)
+[browser_saveImageURL.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_block_notInTreeAudio.js
@@ -0,0 +1,87 @@
+const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html";
+
+var SuspendedType = {
+  NONE_SUSPENDED: 0,
+  SUSPENDED_PAUSE: 1,
+  SUSPENDED_BLOCK: 2,
+  SUSPENDED_PAUSE_DISPOSABLE: 3
+};
+
+function check_audio_suspended(suspendedType) {
+  var audio = content.document.getElementById("testAudio");
+  if (!audio) {
+    ok(false, "Can't get the audio element!");
+  }
+
+  is(audio.computedSuspended, suspendedType,
+     "The suspeded state of audio is correct.");
+}
+
+function check_audio_pause_state(expectPause) {
+  var audio = content.document.getElementById("testAudio");
+  if (!audio) {
+    ok(false, "Can't get the audio element!");
+  }
+
+  is(audio.paused, expectPause,
+    "The pause state of audio is corret.")
+}
+
+function play_not_in_tree_audio() {
+  var audio = content.document.getElementById("testAudio");
+  if (!audio) {
+    ok(false, "Can't get the audio element!");
+  }
+
+  content.document.body.removeChild(audio);
+
+  // Add timeout to ensure the audio is removed from DOM tree.
+  setTimeout(function() {
+    info("Prepare to start playing audio.");
+    audio.play();
+  }, 1000);
+}
+
+add_task(function* setup_test_preference() {
+  yield SpecialPowers.pushPrefEnv({"set": [
+    ["media.useAudioChannelService.testing", true],
+    ["media.block-autoplay-until-in-foreground", true]
+  ]});
+});
+
+add_task(function* block_not_in_tree_media() {
+  info("- open new background tab -");
+  let tab = window.gBrowser.addTab("about:blank");
+  tab.linkedBrowser.loadURI(PAGE);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  info("- tab should not be blocked -");
+  yield waitForTabBlockEvent(tab, false);
+
+  info("- check audio's suspend state -");
+  yield ContentTask.spawn(tab.linkedBrowser, SuspendedType.NONE_SUSPENDED,
+                          check_audio_suspended);
+
+  info("- check audio's playing state -");
+  yield ContentTask.spawn(tab.linkedBrowser, true,
+                          check_audio_pause_state);
+
+  info("- playing audio -");
+  yield ContentTask.spawn(tab.linkedBrowser, null,
+                          play_not_in_tree_audio);
+
+  info("- tab should be blocked -");
+  yield waitForTabBlockEvent(tab, true);
+
+  info("- switch tab -");
+  yield BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+  info("- tab should be resumed -");
+  yield waitForTabBlockEvent(tab, false);
+
+  info("- tab should be audible -");
+  yield waitForTabPlayingEvent(tab, true);
+
+  info("- remove tab -");
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_block_plugIn.js
@@ -0,0 +1,31 @@
+const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_plugIn.html";
+
+add_task(function* setup_test_preference() {
+  setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+  yield SpecialPowers.pushPrefEnv({"set": [
+    ["media.useAudioChannelService.testing", true],
+    ["media.block-autoplay-until-in-foreground", true]
+  ]});
+});
+
+add_task(function* block_plug_in() {
+  info("- open new background tab -");
+  let tab = window.gBrowser.addTab("about:blank");
+  tab.linkedBrowser.loadURI(PAGE);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  info("- tab should be blocked -");
+  yield waitForTabBlockEvent(tab, true);
+
+  info("- switch tab -");
+  yield BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+  info("- tab should be resumed -");
+  yield waitForTabBlockEvent(tab, false);
+
+  info("- tab should be audible -");
+  yield waitForTabPlayingEvent(tab, true);
+
+  info("- remove tab -");
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_block_webAudio.js
@@ -0,0 +1,30 @@
+const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_webAudio.html";
+
+add_task(function* setup_test_preference() {
+  yield SpecialPowers.pushPrefEnv({"set": [
+    ["media.useAudioChannelService.testing", true],
+    ["media.block-autoplay-until-in-foreground", true]
+  ]});
+});
+
+add_task(function* block_web_audio() {
+  info("- open new background tab -");
+  let tab = window.gBrowser.addTab("about:blank");
+  tab.linkedBrowser.loadURI(PAGE);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  info("- tab should be blocked -");
+  yield waitForTabBlockEvent(tab, true);
+
+  info("- switch tab -");
+  yield BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+  info("- tab should be resumed -");
+  yield waitForTabBlockEvent(tab, false);
+
+  info("- tab should be audible -");
+  yield waitForTabPlayingEvent(tab, true);
+
+  info("- remove tab -");
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/toolkit/content/tests/browser/file_nonAutoplayAudio.html
+++ b/toolkit/content/tests/browser/file_nonAutoplayAudio.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <head>
   <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
   <meta content="utf-8" http-equiv="encoding">
 </head>
 <body>
-<audio id="testAudio" src="audio.ogg"></audio>
\ No newline at end of file
+<audio id="testAudio" src="audio.ogg" loop></audio>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/file_plugIn.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<embed type="application/x-test" width="200" height="200"></embed>
+<script>
+var plugin = document.querySelector("embed");
+onload = function() {
+  plugin.startAudioPlayback();
+};
+</script>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/file_webAudio.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<head>
+  <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+  <meta content="utf-8" http-equiv="encoding">
+</head>
+<script type="text/javascript">
+  var ac = new AudioContext();
+  var dest = ac.destination;
+  var osc = ac.createOscillator();
+  osc.connect(dest);
+  osc.start();
+</script>
+<body>
+</body>
\ No newline at end of file
--- a/toolkit/content/tests/browser/head.js
+++ b/toolkit/content/tests/browser/head.js
@@ -62,8 +62,39 @@ function* waitForTabPlayingEvent(tab, ex
       if (event.detail.changed.indexOf("soundplaying") >= 0) {
         is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
         return true;
       }
       return false;
     });
   }
 }
+
+function getTestPlugin(pluginName) {
+  var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"]
+                                 .getService(SpecialPowers.Ci.nsIPluginHost);
+  var tags = ph.getPluginTags();
+  var name = pluginName || "Test Plug-in";
+  for (var tag of tags) {
+    if (tag.name == name) {
+      return tag;
+    }
+  }
+
+  ok(false, "Could not find plugin tag with plugin name '" + name + "'");
+  return null;
+}
+
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+  var oldEnabledState = SpecialPowers.setTestPluginEnabledState(newEnabledState, pluginName);
+  if (!oldEnabledState) {
+    return;
+  }
+  var plugin = getTestPlugin(pluginName);
+  while (plugin.enabledState != newEnabledState) {
+    // Run a nested event loop to wait for the preference change to
+    // propagate to the child. Yuck!
+    SpecialPowers.Services.tm.currentThread.processNextEvent(true);
+  }
+  SimpleTest.registerCleanupFunction(function() {
+    SpecialPowers.setTestPluginEnabledState(oldEnabledState, pluginName);
+  });
+}