From 1eb3d684e4f524b034a4e022c7fc9408b50b482b Mon Sep 17 00:00:00 2001
From: Marc Streckfuss <marc.streckfuss@gmail.com>
Date: Wed, 12 Feb 2020 13:10:50 +0000
Subject: [PATCH] Bug 1353652 - Initial Draft of MPRIS API Provider (Media API
 on Linux) r=alwu

Differential Revision: https://phabricator.services.mozilla.com/D47999

--HG--
extra : moz-landing-system : lando
---
 .clang-format-ignore                       |   3 +
 widget/gtk/MPRISInterfaceDescription.h     | 115 ++++
 widget/gtk/MPRISServiceHandler.cpp         | 653 +++++++++++++++++++++
 widget/gtk/MPRISServiceHandler.h           | 162 +++++
 widget/gtk/MediaKeysEventSourceFactory.cpp |   4 +-
 widget/gtk/moz.build                       |   1 +
 6 files changed, 936 insertions(+), 2 deletions(-)
 create mode 100644 widget/gtk/MPRISInterfaceDescription.h
 create mode 100644 widget/gtk/MPRISServiceHandler.cpp
 create mode 100644 widget/gtk/MPRISServiceHandler.h

diff --git a/.clang-format-ignore b/.clang-format-ignore
index 2ead0193e7195..e11ac133ced57 100644
--- a/.clang-format-ignore
+++ b/.clang-format-ignore
@@ -59,6 +59,9 @@ tools/clang-tidy/test/.*
 # We are testing the incorrect formatting.
 tools/lint/test/files/file-whitespace/
 
+# Contains an XML definition and formatting would break the layout
+widget/gtk/MPRISInterfaceDescription.h
+
 # The XPTCall stubs files have some inline assembly macros
 # that get reformatted badly. See bug 1510781.
 xpcom/reflect/xptcall/md/win32/.*
