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