Bug 1146201 - Delay navigator.requestMediaKeySystemAccess if CDM not downloaded yet or needs update. r=jwwang,ehsan a=sylvestre
authorChris Pearce <cpearce@mozilla.com>
Tue, 31 Mar 2015 20:50:01 +1300
changeset 258236 b7e7470e83b3
parent 258235 ffb13ef5ff0a
child 258237 9383010b69fe
push id4625
push usercpearce@mozilla.com
push date2015-04-03 06:37 +0000
treeherdermozilla-beta@c481e8a84a6c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwwang, ehsan, sylvestre
bugs1146201
milestone38.0
Bug 1146201 - Delay navigator.requestMediaKeySystemAccess if CDM not downloaded yet or needs update. r=jwwang,ehsan a=sylvestre
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/media/eme/MediaKeySystemAccessManager.cpp
dom/media/eme/MediaKeySystemAccessManager.h
dom/media/eme/moz.build
dom/media/gmp/GMPService.cpp
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -195,16 +195,19 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCameraManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagesManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeviceStorageStores)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTimeManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorkerContainer)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedResolveResults)
+#ifdef MOZ_EME
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeySystemAccessManager)
+#endif
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Navigator)
 
 void
 Navigator::Invalidate()
 {
@@ -308,16 +311,23 @@ Navigator::Invalidate()
   }
   mDeviceStorageStores.Clear();
 
   if (mTimeManager) {
     mTimeManager = nullptr;
   }
 
   mServiceWorkerContainer = nullptr;
+
+#ifdef MOZ_EME
+  if (mMediaKeySystemAccessManager) {
+    mMediaKeySystemAccessManager->Shutdown();
+    mMediaKeySystemAccessManager = nullptr;
+  }
+#endif
 }
 
 //*****************************************************************************
 //    Navigator::nsIDOMNavigator
 //*****************************************************************************
 
 NS_IMETHODIMP
 Navigator::GetUserAgent(nsAString& aUserAgent)
@@ -2622,70 +2632,25 @@ Navigator::GetUserAgent(nsPIDOMWindow* a
 
 #ifdef MOZ_EME
 already_AddRefed<Promise>
 Navigator::RequestMediaKeySystemAccess(const nsAString& aKeySystem,
                                        const Optional<Sequence<MediaKeySystemOptions>>& aOptions,
                                        ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(mWindow);
-  nsRefPtr<Promise> p = Promise::Create(go, aRv);
+  nsRefPtr<Promise> promise = Promise::Create(go, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
-  if (!Preferences::GetBool("media.eme.enabled", false)) {
-    // EME disabled by user, send notification to chrome so UI can
-    // inform user.
-    MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem,
-                                          MediaKeySystemStatus::Api_disabled);
-    p->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-    return p.forget();
-  }
-
-  if (aKeySystem.IsEmpty() ||
-      (aOptions.WasPassed() && aOptions.Value().IsEmpty())) {
-    p->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
-    return p.forget();
-  }
-
-  // Parse keysystem, split it out into keySystem prefix, and version suffix.
-  nsAutoString keySystem;
-  int32_t minCdmVersion = NO_CDM_VERSION;
-  if (!ParseKeySystem(aKeySystem,
-                      keySystem,
-                      minCdmVersion)) {
-    // Invalid keySystem string, or unsupported keySystem. Send notification
-    // to chrome to show a failure notice.
-    MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, MediaKeySystemStatus::Cdm_not_supported);
-    p->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-    return p.forget();
+  if (!mMediaKeySystemAccessManager) {
+    mMediaKeySystemAccessManager = new MediaKeySystemAccessManager(mWindow);
   }
 
-  MediaKeySystemStatus status = MediaKeySystemAccess::GetKeySystemStatus(keySystem, minCdmVersion);
-  if (status != MediaKeySystemStatus::Available) {
-    if (status != MediaKeySystemStatus::Error) {
-      // Failed due to user disabling something, send a notification to
-      // chrome, so we can show some UI to explain how the user can rectify
-      // the situation.
-      MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status);
-    }
-    p->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-    return p.forget();
-  }
-
-  // TODO: Wait (async) until the CDM is downloaded, if it's not already.
-
-  if (!aOptions.WasPassed() ||
-      MediaKeySystemAccess::IsSupported(keySystem, aOptions.Value())) {
-    nsRefPtr<MediaKeySystemAccess> access(new MediaKeySystemAccess(mWindow, keySystem));
-    p->MaybeResolve(access);
-    return p.forget();
-  }
-
-  p->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-
-  return p.forget();
+  mMediaKeySystemAccessManager->Request(promise, aKeySystem, aOptions);
+  return promise.forget();
 }
