Bug 1472580 - Ensure we always get a allow/cancel response to an autoplay media permission request. r=smaug draft
authorChris Pearce <cpearce@mozilla.com>
Fri, 06 Jul 2018 21:15:20 +1200
changeset 818506 dbca520a93d8c416f6d64c2da027630181bb5910
parent 818505 f6a97dcdbc12cb53d58be6e7fe19de7825755425
push id116278
push userbmo:cpearce@mozilla.com
push dateSun, 15 Jul 2018 03:47:02 +0000
reviewerssmaug
bugs1472580
milestone63.0a1
Bug 1472580 - Ensure we always get a allow/cancel response to an autoplay media permission request. r=smaug The front end code can't always guarantee to give us an allow/cancel response to a permission request. In particular in these cases: * if we close a tab while showing a doorhanger, or * if we navigate a tab while showing a doorhanger, or * if the permission prompt requested in a background tab and never shown. Handling all of these cases is problematic; we don't get events for all of these where it's easy and cheap to determine that we should cancel the permission request. Canceling the permission request is important in the autoplay-media permission request case as there's objects waiting on the resolution of the permission request, and they leak in ASan builds while running chrome tests if the Gecko size of the permission request doesn't get a notification telling it to stop waiting. But we can however rely on the doorhanger code to drop its reference to the nsIContentPermissionRequest object that we pass to it when the doorhanger goes away. So we can cancel the permission request in our nsIContentPermissionRequest's implementation's destructor in order to easily catch all the above cases. In order to do that, we need to split AutoplayRequest into two; one part being the implementation of nsIContentPermissionRequest (AutoplayPermissionRequest), and the other part being the code to own the PromiseHolder and manage the permission request (AutoplayPermissionManager). AutoplayPermissionRequest keeps a weak reference to AutoplayPermissionManager, so that it can tell the AutoplayPermissionManager to reject the request promise when it's destroyed. This fixes the ASan leak for which I got backed out from earlier in this bug, and also fixes the cases above. MozReview-Commit-ID: KoVkgIqDleW
dom/base/nsGlobalWindowInner.cpp
dom/base/nsPIDOMWindow.h
dom/html/AutoplayPermissionManager.cpp
dom/html/AutoplayPermissionManager.h
dom/html/AutoplayPermissionRequest.cpp
dom/html/AutoplayPermissionRequest.h
dom/html/AutoplayRequest.cpp
dom/html/AutoplayRequest.h
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/html/moz.build
dom/media/AutoplayPolicy.cpp
dom/media/AutoplayPolicy.h
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -12,17 +12,17 @@
 
 // Local Includes
 #include "Navigator.h"
 #include "nsContentSecurityManager.h"
 #include "nsScreen.h"
 #include "nsHistory.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsIDOMStorageManager.h"
-#include "mozilla/dom/AutoplayRequest.h"
+#include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/DOMJSProxyHandler.h"
 #include "mozilla/dom/DOMPrefs.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
 #include "mozilla/dom/Storage.h"
 #include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
@@ -8464,29 +8464,31 @@ GetTopLevelInnerWindow(nsPIDOMWindowInne
   nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
   docShell->GetSameTypeRootTreeItem(getter_AddRefs(rootTreeItem));
   if (!rootTreeItem || !rootTreeItem->GetDocument()) {
     return nullptr;
   }
   return rootTreeItem->GetDocument()->GetInnerWindow();
 }
 
