| /* |
| * Copyright (C) 2021 Igalia S.L |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| #include "config.h" |
| #include "MediaSessionGLib.h" |
| |
| #if USE(GLIB) && ENABLE(MEDIA_SESSION) |
| |
| #include "ApplicationGLib.h" |
| #include "MediaSessionManagerGLib.h" |
| #include <gio/gio.h> |
| #include <wtf/SortedArrayMap.h> |
| |
| namespace WebCore { |
| |
| #define DBUS_MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2" |
| #define DBUS_MPRIS_PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player" |
| #define DBUS_MPRIS_TRACK_PATH "/org/mpris/MediaPlayer2/webkit" |
| |
| static std::optional<PlatformMediaSession::RemoteControlCommandType> getCommand(const char* name) |
| { |
| static const std::pair<ComparableASCIILiteral, PlatformMediaSession::RemoteControlCommandType> commandList[] = { |
| { "Next", PlatformMediaSession::NextTrackCommand }, |
| { "Pause", PlatformMediaSession::PauseCommand }, |
| { "Play", PlatformMediaSession::PlayCommand }, |
| { "PlayPause", PlatformMediaSession::TogglePlayPauseCommand }, |
| { "Previous", PlatformMediaSession::PreviousTrackCommand }, |
| { "Seek", PlatformMediaSession::SeekToPlaybackPositionCommand }, |
| { "Stop", PlatformMediaSession::StopCommand } |
| }; |
| |
| static const SortedArrayMap map { commandList }; |
| auto value = map.get(StringView::fromLatin1(name), PlatformMediaSession::RemoteControlCommandType::NoCommand); |
| if (value == PlatformMediaSession::RemoteControlCommandType::NoCommand) |
| return { }; |
| return value; |
| } |
| |
| static void handleMethodCall(GDBusConnection* /* connection */, const char* /* sender */, const char* objectPath, const char* interfaceName, const char* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) |
| { |
| ASSERT(isMainThread()); |
| auto command = getCommand(methodName); |
| if (!command) { |
| g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "%s.%s.%s is not available now", objectPath, interfaceName, methodName); |
| return; |
| } |
| auto& session = *reinterpret_cast<MediaSessionGLib*>(userData); |
| auto& manager = session.manager(); |
| PlatformMediaSession::RemoteCommandArgument argument; |
| if (*command == PlatformMediaSession::SeekToPlaybackPositionCommand) { |
| int64_t offset; |
| g_variant_get(parameters, "(x)", &offset); |
| argument.time = offset / 1000000; |
| } |
| manager.dispatch(*command, argument); |
| g_dbus_method_invocation_return_value(invocation, nullptr); |
| } |
| |
| enum class MprisProperty : uint8_t { |
| NoProperty, |
| CanControl, |
| CanGoNext, |
| CanGoPrevious, |
| CanPause, |
| CanPlay, |
| CanQuit, |
| CanRaise, |
| CanSeek, |
| DesktopEntry, |
| GetMetadata, |
| GetPlaybackStatus, |
| GetPosition, |
| HasTrackList, |
| Identity, |
| SupportedMimeTypes, |
| SupportedUriSchemes, |
| }; |
| |
| static std::optional<MprisProperty> getMprisProperty(const char* propertyName) |
| { |
| static constexpr std::pair<ComparableASCIILiteral, MprisProperty> propertiesList[] { |
| { "CanControl", MprisProperty::CanControl }, |
| { "CanGoNext", MprisProperty::CanGoNext }, |
| { "CanGoPrevious", MprisProperty::CanGoPrevious }, |
| { "CanPause", MprisProperty::CanPause }, |
| { "CanPlay", MprisProperty::CanPlay }, |
| { "CanQuit", MprisProperty::CanQuit }, |
| { "CanRaise", MprisProperty::CanRaise }, |
| { "CanSeek", MprisProperty::CanSeek }, |
| { "DesktopEntry", MprisProperty::DesktopEntry }, |
| { "HasTrackList", MprisProperty::HasTrackList }, |
| { "Identity", MprisProperty::Identity }, |
| { "Metadata", MprisProperty::GetMetadata }, |
| { "PlaybackStatus", MprisProperty::GetPlaybackStatus }, |
| { "Position", MprisProperty::GetPosition }, |
| { "SupportedMimeTypes", MprisProperty::SupportedMimeTypes }, |
| { "SupportedUriSchemes", MprisProperty::SupportedUriSchemes } |
| }; |
| static constexpr SortedArrayMap map { propertiesList }; |
| auto value = map.get(StringView::fromLatin1(propertyName), MprisProperty::NoProperty); |
| if (value == MprisProperty::NoProperty) |
| return { }; |
| return value; |
| } |
| |
| static GVariant* handleGetProperty(GDBusConnection*, const char* /* sender */, const char* objectPath, const char* interfaceName, const char* propertyName, GError** error, gpointer userData) |
| { |
| ASSERT(isMainThread()); |
| auto property = getMprisProperty(propertyName); |
| if (!property) { |
| g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "%s.%s %s is not supported", objectPath, interfaceName, propertyName); |
| return nullptr; |
| } |
| |
| auto& session = *reinterpret_cast<MediaSessionGLib*>(userData); |
| switch (property.value()) { |
| case MprisProperty::NoProperty: |
| break; |
| case MprisProperty::SupportedUriSchemes: |
| case MprisProperty::SupportedMimeTypes: |
| return g_variant_new_strv(nullptr, 0); |
| case MprisProperty::GetPlaybackStatus: |
| return session.getPlaybackStatusAsGVariant({ }); |
| case MprisProperty::GetMetadata: |
| return session.getMetadataAsGVariant({ }); |
| case MprisProperty::GetPosition: |
| return session.getPositionAsGVariant(); |
| case MprisProperty::Identity: |
| return g_variant_new_string(getApplicationName().ascii().data()); |
| case MprisProperty::DesktopEntry: |
| return g_variant_new_string(""); |
| case MprisProperty::CanSeek: |
| return session.canSeekAsGVariant(); |
| case MprisProperty::HasTrackList: |
| case MprisProperty::CanQuit: |
| case MprisProperty::CanRaise: |
| return g_variant_new_boolean(false); |
| case MprisProperty::CanControl: |
| case MprisProperty::CanGoNext: |
| case MprisProperty::CanGoPrevious: |
| case MprisProperty::CanPlay: |
| case MprisProperty::CanPause: |
| return g_variant_new_boolean(true); |
| } |
| |
| return nullptr; |
| } |
| |
| static gboolean handleSetProperty(GDBusConnection*, const char* /* sender */, const char* /* objectPath */, const char* interfaceName, const char* propertyName, GVariant*, GError** error, gpointer) |
| { |
| ASSERT(isMainThread()); |
| g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s:%s setting is not supported", interfaceName, propertyName); |
| return FALSE; |
| } |
| |
| static const GDBusInterfaceVTable gInterfaceVTable = { |
| handleMethodCall, handleGetProperty, handleSetProperty, { nullptr } |
| }; |
| |
| std::unique_ptr<MediaSessionGLib> MediaSessionGLib::create(MediaSessionManagerGLib& manager, MediaSessionIdentifier identifier) |
| { |
| if (!manager.areDBusNotificationsEnabled()) |
| return makeUnique<MediaSessionGLib>(manager, nullptr, identifier); |
| |
| GUniqueOutPtr<GError> error; |
| GUniquePtr<char> address(g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error.outPtr())); |
| if (error) { |
| g_warning("Unable to get session D-Bus address: %s", error->message); |
| return nullptr; |
| } |
| auto connection = adoptGRef(reinterpret_cast<GDBusConnection*>(g_object_new(G_TYPE_DBUS_CONNECTION, |
| "address", address.get(), |
| "flags", G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, |
| "exit-on-close", TRUE, nullptr))); |
| g_initable_init(G_INITABLE(connection.get()), nullptr, &error.outPtr()); |
| if (error) { |
| g_warning("Unable to connect to D-Bus session bus: %s", error->message); |
| return nullptr; |
| } |
| return makeUnique<MediaSessionGLib>(manager, WTFMove(connection), identifier); |
| } |
| |
| MediaSessionGLib::MediaSessionGLib(MediaSessionManagerGLib& manager, GRefPtr<GDBusConnection>&& connection, MediaSessionIdentifier identifier) |
| : m_identifier(identifier) |
| , m_manager(manager) |
| , m_connection(WTFMove(connection)) |
| { |
| } |
| |
| MediaSessionGLib::~MediaSessionGLib() |
| { |
| if (m_connection) { |
| if (m_rootRegistrationId && !g_dbus_connection_unregister_object(m_connection.get(), m_rootRegistrationId)) |
| g_warning("Unable to unregister MPRIS D-Bus object."); |
| if (m_playerRegistrationId && !g_dbus_connection_unregister_object(m_connection.get(), m_playerRegistrationId)) |
| g_warning("Unable to unregister MPRIS D-Bus player object."); |
| } |
| if (m_ownerId) |
| g_bus_unown_name(m_ownerId); |
| } |
| |
| void MediaSessionGLib::ensureMprisSessionRegistered() |
| { |
| if (m_ownerId || !m_connection) |
| return; |
| |
| const auto& mprisInterface = m_manager.mprisInterface(); |
| GUniqueOutPtr<GError> error; |
| m_rootRegistrationId = g_dbus_connection_register_object(m_connection.get(), DBUS_MPRIS_OBJECT_PATH, mprisInterface->interfaces[0], |
| &gInterfaceVTable, this, nullptr, &error.outPtr()); |
| |
| if (!m_rootRegistrationId) { |
| g_warning("Failed to register MPRIS D-Bus object: %s", error->message); |
| return; |
| } |
| |
| m_playerRegistrationId = g_dbus_connection_register_object(m_connection.get(), DBUS_MPRIS_OBJECT_PATH, mprisInterface->interfaces[1], |
| &gInterfaceVTable, this, nullptr, &error.outPtr()); |
| |
| if (!m_playerRegistrationId) { |
| g_warning("Failed at MPRIS object registration: %s", error->message); |
| return; |
| } |
| |
| const auto& applicationID = getApplicationID(); |
| m_instanceId = applicationID.isEmpty() ? makeString("org.mpris.MediaPlayer2.webkit.instance", getpid(), "-", m_identifier.toUInt64()) : makeString("org.mpris.MediaPlayer2.", applicationID.ascii().data(), ".instance-", m_identifier.toUInt64()); |
| |
| m_ownerId = g_bus_own_name_on_connection(m_connection.get(), m_instanceId.ascii().data(), G_BUS_NAME_OWNER_FLAGS_NONE, nullptr, nullptr, this, nullptr); |
| } |
| |
| void MediaSessionGLib::emitPositionChanged(double time) |
| { |
| if (!m_connection) |
| return; |
| |
| ensureMprisSessionRegistered(); |
| |
| GUniqueOutPtr<GError> error; |
| int64_t position = time * 1000000; |
| if (!g_dbus_connection_emit_signal(m_connection.get(), nullptr, DBUS_MPRIS_OBJECT_PATH, DBUS_MPRIS_PLAYER_INTERFACE, "Seeked", g_variant_new("(x)", position), &error.outPtr())) |
| g_warning("Failed to emit MPRIS Seeked signal: %s", error->message); |
| } |
| |
| void MediaSessionGLib::updateNowPlaying(NowPlayingInfo& nowPlayingInfo) |
| { |
| if (!m_connection) |
| return; |
| |
| GVariantBuilder propertiesBuilder; |
| g_variant_builder_init(&propertiesBuilder, G_VARIANT_TYPE("a{sv}")); |
| g_variant_builder_add(&propertiesBuilder, "{sv}", "Metadata", getMetadataAsGVariant(nowPlayingInfo)); |
| emitPropertiesChanged(g_variant_new("(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &propertiesBuilder, nullptr)); |
| g_variant_builder_clear(&propertiesBuilder); |
| } |
| |
| GVariant* MediaSessionGLib::getMetadataAsGVariant(std::optional<NowPlayingInfo> info) |
| { |
| if (!info) |
| info = nowPlayingInfo(); |
| |
| GVariantBuilder builder; |
| g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); |
| |
| if (!info) |
| return g_variant_builder_end(&builder); |
| |
| g_variant_builder_add(&builder, "{sv}", "mpris:trackid", g_variant_new("o", DBUS_MPRIS_TRACK_PATH)); |
| g_variant_builder_add(&builder, "{sv}", "mpris:length", g_variant_new_int64(info->duration * 1000000)); |
| g_variant_builder_add(&builder, "{sv}", "xesam:title", g_variant_new_string(info->title.utf8().data())); |
| g_variant_builder_add(&builder, "{sv}", "xesam:album", g_variant_new_string(info->album.utf8().data())); |
| if (info->artwork) |
| g_variant_builder_add(&builder, "{sv}", "mpris:artUrl", g_variant_new_string(info->artwork->src.utf8().data())); |
| |
| GVariantBuilder artistBuilder; |
| g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as")); |
| g_variant_builder_add(&artistBuilder, "s", info->artist.utf8().data()); |
| g_variant_builder_add(&builder, "{sv}", "xesam:artist", g_variant_builder_end(&artistBuilder)); |
| |
| return g_variant_builder_end(&builder); |
| } |
| |
| GVariant* MediaSessionGLib::getPlaybackStatusAsGVariant(std::optional<const PlatformMediaSession*> session) |
| { |
| auto state = [this, session = WTFMove(session)]() -> PlatformMediaSession::State { |
| if (session) |
| return session.value()->state(); |
| |
| auto* nowPlayingSession = m_manager.nowPlayingEligibleSession(); |
| if (nowPlayingSession) |
| return nowPlayingSession->state(); |
| |
| return PlatformMediaSession::State::Idle; |
| }(); |
| |
| switch (state) { |
| case PlatformMediaSession::State::Autoplaying: |
| case PlatformMediaSession::State::Playing: |
| return g_variant_new_string("Playing"); |
| case PlatformMediaSession::State::Paused: |
| return g_variant_new_string("Paused"); |
| case PlatformMediaSession::State::Idle: |
| case PlatformMediaSession::State::Interrupted: |
| return g_variant_new_string("Stopped"); |
| } |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| void MediaSessionGLib::emitPropertiesChanged(GVariant* parameters) |
| { |
| if (!m_connection) |
| return; |
| |
| ensureMprisSessionRegistered(); |
| |
| GUniqueOutPtr<GError> error; |
| if (!g_dbus_connection_emit_signal(m_connection.get(), nullptr, DBUS_MPRIS_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", parameters, &error.outPtr())) |
| g_warning("Failed to emit MPRIS properties changed: %s", error->message); |
| } |
| |
| void MediaSessionGLib::playbackStatusChanged(PlatformMediaSession& platformSession) |
| { |
| if (!m_connection) |
| return; |
| |
| GVariantBuilder builder; |
| g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); |
| g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", getPlaybackStatusAsGVariant(&platformSession)); |
| emitPropertiesChanged(g_variant_new("(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr)); |
| g_variant_builder_clear(&builder); |
| } |
| |
| std::optional<NowPlayingInfo> MediaSessionGLib::nowPlayingInfo() |
| { |
| std::optional<NowPlayingInfo> nowPlayingInfo; |
| m_manager.forEachMatchingSession([&](auto& session) { |
| return session.mediaSessionIdentifier() == m_identifier; |
| }, [&](auto& session) { |
| nowPlayingInfo = session.nowPlayingInfo(); |
| }); |
| return nowPlayingInfo; |
| } |
| |
| GVariant* MediaSessionGLib::getPositionAsGVariant() |
| { |
| auto info = nowPlayingInfo(); |
| return g_variant_new_int64(info ? info->currentTime * 1000000 : 0); |
| } |
| |
| GVariant* MediaSessionGLib::canSeekAsGVariant() |
| { |
| bool canSeek = false; |
| m_manager.forEachMatchingSession([&](auto& session) { |
| return session.mediaSessionIdentifier() == m_identifier; |
| }, [&](auto& session) { |
| canSeek = session.supportsSeeking(); |
| }); |
| |
| return g_variant_new_boolean(canSeek); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // USE(GLIB) && ENABLE(MEDIA_SESSION) |