+
 #endif
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -14,17 +14,17 @@
 #include "nsIMozNavigatorNetwork.h"
 #include "nsAutoPtr.h"
 #include "nsWrapperCache.h"
 #include "nsHashKeys.h"
 #include "nsInterfaceHashtable.h"
 #include "nsString.h"
 #include "nsTArray.h"
 #ifdef MOZ_EME
-#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "mozilla/dom/MediaKeySystemAccessManager.h"
 #endif
 
 class nsPluginArray;
 class nsMimeTypeArray;
 class nsPIDOMWindow;
 class nsIDOMNavigatorSystemMessages;
 class nsDOMCameraManager;
 class nsDOMDeviceStorage;
@@ -330,16 +330,18 @@ public:
   // any, else null.
   static already_AddRefed<nsPIDOMWindow> GetWindowFromGlobal(JSObject* aGlobal);
 
 #ifdef MOZ_EME
   already_AddRefed<Promise>
   RequestMediaKeySystemAccess(const nsAString& aKeySystem,
                               const Optional<Sequence<MediaKeySystemOptions>>& aOptions,
                               ErrorResult& aRv);
+private:
+  nsRefPtr<MediaKeySystemAccessManager> mMediaKeySystemAccessManager;
 #endif
 
 private:
   virtual ~Navigator();
 
   bool CheckPermission(const char* type);
   static bool CheckPermission(nsPIDOMWindow* aWindow, const char* aType);
 
