From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Fri, 19 Apr 2024 02:47:55 +0200
Subject: Adding upstream version 124.0.1.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 dom/media/mediasession/MediaMetadata.cpp           | 154 ++++++++++
 dom/media/mediasession/MediaMetadata.h             |  97 ++++++
 dom/media/mediasession/MediaSession.cpp            | 335 +++++++++++++++++++++
 dom/media/mediasession/MediaSession.h              | 132 ++++++++
 dom/media/mediasession/MediaSessionIPCUtils.h      | 102 +++++++
 dom/media/mediasession/moz.build                   |  24 ++
 .../mediasession/test/MediaSessionTestUtils.js     |  30 ++
 dom/media/mediasession/test/browser.toml           |   9 +
 .../test/browser_active_mediasession_among_tabs.js | 198 ++++++++++++
 .../mediasession/test/crashtests/crashtests.list   |   1 +
 .../test/crashtests/inactive-mediasession.html     |  16 +
 .../mediasession/test/file_media_session.html      |  30 ++
 .../test/file_trigger_actionhanlder_frame.html     |  40 +++
 .../test/file_trigger_actionhanlder_window.html    | 107 +++++++
 dom/media/mediasession/test/mochitest.toml         |  14 +
 .../mediasession/test/test_setactionhandler.html   |  84 ++++++
 .../test/test_trigger_actionhanlder.html           |  56 ++++
 17 files changed, 1429 insertions(+)
 create mode 100644 dom/media/mediasession/MediaMetadata.cpp
 create mode 100644 dom/media/mediasession/MediaMetadata.h
 create mode 100644 dom/media/mediasession/MediaSession.cpp
 create mode 100644 dom/media/mediasession/MediaSession.h
 create mode 100644 dom/media/mediasession/MediaSessionIPCUtils.h
 create mode 100644 dom/media/mediasession/moz.build
 create mode 100644 dom/media/mediasession/test/MediaSessionTestUtils.js
 create mode 100644 dom/media/mediasession/test/browser.toml
 create mode 100644 dom/media/mediasession/test/browser_active_mediasession_among_tabs.js
 create mode 100644 dom/media/mediasession/test/crashtests/crashtests.list
 create mode 100644 dom/media/mediasession/test/crashtests/inactive-mediasession.html
 create mode 100644 dom/media/mediasession/test/file_media_session.html
 create mode 100644 dom/media/mediasession/test/file_trigger_actionhanlder_frame.html
 create mode 100644 dom/media/mediasession/test/file_trigger_actionhanlder_window.html
 create mode 100644 dom/media/mediasession/test/mochitest.toml
 create mode 100644 dom/media/mediasession/test/test_setactionhandler.html
 create mode 100644 dom/media/mediasession/test/test_trigger_actionhanlder.html

(limited to 'dom/media/mediasession')

