blob: 17e8149712284f3778bd7eb6c55a4af80d2a6a94 [file] [log] [blame]
/*
* 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(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(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());
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)
{
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))
{
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(), "-", identifier.toUInt64()) : makeString("org.mpris.MediaPlayer2.", applicationID.ascii().data(), "-", identifier.toUInt64());
m_ownerId = g_bus_own_name_on_connection(m_connection.get(), m_instanceId.ascii().data(), G_BUS_NAME_OWNER_FLAGS_NONE, nullptr,
reinterpret_cast<GBusNameLostCallback>(+[](GDBusConnection* connection, const char*, gpointer userData) {
auto& session = *reinterpret_cast<MediaSessionGLib*>(userData);
session.nameLost(connection);
}), this, nullptr);
}
MediaSessionGLib::~MediaSessionGLib()
{
if (m_ownerId)
g_bus_unown_name(m_ownerId);
}
void MediaSessionGLib::nameLost(GDBusConnection* connection)
{
if (UNLIKELY(!m_connection)) {
g_warning("Unable to acquire MPRIS D-Bus session ownership for name %s", m_instanceId.ascii().data());
return;
}
m_connection = nullptr;
if (!m_rootRegistrationId)
return;
if (g_dbus_connection_unregister_object(connection, m_rootRegistrationId))
m_rootRegistrationId = 0;
else
g_warning("Unable to unregister MPRIS D-Bus object.");
if (!m_playerRegistrationId)
return;
if (g_dbus_connection_unregister_object(connection, m_playerRegistrationId))
m_playerRegistrationId = 0;
else
g_warning("Unable to unregister MPRIS D-Bus player object.");
}
void MediaSessionGLib::emitPositionChanged(double time)
{
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)
{
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;
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)
{
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)