new file mode 100644
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.cpp
@@ -0,0 +1,280 @@
+/* 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 "MediaKeySystemAccessManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/EMEUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+  for (size_t i = 0; i < tmp->mRequests.Length(); i++) {
+    tmp->mRequests[i].RejectPromise();
+    tmp->mRequests[i].CancelTimer();
+    NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequests[i].mPromise)
+  }
+  tmp->mRequests.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+  for (size_t i = 0; i < tmp->mRequests.Length(); i++) {
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequests[i].mPromise)
+  }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+MediaKeySystemAccessManager::MediaKeySystemAccessManager(nsPIDOMWindow* aWindow)
+  : mWindow(aWindow)
+  , mAddedObservers(false)
+{
+}
+
+MediaKeySystemAccessManager::~MediaKeySystemAccessManager()
+{
+  Shutdown();
+}
+
+void
+MediaKeySystemAccessManager::Request(Promise* aPromise,
+                                     const nsAString& aKeySystem,
+                                     const Optional<Sequence<MediaKeySystemOptions>>& aOptions)
+{
+  if (aKeySystem.IsEmpty() || (aOptions.WasPassed() && aOptions.Value().IsEmpty())) {
+    aPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    return;
+  }
+  Sequence<MediaKeySystemOptions> optionsNotPassed;
+  const auto& options = aOptions.WasPassed() ? aOptions.Value() : optionsNotPassed;
+  Request(aPromise, aKeySystem, options, RequestType::Initial);
+}
+
+void
+MediaKeySystemAccessManager::Request(Promise* aPromise,
+                                     const nsAString& aKeySystem,
+                                     const Sequence<MediaKeySystemOptions>& aOptions,
+                                     RequestType aType)
+{
+  EME_LOG("MediaKeySystemAccessManager::Request %s", NS_ConvertUTF16toUTF8(aKeySystem).get());
+  if (!Preferences::GetBool("media.eme.enabled", false)) {
+    // EME disabled by user, send notification to chrome so UI can
+    // inform user.
+    MediaKeySystemAccess::NotifyObservers(mWindow,
+                                          aKeySystem,
+                                          MediaKeySystemStatus::Api_disabled);
+    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  // Parse keysystem, split it out into keySystem prefix, and version suffix.
+  nsAutoString keySystem;
+  int32_t minCdmVersion = NO_CDM_VERSION;
+  if (!ParseKeySystem(aKeySystem,
+                      keySystem,
+                      minCdmVersion)) {
+    // Invalid keySystem string, or unsupported keySystem. Send notification
+    // to chrome to show a failure notice.
+    MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, MediaKeySystemStatus::Cdm_not_supported);
+    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  MediaKeySystemStatus status = MediaKeySystemAccess::GetKeySystemStatus(keySystem, minCdmVersion);
+  if ((status == MediaKeySystemStatus::Cdm_not_installed ||
+       status == MediaKeySystemStatus::Cdm_insufficient_version) &&
+      keySystem.EqualsLiteral("com.adobe.primetime")) {
+    // These are cases which could be resolved by downloading a new(er) CDM.
+    // When we send the status to chrome, chrome's GMPProvider will attempt to
+    // download or update the CDM. In AwaitInstall() we add listeners to wait
+    // for the update to complete, and we'll call this function again with
+    // aType==Subsequent once the download has completed and the GMPService
+    // has had a new plugin added. AwaitInstall() sets a timer to fail if the
+    // update/download takes too long or fails.
+    if (aType == RequestType::Initial &&
+        AwaitInstall(aPromise, aKeySystem, aOptions)) {
+      // Notify chrome that we're going to wait for the CDM to download/update.
+      // Note: If we're re-trying, we don't re-send the notificaiton,
+      // as chrome is already displaying the "we can't play, updating"
+      // notification.
+      MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status);
+    } else {
+      // We waited or can't wait for an update and we still can't service
+      // the request. Give up. Chrome will still be showing a "I can't play,
+      // updating" notification.
+      aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    }
+    return;
+  }
+  if (status != MediaKeySystemStatus::Available) {
+    if (status != MediaKeySystemStatus::Error) {
+      // Failed due to user disabling something, send a notification to
+      // chrome, so we can show some UI to explain how the user can rectify
+      // the situation.
+      MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status);
+    }
+    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  if (aOptions.IsEmpty() ||
+      MediaKeySystemAccess::IsSupported(keySystem, aOptions)) {
+    nsRefPtr<MediaKeySystemAccess> access(new MediaKeySystemAccess(mWindow, keySystem));
+    aPromise->MaybeResolve(access);
+    return;
+  }
+
+  aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+}
+
+MediaKeySystemAccessManager::PendingRequest::PendingRequest(Promise* aPromise,
+                                                            const nsAString& aKeySystem,
+                                                            const Sequence<MediaKeySystemOptions>& aOptions,
+                                                            nsITimer* aTimer)
+  : mPromise(aPromise)
+  , mKeySystem(aKeySystem)
+  , mOptions(aOptions)
+  , mTimer(aTimer)
+{
+  MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+MediaKeySystemAccessManager::PendingRequest::PendingRequest(const PendingRequest& aOther)
+  : mPromise(aOther.mPromise)
+  , mKeySystem(aOther.mKeySystem)
+  , mOptions(aOther.mOptions)
+  , mTimer(aOther.mTimer)
+{
+  MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+MediaKeySystemAccessManager::PendingRequest::~PendingRequest()
+{
+  MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+void
+MediaKeySystemAccessManager::PendingRequest::CancelTimer()
+{
+  if (mTimer) {
+    mTimer->Cancel();
+  }
+}
+
+void
+MediaKeySystemAccessManager::PendingRequest::RejectPromise()
+{
+  if (mPromise) {
+    mPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+  }
+}
+
+bool
+MediaKeySystemAccessManager::AwaitInstall(Promise* aPromise,
+                                          const nsAString& aKeySystem,
+                                          const Sequence<MediaKeySystemOptions>& aOptions)
+{
+  EME_LOG("MediaKeySystemAccessManager::AwaitInstall %s", NS_ConvertUTF16toUTF8(aKeySystem).get());
+
+  if (!EnsureObserversAdded()) {
+    NS_WARNING("Failed to add pref observer");
+    return false;
+  }
+
+  nsCOMPtr<nsITimer> timer(do_CreateInstance("@mozilla.org/timer;1"));
+  if (!timer || NS_FAILED(timer->Init(this, 60 * 1000, nsITimer::TYPE_ONE_SHOT))) {
+    NS_WARNING("Failed to create timer to await CDM install.");
+    return false;
+  }
+
+  mRequests.AppendElement(PendingRequest(aPromise, aKeySystem, aOptions, timer));
+  return true;
+}
+
+void
+MediaKeySystemAccessManager::RetryRequest(PendingRequest& aRequest)
+{
+  aRequest.CancelTimer();
+  Request(aRequest.mPromise, aRequest.mKeySystem, aRequest.mOptions, RequestType::Subsequent);
+}
+
+nsresult
+MediaKeySystemAccessManager::Observe(nsISupports* aSubject,
+                                     const char* aTopic,
+                                     const char16_t* aData)
+{
+  EME_LOG("MediaKeySystemAccessManager::Observe %s", aTopic);
+
+  if (!strcmp(aTopic, "gmp-path-added")) {
+    nsTArray<PendingRequest> requests(Move(mRequests));
+    // Retry all pending requests, but this time fail if the CDM is not installed.
+    for (PendingRequest& request : requests) {
+      RetryRequest(request);
+    }
+  } else if (!strcmp(aTopic, "timer-callback")) {
+    // Find the timer that expired and re-run the request for it.
+    nsCOMPtr<nsITimer> timer(do_QueryInterface(aSubject));
+    for (size_t i = 0; i < mRequests.Length(); i++) {
+      if (mRequests[i].mTimer == timer) {
+        EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request");
+        PendingRequest request = mRequests[i];
+        mRequests.RemoveElementAt(i);
+        RetryRequest(request);
+        break;
+      }
+    }
+  }
+  return NS_OK;
+}
+
+bool
+MediaKeySystemAccessManager::EnsureObserversAdded()
+{
+  if (mAddedObservers) {
+    return true;
+  }
+
+  nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+  if (NS_WARN_IF(!obsService)) {
+    return false;
+  }
+  mAddedObservers = NS_SUCCEEDED(obsService->AddObserver(this, "gmp-path-added", false));
+  return mAddedObservers;
+}
+
+void
+MediaKeySystemAccessManager::Shutdown()
+{
+  EME_LOG("MediaKeySystemAccessManager::Shutdown");
+  nsTArray<PendingRequest> requests(Move(mRequests));
+  for (PendingRequest& request : requests) {
+    // Cancel all requests; we're shutting down.
+    request.CancelTimer();
+    request.RejectPromise();
+  }
+  if (mAddedObservers) {
+    nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+    if (obsService) {
+      obsService->RemoveObserver(this, "gmp-path-added");
+      mAddedObservers = false;
+    }
+  }
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.h
@@ -0,0 +1,80 @@
+/* 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/. */
+
+#ifndef mozilla_dom_MediaKeySystemAccessManager_h
+#define mozilla_dom_MediaKeySystemAccessManager_h
+
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "nsIObserver.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeySystemAccessManager final : public nsIObserver
+{
+public:
+
+  explicit MediaKeySystemAccessManager(nsPIDOMWindow* aWindow);
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager, nsIObserver)
+  NS_DECL_NSIOBSERVER
+
+  void Request(Promise* aPromise,
+               const nsAString& aKeySystem,
+               const Optional<Sequence<MediaKeySystemOptions>>& aOptions);
+
+  void Shutdown();
+
+  struct PendingRequest {
+    PendingRequest(Promise* aPromise,
+      const nsAString& aKeySystem,
+      const Sequence<MediaKeySystemOptions>& aOptions,
+      nsITimer* aTimer);
+    PendingRequest(const PendingRequest& aOther);
+    ~PendingRequest();
+    void CancelTimer();
+    void RejectPromise();
+
+    nsRefPtr<Promise> mPromise;
+    const nsString mKeySystem;
+    const Sequence<MediaKeySystemOptions> mOptions;
+    nsCOMPtr<nsITimer> mTimer;
+  };
+
+private:
+
+  enum RequestType {
+    Initial,
+    Subsequent
+  };
+
+  void Request(Promise* aPromise,
+               const nsAString& aKeySystem,
+               const Sequence<MediaKeySystemOptions>& aOptions,
+               RequestType aType);
+
+  ~MediaKeySystemAccessManager();
+
+  bool EnsureObserversAdded();
+
+  bool AwaitInstall(Promise* aPromise,
+                    const nsAString& aKeySystem,
+                    const Sequence<MediaKeySystemOptions>& aOptions);
+
+  void RetryRequest(PendingRequest& aRequest);
+
+  nsTArray<PendingRequest> mRequests;
+
+  nsCOMPtr<nsPIDOMWindow> mWindow;
+  bool mAddedObservers;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
--- a/dom/media/eme/moz.build
+++ b/dom/media/eme/moz.build
@@ -7,16 +7,17 @@
 EXPORTS.mozilla.dom += [
     'MediaEncryptedEvent.h',
     'MediaKeyError.h',
     'MediaKeyMessageEvent.h',
     'MediaKeys.h',
     'MediaKeySession.h',
     'MediaKeyStatusMap.h',
     'MediaKeySystemAccess.h',
+    'MediaKeySystemAccessManager.h',
 ]
 
 EXPORTS.mozilla += [
     'CDMCallbackProxy.h',
     'CDMCaps.h',
     'CDMProxy.h',
     'EMEUtils.h'
 ]
