Bug 1148071 - Fix CDM update behaviour. r=cpearce, a=sledru, ba=jorgev
authorEdwin Flores <edwin@mozilla.com>
Sat, 04 Apr 2015 17:03:16 +1300
changeset 258493 6c7e8d9f955c
parent 258492 7b296a71b115
child 258494 98703ce041e2
push id4679
push userryanvm@gmail.com
push date2015-04-15 17:06 +0000
treeherdermozilla-beta@6c7e8d9f955c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscpearce, sledru
bugs1148071
milestone38.0
Bug 1148071 - Fix CDM update behaviour. r=cpearce, a=sledru, ba=jorgev
dom/media/eme/CDMProxy.cpp
dom/media/gmp/GMPParent.cpp
dom/media/gmp/GMPParent.h
dom/media/gmp/GMPProcessParent.cpp
dom/media/gmp/GMPProcessParent.h
dom/media/gmp/GMPService.cpp
dom/media/gmp/GMPService.h
dom/media/gmp/mozIGeckoMediaPluginService.idl
dom/media/gtest/TestGMPCrossOrigin.cpp
toolkit/mozapps/extensions/internal/GMPProvider.jsm
--- a/dom/media/eme/CDMProxy.cpp
+++ b/dom/media/eme/CDMProxy.cpp
@@ -89,34 +89,48 @@ CDMProxy::gmp_Init(nsAutoPtr<InitData> a
 
   nsCOMPtr<mozIGeckoMediaPluginService> mps =
     do_GetService("@mozilla.org/gecko-media-plugin-service;1");
   if (!mps) {
     RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
-  nsresult rv = mps->GetNodeId(aData->mOrigin,
-                               aData->mTopLevelOrigin,
-                               aData->mInPrivateBrowsing,
-                               mNodeId);
+  nsCString version;
+  nsTArray<nsCString> tags;
+  tags.AppendElement(NS_ConvertUTF16toUTF8(mKeySystem));
+
+  // Add plugin version string to node ID so that we get the same
+  // CDM every time during a stream's playback.
+  nsresult rv =
+    mps->GetPluginVersionForAPI(NS_LITERAL_CSTRING(GMP_API_DECRYPTOR),
+                                &tags, version);
+  if (NS_FAILED(rv)) {
+    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  rv = mps->GetNodeId(aData->mOrigin,
+                      aData->mTopLevelOrigin,
+                      aData->mInPrivateBrowsing,
+                      version,
+                      mNodeId);
+
   MOZ_ASSERT(!GetNodeId().IsEmpty());
   if (NS_FAILED(rv)) {
     RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   EME_LOG("CDMProxy::gmp_Init (%s, %s) %s NodeId=%s",
           NS_ConvertUTF16toUTF8(aData->mOrigin).get(),
           NS_ConvertUTF16toUTF8(aData->mTopLevelOrigin).get(),
           (aData->mInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing"),
           GetNodeId().get());
 
-  nsTArray<nsCString> tags;
-  tags.AppendElement(NS_ConvertUTF16toUTF8(mKeySystem));
   rv = mps->GetGMPDecryptor(&tags, GetNodeId(), &mCDM);
   if (NS_FAILED(rv) || !mCDM) {
     RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
   } else {
     mCallback = new CDMCallbackProxy(this);
     mCDM->Init(mCallback);
     nsRefPtr<nsIRunnable> task(
       NS_NewRunnableMethodWithArg<uint32_t>(this,
--- a/dom/media/gmp/GMPParent.cpp
+++ b/dom/media/gmp/GMPParent.cpp
@@ -55,16 +55,17 @@ extern PRLogModuleInfo* GetGMPLog();
 
 namespace gmp {
 
 GMPParent::GMPParent()
   : mState(GMPStateNotLoaded)
   , mProcess(nullptr)
   , mDeleteProcessOnlyOnUnload(false)
   , mAbnormalShutdownInProgress(false)
+  , mIsBlockingDeletion(false)
   , mAsyncShutdownRequired(false)
   , mAsyncShutdownInProgress(false)
 {
   // Use the parent address to identify it.
   // We could use any unique-to-the-parent value.
   mPluginId.AppendInt(reinterpret_cast<uint64_t>(this));
 }
 
@@ -335,16 +336,29 @@ GMPParent::CloseActive(bool aDieWhenUnlo
 
   // Note: the shutdown of the codecs is async!  don't kill
   // the plugin-container until they're all safely shut down via
   // CloseIfUnused();
   CloseIfUnused();
 }
 
 void
+GMPParent::MarkForDeletion()
+{
+  mDeleteProcessOnlyOnUnload = true;
+  mIsBlockingDeletion = true;
+}
+
+bool
+GMPParent::IsMarkedForDeletion()
+{
+  return mIsBlockingDeletion;
+}
+
+void
 GMPParent::Shutdown()
 {
   LOGD(("%s::%s: %p", __CLASS__, __FUNCTION__, this));
   MOZ_ASSERT(GMPThread() == NS_GetCurrentThread());
 
   MOZ_ASSERT(!mAsyncShutdownTimeout, "Should have canceled shutdown timeout");
 
   if (mAbnormalShutdownInProgress) {
@@ -380,27 +394,38 @@ public:
       obsService->NotifyObservers(nullptr, "gmp-shutdown", mNodeId.get());
     }
     return NS_OK;
   }
   nsString mNodeId;
 };
 
 void
+GMPParent::ChildTerminated()
+{
+  nsRefPtr<GMPParent> self(this);
+  GMPThread()->Dispatch(NS_NewRunnableMethodWithArg<nsRefPtr<GMPParent>>(
+                          mService,
+                          &GeckoMediaPluginService::PluginTerminated,
+                          self),
+                        NS_DISPATCH_NORMAL);
+}
+
+void
 GMPParent::DeleteProcess()
 {
   LOGD(("%s::%s: %p", __CLASS__, __FUNCTION__, this));
 
   if (mState != GMPStateClosing) {
     // Don't Close() twice!
     // Probably remove when bug 1043671 is resolved
     mState = GMPStateClosing;
     Close();
   }
-  mProcess->Delete();
+  mProcess->Delete(NS_NewRunnableMethod(this, &GMPParent::ChildTerminated));
   LOGD(("%s::%s: Shut down process %p", __CLASS__, __FUNCTION__, (void *) mProcess));
   mProcess = nullptr;
   mState = GMPStateNotLoaded;
 
   NS_DispatchToMainThread(
     new NotifyGMPShutdownTask(NS_ConvertUTF8toUTF16(mNodeId)),
     NS_DISPATCH_NORMAL);
 
--- a/dom/media/gmp/GMPParent.h
+++ b/dom/media/gmp/GMPParent.h
@@ -71,16 +71,20 @@ public:
 
   // Called internally to close this if we don't need it
   void CloseIfUnused();
 
   // Notify all active de/encoders that we are closing, either because of
   // normal shutdown or unexpected shutdown/crash.
   void CloseActive(bool aDieWhenUnloaded);
 
+  // Tell the plugin to die after shutdown.
+  void MarkForDeletion();
+  bool IsMarkedForDeletion();
+
   // Called by the GMPService to forcibly close active de/encoders at shutdown
   void Shutdown();
 
   // This must not be called while we're in the middle of abnormal ActorDestroy
   void DeleteProcess();
 
   bool SupportsAPI(const nsCString& aAPI, const nsCString& aTag);
 
@@ -131,16 +135,19 @@ public:
     return nsCOMPtr<nsIFile>(mDirectory).forget();
   }
 
   // GMPSharedMem
   virtual void CheckThread() override;
 
   void AbortAsyncShutdown();
 
+  // Called when the child process has died.
+  void ChildTerminated();
+
 private:
   ~GMPParent();
   nsRefPtr<GeckoMediaPluginService> mService;
   bool EnsureProcessLoaded();
   nsresult ReadGMPMetaData();
 #ifdef MOZ_CRASHREPORTER
   void WriteExtraDataForMinidump(CrashReporter::AnnotationTable& notes);
   void GetCrashID(nsString& aResult);
@@ -181,16 +188,17 @@ private:
   nsCString mDisplayName; // name of plugin displayed to users
   nsCString mDescription; // description of plugin for display to users
   nsCString mVersion;
   nsCString mPluginId;
   nsTArray<nsAutoPtr<GMPCapability>> mCapabilities;
   GMPProcessParent* mProcess;
   bool mDeleteProcessOnlyOnUnload;
   bool mAbnormalShutdownInProgress;
+  bool mIsBlockingDeletion;
 
   nsTArray<nsRefPtr<GMPVideoDecoderParent>> mVideoDecoders;
   nsTArray<nsRefPtr<GMPVideoEncoderParent>> mVideoEncoders;
   nsTArray<nsRefPtr<GMPDecryptorParent>> mDecryptors;
   nsTArray<nsRefPtr<GMPAudioDecoderParent>> mAudioDecoders;
   nsTArray<nsRefPtr<GMPTimerParent>> mTimers;
   nsTArray<nsRefPtr<GMPStorageParent>> mStorage;
   nsCOMPtr<nsIThread> mGMPThread;
--- a/dom/media/gmp/GMPProcessParent.cpp
+++ b/dom/media/gmp/GMPProcessParent.cpp
@@ -1,10 +1,10 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- * vim: sw=4 ts=4 et :
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et :
  * 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 "GMPProcessParent.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsIFile.h"
 
@@ -63,24 +63,29 @@ GMPProcessParent::Launch(int32_t aTimeou
   std::wstring wGMPPath = UTF8ToWide(mGMPPath.c_str());
   mAllowedFilesRead.push_back(wGMPPath + L"\\*");
 #endif
 
   return SyncLaunch(args, aTimeoutMs, base::GetCurrentProcessArchitecture());
 }
 
 void
-GMPProcessParent::Delete()
+GMPProcessParent::Delete(nsCOMPtr<nsIRunnable> aCallback)
 {
-  MessageLoop* currentLoop = MessageLoop::current();
-  MessageLoop* ioLoop = XRE_GetIOMessageLoop();
+  mDeletedCallback = aCallback;
+  XRE_GetIOMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod(this, &GMPProcessParent::DoDelete));
+}
 
-  if (currentLoop == ioLoop) {
-    Join();
-    delete this;
-    return;
+void
+GMPProcessParent::DoDelete()
+{
+  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
+  Join();
+
+  if (mDeletedCallback) {
+    mDeletedCallback->Run();
   }
 
-  ioLoop->PostTask(FROM_HERE, NewRunnableMethod(this, &GMPProcessParent::Delete));
+  delete this;
 }
 
 } // namespace gmp
 } // namespace mozilla
--- a/dom/media/gmp/GMPProcessParent.h
+++ b/dom/media/gmp/GMPProcessParent.h
@@ -10,40 +10,45 @@
 #include "mozilla/Attributes.h"
 #include "base/basictypes.h"
 #include "base/file_path.h"
 #include "base/thread.h"
 #include "base/waitable_event.h"
 #include "chrome/common/child_process_host.h"
 #include "mozilla/ipc/GeckoChildProcessHost.h"
 
+class nsIRunnable;
+
 namespace mozilla {
 namespace gmp {
 
 class GMPProcessParent final : public mozilla::ipc::GeckoChildProcessHost
 {
 public:
   explicit GMPProcessParent(const std::string& aGMPPath);
   ~GMPProcessParent();
 
   // Synchronously launch the plugin process. If the process fails to launch
   // after timeoutMs, this method will return false.
   bool Launch(int32_t aTimeoutMs);
 
-  void Delete();
+  void Delete(nsCOMPtr<nsIRunnable> aCallback = nullptr);
 
   virtual bool CanShutdown() override { return true; }
   const std::string& GetPluginFilePath() { return mGMPPath; }
 
   using mozilla::ipc::GeckoChildProcessHost::GetShutDownEvent;
   using mozilla::ipc::GeckoChildProcessHost::GetChannel;
   using mozilla::ipc::GeckoChildProcessHost::GetChildProcessHandle;
 
 private:
+  void DoDelete();
+
   std::string mGMPPath;
+  nsCOMPtr<nsIRunnable> mDeletedCallback;
 
   DISALLOW_COPY_AND_ASSIGN(GMPProcessParent);
 };
 
 } // namespace gmp
 } // namespace mozilla
 
 #endif // ifndef GMPProcessParent_h
--- a/dom/media/gmp/GMPService.cpp
+++ b/dom/media/gmp/GMPService.cpp
@@ -711,17 +711,18 @@ GeckoMediaPluginService::LoadFromEnviron
 
 NS_IMETHODIMP
 GeckoMediaPluginService::PathRunnable::Run()
 {
   if (mOperation == ADD) {
     mService->AddOnGMPThread(mPath);
   } else {
     mService->RemoveOnGMPThread(mPath,
-                                mOperation == REMOVE_AND_DELETE_FROM_DISK);
+                                mOperation == REMOVE_AND_DELETE_FROM_DISK,
+                                mDefer);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 GeckoMediaPluginService::AddPluginDirectory(const nsAString& aDirectory)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -734,50 +735,64 @@ GeckoMediaPluginService::RemovePluginDir
 {
   MOZ_ASSERT(NS_IsMainThread());
   return GMPDispatch(new PathRunnable(this, aDirectory,
                                       PathRunnable::EOperation::REMOVE));
 }
 
 NS_IMETHODIMP
 GeckoMediaPluginService::RemoveAndDeletePluginDirectory(
-  const nsAString& aDirectory)
+  const nsAString& aDirectory, const bool aDefer)
 {
   MOZ_ASSERT(NS_IsMainThread());
   return GMPDispatch(
     new PathRunnable(this, aDirectory,
-                     PathRunnable::EOperation::REMOVE_AND_DELETE_FROM_DISK));
+                     PathRunnable::EOperation::REMOVE_AND_DELETE_FROM_DISK,
+                     aDefer));
 }
 
 class DummyRunnable : public nsRunnable {
 public:
   NS_IMETHOD Run() { return NS_OK; }
 };
 
 NS_IMETHODIMP
 GeckoMediaPluginService::GetPluginVersionForAPI(const nsACString& aAPI,
                                                 nsTArray<nsCString>* aTags,
                                                 nsACString& aOutVersion)
 {
   NS_ENSURE_ARG(aTags && aTags->Length() > 0);
+  NS_ENSURE_ARG(aOutVersion.IsEmpty());
 
   nsresult rv = EnsurePluginsOnDiskScanned();
   if (NS_FAILED(rv)) {
     NS_WARNING("Failed to load GMPs from disk.");
     return rv;
   }
 
   {
     MutexAutoLock lock(mMutex);
     nsCString api(aAPI);
-    GMPParent* gmp = FindPluginForAPIFrom(0, api, *aTags, nullptr);
-    if (!gmp) {
+    size_t index = 0;
+
+    // We must parse the version number into a float for comparison. Yuck.
+    double maxParsedVersion = -1.;
+
+    while (GMPParent* gmp = FindPluginForAPIFrom(index, api, *aTags, &index)) {
+      double parsedVersion = atof(gmp->GetVersion().get());
+      if (maxParsedVersion < 0 || parsedVersion > maxParsedVersion) {
+        maxParsedVersion = parsedVersion;
+        aOutVersion = gmp->GetVersion();
+      }
+      index++;
+    }
+
+    if (maxParsedVersion < 0) {
       return NS_ERROR_FAILURE;
     }
-    aOutVersion = gmp->GetVersion();
   }
 
   return NS_OK;
 }
 
 nsresult
 GeckoMediaPluginService::EnsurePluginsOnDiskScanned()
 {
@@ -994,55 +1009,89 @@ GeckoMediaPluginService::AddOnGMPThread(
     mPlugins.AppendElement(gmp);
   }
 
   NS_DispatchToMainThread(new NotifyObserversTask("gmp-path-added"), NS_DISPATCH_NORMAL);
 }
 
 void
 GeckoMediaPluginService::RemoveOnGMPThread(const nsAString& aDirectory,
-                                           const bool aDeleteFromDisk)
+                                           const bool aDeleteFromDisk,
+                                           const bool aCanDefer)
 {
   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));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   MutexAutoLock lock(mMutex);
-  for (size_t i = 0; i < mPlugins.Length(); ++i) {
+  for (size_t i = mPlugins.Length() - 1; i < mPlugins.Length(); i--) {
     nsCOMPtr<nsIFile> pluginpath = mPlugins[i]->GetDirectory();
     bool equals;
-    if (NS_SUCCEEDED(directory->Equals(pluginpath, &equals)) && equals) {
-      mPlugins[i]->AbortAsyncShutdown();
-      mPlugins[i]->CloseActive(true);
-      if (aDeleteFromDisk) {
-        pluginpath->Remove(true);
+    if (NS_FAILED(directory->Equals(pluginpath, &equals)) || !equals) {
+      continue;
+    }
+
+    nsRefPtr<GMPParent> gmp = mPlugins[i];
+    if (aDeleteFromDisk && gmp->State() != GMPStateNotLoaded) {
+      // We have to wait for the child process to release its lib handle
+      // before we can delete the GMP.
+      gmp->MarkForDeletion();
+
+      if (!mPluginsWaitingForDeletion.Contains(aDirectory)) {
+        mPluginsWaitingForDeletion.AppendElement(aDirectory);
       }
+    }
+
+    if (gmp->State() == GMPStateNotLoaded || !aCanDefer) {
+      // GMP not in use or shutdown is being forced; can shut it down now.
+      gmp->AbortAsyncShutdown();
+      gmp->CloseActive(true);
       mPlugins.RemoveElementAt(i);
-      return;
     }
   }
-  NS_WARNING("Removing GMP which was never added.");
-  nsCOMPtr<nsIConsoleService> cs = do_GetService(NS_CONSOLESERVICE_CONTRACTID);
-  cs->LogStringMessage(MOZ_UTF16("Removing GMP which was never added."));
+
+  if (aDeleteFromDisk) {
+    if (NS_SUCCEEDED(directory->Remove(true))) {
+      mPluginsWaitingForDeletion.RemoveElement(aDirectory);
+    }
+  }
 }
 
 // May remove when Bug 1043671 is fixed
 static void Dummy(nsRefPtr<GMPParent>& aOnDeathsDoor)
 {
   // exists solely to do nothing and let the Runnable kill the GMPParent
   // when done.
 }
 
 void
-GeckoMediaPluginService::ReAddOnGMPThread(nsRefPtr<GMPParent>& aOld)
+GeckoMediaPluginService::PluginTerminated(const nsRefPtr<GMPParent>& aPlugin)
+{
+  MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
+
+  if (aPlugin->IsMarkedForDeletion()) {
+    nsCString path8;
+    nsRefPtr<nsIFile> dir = aPlugin->GetDirectory();
+    nsresult rv = dir->GetNativePath(path8);
+    NS_ENSURE_SUCCESS_VOID(rv);
+
+    nsString path = NS_ConvertUTF8toUTF16(path8);
+    if (mPluginsWaitingForDeletion.Contains(path)) {
+      RemoveOnGMPThread(path, true /* delete */, true /* can defer */);
+    }
+  }
+}
+
+void
+GeckoMediaPluginService::ReAddOnGMPThread(const nsRefPtr<GMPParent>& aOld)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
   LOGD(("%s::%s: %p", __CLASS__, __FUNCTION__, (void*) aOld));
 
   nsRefPtr<GMPParent> gmp;
   if (!mShuttingDownOnGMPThread) {
     // Don't re-add plugin if we're shutting down. Let the old plugin die.
     gmp = ClonePlugin(aOld);
@@ -1159,16 +1208,17 @@ GeckoMediaPluginService::IsPersistentSto
   *aOutAllowed = mPersistentStorageAllowed.Get(aNodeId);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 GeckoMediaPluginService::GetNodeId(const nsAString& aOrigin,
                                    const nsAString& aTopLevelOrigin,
                                    bool aInPrivateBrowsing,
+                                   const nsACString& aVersion,
                                    nsACString& aOutId)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
   LOGD(("%s::%s: (%s, %s), %s", __CLASS__, __FUNCTION__,
        NS_ConvertUTF16toUTF8(aOrigin).get(),
        NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(),
        (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")));
 
@@ -1192,17 +1242,18 @@ GeckoMediaPluginService::GetNodeId(const
       return rv;
     }
     aOutId = salt;
     mPersistentStorageAllowed.Put(salt, false);
     return NS_OK;
   }
 
   const uint32_t hash = AddToHash(HashString(aOrigin),
-                                  HashString(aTopLevelOrigin));
+                                  HashString(aTopLevelOrigin),
+                                  HashString(aVersion));
 
   if (aInPrivateBrowsing) {
     // For PB mode, we store the node id, indexed by the origin pair,
     // so that if the same origin pair is opened in this session, it gets
     // the same node id.
     nsCString* salt = nullptr;
     if (!(salt = mTempNodeIds.Get(hash))) {
       // No salt stored, generate and temporarily store some for this id.
--- a/dom/media/gmp/GMPService.h
+++ b/dom/media/gmp/GMPService.h
@@ -96,58 +96,62 @@ private:
   void CrashPlugins();
   void SetAsyncShutdownComplete();
 
   void LoadFromEnvironment();
   void ProcessPossiblePlugin(nsIFile* aDir);
 
   void AddOnGMPThread(const nsAString& aDirectory);
   void RemoveOnGMPThread(const nsAString& aDirectory,
-                         const bool aDeleteFromDisk);
+                         const bool aDeleteFromDisk,
+                         const bool aCanDefer);
 
   nsresult SetAsyncShutdownTimeout();
 
   struct DirectoryFilter {
     virtual bool operator()(nsIFile* aPath) = 0;
     ~DirectoryFilter() {}
   };
   void ClearNodeIdAndPlugin(DirectoryFilter& aFilter);
 
   void ForgetThisSiteOnGMPThread(const nsACString& aOrigin);
   void ClearRecentHistoryOnGMPThread(PRTime aSince);
 
 protected:
   friend class GMPParent;
-  void ReAddOnGMPThread(nsRefPtr<GMPParent>& aOld);
+  void ReAddOnGMPThread(const nsRefPtr<GMPParent>& aOld);
+  void PluginTerminated(const nsRefPtr<GMPParent>& aOld);
 private:
   GMPParent* ClonePlugin(const GMPParent* aOriginal);
   nsresult EnsurePluginsOnDiskScanned();
 
   class PathRunnable : public nsRunnable
   {
   public:
     enum EOperation {
       ADD,
       REMOVE,
       REMOVE_AND_DELETE_FROM_DISK,
     };
 
     PathRunnable(GeckoMediaPluginService* aService, const nsAString& aPath,
-                 EOperation aOperation)
+                 EOperation aOperation, bool aDefer = false)
       : mService(aService)
       , mPath(aPath)
       , mOperation(aOperation)
+      , mDefer(aDefer)
     { }
 
     NS_DECL_NSIRUNNABLE
 
   private:
     nsRefPtr<GeckoMediaPluginService> mService;
     nsString mPath;
     EOperation mOperation;
+    bool mDefer;
   };
 
   Mutex mMutex; // Protects mGMPThread and mShuttingDown and mPlugins
   nsTArray<nsRefPtr<GMPParent>> mPlugins;
   nsCOMPtr<nsIThread> mGMPThread;
   bool mShuttingDown;
   bool mShuttingDownOnGMPThread;
 
@@ -171,16 +175,18 @@ private:
   private:
     T mValue;
   };
 
   MainThreadOnly<bool> mWaitingForPluginsAsyncShutdown;
 
   nsTArray<nsRefPtr<GMPParent>> mAsyncShutdownPlugins; // GMP Thread only.
 
+  nsTArray<nsString> mPluginsWaitingForDeletion;
+
 #ifndef MOZ_WIDGET_GONK
   nsCOMPtr<nsIFile> mStorageBaseDir;
 #endif
 
   // Hashes of (origin,topLevelOrigin) to the node id for
   // non-persistent sessions.
   nsClassHashtable<nsUint32HashKey, nsCString> mTempNodeIds;
 
--- a/dom/media/gmp/mozIGeckoMediaPluginService.idl
+++ b/dom/media/gmp/mozIGeckoMediaPluginService.idl
@@ -21,17 +21,17 @@ class GMPVideoHost;
 [ptr] native GMPVideoDecoderProxy(GMPVideoDecoderProxy);
 [ptr] native GMPVideoEncoderProxy(GMPVideoEncoderProxy);
 [ptr] native GMPVideoHost(GMPVideoHost);
 [ptr] native MessageLoop(MessageLoop);
 [ptr] native TagArray(nsTArray<nsCString>);
 [ptr] native GMPDecryptorProxy(GMPDecryptorProxy);
 [ptr] native GMPAudioDecoderProxy(GMPAudioDecoderProxy);
 
-[scriptable, uuid(23ae33ba-661b-4cc8-8095-813971ecdc6b)]
+[scriptable, uuid(d520197c-efcf-47c7-8ef9-1589d0c0f588)]
 interface mozIGeckoMediaPluginService : nsISupports
 {
 
   /**
    * The GMP thread. Callable from any thread.
    */
   readonly attribute nsIThread thread;
 
@@ -94,26 +94,29 @@ interface mozIGeckoMediaPluginService : 
   /**
    * Remove a directory for gecko media plugins.
    * @note Main-thread API.
    */
   void removePluginDirectory(in AString directory);
 
   /**
    * Remove a directory for gecko media plugins and delete it from disk.
+   * If |defer| is true, wait until the plugin is unused before removing.
    * @note Main-thread API.
    */
-  void removeAndDeletePluginDirectory(in AString directory);
+  void removeAndDeletePluginDirectory(in AString directory,
+                                      [optional] in bool defer);
 
   /**
    * Gets the NodeId for a (origin, urlbarOrigin, isInprivateBrowsing) tuple.
    */
   ACString getNodeId(in AString origin,
                      in AString topLevelOrigin,
-                     in bool inPrivateBrowsingMode);
+                     in bool inPrivateBrowsingMode,
+                     in ACString version);
 
   /**
    * Clears storage data associated with the site.
    */
   void forgetThisSite(in AString site);
 
   /**
    * Returns true if the given node id is allowed to store things
--- a/dom/media/gtest/TestGMPCrossOrigin.cpp
+++ b/dom/media/gtest/TestGMPCrossOrigin.cpp
@@ -319,16 +319,17 @@ GetNodeId(const nsAString& aOrigin,
 {
   nsRefPtr<GeckoMediaPluginService> service =
     GeckoMediaPluginService::GetGeckoMediaPluginService();
   EXPECT_TRUE(service);
   nsCString nodeId;
   nsresult rv = service->GetNodeId(aOrigin,
                                    aTopLevelOrigin,
                                    aInPBMode,
+                                   NS_LITERAL_CSTRING(""),
                                    nodeId);
   EXPECT_TRUE(NS_SUCCEEDED(rv));
   return nodeId;
 }
 
 static bool
 IsGMPStorageIsEmpty()
 {
--- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -389,17 +389,17 @@ GMPWrapper.prototype = {
     }
   },
 
   onPrefVersionChanged: function() {
     AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
     if (this._gmpPath) {
       this._log.info("onPrefVersionChanged() - unregistering gmp directory " +
                      this._gmpPath);
-      gmpService.removeAndDeletePluginDirectory(this._gmpPath);
+      gmpService.removeAndDeletePluginDirectory(this._gmpPath, true /* can defer */);
     }
     AddonManagerPrivate.callAddonListeners("onUninstalled", this);
 
     AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
                                              null, false);
     AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
     this._gmpPath = null;
     if (this.isInstalled) {