diff --git a/dom/media/mediasession/MediaMetadata.cpp b/dom/media/mediasession/MediaMetadata.cpp
new file mode 100644
index 0000000000..8ee2c217ac
--- /dev/null
+++ b/dom/media/mediasession/MediaMetadata.cpp
@@ -0,0 +1,154 @@
+/* -*- 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/MediaMetadata.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsNetUtil.h"
+
+namespace mozilla::dom {
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaMetadata, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaMetadata)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaMetadata)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaMetadata)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+MediaMetadata::MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle,
+                             const nsString& aArtist, const nsString& aAlbum)
+    : MediaMetadataBase(aTitle, aArtist, aAlbum), mParent(aParent) {
+  MOZ_ASSERT(mParent);
+}
+
+nsIGlobalObject* MediaMetadata::GetParentObject() const { return mParent; }
+
+JSObject* MediaMetadata::WrapObject(JSContext* aCx,
+                                    JS::Handle<JSObject*> aGivenProto) {
+  return MediaMetadata_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaMetadata> MediaMetadata::Constructor(
+    const GlobalObject& aGlobal, const MediaMetadataInit& aInit,
+    ErrorResult& aRv) {
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!global) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  RefPtr<MediaMetadata> mediaMetadata =
+      new MediaMetadata(global, aInit.mTitle, aInit.mArtist, aInit.mAlbum);
+  mediaMetadata->SetArtworkInternal(aInit.mArtwork, aRv);
+  return aRv.Failed() ? nullptr : mediaMetadata.forget();
+}
+
+void MediaMetadata::GetTitle(nsString& aRetVal) const { aRetVal = mTitle; }
+
+void MediaMetadata::SetTitle(const nsAString& aTitle) { mTitle = aTitle; }
+
+void MediaMetadata::GetArtist(nsString& aRetVal) const { aRetVal = mArtist; }
+
+void MediaMetadata::SetArtist(const nsAString& aArtist) { mArtist = aArtist; }
+
+void MediaMetadata::GetAlbum(nsString& aRetVal) const { aRetVal = mAlbum; }
+
+void MediaMetadata::SetAlbum(const nsAString& aAlbum) { mAlbum = aAlbum; }
+
+void MediaMetadata::GetArtwork(JSContext* aCx, nsTArray<JSObject*>& aRetVal,
+                               ErrorResult& aRv) const {
+  // Convert the MediaImages to JS Objects
+  if (!aRetVal.SetCapacity(mArtwork.Length(), fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return;
+  }
+
+  for (size_t i = 0; i < mArtwork.Length(); ++i) {
+    JS::Rooted<JS::Value> value(aCx);
+    if (!ToJSValue(aCx, mArtwork[i], &value)) {
+      aRv.NoteJSContextException(aCx);
+      return;
+    }
+
+    JS::Rooted<JSObject*> object(aCx, &value.toObject());
+    if (!JS_FreezeObject(aCx, object)) {
+      aRv.NoteJSContextException(aCx);
+      return;
+    }
+
+    aRetVal.AppendElement(object);
+  }
+}
+
+void MediaMetadata::SetArtwork(JSContext* aCx,
+                               const Sequence<JSObject*>& aArtwork,
+                               ErrorResult& aRv) {
+  // Convert the JS Objects to MediaImages
+  Sequence<MediaImage> artwork;
+  if (!artwork.SetCapacity(aArtwork.Length(), fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return;
+  }
+
+  for (JSObject* object : aArtwork) {
+    JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*object));
+
+    MediaImage* image = artwork.AppendElement(fallible);
+    MOZ_ASSERT(image, "The capacity is preallocated");
+    if (!image->Init(aCx, value)) {
+      aRv.NoteJSContextException(aCx);
+      return;
+    }
+  }
+
+  SetArtworkInternal(artwork, aRv);
+};
+
+static nsIURI* GetEntryBaseURL() {
+  nsCOMPtr<Document> doc = GetEntryDocument();
+  return doc ? doc->GetDocBaseURI() : nullptr;
+}
+
+// `aURL` is an inout parameter.
+static nsresult ResolveURL(nsString& aURL, nsIURI* aBaseURI) {
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL,
+                          /* UTF-8 for charset */ nullptr, aBaseURI);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsAutoCString spec;
+  rv = uri->GetSpec(spec);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  CopyUTF8toUTF16(spec, aURL);
+  return NS_OK;
+}
+
+void MediaMetadata::SetArtworkInternal(const Sequence<MediaImage>& aArtwork,
+                                       ErrorResult& aRv) {
+  nsTArray<MediaImage> artwork;
+  artwork.Assign(aArtwork);
+
+  nsCOMPtr<nsIURI> baseURI = GetEntryBaseURL();
+  for (MediaImage& image : artwork) {
+    nsresult rv = ResolveURL(image.mSrc, baseURI);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      aRv.ThrowTypeError<MSG_INVALID_URL>(NS_ConvertUTF16toUTF8(image.mSrc));
+      return;
+    }
+  }
+  mArtwork = std::move(artwork);
+}
+
+}  // namespace mozilla::dom
diff --git a/dom/media/mediasession/MediaMetadata.h b/dom/media/mediasession/MediaMetadata.h
new file mode 100644
index 0000000000..6c552e6465
--- /dev/null
+++ b/dom/media/mediasession/MediaMetadata.h
@@ -0,0 +1,97 @@
+/* -*- 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 mozilla_dom_MediaMetadata_h
+#define mozilla_dom_MediaMetadata_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class MediaMetadataBase {
+ public:
+  MediaMetadataBase() = default;
+  MediaMetadataBase(const nsString& aTitle, const nsString& aArtist,
+                    const nsString& aAlbum)
+      : mTitle(aTitle), mArtist(aArtist), mAlbum(aAlbum) {}
+
+  static MediaMetadataBase EmptyData() { return MediaMetadataBase(); }
+
+  nsString mTitle;
+  nsString mArtist;
+  nsString mAlbum;
+  CopyableTArray<MediaImage> mArtwork;
+};
+
+class MediaMetadata final : public nsISupports,
+                            public nsWrapperCache,
+                            private MediaMetadataBase {
+ public:
+  // Ref counting and cycle collection
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaMetadata)
+
+  // WebIDL methods
+  nsIGlobalObject* GetParentObject() const;
+
+  JSObject* WrapObject(JSContext* aCx,
+                       JS::Handle<JSObject*> aGivenProto) override;
+
+  static already_AddRefed<MediaMetadata> Constructor(
+      const GlobalObject& aGlobal, const MediaMetadataInit& aInit,
+      ErrorResult& aRv);
+
+  void GetTitle(nsString& aRetVal) const;
+
+  void SetTitle(const nsAString& aTitle);
+
+  void GetArtist(nsString& aRetVal) const;
+
+  void SetArtist(const nsAString& aArtist);
+
+  void GetAlbum(nsString& aRetVal) const;
+
+  void SetAlbum(const nsAString& aAlbum);
+
+  void GetArtwork(JSContext* aCx, nsTArray<JSObject*>& aRetVal,
+                  ErrorResult& aRv) const;
+
+  void SetArtwork(JSContext* aCx, const Sequence<JSObject*>& aArtwork,
+                  ErrorResult& aRv);
+
+  // This would expose MediaMetadataBase's members as public, so use this method
+  // carefully. Now we only use this when we want to update the metadata to the
+  // media session controller in the chrome process.
+  MediaMetadataBase* AsMetadataBase() { return this; }
+
+ private:
+  MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle,
+                const nsString& aArtist, const nsString& aAlbum);
+
+  ~MediaMetadata() = default;
+
+  // Perform `convert artwork algorithm`. Set `mArtwork` to a converted
+  // `aArtwork` if the conversion works, otherwise throw a type error in `aRv`.
+  void SetArtworkInternal(const Sequence<MediaImage>& aArtwork,
+                          ErrorResult& aRv);
+
+  nsCOMPtr<nsIGlobalObject> mParent;
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_MediaMetadata_h
diff --git a/dom/media/mediasession/MediaSession.cpp b/dom/media/mediasession/MediaSession.cpp
new file mode 100644
index 0000000000..e55fa28d96
--- /dev/null
+++ b/dom/media/mediasession/MediaSession.cpp
@@ -0,0 +1,335 @@
+/* -*- 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/BrowsingContext.h"
+#include "mozilla/dom/ContentMediaController.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/EnumeratedArrayCycleCollection.h"
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...)                        \
+  MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+          ("MediaSession=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to
+// unregister MediaSession from document's activity listeners.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaSession)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaMetadata)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionHandlers)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaSession)
+  tmp->Shutdown();
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaMetadata)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionHandlers)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDoc)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaSession)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaSession)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaSession)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
+NS_INTERFACE_MAP_END
+
+MediaSession::MediaSession(nsPIDOMWindowInner* aParent)
+    : mParent(aParent), mDoc(mParent->GetExtantDoc()) {
+  MOZ_ASSERT(mParent);
+  MOZ_ASSERT(mDoc);
+  mDoc->RegisterActivityObserver(this);
+  if (mDoc->IsCurrentActiveDocument()) {
+    SetMediaSessionDocStatus(SessionDocStatus::eActive);
+  }
+}
+
+void MediaSession::Shutdown() {
+  if (mDoc) {
+    mDoc->UnregisterActivityObserver(this);
+  }
+  if (mParent) {
+    SetMediaSessionDocStatus(SessionDocStatus::eInactive);
+  }
+}
+
+void MediaSession::NotifyOwnerDocumentActivityChanged() {
+  const bool isDocActive = mDoc->IsCurrentActiveDocument();
+  LOG("Document activity changed, isActive=%d", isDocActive);
+  if (isDocActive) {
+    SetMediaSessionDocStatus(SessionDocStatus::eActive);
+  } else {
+    SetMediaSessionDocStatus(SessionDocStatus::eInactive);
+  }
+}
+
+void MediaSession::SetMediaSessionDocStatus(SessionDocStatus aState) {
+  if (mSessionDocState == aState) {
+    return;
+  }
+  mSessionDocState = aState;
+  NotifyMediaSessionDocStatus(mSessionDocState);
+}
+
+nsPIDOMWindowInner* MediaSession::GetParentObject() const { return mParent; }
+
+JSObject* MediaSession::WrapObject(JSContext* aCx,
+                                   JS::Handle<JSObject*> aGivenProto) {
+  return MediaSession_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+MediaMetadata* MediaSession::GetMetadata() const { return mMediaMetadata; }
+
+void MediaSession::SetMetadata(MediaMetadata* aMetadata) {
+  mMediaMetadata = aMetadata;
+  NotifyMetadataUpdated();
+}
+
+void MediaSession::SetPlaybackState(
+    const MediaSessionPlaybackState& aPlaybackState) {
+  if (mDeclaredPlaybackState == aPlaybackState) {
+    return;
+  }
+  mDeclaredPlaybackState = aPlaybackState;
+  NotifyPlaybackStateUpdated();
+}
+
+MediaSessionPlaybackState MediaSession::PlaybackState() const {
+  return mDeclaredPlaybackState;
+}
+
+void MediaSession::SetActionHandler(MediaSessionAction aAction,
+                                    MediaSessionActionHandler* aHandler) {
+  MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+  // If the media session changes its supported action, then we would propagate
+  // this information to the chrome process in order to run the media session
+  // actions update algorithm.
+  // https://w3c.github.io/mediasession/#supported-media-session-actions
+  RefPtr<MediaSessionActionHandler>& hanlder = mActionHandlers[aAction];
+  if (!hanlder && aHandler) {
+    NotifyEnableSupportedAction(aAction);
+  } else if (hanlder && !aHandler) {
+    NotifyDisableSupportedAction(aAction);
+  }
+  mActionHandlers[aAction] = aHandler;
+}
+
+MediaSessionActionHandler* MediaSession::GetActionHandler(
+    MediaSessionAction aAction) const {
+  MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+  return mActionHandlers[aAction];
+}
+
+void MediaSession::SetPositionState(const MediaPositionState& aState,
+                                    ErrorResult& aRv) {
+  // https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate
+  // If the state is an empty dictionary then clear the position state.
+  if (!aState.IsAnyMemberPresent()) {
+    mPositionState.reset();
+    return;
+  }
+
+  // If the duration is not present, throw a TypeError.
+  if (!aState.mDuration.WasPassed()) {
+    return aRv.ThrowTypeError("Duration is not present");
+  }
+
+  // If the duration is negative, throw a TypeError.
+  if (aState.mDuration.WasPassed() && aState.mDuration.Value() < 0.0) {
+    return aRv.ThrowTypeError(nsPrintfCString(
+        "Invalid duration %f, it can't be negative", aState.mDuration.Value()));
+  }
+
+  // If the position is negative or greater than duration, throw a TypeError.
+  if (aState.mPosition.WasPassed() &&
+      (aState.mPosition.Value() < 0.0 ||
+       aState.mPosition.Value() > aState.mDuration.Value())) {
+    return aRv.ThrowTypeError(nsPrintfCString(
+        "Invalid position %f, it can't be negative or greater than duration",
+        aState.mPosition.Value()));
+  }
+
+  // If the playbackRate is zero, throw a TypeError.
+  if (aState.mPlaybackRate.WasPassed() && aState.mPlaybackRate.Value() == 0.0) {
+    return aRv.ThrowTypeError("The playbackRate is zero");
+  }
+
+  // If the position is not present, set it to zero.
+  double position = aState.mPosition.WasPassed() ? aState.mPosition.Value() : 0;
+
+  // If the playbackRate is not present, set it to 1.0.
+  double playbackRate =
+      aState.mPlaybackRate.WasPassed() ? aState.mPlaybackRate.Value() : 1.0;
+
+  // Update the position state and last position updated time.
+  MOZ_ASSERT(aState.mDuration.WasPassed());
+  mPositionState =
+      Some(PositionState(aState.mDuration.Value(), playbackRate, position));
+  NotifyPositionStateChanged();
+}
+
+void MediaSession::NotifyHandler(const MediaSessionActionDetails& aDetails) {
+  DispatchNotifyHandler(aDetails);
+}
+
+void MediaSession::DispatchNotifyHandler(
+    const MediaSessionActionDetails& aDetails) {
+  class Runnable final : public mozilla::Runnable {
+   public:
+    Runnable(const MediaSession* aSession,
+             const MediaSessionActionDetails& aDetails)
+        : mozilla::Runnable("MediaSession::DispatchNotifyHandler"),
+          mSession(aSession),
+          mDetails(aDetails) {}
+
+    MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+      if (RefPtr<MediaSessionActionHandler> handler =
+              mSession->GetActionHandler(mDetails.mAction)) {
+        handler->Call(mDetails);
+      }
+      return NS_OK;
+    }
+
+   private:
+    RefPtr<const MediaSession> mSession;
+    MediaSessionActionDetails mDetails;
+  };
+
+  RefPtr<nsIRunnable> runnable = new Runnable(this, aDetails);
+  NS_DispatchToMainThread(runnable);
+}
+
+bool MediaSession::IsSupportedAction(MediaSessionAction aAction) const {
+  MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+  return mActionHandlers[aAction] != nullptr;
+}
+
+bool MediaSession::IsActive() const {
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC);
+  RefPtr<WindowContext> wc = currentBC->GetTopWindowContext();
+  if (!wc) {
+    return false;
+  }
+  Maybe<uint64_t> activeSessionContextId = wc->GetActiveMediaSessionContextId();
+  if (!activeSessionContextId) {
+    return false;
+  }
+  LOG("session context Id=%" PRIu64 ", active session context Id=%" PRIu64,
+      currentBC->Id(), *activeSessionContextId);
+  return *activeSessionContextId == currentBC->Id();
+}
+
+void MediaSession::NotifyMediaSessionDocStatus(SessionDocStatus aState) {
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC, "Update session status after context destroyed!");
+
+  RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC);
+  if (!updater) {
+    return;
+  }
+  if (aState == SessionDocStatus::eActive) {
+    updater->NotifySessionCreated(currentBC->Id());
+    // If media session set its attributes before its document becomes active,
+    // then we would notify those attributes which hasn't been notified as well
+    // because attributes update would only happen if its document is already
+    // active.
+    NotifyMediaSessionAttributes();
+  } else {
+    updater->NotifySessionDestroyed(currentBC->Id());
+  }
+}
+
+void MediaSession::NotifyMediaSessionAttributes() {
+  MOZ_ASSERT(mSessionDocState == SessionDocStatus::eActive);
+  if (mDeclaredPlaybackState != MediaSessionPlaybackState::None) {
+    NotifyPlaybackStateUpdated();
+  }
+  if (mMediaMetadata) {
+    NotifyMetadataUpdated();
+  }
+  for (size_t idx = 0; idx < ArrayLength(mActionHandlers); idx++) {
+    MediaSessionAction action = static_cast<MediaSessionAction>(idx);
+    if (mActionHandlers[action]) {
+      NotifyEnableSupportedAction(action);
+    }
+  }
+  if (mPositionState) {
+    NotifyPositionStateChanged();
+  }
+}
+
+void MediaSession::NotifyPlaybackStateUpdated() {
+  if (mSessionDocState != SessionDocStatus::eActive) {
+    return;
+  }
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC,
+             "Update session playback state after context destroyed!");
+  if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+    updater->SetDeclaredPlaybackState(currentBC->Id(), mDeclaredPlaybackState);
+  }
+}
+
+void MediaSession::NotifyMetadataUpdated() {
+  if (mSessionDocState != SessionDocStatus::eActive) {
+    return;
+  }
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC, "Update session metadata after context destroyed!");
+
+  Maybe<MediaMetadataBase> metadata;
+  if (GetMetadata()) {
+    metadata.emplace(*(GetMetadata()->AsMetadataBase()));
+  }
+  if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+    updater->UpdateMetadata(currentBC->Id(), metadata);
+  }
+}
+
+void MediaSession::NotifyEnableSupportedAction(MediaSessionAction aAction) {
+  if (mSessionDocState != SessionDocStatus::eActive) {
+    return;
+  }
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+  if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+    updater->EnableAction(currentBC->Id(), aAction);
+  }
+}
+
+void MediaSession::NotifyDisableSupportedAction(MediaSessionAction aAction) {
+  if (mSessionDocState != SessionDocStatus::eActive) {
+    return;
+  }
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+  if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+    updater->DisableAction(currentBC->Id(), aAction);
+  }
+}
+
+void MediaSession::NotifyPositionStateChanged() {
+  if (mSessionDocState != SessionDocStatus::eActive) {
+    return;
+  }
+  RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+  MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+  if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+    updater->UpdatePositionState(currentBC->Id(), *mPositionState);
+  }
+}
+
+}  // namespace mozilla::dom
diff --git a/dom/media/mediasession/MediaSession.h b/dom/media/mediasession/MediaSession.h
new file mode 100644
index 0000000000..db6864c842
--- /dev/null
+++ b/dom/media/mediasession/MediaSession.h
@@ -0,0 +1,132 @@
+/* -*- 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 mozilla_dom_MediaSession_h
+#define mozilla_dom_MediaSession_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/EnumeratedArray.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocumentActivity.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class Document;
+class MediaMetadata;
+
+// https://w3c.github.io/mediasession/#position-state
+struct PositionState {
+  PositionState() = default;
+  PositionState(double aDuration, double aPlaybackRate,
+                double aLastReportedTime)
+      : mDuration(aDuration),
+        mPlaybackRate(aPlaybackRate),
+        mLastReportedPlaybackPosition(aLastReportedTime) {}
+  double mDuration;
+  double mPlaybackRate;
+  double mLastReportedPlaybackPosition;
+};
+
+class MediaSession final : public nsIDocumentActivity, public nsWrapperCache {
+ public:
+  // Ref counting and cycle collection
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession)
+  NS_DECL_NSIDOCUMENTACTIVITY
+
+  explicit MediaSession(nsPIDOMWindowInner* aParent);
+
+  // WebIDL methods
+  nsPIDOMWindowInner* GetParentObject() const;
+
+  JSObject* WrapObject(JSContext* aCx,
+                       JS::Handle<JSObject*> aGivenProto) override;
+
+  MediaMetadata* GetMetadata() const;
+
+  void SetMetadata(MediaMetadata* aMetadata);
+
+  void SetPlaybackState(const MediaSessionPlaybackState& aPlaybackState);
+
+  MediaSessionPlaybackState PlaybackState() const;
+
+  void SetActionHandler(MediaSessionAction aAction,
+                        MediaSessionActionHandler* aHandler);
+
+  void SetPositionState(const MediaPositionState& aState, ErrorResult& aRv);
+
+  bool IsSupportedAction(MediaSessionAction aAction) const;
+
+  // Use this method to trigger media session action handler asynchronously.
+  void NotifyHandler(const MediaSessionActionDetails& aDetails);
+
+  void Shutdown();
+
+  // `MediaStatusManager` would determine which media session is an active media
+  // session and update it from the chrome process. This active session is not
+  // 100% equal to the active media session in the spec, which is a globally
+  // active media session *among all tabs*. The active session here is *among
+  // different windows but in same tab*, so each tab can have at most one
+  // active media session.
+  bool IsActive() const;
+
+ private:
+  // When the document which media session belongs to is going to be destroyed,
+  // or is in the bfcache, then the session would be inactive. Otherwise, it's
+  // active all the time.
+  enum class SessionDocStatus : bool {
+    eInactive = false,
+    eActive = true,
+  };
+  void SetMediaSessionDocStatus(SessionDocStatus aState);
+
+  // These methods are used to propagate media session's status to the chrome
+  // process.
+  void NotifyMediaSessionDocStatus(SessionDocStatus aState);
+  void NotifyMediaSessionAttributes();
+  void NotifyPlaybackStateUpdated();
+  void NotifyMetadataUpdated();
+  void NotifyEnableSupportedAction(MediaSessionAction aAction);
+  void NotifyDisableSupportedAction(MediaSessionAction aAction);
+  void NotifyPositionStateChanged();
+
+  void DispatchNotifyHandler(const MediaSessionActionDetails& aDetails);
+
+  MediaSessionActionHandler* GetActionHandler(MediaSessionAction aAction) const;
+
+  ~MediaSession() = default;
+
+  nsCOMPtr<nsPIDOMWindowInner> mParent;
+
+  RefPtr<MediaMetadata> mMediaMetadata;
+
+  EnumeratedArray<MediaSessionAction, MediaSessionAction::EndGuard_,
+                  RefPtr<MediaSessionActionHandler>>
+      mActionHandlers;
+
+  // This is used as is a hint for the user agent to determine whether the
+  // browsing context is playing or paused.
+  // https://w3c.github.io/mediasession/#declared-playback-state
+  MediaSessionPlaybackState mDeclaredPlaybackState =
+      MediaSessionPlaybackState::None;
+
+  Maybe<PositionState> mPositionState;
+  RefPtr<Document> mDoc;
+  SessionDocStatus mSessionDocState = SessionDocStatus::eInactive;
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_MediaSession_h
diff --git a/dom/media/mediasession/MediaSessionIPCUtils.h b/dom/media/mediasession/MediaSessionIPCUtils.h
new file mode 100644
index 0000000000..c44f4b4553
--- /dev/null
+++ b/dom/media/mediasession/MediaSessionIPCUtils.h
@@ -0,0 +1,102 @@
+/* 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 DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
+#define DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
+
+#include "ipc/EnumSerializer.h"
+#include "MediaMetadata.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+namespace dom {
+
+typedef Maybe<MediaMetadataBase> MaybeMediaMetadataBase;
+
+}  // namespace dom
+}  // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::MediaImage> {
+  typedef mozilla::dom::MediaImage paramType;
+
+  static void Write(MessageWriter* aWriter, const paramType& aParam) {
+    WriteParam(aWriter, aParam.mSizes);
+    WriteParam(aWriter, aParam.mSrc);
+    WriteParam(aWriter, aParam.mType);
+  }
+
+  static bool Read(MessageReader* aReader, paramType* aResult) {
+    if (!ReadParam(aReader, &(aResult->mSizes)) ||
+        !ReadParam(aReader, &(aResult->mSrc)) ||
+        !ReadParam(aReader, &(aResult->mType))) {
+      return false;
+    }
+    return true;
+  }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaMetadataBase> {
+  typedef mozilla::dom::MediaMetadataBase paramType;
+
+  static void Write(MessageWriter* aWriter, const paramType& aParam) {
+    WriteParam(aWriter, aParam.mTitle);
+    WriteParam(aWriter, aParam.mArtist);
+    WriteParam(aWriter, aParam.mAlbum);
+    WriteParam(aWriter, aParam.mArtwork);
+  }
+
+  static bool Read(MessageReader* aReader, paramType* aResult) {
+    if (!ReadParam(aReader, &(aResult->mTitle)) ||
+        !ReadParam(aReader, &(aResult->mArtist)) ||
+        !ReadParam(aReader, &(aResult->mAlbum)) ||
+        !ReadParam(aReader, &(aResult->mArtwork))) {
+      return false;
+    }
+    return true;
+  }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::PositionState> {
+  typedef mozilla::dom::PositionState paramType;
+
+  static void Write(MessageWriter* aWriter, const paramType& aParam) {
+    WriteParam(aWriter, aParam.mDuration);
+    WriteParam(aWriter, aParam.mPlaybackRate);
+    WriteParam(aWriter, aParam.mLastReportedPlaybackPosition);
+  }
+
+  static bool Read(MessageReader* aReader, paramType* aResult) {
+    if (!ReadParam(aReader, &(aResult->mDuration)) ||
+        !ReadParam(aReader, &(aResult->mPlaybackRate)) ||
+        !ReadParam(aReader, &(aResult->mLastReportedPlaybackPosition))) {
+      return false;
+    }
+    return true;
+  }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaSessionPlaybackState>
+    : public ContiguousEnumSerializer<
+          mozilla::dom::MediaSessionPlaybackState,
+          mozilla::dom::MediaSessionPlaybackState::None,
+          mozilla::dom::MediaSessionPlaybackState::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaSessionAction>
+    : public ContiguousEnumSerializer<
+          mozilla::dom::MediaSessionAction,
+          mozilla::dom::MediaSessionAction::Play,
+          mozilla::dom::MediaSessionAction::EndGuard_> {};
+
+}  // namespace IPC
+
+#endif  // DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
diff --git a/dom/media/mediasession/moz.build b/dom/media/mediasession/moz.build
new file mode 100644
index 0000000000..805bf67971
--- /dev/null
+++ b/dom/media/mediasession/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_MANIFESTS += ["test/mochitest.toml"]
+
+EXPORTS.mozilla.dom += [
+    "MediaMetadata.h",
+    "MediaSession.h",
+    "MediaSessionIPCUtils.h",
+]
+
+UNIFIED_SOURCES += [
+    "MediaMetadata.cpp",
+    "MediaSession.cpp",
+]
+
+CRASHTEST_MANIFESTS += ["test/crashtests/crashtests.list"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediasession/test/MediaSessionTestUtils.js b/dom/media/mediasession/test/MediaSessionTestUtils.js
new file mode 100644
index 0000000000..1ab0e1fe9b
--- /dev/null
+++ b/dom/media/mediasession/test/MediaSessionTestUtils.js
@@ -0,0 +1,30 @@
+const gMediaSessionActions = [
+  "play",
+  "pause",
+  "seekbackward",
+  "seekforward",
+  "previoustrack",
+  "nexttrack",
+  "skipad",
+  "seekto",
+  "stop",
+];
+
+// gCommands and gResults are used in `test_active_mediasession_within_page.html`
+const gCommands = {
+  createMainFrameSession: "create-main-frame-session",
+  createChildFrameSession: "create-child-frame-session",
+  destroyChildFrameSessions: "destroy-child-frame-sessions",
+  destroyActiveChildFrameSession: "destroy-active-child-frame-session",
+  destroyInactiveChildFrameSession: "destroy-inactive-child-frame-session",
+};
+
+const gResults = {
+  mainFrameSession: "main-frame-session",
+  childFrameSession: "child-session-unchanged",
+  childFrameSessionUpdated: "child-session-changed",
+};
+
+function nextWindowMessage() {
+  return new Promise(r => (window.onmessage = event => r(event)));
+}
diff --git a/dom/media/mediasession/test/browser.toml b/dom/media/mediasession/test/browser.toml
new file mode 100644
index 0000000000..1732d096a0
--- /dev/null
+++ b/dom/media/mediasession/test/browser.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+subsuite = "media-bc"
+tags = "mediacontrol"
+support-files = [
+  "file_media_session.html",
+  "../../test/gizmo.mp4",
+]
+
+["browser_active_mediasession_among_tabs.js"]
diff --git a/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js
new file mode 100644
index 0000000000..a70d4502b6
--- /dev/null
+++ b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js
@@ -0,0 +1,198 @@
+/* eslint-disable no-undef */
+"use strict";
+
+const PAGE =
+  "https://example.com/browser/dom/media/mediasession/test/file_media_session.html";
+
+const ACTION = "previoustrack";
+
+add_task(async function setupTestingPref() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["media.mediacontrol.testingevents.enabled", true]],
+  });
+});
+
+/**
+ * When multiple tabs are all having media session, the latest created one would
+ * become an active session. When the active media session is destroyed via
+ * closing the tab, the previous active session would become current active
+ * session again.
+ */
+add_task(async function testActiveSessionWhenClosingTab() {
+  info(`open tab1 and load media session test page`);
+  const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab1 should become active session`);
+  await checkIfActionReceived(tab1, ACTION);
+
+  info(`open tab2 and load media session test page`);
+  const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab2 should become active session`);
+  await checkIfActionReceived(tab2, ACTION);
+  await checkIfActionNotReceived(tab1, ACTION);
+
+  info(`remove tab2`);
+  const controllerChanged = waitUntilMainMediaControllerChanged();
+  BrowserTestUtils.removeTab(tab2);
+  await controllerChanged;
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab1 should become active session again`);
+  await checkIfActionReceived(tab1, ACTION);
+
+  info(`remove tab1`);
+  BrowserTestUtils.removeTab(tab1);
+});
+
+/**
+ * This test is similar with `testActiveSessionWhenClosingTab`, the difference
+ * is that the way we use to destroy active session is via naviagation, not
+ * closing tab.
+ */
+add_task(async function testActiveSessionWhenNavigatingTab() {
+  info(`open tab1 and load media session test page`);
+  const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab1 should become active session`);
+  await checkIfActionReceived(tab1, ACTION);
+
+  info(`open tab2 and load media session test page`);
+  const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab2 should become active session`);
+  await checkIfActionReceived(tab2, ACTION);
+  await checkIfActionNotReceived(tab1, ACTION);
+
+  info(`navigate tab2 to blank page`);
+  const controllerChanged = waitUntilMainMediaControllerChanged();
+  BrowserTestUtils.startLoadingURIString(tab2.linkedBrowser, "about:blank");
+  await controllerChanged;
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab1 should become active session`);
+  await checkIfActionReceived(tab1, ACTION);
+
+  info(`remove tabs`);
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+});
+
+/**
+ * If we create a media session in a tab where no any playing media exists, then
+ * that session would not involve in global active media session selection. The
+ * current active media session would remain unchanged.
+ */
+add_task(async function testCreatingSessionWithoutPlayingMedia() {
+  info(`open tab1 and load media session test page`);
+  const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(`session in tab1 should become active session`);
+  await checkIfActionReceived(tab1, ACTION);
+
+  info(`open tab2 and load media session test page`);
+  const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+  info(`pressing '${ACTION}' key`);
+  MediaControlService.generateMediaControlKey(ACTION);
+
+  info(
+    `session in tab1 is still an active session because there is no media playing in tab2`
+  );
+  await checkIfActionReceived(tab1, ACTION);
+  await checkIfActionNotReceived(tab2, ACTION);
+
+  info(`remove tabs`);
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+});
+
+/**
+ * The following are helper functions
+ */
+async function startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab) {
+  await Promise.all([
+    BrowserUtils.promiseObserved("active-media-session-changed"),
+    SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+      const video = content.document.getElementById("testVideo");
+      if (!video) {
+        ok(false, `can't get the media element!`);
+      }
+      video.play();
+    }),
+  ]);
+}
+
+async function checkIfActionReceived(tab, action) {
+  await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => {
+    return new Promise(resolve => {
+      const result = content.document.getElementById("result");
+      if (!result) {
+        ok(false, `can't get the element for showing result!`);
+      }
+
+      function checkAction() {
+        is(
+          result.innerHTML,
+          expectedAction,
+          `received '${expectedAction}' correctly`
+        );
+        // Reset the result after finishing checking result, then we can dispatch
+        // same action again without worrying about previous result.
+        result.innerHTML = "";
+        resolve();
+      }
+
+      if (result.innerHTML == "") {
+        info(`wait until receiving action`);
+        result.addEventListener("actionChanged", () => checkAction(), {
+          once: true,
+        });
+      } else {
+        checkAction();
+      }
+    });
+  });
+}
+
+async function checkIfActionNotReceived(tab, action) {
+  await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => {
+    return new Promise(resolve => {
+      const result = content.document.getElementById("result");
+      if (!result) {
+        ok(false, `can't get the element for showing result!`);
+      }
+      is(result.innerHTML, "", `should not receive any action`);
+      ok(result.innerHTML != expectedAction, `not receive '${expectedAction}'`);
+      resolve();
+    });
+  });
+}
+
+function waitUntilMainMediaControllerChanged() {
+  return BrowserUtils.promiseObserved("main-media-controller-changed");
+}
diff --git a/dom/media/mediasession/test/crashtests/crashtests.list b/dom/media/mediasession/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..9ca4956ab6
--- /dev/null
+++ b/dom/media/mediasession/test/crashtests/crashtests.list
@@ -0,0 +1 @@
+load inactive-mediasession.html
diff --git a/dom/media/mediasession/test/crashtests/inactive-mediasession.html b/dom/media/mediasession/test/crashtests/inactive-mediasession.html
new file mode 100644
index 0000000000..b24fb887ff
--- /dev/null
+++ b/dom/media/mediasession/test/crashtests/inactive-mediasession.html
@@ -0,0 +1,16 @@
+<html>
+<head></head>
+<script>
+const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame');
+document.documentElement.appendChild(frame);
+
+const windowPointer = frame.contentWindow;
+document.documentElement.replaceWith();
+
+// Setting attributes on inactive media session should not cause crash.
+windowPointer.navigator.mediaSession.setActionHandler('nexttrack', null);
+windowPointer.navigator.mediaSession.playbackState = "playing";
+windowPointer.navigator.mediaSession.setPositionState();
+windowPointer.navigator.mediaSession.metadata = null;
+</script>
+</html>
diff --git a/dom/media/mediasession/test/file_media_session.html b/dom/media/mediasession/test/file_media_session.html
new file mode 100644
index 0000000000..7cff7f4f69
--- /dev/null
+++ b/dom/media/mediasession/test/file_media_session.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Media Session and non-autoplay media</title>
+</head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<h1 id="result"></h1>
+<script type="text/javascript">
+
+const MediaSessionActions = [
+  "play",
+  "pause",
+  "seekbackward",
+  "seekforward",
+  "previoustrack",
+  "nexttrack",
+  "stop",
+];
+
+for (const action of MediaSessionActions) {
+  navigator.mediaSession.setActionHandler(action, () => {
+    document.getElementById("result").innerHTML = action;
+    document.getElementById("result").dispatchEvent(new CustomEvent("actionChanged"));
+  });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html
new file mode 100644
index 0000000000..4d55db2189
--- /dev/null
+++ b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Test frame for triggering media session's action handler</title>
+    <script src="MediaSessionTestUtils.js"></script>
+  </head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<script>
+
+const video = document.getElementById("testVideo");
+const w = window.opener || window.parent;
+
+window.onmessage = async event => {
+  if (event.data == "play") {
+    await video.play();
+    // As we can't observe `media-displayed-playback-changed` notification,
+    // that can only be observed in the chrome process. Therefore, we use a
+    // workaround instead which is to wait for a while to ensure that the
+    // controller has already been created in the chrome process.
+    let timeupdatecount = 0;
+    await new Promise(r => video.ontimeupdate = () => {
+      if (++timeupdatecount == 3) {
+        video.ontimeupdate = null;
+        r();
+      }
+    });
+    w.postMessage("played", "*");
+  }
+}
+
+// Setup the action handlers which would post the result back to the main window.
+for (const action of gMediaSessionActions) {
+  navigator.mediaSession.setActionHandler(action, () => {
+    w.postMessage(action, "*");
+  });
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_window.html b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html
new file mode 100644
index 0000000000..f3316d6dad
--- /dev/null
+++ b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Test window for triggering media session's action handler</title>
+    <script src="/tests/SimpleTest/SimpleTest.js"></script>
+    <script src="MediaSessionTestUtils.js"></script>
+  </head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<iframe id="childFrame"></iframe>
+<script>
+
+var triggeredActionNums = 0;
+
+nextWindowMessage().then(
+  async (event) => {
+    const testInfo = event.data;
+    await createSession(testInfo);
+    // Media session would only become active if there is any media currently
+    // playing. Non-active media session won't receive any actions. Therefore,
+    // we start media playback before testing media session.
+    await startMediaPlayback(testInfo);
+    for (const action of gMediaSessionActions) {
+      await waitUntilActionHandlerTriggered(action, testInfo);
+    }
+    endTestAndReportResult();
+  });
+
+/**
+ * The following are helper functions
+ */
+async function startMediaPlayback({shouldCreateFrom}) {
+  info(`wait until media starts playing`);
+  if (shouldCreateFrom == "main-frame") {
+    const video = document.getElementById("testVideo");
+    await video.play();
+    // As we can't observe `media-displayed-playback-changed` notification,
+    // that can only be observed in the chrome process. Therefore, we use a
+    // workaround instead which is to wait for a while to ensure that the
+    // controller has already been created in the chrome process.
+    let timeupdatecount = 0;
+    await new Promise(r => video.ontimeupdate = () => {
+      if (++timeupdatecount == 3) {
+        video.ontimeupdate = null;
+        r();
+      }
+    });
+  } else {
+    const iframe = document.getElementById("childFrame");
+    iframe.contentWindow.postMessage("play", "*");
+    await new Promise(r => {
+      window.onmessage = event => {
+        is(event.data, "played", `media started playing in child-frame`);
+        r();
+      };
+    });
+  }
+}
+
+async function createSession({shouldCreateFrom, origin}) {
+  info(`create media session in ${shouldCreateFrom}`);
+  if (shouldCreateFrom == "main-frame") {
+    // Simply referencing media session will create media session.
+    navigator.mediaSession;
+    return;
+  };
+  const frame = document.getElementById("childFrame");
+  const originURL = origin == "same-origin"
+    ? "http://mochi.test:8888" : "http://example.org";
+  frame.src = originURL + "/tests/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html";
+  await new Promise(r => frame.onload = r);
+}
+
+async function waitUntilActionHandlerTriggered(action, {shouldCreateFrom}) {
+  info(`wait until '${action}' handler of media session created in ` +
+       `${shouldCreateFrom} is triggered`);
+  if (shouldCreateFrom == "main-frame") {
+    let promise = new Promise(resolve => {
+      navigator.mediaSession.setActionHandler(action, () => {
+        ok(true, `Triggered ${action} handler`);
+        triggeredActionNums++;
+        resolve();
+      });
+    });
+    SpecialPowers.generateMediaControlKeyTestEvent(action);
+    await promise;
+    return;
+  }
+  SpecialPowers.generateMediaControlKeyTestEvent(action);
+  if ((await nextWindowMessage()).data == action) {
+    info(`Triggered ${action} handler in child-frame`);
+    triggeredActionNums++;
+  }
+}
+
+function endTestAndReportResult() {
+  const w = window.opener || window.parent;
+  if (triggeredActionNums == gMediaSessionActions.length) {
+    w.postMessage("success", "*");
+  } else {
+    w.postMessage("fail", "*");
+  }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/mochitest.toml b/dom/media/mediasession/test/mochitest.toml
new file mode 100644
index 0000000000..130c08ab00
--- /dev/null
+++ b/dom/media/mediasession/test/mochitest.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+subsuite = "media"
+tags = "mediasession mediacontrol"
+
+support-files = [
+  "../../test/gizmo.mp4",
+  "file_trigger_actionhanlder_frame.html",
+  "file_trigger_actionhanlder_window.html",
+  "MediaSessionTestUtils.js",
+]
+
+["test_setactionhandler.html"]
+
+["test_trigger_actionhanlder.html"]
diff --git a/dom/media/mediasession/test/test_setactionhandler.html b/dom/media/mediasession/test/test_setactionhandler.html
new file mode 100644
index 0000000000..77d4703a3b
--- /dev/null
+++ b/dom/media/mediasession/test/test_setactionhandler.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title></title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+const ACTIONS = [
+  "play",
+  "pause",
+  "seekbackward",
+  "seekforward",
+  "previoustrack",
+  "nexttrack",
+  "skipad",
+  "seekto",
+  "stop",
+];
+
+(async function testSetActionHandler() {
+  for (const action of ACTIONS) {
+    info(`Test setActionHandler for '${action}'`);
+    generateAction(action);
+    ok(true, "it's ok to do " + action + " without any handler");
+
+    let expectedDetails = generateActionDetails(action);
+
+    let fired = false;
+    await setHandlerAndTakeAction(action, details => {
+      ok(hasSameValue(details, expectedDetails), "get expected details for " + action);
+      fired = !fired;
+      clearActionHandler(action);
+    });
+
+    generateAction(action);
+    ok(fired, "handler of " + action + " is cleared");
+  }
+
+  SimpleTest.finish();
+})();
+
+function generateAction(action) {
+  let details = generateActionDetails(action);
+  SpecialPowers.wrap(navigator.mediaSession).notifyHandler(details);
+}
+
+function generateActionDetails(action) {
+  let details = { action };
+  if (action == "seekbackward" || action == "seekforward") {
+    details.seekOffset = 3.14159;
+  } else if (action == "seekto") {
+    details.seekTime = 1.618;
+  }
+  return details;
+}
+
+function setHandlerAndTakeAction(action, handler) {
+  let promise = new Promise(resolve => {
+    navigator.mediaSession.setActionHandler(action, details => {
+      handler(details);
+      resolve();
+    });
+  });
+  generateAction(action);
+  return promise;
+}
+
+function hasSameValue(a, b) {
+  // The order of the object matters when stringify the object.
+  return JSON.stringify(a) == JSON.stringify(b);
+}
+
+function clearActionHandler(action) {
+  navigator.mediaSession.setActionHandler(action, null);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/test_trigger_actionhanlder.html b/dom/media/mediasession/test/test_trigger_actionhanlder.html
new file mode 100644
index 0000000000..a0e170c868
--- /dev/null
+++ b/dom/media/mediasession/test/test_trigger_actionhanlder.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Test of triggering media session's action handlers</title>
+    <script src="/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+    <script src="MediaSessionTestUtils.js"></script>
+  </head>
+<body>
+<script>
+/**
+ * This test is used to test if pressing media control keys can trigger media
+ * session's corresponding action handler under different situations.
+ */
+const testCases = [
+  {
+    name: "Triggering action handlers for session created in [main-frame]",
+    shouldCreateFrom: "main-frame",
+  },
+  {
+    name: "Triggering action handlers for session created in [same-origin] [child-frame]",
+    shouldCreateFrom: "child-frame",
+    origin: "same-origin",
+  },
+  {
+    name: "Triggering action handlers for session created in [cross-origin] [child-frame]",
+    shouldCreateFrom: "child-frame",
+    origin: "cross-origin",
+  },
+];
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [
+  ["media.mediacontrol.testingevents.enabled", true],
+]}, startTest());
+
+async function startTest() {
+  for (const testCase of testCases) {
+    info(`- loading test '${testCase.name}' in a new window -`);
+    const testURL = "file_trigger_actionhanlder_window.html";
+    const testWindow = window.open(testURL, "", "width=500,height=500");
+    await new Promise(r => testWindow.onload = r);
+
+    info("- start running test -");
+    testWindow.postMessage(testCase, window.origin);
+    is((await nextWindowMessage()).data, "success",
+       `- finished test '${testCase.name}' -`);
+    testWindow.close();
+  }
+  SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
-- 
cgit v1.2.3