@@ -28,13 +29,14 @@ UNIFIED_SOURCES += [
     'EMEUtils.cpp',
     'MediaEncryptedEvent.cpp',
     'MediaKeyError.cpp',
     'MediaKeyMessageEvent.cpp',
     'MediaKeys.cpp',
     'MediaKeySession.cpp',
     'MediaKeyStatusMap.cpp',
     'MediaKeySystemAccess.cpp',
+    'MediaKeySystemAccessManager.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 FAIL_ON_WARNINGS = True
--- a/dom/media/gmp/GMPService.cpp
+++ b/dom/media/gmp/GMPService.cpp
@@ -940,16 +940,35 @@ GeckoMediaPluginService::ClonePlugin(con
   }
 
   MutexAutoLock lock(mMutex);
   mPlugins.AppendElement(gmp);
 
   return gmp.get();
 }
 
+class NotifyObserversTask final : public nsRunnable {
+public:
+  explicit NotifyObserversTask(const char* aTopic)
+    : mTopic(aTopic)
+  {}
+  NS_IMETHOD Run() override {
+    MOZ_ASSERT(NS_IsMainThread());
+    nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+    MOZ_ASSERT(obsService);
+    if (obsService) {
+      obsService->NotifyObservers(nullptr, mTopic, nullptr);
+    }
+    return NS_OK;
+  }
+private:
+  ~NotifyObserversTask() {}
+  const char* mTopic;
+};
+
 void
 GeckoMediaPluginService::AddOnGMPThread(const nsAString& aDirectory)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
   LOGD(("%s::%s: %s", __CLASS__, __FUNCTION__, NS_LossyConvertUTF16toASCII(aDirectory).get()));
 
   nsCOMPtr<nsIFile> directory;
   nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory));
@@ -965,18 +984,22 @@ GeckoMediaPluginService::AddOnGMPThread(
   mozilla::SyncRunnable::DispatchToThread(mainThread, task);
   nsRefPtr<GMPParent> gmp = task->GetParent();
   rv = gmp ? gmp->Init(this, directory) : NS_ERROR_NOT_AVAILABLE;
   if (NS_FAILED(rv)) {
     NS_WARNING("Can't Create GMPParent");
     return;
   }
 
-  MutexAutoLock lock(mMutex);
-  mPlugins.AppendElement(gmp);
+  {
+    MutexAutoLock lock(mMutex);
+    mPlugins.AppendElement(gmp);
+  }
+
+  NS_DispatchToMainThread(new NotifyObserversTask("gmp-path-added"), NS_DISPATCH_NORMAL);
 }
 
 void
 GeckoMediaPluginService::RemoveOnGMPThread(const nsAString& aDirectory,
                                            const bool aDeleteFromDisk)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
   LOGD(("%s::%s: %s", __CLASS__, __FUNCTION__, NS_LossyConvertUTF16toASCII(aDirectory).get()));
@@ -1470,29 +1493,16 @@ GeckoMediaPluginService::ForgetThisSiteO
     }
   private:
     const nsACString& mOrigin;
   } filter(aOrigin);
 
   ClearNodeIdAndPlugin(filter);
 }
 