diff --git a/widget/gtk/MPRISInterfaceDescription.h b/widget/gtk/MPRISInterfaceDescription.h
new file mode 100644
index 0000000000000..9ff4393237e45
--- /dev/null
+++ b/widget/gtk/MPRISInterfaceDescription.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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 WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
+#define WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
+
+#include <gio/gio.h>
+
+extern const gchar introspection_xml[] =
+    // adopted from https://github.com/freedesktop/mpris-spec/blob/master/spec/org.mpris.MediaPlayer2.xml
+    // everything starting with tp can be removed, as it is used for HTML Spec Documentation Generation
+    "<node>"
+        "<interface name=\"org.mpris.MediaPlayer2\">"
+            "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "<method name=\"Raise\"/>"
+            "<method name=\"Quit\"/>"
+            "<property name=\"CanQuit\" type=\"b\" access=\"read\"/>"
+        #ifdef MPRIS_FULLSCREEN
+            "<property name=\"Fullscreen\" type=\"b\" access=\"readwrite\">"
+                "<annotation name=\"org.mpris.MediaPlayer2.property.optional\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanSetFullscreen\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.mpris.MediaPlayer2.property.optional\" value=\"true\"/>"
+            "</property>"
+        #endif
+            "<property name=\"CanRaise\" type=\"b\" access=\"read\"/>"
+            "<property name=\"HasTrackList\" type=\"b\" access=\"read\"/>"
+            "<property name=\"Identity\" type=\"s\" access=\"read\"/>"
+        #ifdef MPRIS_DESKTOP_ENTRY
+            "<property name=\"DesktopEntry\" type=\"s\" access=\"read\">"
+                "<annotation name=\"org.mpris.MediaPlayer2.property.optional\" value=\"true\"/>"
+            "</property>"
+        #endif
+            "<property name=\"SupportedUriSchemes\" type=\"as\" access=\"read\"/>"
+            "<property name=\"SupportedMimeTypes\" type=\"as\" access=\"read\"/>"
+        "</interface>"
+            // Note that every property emits a changed signal (which is default) apart from Position.
+        "<interface name=\"org.mpris.MediaPlayer2.Player\">"
+            "<method name=\"Next\"/>"
+            "<method name=\"Previous\"/>"
+            "<method name=\"Pause\"/>"
+            "<method name=\"PlayPause\"/>"
+            "<method name=\"Stop\"/>"
+            "<method name=\"Play\"/>"
+            "<method name=\"Seek\">"
+                "<arg direction=\"in\" type=\"x\" name=\"Offset\"/>"
+            "</method>"
+            "<method name=\"SetPosition\">"
+                "<arg direction=\"in\" type=\"o\" name=\"TrackId\"/>"
+                "<arg direction=\"in\" type=\"x\" name=\"Position\"/>"
+            "</method>"
+            "<method name=\"OpenUri\">"
+                "<arg direction=\"in\" type=\"s\" name=\"Uri\"/>"
+            "</method>"
+            "<property name=\"PlaybackStatus\" type=\"s\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+        #ifdef MPRIS_LOOP_STATUS
+            "<property name=\"LoopStatus\" type=\"s\" access=\"readwrite\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+                "<annotation name=\"org.mpris.MediaPlayer2.property.optional\" value=\"true\"/>"
+            "</property>"
+        #endif
+            "<property name=\"Rate\" type=\"d\" access=\"readwrite\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+        #ifdef MRPIS_SHUFFLE
+            "<property name=\"Shuffle\" type=\"b\" access=\"readwrite\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+                "<annotation name=\"org.mpris.MediaPlayer2.property.optional\" value=\"true\"/>"
+            "</property>"
+        #endif
+            "<property name=\"Metadata\" type=\"a{sv}\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"Volume\" type=\"d\" access=\"readwrite\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"Position\" type=\"x\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>"
+            "</property>"
+            "<property name=\"MinimumRate\" type=\"d\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"MaximumRate\" type=\"d\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanGoNext\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanGoPrevious\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanPlay\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanPause\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanSeek\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+            "</property>"
+            "<property name=\"CanControl\" type=\"b\" access=\"read\">"
+                "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>"
+            "</property>"
+            "<signal name=\"Seeked\">"
+                "<arg name=\"Position\" type=\"x\"/>"
+            "</signal>"
+        "</interface>"
+    "</node>";
+
+#endif  // WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp
new file mode 100644
index 0000000000000..b1751709b557c
--- /dev/null
+++ b/widget/gtk/MPRISServiceHandler.cpp
@@ -0,0 +1,653 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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 "MPRISServiceHandler.h"
+
+#include <stdint.h>
+#include <inttypes.h>
+#include <unordered_map>
+
+#include "MPRISInterfaceDescription.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Sprintf.h"
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...)                        \
+  MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+          ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+enum class Method : uint8_t {
+  eQuit,
+  eRaise,
+  eNext,
+  ePrevious,
+  ePause,
+  ePlayPause,
+  eStop,
+  ePlay,
+  eSeek,
+  eSetPosition,
+  eOpenUri,
+  eUnknown
+};
+
+static inline Method GetMethod(const gchar* aMethodName) {
+  const std::unordered_map<std::string, Method> map = {
+      {"Quit", Method::eQuit},      {"Raise", Method::eRaise},
+      {"Next", Method::eNext},      {"Previous", Method::ePrevious},
+      {"Pause", Method::ePause},    {"PlayPause", Method::ePlayPause},
+      {"Stop", Method::eStop},      {"Play", Method::ePlay},
+      {"Seek", Method::eSeek},      {"SetPosition", Method::eSetPosition},
+      {"OpenUri", Method::eOpenUri}};
+
+  auto it = map.find(aMethodName);
+  return (it == map.end() ? Method::eUnknown : it->second);
+}
+
+static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender,
+                             const gchar* aObjectPath,
+                             const gchar* aInterfaceName,
+                             const gchar* aMethodName, GVariant* aParameters,
+                             GDBusMethodInvocation* aInvocation,
+                             gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  MOZ_ASSERT(NS_IsMainThread());
+  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+  std::string error;
+
+  switch (GetMethod(aMethodName)) {
+    case Method::eUnknown:
+      g_dbus_method_invocation_return_error(
+          aInvocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Invalid Method");
+      return;
+    case Method::eQuit:
+      if (handler->CanQuit()) {
+        handler->Quit();
+      } else {
+        error = "Cannot invoke Quit() when CanQuit() returns false";
+      }
+      break;
+    case Method::eRaise:
+      if (handler->CanRaise()) {
+        handler->Raise();
+      } else {
+        error = "Cannot invoke Raise() when CanRaise() returns false";
+      }
+      break;
+    case Method::eNext:
+      if (handler->CanGoNext()) {
+        handler->Next();
+      } else {
+        error = "Cannot invoke Next() when CanGoNext() returns false";
+      }
+      break;
+    case Method::ePrevious:
+      if (handler->CanGoPrevious()) {
+        handler->Previous();
+      } else {
+        error = "Cannot invoke Previous() when CanGoPrevious() returns false";
+      }
+      break;
+    case Method::ePause:
+      if (handler->CanPause()) {
+        handler->Pause();
+      } else {
+        error = "Cannot invoke Pause() when CanPause() returns false";
+      }
+      break;
+    case Method::ePlayPause:
+      // According to Spec this should only fail if canPause is false, but Play
+      // may be forbidden due to CanPlay. This means in theory even though
+      // CanPlay is false, this method would be able to Play something which
+      // means when CanPause is false, CanPlay _has to be_ false as well.
+      if (handler->CanPlay() && handler->CanPause()) {
+        handler->PlayPause();
+      } else {
+        error =
+            "Cannot invoke PlayPause() when either CanPlay() or CanPause() "
+            "returns false";
+      }
+      break;
+    case Method::eStop:
+      handler->Stop();  // Stop is mandatory
+      break;
+    case Method::ePlay:
+      if (handler->CanPlay()) {
+        handler->Play();
+      } else {
+        error = "Cannot invoke Play() when CanPlay() returns false";
+      }
+      break;
+    case Method::eSeek:
+      if (handler->CanSeek()) {
+        gint64 position;
+        g_variant_get(aParameters, "(x)", &position);
+        handler->Seek(position);
+      } else {
+        error = "Cannot invoke Seek() when CanSeek() returns false";
+      }
+      break;
+    case Method::eSetPosition:
+      if (handler->CanSeek()) {
+        gchar* trackId;
+        gint64 position;
+        g_variant_get(aParameters, "(ox)", &trackId, &position);
+        handler->SetPosition(trackId, position);
+      } else {
+        error = "Cannot invoke SetPosition() when CanSeek() returns false";
+      }
+      break;
+    case Method::eOpenUri:
+      gchar* uri;
+      g_variant_get(aParameters, "(s)", &uri);
+      if (!handler->OpenUri(uri)) {
+        error = "Could not open URI";
+      }
+      break;
+  }
+
+  if (!error.empty()) {
+    g_dbus_method_invocation_return_error(
+        aInvocation, G_IO_ERROR, G_IO_ERROR_READ_ONLY, "%s", error.c_str());
+  }
+}
+
+enum class Property : uint8_t {
+  eIdentity,
+  eHasTrackList,
+  eCanRaise,
+  eCanQuit,
+  eSupportedUriSchemes,
+  eSupportedMimeTypes,
+  eCanGoNext,
+  eCanGoPrevious,
+  eCanPlay,
+  eCanPause,
+  eCanSeek,
+  eCanControl,
+  eGetVolume,
+  eGetPosition,
+  eGetMinimumRate,
+  eGetMaximumRate,
+  eGetRate,
+  eGetPlaybackStatus,
+  eGetMetadata,
+  eUnknown
+};
+
+static inline Property GetProperty(const gchar* aPropertyName) {
+  const std::unordered_map<std::string, Property> map = {
+      {"Identity", Property::eIdentity},
+      {"HasTrackList", Property::eHasTrackList},
+      {"CanRaise", Property::eCanRaise},
+      {"CanQuit", Property::eCanQuit},
+      {"SupportedUriSchemes", Property::eSupportedUriSchemes},
+      {"SupportedMimeTypes", Property::eSupportedMimeTypes},
+      {"CanGoNext", Property::eCanGoNext},
+      {"CanGoPrevious", Property::eCanGoPrevious},
+      {"CanPlay", Property::eCanPlay},
+      {"CanPause", Property::eCanPause},
+      {"CanSeek", Property::eCanSeek},
+      {"CanControl", Property::eCanControl},
+      {"Volume", Property::eGetVolume},
+      {"Position", Property::eGetPosition},
+      {"MinimumRate", Property::eGetMinimumRate},
+      {"MaximumRate", Property::eGetMaximumRate},
+      {"Rate", Property::eGetRate},
+      {"PlaybackStatus", Property::eGetPlaybackStatus},
+      {"Metadata", Property::eGetMetadata}};
+
+  auto it = map.find(aPropertyName);
+  return (it == map.end() ? Property::eUnknown : it->second);
+}
+
+static GVariant* HandleGetProperty(GDBusConnection* aConnection,
+                                   const gchar* aSender,
+                                   const gchar* aObjectPath,
+                                   const gchar* aInterfaceName,
+                                   const gchar* aPropertyName, GError** aError,
+                                   gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  MOZ_ASSERT(NS_IsMainThread());
+  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+
+  switch (GetProperty(aPropertyName)) {
+    case Property::eUnknown:
+      g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown Property");
+      return nullptr;
+    case Property::eIdentity:
+      return g_variant_new_string(handler->Identity());
+    case Property::eHasTrackList:
+      return g_variant_new_boolean(handler->HasTrackList());
+    case Property::eCanRaise:
+      return g_variant_new_boolean(handler->CanRaise());
+    case Property::eCanQuit:
+      return g_variant_new_boolean(handler->CanQuit());
+    case Property::eSupportedUriSchemes:
+      return handler->SupportedUriSchemes();
+    case Property::eSupportedMimeTypes:
+      return handler->SupportedMimeTypes();
+    case Property::eCanGoNext:
+      return g_variant_new_boolean(handler->CanGoNext());
+    case Property::eCanGoPrevious:
+      return g_variant_new_boolean(handler->CanGoPrevious());
+    case Property::eCanPlay:
+      return g_variant_new_boolean(handler->CanPlay());
+    case Property::eCanPause:
+      return g_variant_new_boolean(handler->CanPause());
+    case Property::eCanSeek:
+      return g_variant_new_boolean(handler->CanSeek());
+    case Property::eCanControl:
+      return g_variant_new_boolean(handler->CanControl());
+    case Property::eGetVolume:
+      return g_variant_new_double(handler->GetVolume());
+    case Property::eGetPosition:
+      return g_variant_new_int64(handler->GetPosition());
+    case Property::eGetMinimumRate:
+      return g_variant_new_double(handler->GetMinimumRate());
+    case Property::eGetMaximumRate:
+      return g_variant_new_double(handler->GetMaximumRate());
+    case Property::eGetRate:
+      return g_variant_new_double(handler->GetRate());
+    case Property::eGetPlaybackStatus:
+      if (GVariant* state = handler->GetPlaybackStatus()) {
+        return state;
+      }
+      g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
+                  "Invalid Playback Status");
+      return nullptr;
+    case Property::eGetMetadata:
+      std::vector<struct MPRISMetadata> list = handler->GetDefaultMetadata();
+      GVariantBuilder builder;
+      g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+      for (auto const& data : list) {
+        g_variant_builder_add(&builder, "{sv}", data.mKey, data.mValue);
+      }
+      return g_variant_builder_end(&builder);
+  }
+
+  MOZ_ASSERT_UNREACHABLE("Switch Statement incomplete");
+  return nullptr;
+}
+
+static gboolean HandleSetProperty(GDBusConnection* aConnection,
+                                  const gchar* aSender,
+                                  const gchar* aObjectPath,
+                                  const gchar* aInterfaceName,
+                                  const gchar* aPropertyName, GVariant* aValue,
+                                  GError** aError, gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  MOZ_ASSERT(NS_IsMainThread());
+  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+
+  if (g_strcmp0(aPropertyName, "Volume") == 0) {
+    if (!handler->SetVolume(g_variant_get_double(aValue))) {
+      g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
+                  "Could not set the Volume");
+      return false;
+    }
+  } else if (g_strcmp0(aPropertyName, "Rate") == 0) {
+    if (!handler->SetRate(g_variant_get_double(aValue))) {
+      g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
+                  "Could not set the Rate");
+      return false;
+    }
+  } else {
+    g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown Property");
+    return false;
+  }
+
+  GVariantBuilder
+      propertiesBuilder;  // a builder for the list of changed properties
+  g_variant_builder_init(&propertiesBuilder, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add(&propertiesBuilder, "{sv}", aPropertyName, aValue);
+
+  return g_dbus_connection_emit_signal(
+      aConnection, nullptr, aObjectPath, "org.freedesktop.DBus.Properties",
+      "PropertiesChanged",
+      g_variant_new("(sa{sv}as)", aInterfaceName, &propertiesBuilder, nullptr),
+      aError);
+}
+
+static const GDBusInterfaceVTable gInterfaceVTable = {
+    HandleMethodCall, HandleGetProperty, HandleSetProperty};
+
+void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection,
+                                               const gchar* aName,
+                                               gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection,
+                                                               aName);
+}
+
+void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection,
+                                           const gchar* aName,
+                                           gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName);
+}
+
+void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection,
+                                              const gchar* aName,
+                                              gpointer aUserData) {
+  MOZ_ASSERT(aUserData);
+  static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection,
+                                                              aName);
+}
+
+void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection,
+                                         const gchar* aName) {
+  LOG("OnNameAcquired: %s", aName);
+  mConnection = aConnection;
+}
+
+void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection,
+                                     const gchar* aName) {
+  LOG("OnNameLost: %s", aName);
+  mConnection = nullptr;
+  if (!mRootRegistrationId) {
+    return;
+  }
+
+  if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) {
+    mRootRegistrationId = 0;
+  } else {
+    // Note: Most code examples in the internet probably dont't even check the
+    // result here, but
+    // according to the spec it _can_ return false.
+    LOG("Unable to unregister root object from within onNameLost!");
+  }
+
+  if (!mPlayerRegistrationId) {
+    return;
+  }
+
+  if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) {
+    mPlayerRegistrationId = 0;
+  } else {
+    // Note: Most code examples in the internet probably dont't even check the
+    // result here, but
+    // according to the spec it _can_ return false.
+    LOG("Unable to unregister object from within onNameLost!");
+  }
+}
+
+void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection,
+                                        const gchar* aName) {
+  GError* error = nullptr;
+  LOG("OnBusAcquired: %s", aName);
+
+  mRootRegistrationId = g_dbus_connection_register_object(
+      aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0],
+      &gInterfaceVTable, this, /* user_data */
+      nullptr,                 /* user_data_free_func */
+      &error);                 /* GError** */
+
+  if (mRootRegistrationId == 0) {
+    LOG("Failed at root registration: %s",
+        error ? error->message : "Unknown Error");
+    if (error) {
+      g_error_free(error);
+    }
+    return;
+  }
+
+  mPlayerRegistrationId = g_dbus_connection_register_object(
+      aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1],
+      &gInterfaceVTable, this, /* user_data */
+      nullptr,                 /* user_data_free_func */
+      &error);                 /* GError** */
+
+  if (mPlayerRegistrationId == 0) {
+    LOG("Failed at object registration %s",
+        error ? error->message : "Unknown Error");
+    if (error) {
+      g_error_free(error);
+    }
+  }
+}
+
+bool MPRISServiceHandler::Open() {
+  MOZ_ASSERT(!mInitialized);
+  MOZ_ASSERT(NS_IsMainThread());
+  GError* error = nullptr;
+  gchar serviceName[256];
+  SprintfLiteral(serviceName, DBUS_MRPIS_SERVICE_NAME ".instance%d", getpid());
+  mOwnerId =
+      g_bus_own_name(G_BUS_TYPE_SESSION, serviceName,
+                     // Enter a waiting queue until this service name is free
+                     // (likely another FF instance is running/has been crashed)
+                     G_BUS_NAME_OWNER_FLAGS_NONE, OnBusAcquiredStatic,
+                     OnNameAcquiredStatic, OnNameLostStatic, this, nullptr);
+
+  /* parse introspection data */
+  mIntrospectionData = g_dbus_node_info_new_for_xml(introspection_xml, &error);
+
+  if (!mIntrospectionData) {
+    LOG("Failed at parsing XML Interface definition %s",
+        error ? error->message : "Unknown Error");
+    if (error) {
+      g_error_free(error);
+    }
+    return false;
+  }
+
+  mInitialized = true;
+  return true;
+}
+
+MPRISServiceHandler::~MPRISServiceHandler() {
+  MOZ_ASSERT(!mInitialized);  // Close hasn't been called!
+}
+
+void MPRISServiceHandler::Close() {
+  gchar serviceName[256];
+  SprintfLiteral(serviceName, DBUS_MRPIS_SERVICE_NAME ".instance%" PRId32,
+                 getpid());
+
+  OnNameLost(mConnection, serviceName);
+
+  if (mOwnerId != 0) {
+    g_bus_unown_name(mOwnerId);
+  }
+  if (mIntrospectionData) {
+    g_dbus_node_info_unref(mIntrospectionData);
+  }
+
+  mInitialized = false;
+  MediaControlKeysEventSource::Close();
+}
+
+bool MPRISServiceHandler::IsOpened() const { return mInitialized; }
+
+bool MPRISServiceHandler::HasTrackList() { return false; }
+
+const char* MPRISServiceHandler::Identity() { return "Mozilla Firefox"; }
+
+GVariant* MPRISServiceHandler::SupportedUriSchemes() {
+  GVariantBuilder builder;
+  g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
+  return g_variant_builder_end(&builder);
+}
+
+GVariant* MPRISServiceHandler::SupportedMimeTypes() {
+  GVariantBuilder builder;
+  g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
+  return g_variant_builder_end(&builder);
+}
+
+constexpr bool MPRISServiceHandler::CanRaise() { return false; }
+
+void MPRISServiceHandler::Raise() {
+  MOZ_ASSERT_UNREACHABLE("CanRaise is false, this method is not implemented");
+}
+
+constexpr bool MPRISServiceHandler::CanQuit() { return false; }
+
+void MPRISServiceHandler::Quit() {
+  MOZ_ASSERT_UNREACHABLE("CanQuit is false, this method is not implemented");
+}
+
+bool MPRISServiceHandler::CanGoNext() const { return true; }
+
+bool MPRISServiceHandler::CanGoPrevious() const { return true; }
+
+bool MPRISServiceHandler::CanPlay() const { return true; }
+
+bool MPRISServiceHandler::CanPause() const { return true; }
+
+// We don't support Seeking or Setting/Getting the Position yet
+bool MPRISServiceHandler::CanSeek() const { return false; }
+
+bool MPRISServiceHandler::CanControl() const {
+  return true;  // we don't support LoopStatus, Shuffle, Rate or Volume, but at
+                // least KDE blocks Play/Pause when CanControl is false.
+}
+
+// We don't support getting the volume (yet) so return a dummy value.
+double MPRISServiceHandler::GetVolume() const { return 1.0f; }
+
+// we don't support setting the volume yet, so this is a no-op
+bool MPRISServiceHandler::SetVolume(double aVolume) {
+  if (aVolume > 1.0f || aVolume < 0.0f) {
+    return false;
+  }
+  LOG("Volume set to %f", aVolume);
+  return true;
+}
+int64_t MPRISServiceHandler::GetPosition() const { return 0; }
+
+constexpr double MPRISServiceHandler::GetMinimumRate() { return 1.0f; }
+
+constexpr double MPRISServiceHandler::GetMaximumRate() { return 1.0f; }
+
+// Getting and Setting the Rate doesn't work yet, so it will be locked to 1.0
+double MPRISServiceHandler::GetRate() const { return 1.0f; }
+
+bool MPRISServiceHandler::SetRate(double aRate) {
+  if (aRate > GetMaximumRate() || aRate < GetMinimumRate()) {
+    return false;
+  }
+
+  LOG("Set Playback Rate to %f", aRate);
+  return true;
+}
+
+void MPRISServiceHandler::SetPlaybackState(dom::PlaybackState aState) {
+  LOG("SetPlaybackState");
+  if (mPlaybackState == aState) {
+    return;
+  }
+
+  MediaControlKeysEventSource::SetPlaybackState(aState);
+
+  if (!mConnection) {
+    return;  // No D-Bus Connection, no event
+  }
+
+  GVariant* state = GetPlaybackStatus();
+  if (!state) {
+    return;  // Invalid state
+  }
+
+  GVariantBuilder builder;
+  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+  g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state);
+  g_dbus_connection_emit_signal(
+      mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH,
+      "org.freedesktop.DBus.Properties", "PropertiesChanged",
+      g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2", &builder, nullptr),
+      nullptr);
+}
+
+GVariant* MPRISServiceHandler::GetPlaybackStatus() const {
+  switch (GetPlaybackState()) {
+    case dom::PlaybackState::ePlaying:
+      return g_variant_new_string("Playing");
+    case dom::PlaybackState::ePaused:
+      return g_variant_new_string("Paused");
+    case dom::PlaybackState::eStopped:
+      return g_variant_new_string("Stopped");
+    default:
+      MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
+      return nullptr;
+  }
+}
+
+void MPRISServiceHandler::EmitEvent(mozilla::dom::MediaControlKeysEvent event) {
+  for (auto& listener : mListeners) {
+    listener->OnKeyPressed(event);
+  }
+}
+
+void MPRISServiceHandler::Next() {
+  LOG("Next");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::eNextTrack);
+}
+
+void MPRISServiceHandler::Previous() {
+  LOG("Previous");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::ePrevTrack);
+}
+
+void MPRISServiceHandler::Pause() {
+  LOG("Pause");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::ePause);
+}
+
+void MPRISServiceHandler::PlayPause() {
+  LOG("PlayPause");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::ePlayPause);
+}
+
+void MPRISServiceHandler::Stop() {
+  LOG("Stop");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::eStop);
+}
+
+void MPRISServiceHandler::Play() {
+  LOG("Play");
+  EmitEvent(mozilla::dom::MediaControlKeysEvent::ePlay);
+}
+
+// Caution, Seek can really be negative, like -1000000 during testing
+void MPRISServiceHandler::Seek(int64_t aOffset) {
+  LOG("Seek(%" PRId64 ")", aOffset);
+}
+
+// The following two methods are untested as my Desktop Widget didn't issue
+// these calls.
+void MPRISServiceHandler::SetPosition(char* aTrackId, int64_t aPosition) {
+  LOG("SetPosition(%s, %" PRId64 ")", aTrackId, aPosition);
+}
+
+bool MPRISServiceHandler::OpenUri(char* aUri) {
+  LOG("OpenUri(%s)", aUri);
+  return false;
+}
+
+std::vector<struct MPRISMetadata> MPRISServiceHandler::GetDefaultMetadata() {
+  std::vector<struct MPRISMetadata> list;
+
+  list.push_back({"mpris:trackid", g_variant_new("o", "/valid/path")});
+  list.push_back({"xesam:title", g_variant_new_string("Firefox")});
+
+  GVariantBuilder artistBuilder;  // Artists is a list.
+  g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as"));
+  g_variant_builder_add(&artistBuilder, "s", "Mozilla");
+  GVariant* artists = g_variant_builder_end(&artistBuilder);
+
+  list.push_back({"xesam:artist", artists});
+  return list;
+}
+
+}  // namespace widget
+}  // namespace mozilla
diff --git a/widget/gtk/MPRISServiceHandler.h b/widget/gtk/MPRISServiceHandler.h
new file mode 100644
index 0000000000000..fc6b3f8c62cd2
--- /dev/null
+++ b/widget/gtk/MPRISServiceHandler.h
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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 WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
+#define WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
+
+#include <gio/gio.h>
+#include "mozilla/dom/MediaControlKeysEvent.h"
+#include "mozilla/Attributes.h"
+
+#define DBUS_MRPIS_SERVICE_NAME "org.mpris.MediaPlayer2.firefox"
+#define DBUS_MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2"
+
+namespace mozilla {
+namespace widget {
+
+struct MPRISMetadata {
+  const char* mKey;
+  GVariant* mValue;
+};
+
+/**
+ * This class implements the "MPRIS" D-Bus Service
+ * (https://specifications.freedesktop.org/mpris-spec/2.2),
+ * which is used to communicate with the Desktop Environment about the
+ * Multimedia playing in Gecko.
+ * Note that this interface requires many methods which may not be supported by
+ * Gecko, the interface
+ * however provides CanXYZ properties for these methods, so the method is
+ * defined but won't be executed.
+ *
+ * Also note that the following defines are for parts that the MPRIS Spec
+ * defines optional. The code won't
+ * compile with any of the defines set, yet, as those aren't implemented yet and
+ * probably never will be of
+ * use for gecko. For sake of completeness, they have been added until the
+ * decision about their implementation
+ * is finally made.
+ *
+ * The constexpr'ed methods are capabilities of the user agent known at compile
+ * time, e.g. we decided at
+ * compile time whether we ever want to support closing the user agent via MPRIS
+ * (Quit() and CanQuit()).
+ *
+ * Other properties like CanPlay() might depend on the runtime state (is there
+ * media available for playback?)
+ * and thus aren't a constexpr but merely a const method.
+ */
+class MPRISServiceHandler final : public dom::MediaControlKeysEventSource {
+  NS_INLINE_DECL_REFCOUNTING(MPRISServiceHandler, override)
+ public:
+  // Note that this constructor does NOT initialize the MPRIS Service but only
+  // this class. The method Open() is responsible for registering and MAY FAIL.
+  MPRISServiceHandler() = default;
+  bool Open() override;
+  void Close() override;
+  bool IsOpened() const override;
+
+  // From the EventSource.
+  void SetPlaybackState(dom::PlaybackState aState) override;
+
+  // GetPlaybackState returns dom::PlaybackState. GetPlaybackStatus returns this
+  // state converted into d-bus variants.
+  GVariant* GetPlaybackStatus() const;
+
+// Implementations of the MPRIS API Methods/Properties. constexpr'ed properties
+// will be what the user agent doesn't support and thus they are known at
+// compile time.
+#ifdef MPRIS_FULLSCREEN
+  bool GetFullscreen();
+  void SetFullscreen(bool aFullscreen);
+  bool CanSetFullscreen();
+#endif
+  bool HasTrackList();
+  const char* Identity();
+#ifdef MPRIS_DESKTOP_ENTRY
+  const char* DesktopEntry();
+#endif
+  GVariant* SupportedUriSchemes();
+  GVariant* SupportedMimeTypes();
+  constexpr bool CanRaise();
+  void Raise();
+  constexpr bool CanQuit();
+  void Quit();
+
+  // :Player::Methods
+  void Next();
+  void Previous();
+  void Pause();
+  void PlayPause();
+  void Stop();
+  void Play();
+  void Seek(int64_t aOffset);
+  void SetPosition(char* aTrackId, int64_t aPosition);
+  // bool is our custom addition: return false whether opening fails/is not
+  // supported for that URI it will raise a DBUS Error
+  bool OpenUri(char* aUri);
+
+#ifdef MPRIS_LOOP_STATUS
+  MPRISLoopStatus GetLoopStatus();
+#endif
+
+  double GetRate() const;
+  bool SetRate(double aRate);
+  constexpr double GetMinimumRate();
+  constexpr double GetMaximumRate();
+
+#ifdef MPRIS_SHUFFLE
+  bool GetShuffle() const;
+  void SetShuffle(bool aShuffle);
+#endif
+
+  std::vector<struct MPRISMetadata> GetDefaultMetadata();
+  double GetVolume() const;
+  bool SetVolume(double aVolume);
+  int64_t GetPosition() const;
+
+  bool CanGoNext() const;
+  bool CanGoPrevious() const;
+  bool CanPlay() const;
+  bool CanPause() const;
+  bool CanSeek() const;
+  bool CanControl() const;
+
+ private:
+  ~MPRISServiceHandler();
+
+  // Note: Registration Ids for the D-Bus start with 1, so a value of 0
+  // indicates an error (or an object which wasn't initialized yet)
+
+  // a handle to our bus registration/ownership
+  guint mOwnerId = 0;
+  // This is for the interface org.mpris.MediaPlayer2
+  guint mRootRegistrationId = 0;
+  // This is for the interface org.mpris.MediaPlayer2.Player
+  guint mPlayerRegistrationId = 0;
+  GDBusNodeInfo* mIntrospectionData = nullptr;
+  GDBusConnection* mConnection = nullptr;
+  bool mInitialized = false;
+
+  // non-public API, called from events
+  void OnNameAcquired(GDBusConnection* aConnection, const gchar* aName);
+  void OnNameLost(GDBusConnection* aConnection, const gchar* aName);
+  void OnBusAcquired(GDBusConnection* aConnection, const gchar* aName);
+
+  static void OnNameAcquiredStatic(GDBusConnection* aConnection,
+                                   const gchar* aName, gpointer aUserData);
+  static void OnNameLostStatic(GDBusConnection* aConnection, const gchar* aName,
+                               gpointer aUserData);
+  static void OnBusAcquiredStatic(GDBusConnection* aConnection,
+                                  const gchar* aName, gpointer aUserData);
+
+  void EmitEvent(mozilla::dom::MediaControlKeysEvent event);
+};
+
+}  // namespace widget
+}  // namespace mozilla
+
+#endif  // WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
diff --git a/widget/gtk/MediaKeysEventSourceFactory.cpp b/widget/gtk/MediaKeysEventSourceFactory.cpp
index 8784d1a1cf637..997cd06360504 100644
--- a/widget/gtk/MediaKeysEventSourceFactory.cpp
+++ b/widget/gtk/MediaKeysEventSourceFactory.cpp
@@ -3,12 +3,12 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MediaKeysEventSourceFactory.h"
+#include "MPRISServiceHandler.h"
 
 namespace mozilla::widget {
 
 mozilla::dom::MediaControlKeysEventSource* CreateMediaControlKeysEventSource() {
-  // TODO : will implement this in bug 1353652.
-  return nullptr;
+  return new MPRISServiceHandler();
 }
 
 }  // namespace mozilla::widget
diff --git a/widget/gtk/moz.build b/widget/gtk/moz.build
index 1c8ee424aeb11..d62391bd66e04 100644
--- a/widget/gtk/moz.build
+++ b/widget/gtk/moz.build
@@ -32,6 +32,7 @@ EXPORTS.mozilla += [
 UNIFIED_SOURCES += [
     'IMContextWrapper.cpp',
     'mozcontainer.cpp',
+    'MPRISServiceHandler.cpp',
     'NativeKeyBindings.cpp',
     'nsAppShell.cpp',
     'nsBidiKeyboard.cpp',
-- 
GitLab