From bc713acafdfdc146c39480e28aa4e597d77e50b4 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sat, 15 Mar 2025 21:25:22 +0200 Subject: [PATCH] m-mpris: add MPRIS plugin Add a plugin module that can list active MPRIS media players, and send Pause commands to them. --- modules/flatpak-utils.h | 146 ++++++++ modules/meson.build | 17 + modules/module-mpris.c | 676 ++++++++++++++++++++++++++++++++++++ src/config/wireplumber.conf | 7 + 4 files changed, 846 insertions(+) create mode 100644 modules/flatpak-utils.h create mode 100644 modules/module-mpris.c diff --git a/modules/flatpak-utils.h b/modules/flatpak-utils.h new file mode 100644 index 00000000..f73b2f2b --- /dev/null +++ b/modules/flatpak-utils.h @@ -0,0 +1,146 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef FLATPAK_UTILS_H +#define FLATPAK_UTILS_H + +#define HAVE_GLIB2 + +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_VFS_H +#include +#endif + +#ifdef HAVE_GLIB2 +#include +#endif + +#include +#include +#include + +static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **instance_id, char **devices) +{ +#ifdef HAVE_GLIB2 + /* + * See flatpak-metadata(5) + * + * The .flatpak-info file is in GLib key_file .ini format. + */ + g_autoptr(GKeyFile) metadata = NULL; + char *s; + + metadata = g_key_file_new(); + if (!g_key_file_load_from_data(metadata, buf, size, G_KEY_FILE_NONE, NULL)) + return -EINVAL; + + if (app_id) { + s = g_key_file_get_value(metadata, "Application", "name", NULL); + *app_id = s ? strdup(s) : NULL; + g_free(s); + } + + if (devices) { + s = g_key_file_get_value(metadata, "Context", "devices", NULL); + *devices = s ? strdup(s) : NULL; + g_free(s); + } + + if (instance_id) { + s = g_key_file_get_value(metadata, "Instance", "instance-id", NULL); + *instance_id = s ? strdup(s) : NULL; + g_free(s); + } + + return 0; +#else + return -ENOTSUP; +#endif +} + +static int pw_check_flatpak(pid_t pid, char **app_id, char **instance_id, char **devices) +{ +#if defined(__linux__) + char root_path[2048]; + struct stat stat_buf; + int res; + + if (app_id) + *app_id = NULL; + if (instance_id) + *instance_id = NULL; + if (devices) + *devices = NULL; + + snprintf(root_path, sizeof(root_path), "/proc/%d/root", (int)pid); + + spa_autoclose int root_fd = openat(AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (root_fd < 0) { + res = -errno; + pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res)); + + if (res == -EACCES) { + /* If we can't access the root filesystem, consider not sandboxed. + * This should not happen but for now it is a workaround for selinux + * where we can't access the gnome-shell root when it connects for + * screen sharing. + */ + return 0; + } + + /* Not able to open the root dir shouldn't happen. Probably the app died and + * we're failing due to /proc/$pid not existing. In that case fail instead + * of treating this as privileged. */ + return res; + } + + spa_autoclose int info_fd = openat(root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (info_fd < 0) { + if (errno == ENOENT) { + pw_log_debug("no .flatpak-info, client on the host"); + /* No file => on the host */ + return 0; + } + res = -errno; + pw_log_error("error opening .flatpak-info: %m"); + return res; + } + if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) { + /* Some weird fd => failure, assume sandboxed */ + pw_log_error("error fstat .flatpak-info: %m"); + } else if (app_id || instance_id || devices) { + /* Parse the application ID if needed */ + const size_t size = stat_buf.st_size; + + if (size > 0) { + void *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, info_fd, 0); + if (buf != MAP_FAILED) { + res = pw_check_flatpak_parse_metadata(buf, size, app_id, instance_id, devices); + munmap(buf, size); + } else { + res = -errno; + } + } else { + res = -EINVAL; + } + + if (res == -EINVAL) + pw_log_error("PID %d .flatpak-info file is malformed", + (int)pid); + else if (res < 0) + pw_log_error("PID %d .flatpak-info parsing failed: %s", + (int)pid, spa_strerror(res)); + } + + return 1; +#else + return 0; +#endif +} + +#endif /* FLATPAK_UTILS_H */ diff --git a/modules/meson.build b/modules/meson.build index a3aadae3..0daea5b0 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -155,3 +155,20 @@ shared_library( install_dir : wireplumber_module_dir, dependencies : [wp_dep, pipewire_dep], ) + +module_mpris_c_args = [] +if cc.has_header('sys/vfs.h') + module_mpris_c_args += ['-DHAVE_SYS_VFS_H'] +endif + +shared_library( + 'wireplumber-module-mpris', + [ + 'module-mpris.c', + ], + install : true, + install_dir : wireplumber_module_dir, + c_args : module_mpris_c_args, + dependencies : [wp_dep, giounix_dep, pipewire_dep], +) + diff --git a/modules/module-mpris.c b/modules/module-mpris.c new file mode 100644 index 00000000..10ee8154 --- /dev/null +++ b/modules/module-mpris.c @@ -0,0 +1,676 @@ +/* WirePlumber + * +-- Copyright © 2025 Pauli Virtanen +-- @author Pauli Virtanen + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include +#include + +#include + +#include "dbus-connection-state.h" +#include "flatpak-utils.h" + + +WP_DEFINE_LOCAL_LOG_TOPIC ("m-mpris") + +#define NAME "mpris" + +#define PLAYER_TIMEOUT_MSEC 3000 + +struct _Item +{ + gchar *desktop_entry; + guint32 pid; + gchar *flatpak_app_id; + gchar *flatpak_instance_id; +}; +typedef struct _Item Item; + +struct _Players +{ + grefcount rc; + GMutex lock; + GHashTable *items; + GCancellable *cancellable; + GDBusConnection *conn; +}; +typedef struct _Players Players; + +struct _ItemUpdate { + Players *players; + gchar *bus_name; +}; +typedef struct _ItemUpdate ItemUpdate; + +struct _WpMprisPlugin +{ + WpPlugin parent; + WpPlugin *dbus; + GDBusConnection *conn; + guint name_signal; + Players *players; +}; + +struct _WpMprisPluginOperation +{ + GObject parent; + GDBusConnection *conn; + const char *name; + gint result; +}; + +enum { + ACTION_GET_PLAYERS, + ACTION_PAUSE, + ACTION_MATCH_PID, + N_SIGNALS +}; + +enum { + PROP_0, + PROP_RESULT +}; + +static guint signals[N_SIGNALS] = {0}; + +G_DECLARE_FINAL_TYPE (WpMprisPlugin, wp_mpris_plugin, WP, MPRIS_PLUGIN, WpPlugin) +G_DEFINE_TYPE (WpMprisPlugin, wp_mpris_plugin, WP_TYPE_PLUGIN) + +#define WP_TYPE_MPRIS_PLUGIN_OPERATION wp_mpris_plugin_operation_get_type () +G_DECLARE_FINAL_TYPE (WpMprisPluginOperation, wp_mpris_plugin_operation, WP, MPRIS_PLUGIN_OPERATION, GObject) +G_DEFINE_TYPE (WpMprisPluginOperation, wp_mpris_plugin_operation, G_TYPE_OBJECT) + + +/* + * Media Player items + * + * Since GDBus callbacks may be issued "late", use separate refcounted object. + * Although everything likely runs from main context, add locking to be sure. + */ + +static void item_free (gpointer data) +{ + Item *item = data; + + free(item->desktop_entry); + free(item); +} + +static Players *players_new (GDBusConnection *conn) +{ + Players *players; + + players = g_new0 (Players, 1); + g_ref_count_init(&players->rc); + g_mutex_init(&players->lock); + players->items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, item_free); + players->cancellable = g_cancellable_new(); + players->conn = g_object_ref (conn); + + return players; +} + +static Players *players_ref (Players *players) +{ + g_ref_count_inc(&players->rc); + return players; +} + +static void players_unref (Players *players) +{ + if (!g_ref_count_dec(&players->rc)) + return; + + g_mutex_clear (&players->lock); + g_clear_object (&players->items); + g_clear_object (&players->conn); + g_clear_object (&players->cancellable); + g_free (players); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Players, players_unref) + +static Item *players_ensure_item (Players *players, const gchar *bus_name) +{ + Item *item; + + item = g_hash_table_lookup (players->items, bus_name); + if (!item) { + item = g_new0 (Item, 1); + g_hash_table_insert (players->items, g_strdup (bus_name), item); + } + + return item; +} + +static ItemUpdate *item_update_new (Players *players, const gchar *bus_name) +{ + ItemUpdate *update; + + update = g_new0 (ItemUpdate, 1); + update->players = players_ref (players); + update->bus_name = g_strdup (bus_name); + return update; +} + +static void item_update_free (ItemUpdate *update) +{ + players_unref (update->players); + g_free (update->bus_name); + g_free (update); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ItemUpdate, item_update_free) + +static void item_get_flatpak_app_id (ItemUpdate *update, Item *item) +{ + spa_autofree char *app_id = NULL; + spa_autofree char *instance_id = NULL; + int res; + + g_clear_pointer (&item->flatpak_app_id, g_free); + g_clear_pointer (&item->flatpak_instance_id, g_free); + + if (!item->pid) + return; + + res = pw_check_flatpak (item->pid, &app_id, &instance_id, NULL); + if (res < 0) { + wp_info ("%p: failed to get Flatpak status for '%s': %d (%s)", update->players, update->bus_name, + -res, spa_strerror (res)); + return; + } + + if (app_id) + item->flatpak_app_id = g_strdup (app_id); + + if (instance_id) + item->flatpak_instance_id = g_strdup (instance_id); + + wp_debug ("%p: player '%s' Flatpak App Id = %s, Instance Id = %s", update->players, update->bus_name, + item->flatpak_app_id ? item->flatpak_app_id : "-", + item->flatpak_instance_id ? item->flatpak_instance_id : "-"); +} + +static void item_pid_cb (GObject *source_object, GAsyncResult* res, gpointer data) +{ + g_autoptr (ItemUpdate) update = data; + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&update->players->lock); + Item *item; + g_autoptr (GVariant) result = NULL; + g_autoptr (GError) error = NULL; + + result = g_dbus_connection_call_finish (update->players->conn, res, &error); + if (!result) { + wp_info ("%p: failed to get PID for '%s': %s", update->players, update->bus_name, error->message); + return; + } + + item = players_ensure_item (update->players, update->bus_name); + g_variant_get (result, "(u)", &item->pid); + + wp_debug ("%p: player '%s' PID = %u", update->players, update->bus_name, item->pid); + + item_get_flatpak_app_id (update, item); +} + +static void item_desktop_entry_cb (GObject *source_object, GAsyncResult* res, gpointer data) +{ + g_autoptr (ItemUpdate) update = data; + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&update->players->lock); + Item *item; + g_autoptr (GVariant) result = NULL; + g_autoptr (GVariant) value = NULL; + g_autoptr (GError) error = NULL; + + result = g_dbus_connection_call_finish (update->players->conn, res, &error); + if (!result) { + wp_info ("%p: failed to get DesktopEntry for '%s': %s", update->players, update->bus_name, error->message); + return; + } + + g_variant_get (result, "(v)", &value); + if (!g_str_equal(g_variant_get_type_string (value), "s")) { + wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name); + return; + } + + item = players_ensure_item (update->players, update->bus_name); + g_clear_pointer (&item->desktop_entry, g_free); + g_variant_get (value, "s", &item->desktop_entry); + + wp_debug ("%p: player '%s' DesktopEntry = %s", update->players, update->bus_name, item->desktop_entry); +} + +static void players_add (Players *players, const gchar *bus_name) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&players->lock); + Item *item; + + if (g_cancellable_is_cancelled (players->cancellable)) + return; + + wp_debug ("%p: add player '%s'", players, bus_name); + + item = players_ensure_item (players, bus_name); + g_clear_pointer (&item->desktop_entry, g_free); + item->pid = 0; + + g_dbus_connection_call (players->conn, + "org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "GetConnectionUnixProcessID", + g_variant_new ("(s)", bus_name), G_VARIANT_TYPE ("(u)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, PLAYER_TIMEOUT_MSEC, + players->cancellable, item_pid_cb, item_update_new (players, bus_name)); + + g_dbus_connection_call (players->conn, + bus_name, "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", "Get", + g_variant_new ("(ss)", "org.mpris.MediaPlayer2", "DesktopEntry"), G_VARIANT_TYPE ("(v)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, PLAYER_TIMEOUT_MSEC, + players->cancellable, item_desktop_entry_cb, item_update_new (players, bus_name)); +} + +static void players_remove (Players *players, const gchar *bus_name) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&players->lock); + + wp_debug ("%p: remove player '%s'", players, bus_name); + g_hash_table_remove (players->items, bus_name); +} + + +/* + * Media Player monitoring + */ + +static void on_name_owner_changed(GDBusConnection* connection, + const gchar* sender_name, + const gchar* object_path, + const gchar* interface_name, + const gchar* signal_name, + GVariant* parameters, + gpointer user_data) +{ + Players *players = user_data; + const gchar *bus_name; + const gchar *old_owner; + const gchar *new_owner; + + g_variant_get (parameters, "(&s&s&s)", &bus_name, &old_owner, &new_owner); + + if (!g_str_has_prefix (bus_name, "org.mpris.MediaPlayer2.")) + return; + + if (strlen (new_owner) == 0) + players_remove (players, bus_name); + else + players_add (players, bus_name); +} + +static void list_names_cb (GObject *source_object, GAsyncResult* res, gpointer data) +{ + g_autoptr (Players) players = data; + g_autoptr (GVariant) result = NULL; + g_autoptr (GError) error = NULL; + GVariantIter *iter; + const gchar *bus_name; + + result = g_dbus_connection_call_finish (players->conn, res, &error); + if (!result) { + wp_info ("%p: failed to ListNames: %s", players, error->message); + return; + } + + g_variant_get (result, "(as)", &iter); + while (g_variant_iter_loop (iter, "&s", &bus_name)) { + if (!g_str_has_prefix (bus_name, "org.mpris.MediaPlayer2.")) + continue; + + players_add (players, bus_name); + } + g_variant_iter_free (iter); +} + +static void do_list_names (Players *players) +{ + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&players->lock); + + if (g_cancellable_is_cancelled (players->cancellable)) + return; + + g_dbus_connection_call (players->conn, + "org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "ListNames", + NULL, G_VARIANT_TYPE ("(as)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, + players->cancellable, list_names_cb, players_ref (players)); +} + +static void clear_state (WpMprisPlugin *self) +{ + if (self->conn) { + if (self->name_signal) { + g_dbus_connection_signal_unsubscribe (self->conn, self->name_signal); + self->name_signal = 0; + } + g_clear_object (&self->conn); + } + + if (self->players) { + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock); + + g_cancellable_cancel (self->players->cancellable); + } + g_clear_pointer (&self->players, players_unref); +} + +static void +on_dbus_state_changed (GObject * dbus, GParamSpec * spec, + WpMprisPlugin *self) +{ + WpDBusConnectionState state = -1; + g_object_get (dbus, "state", &state, NULL); + + switch (state) { + case WP_DBUS_CONNECTION_STATE_CONNECTED: { + g_autoptr (GDBusConnection) conn = NULL; + + g_object_get (dbus, "connection", &conn, NULL); + g_return_if_fail (conn); + g_return_if_fail (!self->conn); + g_return_if_fail (!self->players); + g_return_if_fail (!self->name_signal); + + self->players = players_new (conn); + self->conn = g_object_ref (conn); + self->name_signal = g_dbus_connection_signal_subscribe (conn, + "org.freedesktop.DBus", "org.freedesktop.DBus", "NameOwnerChanged", + "/org/freedesktop/DBus", "org.mpris.MediaPlayer2", + G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, on_name_owner_changed, + players_ref (self->players), (GDestroyNotify) players_unref); + + do_list_names (self->players); + break; + } + + case WP_DBUS_CONNECTION_STATE_CONNECTING: + case WP_DBUS_CONNECTION_STATE_CLOSED: + clear_state (self); + break; + + default: + g_assert_not_reached (); + } +} + + +/* + * WpMprisPluginOperation + */ + +static void +wp_mpris_plugin_operation_init (WpMprisPluginOperation * self) +{ +} + +static void +wp_mpris_plugin_operation_finalize (GObject *object) +{ + WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object); + + g_clear_object (&self->conn); +} + +static void +wp_mpris_plugin_operation_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object); + + switch (property_id) { + case PROP_RESULT: + g_value_set_int (value, self->result); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_mpris_plugin_operation_class_init (WpMprisPluginOperationClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->finalize = wp_mpris_plugin_operation_finalize; + object_class->get_property = wp_mpris_plugin_operation_get_property; + + g_object_class_install_property (object_class, PROP_RESULT, + g_param_spec_int ("result", "result", + "Result from the operation (0 if not completed)", G_MININT, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + +static WpMprisPluginOperation * +wp_mpris_plugin_operation_new (GDBusConnection *conn, const gchar *name) +{ + WpMprisPluginOperation * self; + + self = g_object_new (WP_TYPE_MPRIS_PLUGIN_OPERATION, NULL); + self->name = name; + if (conn) + self->conn = g_object_ref (conn); + + return self; +} + +static void +wp_mpris_plugin_operation_complete (WpMprisPluginOperation * self, gint result) +{ + g_return_if_fail(result); + g_return_if_fail(!self->result); + self->result = result; + g_object_notify (G_OBJECT (self), "result"); +} + + +/* + * WpMprisPlugin + */ + +static void +wp_mpris_plugin_init (WpMprisPlugin * self) +{ +} + +static void +wp_mpris_plugin_enable (WpPlugin * plugin, WpTransition * transition) +{ + WpMprisPlugin *self = WP_MPRIS_PLUGIN (plugin); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + g_return_if_fail(!self->dbus); + + self->dbus = wp_plugin_find (core, "dbus-connection"); + if (!self->dbus) { + wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_INVARIANT, + "dbus-connection module must be loaded before mpris")); + return; + } + + g_signal_connect_object (self->dbus, "notify::state", + G_CALLBACK (on_dbus_state_changed), self, 0); + on_dbus_state_changed (G_OBJECT (self->dbus), NULL, self); + + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); +} + +static void +wp_mpris_plugin_disable (WpPlugin * plugin) +{ + WpMprisPlugin *self = WP_MPRIS_PLUGIN (plugin); + + clear_state(self); + if (self->dbus) + g_signal_handlers_disconnect_by_data (self->dbus, self); + g_clear_object(&self->dbus); + + wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED); +} + +static gpointer +wp_mpris_plugin_get_players (WpMprisPlugin *self) +{ + g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY); + + g_variant_builder_init (&b, G_VARIANT_TYPE ("av")); + + if (self->players) { + g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, self->players->items); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *bus_name = key; + const Item *item = value; + g_auto (GVariantDict) dict = G_VARIANT_DICT_INIT (NULL); + + g_variant_dict_insert (&dict, "name", "s", bus_name); + if (item->pid) + g_variant_dict_insert (&dict, "pid", "u", item->pid); + if (item->desktop_entry) + g_variant_dict_insert (&dict, "desktop-entry", "s", item->desktop_entry); + if (item->flatpak_app_id) + g_variant_dict_insert (&dict, "flatpak-app-id", "s", item->flatpak_app_id); + if (item->flatpak_instance_id) + g_variant_dict_insert (&dict, "flatpak-instance-id", "s", item->flatpak_instance_id); + + g_variant_builder_add (&b, "v", g_variant_dict_end (&dict)); + } + } + + return g_variant_builder_end (&b); +} + +static void operation_complete (GObject *source_object, GAsyncResult* res, gpointer data) +{ + g_autoptr (WpMprisPluginOperation) op = data; + g_autoptr (GVariant) result = NULL; + g_autoptr (GError) error = NULL; + + result = g_dbus_connection_call_finish (op->conn, res, &error); + if (!result) { + wp_info ("operation %s failed: %s", op->name ? op->name : "", error->message); + wp_mpris_plugin_operation_complete (op, -EIO); + return; + } + + wp_mpris_plugin_operation_complete (op, 1); +} + +static WpMprisPluginOperation * +wp_mpris_plugin_pause (WpMprisPlugin *self, const gchar *bus_name) +{ + g_autoptr (GVariant) result = NULL; + WpMprisPluginOperation *op; + + op = wp_mpris_plugin_operation_new (self->conn, "Pause"); + + if (!self->conn) { + wp_mpris_plugin_operation_complete (op, -EIO); + return op; + } + + g_dbus_connection_call (self->conn, + bus_name, "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", "Pause", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, PLAYER_TIMEOUT_MSEC, + NULL, operation_complete, g_object_ref (op)); + + return op; +} + +static pid_t get_parent_pid(pid_t pid) +{ + g_autofree gchar *path = NULL; + g_autofree gchar *stat = NULL; + int ppid; + + path = g_strdup_printf ("/proc/%d/stat", (int)pid); + if (!g_file_get_contents (path, &stat, NULL, NULL)) + return 0; + + if (sscanf (stat, "%*d %*s %*s %d", &ppid) == 1) + return ppid; + + return 0; +} + +static gboolean +match_pid(pid_t parent, pid_t child) +{ + pid_t p = child; + int j; + + for (j = 0; j < 100 && p > 1; ++j, p = get_parent_pid(p)) { + if (parent == p) { + wp_trace ("matched pid: %d is %d-child of %d", (int)child, j, (int)parent); + return TRUE; + } + } + return FALSE; +} + +static gboolean +wp_mpris_plugin_match_pid (WpMprisPlugin *self, gint parent, gint child) +{ + return match_pid(parent, child); +} + +static void +wp_mpris_plugin_class_init (WpMprisPluginClass * klass) +{ + WpPluginClass *plugin_class = (WpPluginClass *) klass; + + plugin_class->enable = wp_mpris_plugin_enable; + plugin_class->disable = wp_mpris_plugin_disable; + + signals[ACTION_GET_PLAYERS] = g_signal_new_class_handler ( + "get-players", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_mpris_plugin_get_players, + NULL, NULL, NULL, G_TYPE_VARIANT, 0); + signals[ACTION_PAUSE] = g_signal_new_class_handler ( + "pause", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_mpris_plugin_pause, + NULL, NULL, NULL, WP_TYPE_MPRIS_PLUGIN_OPERATION, 1, G_TYPE_STRING); + signals[ACTION_MATCH_PID] = g_signal_new_class_handler ( + "match-pid", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_mpris_plugin_match_pid, + NULL, NULL, NULL, G_TYPE_BOOLEAN, 2, G_TYPE_INT, G_TYPE_INT); +} + + +/* + * Module + */ + +WP_PLUGIN_EXPORT GObject * +wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error) +{ + return G_OBJECT (g_object_new (wp_mpris_plugin_get_type (), + "name", NAME, + "core", core, + NULL)); +} diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index dcbb67a7..545f3ab1 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -346,6 +346,13 @@ wireplumber.components = [ provides = support.session-services } + ## Provides support for MPRIS + { + name = libwireplumber-module-mpris, type = module + provides = support.mpris + requires = [ support.dbus ] + } + ## Device monitors' optional features { type = virtual, provides = monitor.alsa.reserve-device,