-class StorageClearedTask : public nsRunnable {
-public:
-  NS_IMETHOD Run() {
-    MOZ_ASSERT(NS_IsMainThread());
-    nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
-    MOZ_ASSERT(obsService);
-    if (obsService) {
-      obsService->NotifyObservers(nullptr, "gmp-clear-storage-complete", nullptr);
-    }
-    return NS_OK;
-  }
-};
-
 void
 GeckoMediaPluginService::ClearRecentHistoryOnGMPThread(PRTime aSince)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
   LOGD(("%s::%s: since=%lld", __CLASS__, __FUNCTION__, (int64_t)aSince));
 
   nsCOMPtr<nsIFile> storagePath;
   nsCOMPtr<nsIFile> temp;
@@ -1567,17 +1577,17 @@ GeckoMediaPluginService::ClearRecentHist
     }
   private:
     const PRTime mSince;
     const nsCOMPtr<nsIFile> mStoragePath;
   } filter(aSince, storagePath.forget());
 
   ClearNodeIdAndPlugin(filter);
 
-  NS_DispatchToMainThread(new StorageClearedTask(), NS_DISPATCH_NORMAL);
+  NS_DispatchToMainThread(new NotifyObserversTask("gmp-clear-storage-complete"), NS_DISPATCH_NORMAL);
 }
 
 NS_IMETHODIMP
 GeckoMediaPluginService::ForgetThisSite(const nsAString& aOrigin)
 {
   MOZ_ASSERT(NS_IsMainThread());
   return GMPDispatch(NS_NewRunnableMethodWithArg<nsCString>(
       this, &GeckoMediaPluginService::ForgetThisSiteOnGMPThread,
@@ -1606,13 +1616,13 @@ GeckoMediaPluginService::ClearStorage()
   nsresult rv = GetStorageDir(getter_AddRefs(path));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   if (NS_FAILED(DeleteDir(path))) {
     NS_WARNING("Failed to delete GMP storage directory");
   }
-  NS_DispatchToMainThread(new StorageClearedTask(), NS_DISPATCH_NORMAL);
+  NS_DispatchToMainThread(new NotifyObserversTask("gmp-clear-storage-complete"), NS_DISPATCH_NORMAL);
 }
 
 } // namespace gmp
 } // namespace mozilla