-already_AddRefed<mozilla::AutoplayRequest>
-nsPIDOMWindowInner::GetAutoplayRequest()
-{
-  // The AutoplayRequest is stored on the top level window.
+already_AddRefed<mozilla::AutoplayPermissionManager>
+nsPIDOMWindowInner::GetAutoplayPermissionManager()
+{
+  // The AutoplayPermissionManager is stored on the top level window.
   nsPIDOMWindowInner* window = GetTopLevelInnerWindow(this);
   if (!window) {
     return nullptr;
   }
-  if (!window->mAutoplayRequest) {
-    window->mAutoplayRequest = AutoplayRequest::Create(nsGlobalWindowInner::Cast(window));
-  }
-  RefPtr<mozilla::AutoplayRequest> request = window->mAutoplayRequest;
-  return request.forget();
+  if (!window->mAutoplayPermissionManager) {
+    window->mAutoplayPermissionManager =
+      new AutoplayPermissionManager(nsGlobalWindowInner::Cast(window));
+  }
+  RefPtr<mozilla::AutoplayPermissionManager> manager =
+    window->mAutoplayPermissionManager;
+  return manager.forget();
 }
 
 // XXX: Can we define this in a header instead of here?
 namespace mozilla {
 namespace dom {
 extern uint64_t
 NextWindowID();
 } // namespace dom
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -40,17 +40,17 @@ class nsPIDOMWindowInner;
 class nsPIDOMWindowOuter;
 class nsPIWindowRoot;
 class nsXBLPrototypeHandler;
 
 typedef uint32_t SuspendTypes;
 
 namespace mozilla {
 class ThrottledEventQueue;
-class AutoplayRequest;
+class AutoplayPermissionManager;
 namespace dom {
 class AudioContext;
 class ClientInfo;
 class ClientState;
 class DocGroup;
 class TabGroup;
 class Element;
 class Navigator;
@@ -620,19 +620,20 @@ public:
 
   virtual nsresult Focus() = 0;
   virtual nsresult Close() = 0;
 
   mozilla::dom::DocGroup* GetDocGroup() const;
   virtual nsISerialEventTarget*
   EventTargetFor(mozilla::TaskCategory aCategory) const = 0;
 
-  // Returns the AutoplayRequest that documents in this window should use
-  // to request permission to autoplay.
-  already_AddRefed<mozilla::AutoplayRequest> GetAutoplayRequest();
+  // Returns the AutoplayPermissionManager that documents in this window should
+  // use to request permission to autoplay.
+  already_AddRefed<mozilla::AutoplayPermissionManager>
+  GetAutoplayPermissionManager();
 
 protected:
   void CreatePerformanceObjectIfNeeded();
 
   // Lazily instantiate an about:blank document if necessary, and if
   // we have what it takes to do so.
   void MaybeCreateDoc();
 
@@ -710,17 +711,17 @@ protected:
   uint32_t mNumOfIndexedDBDatabases;
 
   // The number of open WebSockets.
   uint32_t mNumOfOpenWebSockets;
 
   // If we're in the process of requesting permission for this window to
   // play audible media, or we've already been granted permission by the
   // user, this is non-null, and encapsulates the request.
-  RefPtr<mozilla::AutoplayRequest> mAutoplayRequest;
+  RefPtr<mozilla::AutoplayPermissionManager> mAutoplayPermissionManager;
 
   // The event dispatch code sets and unsets this while keeping
   // the event object alive.
   mozilla::dom::Event* mEvent;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsPIDOMWindowInner, NS_PIDOMWINDOWINNER_IID)
 
rename from dom/html/AutoplayRequest.cpp
rename to dom/html/AutoplayPermissionManager.cpp
--- a/dom/html/AutoplayRequest.cpp
+++ b/dom/html/AutoplayPermissionManager.cpp
@@ -1,167 +1,89 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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 "mozilla/dom/AutoplayRequest.h"
+#include "mozilla/AutoplayPermissionManager.h"
+#include "mozilla/AutoplayPermissionRequest.h"
 
 #include "nsGlobalWindowInner.h"
 #include "nsISupportsImpl.h"
 #include "mozilla/Logging.h"
 #include "nsContentPermissionHelper.h"
 
 extern mozilla::LazyLogModule gMediaElementLog;
 
 #define PLAY_REQUEST_LOG(msg, ...)                                             \
   MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
 
 namespace mozilla {
 
-NS_IMPL_ISUPPORTS(AutoplayRequest, nsIContentPermissionRequest)
-
-AutoplayRequest::AutoplayRequest(nsGlobalWindowInner* aWindow,
-                                 nsIPrincipal* aNodePrincipal,
-                                 nsIEventTarget* aMainThreadTarget)
-  : mWindow(do_GetWeakReference(aWindow))
-  , mNodePrincipal(aNodePrincipal)
-  , mMainThreadTarget(aMainThreadTarget)
-  , mRequester(new dom::nsContentPermissionRequester(aWindow))
-{
-  MOZ_RELEASE_ASSERT(mNodePrincipal);
-}
-
-AutoplayRequest::~AutoplayRequest() {}
-
-already_AddRefed<AutoplayRequest>
-AutoplayRequest::Create(nsGlobalWindowInner* aWindow)
-{
-  if (!aWindow || !aWindow->GetPrincipal() ||
-      !aWindow->EventTargetFor(TaskCategory::Other)) {
-    return nullptr;
-  }
-  RefPtr<AutoplayRequest> request =
-    new AutoplayRequest(aWindow,
-                        aWindow->GetPrincipal(),
-                        aWindow->EventTargetFor(TaskCategory::Other));
-  PLAY_REQUEST_LOG("AutoplayRequest %p Create()", request.get());
-  return request.forget();
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetPrincipal(nsIPrincipal** aRequestingPrincipal)
-{
-  NS_ENSURE_ARG_POINTER(aRequestingPrincipal);
-
-  nsCOMPtr<nsIPrincipal> principal = mNodePrincipal;
-  principal.forget(aRequestingPrincipal);
-
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetTypes(nsIArray** aTypes)
-{
-  NS_ENSURE_ARG_POINTER(aTypes);
-
-  nsTArray<nsString> emptyOptions;
-  return dom::nsContentPermissionUtils::CreatePermissionArray(
-    NS_LITERAL_CSTRING("autoplay-media"),
-    NS_LITERAL_CSTRING("unused"),
-    emptyOptions,
-    aTypes);
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
-{
-  NS_ENSURE_ARG_POINTER(aRequestingWindow);
-
-  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow);
-  if (!window) {
-    return NS_ERROR_FAILURE;
-  }
-  window.forget(aRequestingWindow);
-
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetElement(dom::Element** aRequestingElement)
-{
-  NS_ENSURE_ARG_POINTER(aRequestingElement);
-  *aRequestingElement = nullptr;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetIsHandlingUserInput(bool* aIsHandlingUserInput)
-{
-  NS_ENSURE_ARG_POINTER(aIsHandlingUserInput);
-  *aIsHandlingUserInput = false;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::Cancel()
-{
-  MOZ_ASSERT(mRequestDispatched);
-  PLAY_REQUEST_LOG("AutoplayRequest %p Cancel()", this);
-  mRequestDispatched = false;
-  mPromiseHolder.RejectIfExists(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::Allow(JS::HandleValue aChoices)
-{
-  MOZ_ASSERT(mRequestDispatched);
-  PLAY_REQUEST_LOG("AutoplayRequest %p Allow()", this);
-  mRequestDispatched = false;
-  mPromiseHolder.ResolveIfExists(true, __func__);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-AutoplayRequest::GetRequester(nsIContentPermissionRequester** aRequester)
-{
-  NS_ENSURE_ARG_POINTER(aRequester);
-
-  nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
-  requester.forget(aRequester);
-
-  return NS_OK;
-}
-
 RefPtr<GenericPromise>
-AutoplayRequest::RequestWithPrompt()
+AutoplayPermissionManager::RequestWithPrompt()
 {
   // If we've already requested permission, we'll just return the promise,
   // as we don't want to show multiple permission requests at once.
   // The promise is non-exclusive, so if the request has already completed,
   // the ThenValue will run immediately.
   if (mRequestDispatched) {
-    PLAY_REQUEST_LOG(
-      "AutoplayRequest %p RequestWithPrompt() request already dispatched",
-      this);
+    PLAY_REQUEST_LOG("AutoplayPermissionManager %p RequestWithPrompt() request "
+                     "already dispatched",
+                     this);
     return mPromiseHolder.Ensure(__func__);
   }
 
   nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow);
   if (!window) {
     return GenericPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,
                                            __func__);
   }
-  nsCOMPtr<nsIContentPermissionRequest> request = do_QueryInterface(this);
-  MOZ_RELEASE_ASSERT(request);
+
+  nsCOMPtr<nsIContentPermissionRequest> request =
+    AutoplayPermissionRequest::Create(nsGlobalWindowInner::Cast(window), this);
+  if (!request) {
+    return GenericPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,
+                                           __func__);
+  }
+
+  // Dispatch the request.
   nsCOMPtr<nsIRunnable> f = NS_NewRunnableFunction(
-    "AutoplayRequest::RequestWithPrompt", [window, request]() {
+    "AutoplayPermissionManager::RequestWithPrompt", [window, request]() {
       dom::nsContentPermissionUtils::AskPermission(request, window);
     });
-  mMainThreadTarget->Dispatch(f, NS_DISPATCH_NORMAL);
+  window->EventTargetFor(TaskCategory::Other)->Dispatch(f, NS_DISPATCH_NORMAL);
 
   mRequestDispatched = true;
   return mPromiseHolder.Ensure(__func__);
 }
 
+AutoplayPermissionManager::AutoplayPermissionManager(
+  nsGlobalWindowInner* aWindow)
+  : mWindow(do_GetWeakReference(aWindow))
+{
+  PLAY_REQUEST_LOG("AutoplayPermissionManager %p Create()", this);
+}
+
+AutoplayPermissionManager::~AutoplayPermissionManager()
+{
+  // If we made a request, it should have been resolved.
+  MOZ_ASSERT(!mRequestDispatched);
+}
+
+void
+AutoplayPermissionManager::DenyPlayRequest()
+{
+  PLAY_REQUEST_LOG("AutoplayPermissionManager %p DenyPlayRequest()", this);
+  mRequestDispatched = false;
+  mPromiseHolder.RejectIfExists(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
+}
+
+void
+AutoplayPermissionManager::ApprovePlayRequest()
+{
+  PLAY_REQUEST_LOG("AutoplayPermissionManager %p ApprovePlayRequest()", this);
+  mRequestDispatched = false;
+  mPromiseHolder.ResolveIfExists(true, __func__);
+}
+
 } // namespace mozilla
rename from dom/html/AutoplayRequest.h
rename to dom/html/AutoplayPermissionManager.h
--- a/dom/html/AutoplayRequest.h
+++ b/dom/html/AutoplayPermissionManager.h
@@ -1,62 +1,69 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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 __AutoplayRequest_h__
-#define __AutoplayRequest_h__
+#ifndef __AutoplayPermissionRequestManager_h__
+#define __AutoplayPermissionRequestManager_h__
 
 #include "mozilla/MozPromise.h"
+#include "mozilla/WeakPtr.h"
 #include "nsIContentPermissionPrompt.h"
+#include "nsISupports.h"
 #include "nsIWeakReferenceUtils.h"
+#include "nsWeakReference.h"
 
 class nsGlobalWindowInner;
 class nsIEventTarget;
 
 namespace mozilla {
 
 // Encapsulates requesting permission from the user to autoplay with a
-// doorhanger. The AutoplayRequest is stored on the top level window,
-// and all documents in the tab use the top level window's AutoplayRequest.
-// The AutoplayRequest ensures that multiple requests are merged into one,
-// in order to avoid showing multiple doorhangers on one tab at once.
-class AutoplayRequest final : public nsIContentPermissionRequest
+// doorhanger. The AutoplayPermissionManager is stored on the top level window,
+// and all documents in the tab use the top level window's
+// AutoplayPermissionManager. The AutoplayPermissionManager ensures that
+// multiple requests are merged into one, in order to avoid showing multiple
+// doorhangers on one tab at once.
+class AutoplayPermissionManager final
+  : public SupportsWeakPtr<AutoplayPermissionManager>
 {
 public:
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSICONTENTPERMISSIONREQUEST
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(AutoplayPermissionManager)
+  NS_INLINE_DECL_REFCOUNTING(AutoplayPermissionManager)
+
+  // Creates a new AutoplayPermissionManager. Don't call this directly, use
+  // AutoplayPolicy::RequestFor() to retrieve the appropriate
+  // AutoplayPermissionManager to use for a document/window.
+  explicit AutoplayPermissionManager(nsGlobalWindowInner* aWindow);
 
-  // Creates a new AutoplayRequest. Don't call this directly, use
-  // AutoplayPolicy::RequestFor() to retrieve the appropriate AutoplayRequest
-  // to use for a document/window.
-  static already_AddRefed<AutoplayRequest> Create(nsGlobalWindowInner* aWindow);
-
-  // Requests permission to autoplay via a user prompt. Promise
+  // Requests permission to autoplay via a user prompt. The returned MozPromise
   // resolves/rejects when the user grants/denies permission to autoplay.
-  // If there is a stored permission for this window's origin, the stored
-  // the parent process will resolve/reject the autoplay request using the
-  // stored permission immediately (so the promise rejects in the content
-  // process after an IPC round trip).
+  // If the request doorhanger is dismissed, i.e. tab closed, ESC pressed,
+  // etc, the MozPromise is rejected. If there is a stored permission for this
+  // window's origin, the parent process will approve/deny the
+  // autoplay request using the stored permission immediately (so the MozPromise
+  // rejects in the content process after an IPC round trip).
   RefPtr<GenericPromise> RequestWithPrompt();
 
+  // Called by AutoplayPermissionRequest to approve the play request,
+  // and resolve the MozPromise returned by RequestWithPrompt().
+  void ApprovePlayRequest();
+  // Called by AutoplayPermissionRequest to deny the play request,
+  // and reject the MozPromise returned by RequestWithPrompt().
+  void DenyPlayRequest();
+
 private:
-  AutoplayRequest(nsGlobalWindowInner* aWindow,
-                  nsIPrincipal* aNodePrincipal,
-                  nsIEventTarget* aMainThreadTarget);
-  ~AutoplayRequest();
+  ~AutoplayPermissionManager();
 
   nsWeakPtr mWindow;
-  nsCOMPtr<nsIPrincipal> mNodePrincipal;
-  nsCOMPtr<nsIEventTarget> mMainThreadTarget;
-  nsCOMPtr<nsIContentPermissionRequester> mRequester;
   MozPromiseHolder<GenericPromise> mPromiseHolder;
   // Tracks whether we've dispatched a request to chrome in the parent process
   // to prompt for user permission to autoplay. This flag ensures we don't
   // request multiple times concurrently.
   bool mRequestDispatched = false;
 };
 
 } // namespace mozilla
 
-#endif // __AutoplayRequest_h__
+#endif // __AutoplayPermissionRequestManager_h__
new file mode 100644
--- /dev/null
+++ b/dom/html/AutoplayPermissionRequest.cpp
@@ -0,0 +1,141 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/AutoplayPermissionRequest.h"
+#include "mozilla/AutoplayPermissionManager.h"
+
+#include "mozilla/Logging.h"
+
+extern mozilla::LazyLogModule gMediaElementLog;
+
+#define PLAY_REQUEST_LOG(msg, ...)                                             \
+  MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(AutoplayPermissionRequest, nsIContentPermissionRequest)
+
+AutoplayPermissionRequest::AutoplayPermissionRequest(
+  AutoplayPermissionManager* aManager,
+  nsGlobalWindowInner* aWindow,
+  nsIPrincipal* aNodePrincipal,
+  nsIEventTarget* aMainThreadTarget)
+  : mManager(aManager)
+  , mWindow(do_GetWeakReference(aWindow))
+  , mNodePrincipal(aNodePrincipal)
+  , mMainThreadTarget(aMainThreadTarget)
+  , mRequester(new dom::nsContentPermissionRequester(aWindow))
+{
+  MOZ_ASSERT(mNodePrincipal);
+}
+
+AutoplayPermissionRequest::~AutoplayPermissionRequest()
+{
+  Cancel();
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetPrincipal(nsIPrincipal** aRequestingPrincipal)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingPrincipal);
+
+  nsCOMPtr<nsIPrincipal> principal = mNodePrincipal;
+  principal.forget(aRequestingPrincipal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetTypes(nsIArray** aTypes)
+{
+  NS_ENSURE_ARG_POINTER(aTypes);
+
+  nsTArray<nsString> emptyOptions;
+  return dom::nsContentPermissionUtils::CreatePermissionArray(
+    NS_LITERAL_CSTRING("autoplay-media"),
+    NS_LITERAL_CSTRING("unused"),
+    emptyOptions,
+    aTypes);
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingWindow);
+
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow);
+  if (!window) {
+    return NS_ERROR_FAILURE;
+  }
+  window.forget(aRequestingWindow);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetElement(dom::Element** aRequestingElement)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingElement);
+  *aRequestingElement = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetIsHandlingUserInput(bool* aIsHandlingUserInput)
+{
+  NS_ENSURE_ARG_POINTER(aIsHandlingUserInput);
+  *aIsHandlingUserInput = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::Cancel()
+{
+  if (mManager) {
+    mManager->DenyPlayRequest();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::Allow(JS::HandleValue aChoices)
+{
+  if (mManager) {
+    mManager->ApprovePlayRequest();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayPermissionRequest::GetRequester(
+  nsIContentPermissionRequester** aRequester)
+{
+  NS_ENSURE_ARG_POINTER(aRequester);
+
+  nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+  requester.forget(aRequester);
+
+  return NS_OK;
+}
+
+already_AddRefed<AutoplayPermissionRequest>
+AutoplayPermissionRequest::Create(nsGlobalWindowInner* aWindow,
+                                  AutoplayPermissionManager* aManager)
+{
+  if (!aWindow || !aWindow->GetPrincipal() ||
+      !aWindow->EventTargetFor(TaskCategory::Other)) {
+    return nullptr;
+  }
+  RefPtr<AutoplayPermissionRequest> request =
+    new AutoplayPermissionRequest(aManager,
+                                  aWindow,
+                                  aWindow->GetPrincipal(),
+                                  aWindow->EventTargetFor(TaskCategory::Other));
+  PLAY_REQUEST_LOG("AutoplayPermissionRequest %p Create()", request.get());
+  return request.forget();
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/html/AutoplayPermissionRequest.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 AutoplayPermissionRequest_h_
+#define AutoplayPermissionRequest_h_
+
+#include "nsGlobalWindowInner.h"
+#include "nsISupportsImpl.h"
+#include "nsContentPermissionHelper.h"
+
+namespace mozilla {
+
+class AutoplayPermissionManager;
+
+// The AutoplayPermissionRequest is the object we pass off to the chrome JS
+// code to represent a request for permission to autoplay. Unfortunately the
+// front end code doesn't guarantee to give us an approve/cancel callback in
+// all cases. If chrome JS dismisses the permission request for whatever
+// reason (tab closed, user presses ESC, navigation, etc), the permission UI
+// code will drop its reference to the AutoplayPermissionRequest and it will
+// be destroyed. The AutoplayPermissionRequest keeps a weak reference to
+// the AutoplayPermissionManager. If the AutoplayPermissionManager is still
+// alive when the AutoplayPermissionRequest's destructor runs, the
+// AutoplayPermissionRequest's destructor calls the AutoplayPermissionManager
+// back with a cancel operation. Thus the AutoplayPermissionManager can
+// guarantee to always approve or cancel requests to play.
+class AutoplayPermissionRequest final : public nsIContentPermissionRequest
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSICONTENTPERMISSIONREQUEST
+
+  static already_AddRefed<AutoplayPermissionRequest> Create(
+    nsGlobalWindowInner* aWindow,
+    AutoplayPermissionManager* aManager);
+
+private:
+  AutoplayPermissionRequest(AutoplayPermissionManager* aManager,
+                            nsGlobalWindowInner* aWindow,
+                            nsIPrincipal* aNodePrincipal,
+                            nsIEventTarget* aMainThreadTarget);
+  ~AutoplayPermissionRequest();
+
+  WeakPtr<AutoplayPermissionManager> mManager;
+
+  nsWeakPtr mWindow;
+  nsCOMPtr<nsIPrincipal> mNodePrincipal;
+  nsCOMPtr<nsIEventTarget> mMainThreadTarget;
+  nsCOMPtr<nsIContentPermissionRequester> mRequester;
+};
+
+} // namespace mozilla
+
+#endif // AutoplayPermissionRequest_h_
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -53,17 +53,17 @@
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/StaticPrefs.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/dom/AudioTrack.h"
 #include "mozilla/dom/AudioTrackList.h"
-#include "mozilla/dom/AutoplayRequest.h"
+#include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/BlobURLProtocolHandler.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/HTMLAudioElement.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "mozilla/dom/HTMLSourceElement.h"
 #include "mozilla/dom/HTMLVideoElement.h"
 #include "mozilla/dom/MediaEncryptedEvent.h"
@@ -4096,17 +4096,18 @@ HTMLMediaElement::EnsureAutoplayRequeste
     // Await for the previous request to be approved or denied. This
     // play request's promise will be fulfilled with all other pending
     // promises when the permission prompt is resolved.
     LOG(LogLevel::Debug,
         ("%p EnsureAutoplayRequested() existing request, bailing.", this));
     return;
   }
 
-  RefPtr<AutoplayRequest> request = AutoplayPolicy::RequestFor(*OwnerDoc());
+  RefPtr<AutoplayPermissionManager> request =
+    AutoplayPolicy::RequestFor(*OwnerDoc());
   if (!request) {
     AsyncRejectPendingPlayPromises(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
   RefPtr<HTMLMediaElement> self = this;
   request->RequestWithPrompt()
     ->Then(mAbstractMainThread,
            __func__,
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -40,17 +40,16 @@
 typedef uint16_t nsMediaNetworkState;
 typedef uint16_t nsMediaReadyState;
 typedef uint32_t SuspendTypes;
 typedef uint32_t AudibleChangedReasons;
 typedef uint8_t AudibleState;
 
 namespace mozilla {
 class AbstractThread;
-class AutoplayRequest;
 class ChannelMediaDecoder;
 class DecoderDoctorDiagnostics;
 class DOMMediaStream;
 class ErrorResult;
 class MediaResource;
 class MediaDecoder;
 class MediaInputPort;
 class MediaStream;
--- a/dom/html/moz.build
+++ b/dom/html/moz.build
@@ -43,21 +43,22 @@ EXPORTS += [
     'nsIHTMLDocument.h',
     'nsIRadioGroupContainer.h',
     'nsIRadioVisitor.h',
     'nsITextControlElement.h',
     'nsTextEditorState.h',
 ]
 
 EXPORTS.mozilla += [
+    'AutoplayPermissionManager.h',
+    'AutoplayPermissionRequest.h',
     'TextInputListener.h',
 ]
 
 EXPORTS.mozilla.dom += [
-    'AutoplayRequest.h',
     'HTMLAllCollection.h',
     'HTMLAnchorElement.h',
     'HTMLAreaElement.h',
     'HTMLAudioElement.h',
     'HTMLBodyElement.h',
     'HTMLBRElement.h',
     'HTMLButtonElement.h',
     'HTMLCanvasElement.h',
@@ -218,17 +219,18 @@ UNIFIED_SOURCES += [
     'RadioNodeList.cpp',
     'TextTrackManager.cpp',
     'TimeRanges.cpp',
     'ValidityState.cpp',
     'VideoDocument.cpp',
 ]
 
 SOURCES += [
-    'AutoplayRequest.cpp',
+    'AutoplayPermissionManager.cpp',
+    'AutoplayPermissionRequest.cpp',
     # Includes npapi.h.
     'PluginDocument.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'htmlMenuBuilder.js',
     'htmlMenuBuilder.manifest'
 ]
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -4,17 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AutoplayPolicy.h"
 
 #include "mozilla/EventStateManager.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/AudioContext.h"
-#include "mozilla/dom/AutoplayRequest.h"
+#include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "nsContentUtils.h"
 #include "nsIDocument.h"
 #include "MediaManager.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsPIDOMWindow.h"
@@ -69,28 +69,28 @@ IsWindowAllowedToPlay(nsPIDOMWindowInner
     // Document has been activated by user gesture.
     return true;
   }
 
   return false;
 }
 
 /* static */
-already_AddRefed<AutoplayRequest>
+already_AddRefed<AutoplayPermissionManager>
 AutoplayPolicy::RequestFor(const nsIDocument& aDocument)
 {
   nsIDocument* document = ApproverDocOf(aDocument);
   if (!document) {
     return nullptr;
   }
   nsPIDOMWindowInner* window = document->GetInnerWindow();
   if (!window) {
     return nullptr;
   }
-  return window->GetAutoplayRequest();
+  return window->GetAutoplayPermissionManager();
 }
 
 /* static */ Authorization
 AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
 {
   if (Preferences::GetBool("media.autoplay.enabled")) {
     return Authorization::Allowed;
   }
--- a/dom/media/AutoplayPolicy.h
+++ b/dom/media/AutoplayPolicy.h
@@ -8,17 +8,17 @@
 #define AutoplayPolicy_h_
 
 #include "mozilla/NotNull.h"
 
 class nsIDocument;
 
 namespace mozilla {
 
-class AutoplayRequest;
+class AutoplayPermissionManager;
 
 namespace dom {
 
 class HTMLMediaElement;
 class AudioContext;
 
 enum class Authorization
 {
@@ -43,18 +43,18 @@ class AutoplayPolicy
 {
 public:
   // Returns whether a given media element is allowed to play.
   static Authorization IsAllowedToPlay(const HTMLMediaElement& aElement);
 
   // Returns whether a given AudioContext is allowed to play.
   static bool IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext);
 
-  // Returns the AutoplayRequest that a given document must request on
+  // Returns the AutoplayPermissionManager that a given document must request on
   // for autoplay permission.
-  static already_AddRefed<AutoplayRequest> RequestFor(
+  static already_AddRefed<AutoplayPermissionManager> RequestFor(
     const nsIDocument& aDocument);
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif