From f08ffad178e05f4e1c3c6d601a1ab9a69b38a79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Thu, 20 May 2021 19:03:33 +0200 Subject: [PATCH 1/7] Initial MPRIS support Implement a working MPRIS DBus connection with a separate API to control it. Right now only player playback state is reflected and Play/Pause/PlayPause calls work. --- .gitattributes | 1 + data/gstclapper-mpris-gdbus.xml | 52 ++ lib/gst/clapper/clapper.h | 1 + lib/gst/clapper/gstclapper-mpris-private.h | 36 ++ lib/gst/clapper/gstclapper-mpris.c | 530 ++++++++++++++++++ lib/gst/clapper/gstclapper-mpris.h | 54 ++ lib/gst/clapper/gstclapper.c | 73 ++- lib/gst/clapper/gstclapper.h | 8 +- lib/gst/clapper/meson.build | 15 +- pkgs/flatpak/com.github.rafostar.Clapper.json | 1 + src/misc.js | 53 +- src/player.js | 18 +- 12 files changed, 826 insertions(+), 16 deletions(-) create mode 100644 data/gstclapper-mpris-gdbus.xml create mode 100644 lib/gst/clapper/gstclapper-mpris-private.h create mode 100644 lib/gst/clapper/gstclapper-mpris.c create mode 100644 lib/gst/clapper/gstclapper-mpris.h diff --git a/.gitattributes b/.gitattributes index f02fef80..9611fd1b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ extras/**/* linguist-vendored lib/**/* linguist-vendored lib/**/**/* linguist-vendored +lib/gst/clapper/gstclapper-mpris* linguist-vendored=false lib/gst/clapper/gtk4/* linguist-vendored=false diff --git a/data/gstclapper-mpris-gdbus.xml b/data/gstclapper-mpris-gdbus.xml new file mode 100644 index 00000000..c0866feb --- /dev/null +++ b/data/gstclapper-mpris-gdbus.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/gst/clapper/clapper.h b/lib/gst/clapper/clapper.h index eb8c483c..ec3b5c40 100644 --- a/lib/gst/clapper/clapper.h +++ b/lib/gst/clapper/clapper.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #endif /* __CLAPPER_H__ */ diff --git a/lib/gst/clapper/gstclapper-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h new file mode 100644 index 00000000..a44a51fc --- /dev/null +++ b/lib/gst/clapper/gstclapper-mpris-private.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_CLAPPER_MPRIS_PRIVATE_H__ +#define __GST_CLAPPER_MPRIS_PRIVATE_H__ + +#include +#include + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper); + +G_GNUC_INTERNAL +void gst_clapper_mpris_set_playback_status (GstClapperMpris *self, const gchar *status); + +G_END_DECLS + +#endif /* __GST_CLAPPER_MPRIS_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c new file mode 100644 index 00000000..5a495914 --- /dev/null +++ b/lib/gst/clapper/gstclapper-mpris.c @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2021 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstclapper-mpris-gdbus.h" +#include "gstclapper-mpris.h" +#include "gstclapper-mpris-private.h" + +enum +{ + PROP_0, + PROP_OWN_NAME, + PROP_ID_PATH, + PROP_IDENTITY, + PROP_DESKTOP_ENTRY, + PROP_DEFAULT_ART_URL, + PROP_LAST +}; + +struct _GstClapperMpris +{ + GObject parent; + + GstClapperMprisMediaPlayer2 *base_skeleton; + GstClapperMprisMediaPlayer2Player *player_skeleton; + + guint name_id; + + /* Properties */ + gchar *own_name; + gchar *id_path; + gchar *identity; + gchar *desktop_entry; + gchar *default_art_url; + + /* Current status */ + gchar *playback_status; + gboolean can_play; + + GThread *thread; + GMutex lock; + GCond cond; + GMainContext *context; + GMainLoop *loop; +}; + +struct _GstClapperMprisClass +{ + GObjectClass parent_class; +}; + +GST_DEBUG_CATEGORY_STATIC (gst_clapper_mpris_debug); +#define GST_CAT_DEFAULT gst_clapper_mpris_debug + +#define parent_class gst_clapper_mpris_parent_class +G_DEFINE_TYPE (GstClapperMpris, gst_clapper_mpris, G_TYPE_OBJECT); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void gst_clapper_mpris_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_clapper_mpris_dispose (GObject * object); +static void gst_clapper_mpris_finalize (GObject * object); +static void gst_clapper_mpris_constructed (GObject * object); +static gpointer gst_clapper_mpris_main (gpointer data); + +static void unregister (GstClapperMpris * self); + +static void +gst_clapper_mpris_init (GstClapperMpris * self) +{ + GST_DEBUG_CATEGORY_INIT (gst_clapper_mpris_debug, "ClapperMpris", 0, + "GstClapperMpris"); + GST_TRACE_OBJECT (self, "Initializing"); + + self = gst_clapper_mpris_get_instance_private (self); + + g_mutex_init (&self->lock); + g_cond_init (&self->cond); + + self->context = g_main_context_new (); + self->loop = g_main_loop_new (self->context, FALSE); + + self->base_skeleton = gst_clapper_mpris_media_player2_skeleton_new (); + self->player_skeleton = gst_clapper_mpris_media_player2_player_skeleton_new (); + + self->name_id = 0; + self->own_name = NULL; + self->id_path = NULL; + self->identity = NULL; + self->desktop_entry = NULL; + self->default_art_url = NULL; + + self->playback_status = g_strdup ("Stopped"); + + GST_TRACE_OBJECT (self, "Initialized"); +} + +static void +gst_clapper_mpris_class_init (GstClapperMprisClass * klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->set_property = gst_clapper_mpris_set_property; + gobject_class->dispose = gst_clapper_mpris_dispose; + gobject_class->finalize = gst_clapper_mpris_finalize; + gobject_class->constructed = gst_clapper_mpris_constructed; + + param_specs[PROP_OWN_NAME] = + g_param_spec_string ("own-name", "DBus own name", + "DBus name to own on connection", + NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + param_specs[PROP_ID_PATH] = + g_param_spec_string ("id-path", "DBus id path", + "A valid D-Bus path describing this player", + NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + param_specs[PROP_IDENTITY] = + g_param_spec_string ("identity", "Player name", + "A friendly name to identify the media player", + NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + param_specs[PROP_DESKTOP_ENTRY] = + g_param_spec_string ("desktop-entry", "Desktop entry filename", + "The basename of an installed .desktop file", + NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + param_specs[PROP_DEFAULT_ART_URL] = + g_param_spec_string ("default-art-url", "Default Art URL", + "Default art to show when media does not provide one", + NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} + +static void +gst_clapper_mpris_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (object); + + switch (prop_id) { + case PROP_OWN_NAME: + self->own_name = g_value_dup_string (value); + break; + case PROP_ID_PATH: + self->id_path = g_value_dup_string (value); + break; + case PROP_IDENTITY: + self->identity = g_value_dup_string (value); + break; + case PROP_DESKTOP_ENTRY: + self->desktop_entry = g_value_dup_string (value); + break; + case PROP_DEFAULT_ART_URL: + self->default_art_url = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_clapper_mpris_dispose (GObject * object) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (object); + + GST_TRACE_OBJECT (self, "Stopping main thread"); + + if (self->loop) { + g_main_loop_quit (self->loop); + + if (self->thread != g_thread_self ()) + g_thread_join (self->thread); + else + g_thread_unref (self->thread); + self->thread = NULL; + + g_main_loop_unref (self->loop); + self->loop = NULL; + + g_main_context_unref (self->context); + self->context = NULL; + } + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +gst_clapper_mpris_finalize (GObject * object) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + g_free (self->own_name); + g_free (self->id_path); + g_free (self->identity); + g_free (self->desktop_entry); + g_free (self->default_art_url); + g_free (self->playback_status); + + if (self->base_skeleton) + g_object_unref (self->base_skeleton); + if (self->player_skeleton) + g_object_unref (self->player_skeleton); + + g_mutex_clear (&self->lock); + g_cond_clear (&self->cond); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gst_clapper_mpris_constructed (GObject * object) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (object); + + GST_TRACE_OBJECT (self, "Constructed"); + + g_mutex_lock (&self->lock); + self->thread = g_thread_new ("GstClapperMpris", + gst_clapper_mpris_main, self); + while (!self->loop || !g_main_loop_is_running (self->loop)) + g_cond_wait (&self->cond, &self->lock); + g_mutex_unlock (&self->lock); + + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static gboolean +main_loop_running_cb (gpointer user_data) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); + + GST_TRACE_OBJECT (self, "Main loop running now"); + + g_mutex_lock (&self->lock); + g_cond_signal (&self->cond); + g_mutex_unlock (&self->lock); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_play_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + gst_clapper_play (clapper); + gst_clapper_mpris_media_player2_player_complete_play (player_skeleton, invocation); + + return TRUE; +} + +static gboolean +handle_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + gst_clapper_pause (clapper); + gst_clapper_mpris_media_player2_player_complete_pause (player_skeleton, invocation); + + return TRUE; +} + +static gboolean +handle_play_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + gst_clapper_toggle_play (clapper); + gst_clapper_mpris_media_player2_player_complete_play_pause (player_skeleton, invocation); + + return TRUE; +} + +static void +unregister (GstClapperMpris * self) +{ + if (!self->name_id) + return; + + GST_DEBUG_OBJECT (self, "Unregister"); + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->base_skeleton)); + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->player_skeleton)); + g_bus_unown_name (self->name_id); + self->name_id = 0; +} + +static const gchar * +_get_mpris_trackid (GstClapperMpris * self) +{ + /* TODO: Support more tracks */ + return g_strdup_printf ("%s%s%i", self->id_path, "/Track/", 0); +} + +static void +name_acquired_cb (GDBusConnection * connection, + const gchar *name, gpointer user_data) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); + GVariantBuilder builder; + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->base_skeleton), + connection, "/org/mpris/MediaPlayer2", NULL); + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->player_skeleton), + connection, "/org/mpris/MediaPlayer2", NULL); + + if (self->identity) + gst_clapper_mpris_media_player2_set_identity (self->base_skeleton, self->identity); + if (self->desktop_entry) + gst_clapper_mpris_media_player2_set_desktop_entry (self->base_skeleton, self->desktop_entry); + + gst_clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, "Stopped"); + gst_clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, 0.01); + gst_clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, 2.0); + gst_clapper_mpris_media_player2_player_set_can_control (self->player_skeleton, TRUE); + + g_object_bind_property (self->player_skeleton, "can-play", + self->player_skeleton, "can-pause", G_BINDING_DEFAULT); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + g_variant_builder_add (&builder, "{sv}", "mpris:trackid", g_variant_new_string (_get_mpris_trackid (self))); + g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (0)); + if (self->default_art_url) + g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url)); + gst_clapper_mpris_media_player2_player_set_metadata (self->player_skeleton, g_variant_builder_end (&builder)); + + GST_DEBUG_OBJECT (self, "Ready"); +} + +static void +name_lost_cb (GDBusConnection * connection, + const gchar * name, gpointer user_data) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); + + unregister (self); +} + +static gboolean +mpris_update_props_dispatch (gpointer user_data) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); + + GST_DEBUG_OBJECT (self, "Updating MPRIS props"); + g_mutex_lock (&self->lock); + + if (gst_clapper_mpris_media_player2_player_get_can_play ( + self->player_skeleton) != self->can_play) { + /* "can-play" is bound with "can-pause" */ + GST_DEBUG_OBJECT (self, "CanPlay/CanPause: %s", self->can_play ? "yes" : "no"); + gst_clapper_mpris_media_player2_player_set_can_play ( + self->player_skeleton, self->can_play); + } + if (strcmp (gst_clapper_mpris_media_player2_player_get_playback_status ( + self->player_skeleton), self->playback_status) != 0) { + GST_DEBUG_OBJECT (self, "PlaybackStatus: %s", self->playback_status); + gst_clapper_mpris_media_player2_player_set_playback_status ( + self->player_skeleton, self->playback_status); + } + + g_mutex_unlock (&self->lock); + + return G_SOURCE_REMOVE; +} + +static void +mpris_dispatcher_update_dispatch (GstClapperMpris * self) +{ + if (!self->name_id) + return; + + GST_DEBUG_OBJECT (self, "Queued update props dispatch"); + + g_main_context_invoke_full (self->context, + G_PRIORITY_DEFAULT, mpris_update_props_dispatch, + g_object_ref (self), g_object_unref); +} + +static gpointer +gst_clapper_mpris_main (gpointer data) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (data); + + GDBusConnectionFlags flags; + GDBusConnection *connection; + GSource *source; + gchar *address; + + GST_TRACE_OBJECT (self, "Starting main thread"); + + g_main_context_push_thread_default (self->context); + + source = g_idle_source_new (); + g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, self, + NULL); + g_source_attach (source, self->context); + g_source_unref (source); + + address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (!address) { + GST_WARNING_OBJECT (self, "No MPRIS bus address"); + goto no_mpris; + } + + GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus address"); + + flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION; + connection = g_dbus_connection_new_for_address_sync (address, + flags, NULL, NULL, NULL); + g_free (address); + + if (!connection) { + GST_WARNING_OBJECT (self, "No MPRIS bus connection"); + goto no_mpris; + } + + GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus connection"); + + self->name_id = g_bus_own_name_on_connection (connection, self->own_name, + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusNameAcquiredCallback) name_acquired_cb, + (GBusNameLostCallback) name_lost_cb, + self, NULL); + g_object_unref (connection); + goto done; + +no_mpris: + g_warning ("GstClapperMpris: failed to create DBus connection"); + +done: + GST_TRACE_OBJECT (self, "Starting main loop"); + g_main_loop_run (self->loop); + GST_TRACE_OBJECT (self, "Stopped main loop"); + + unregister (self); + g_main_context_pop_thread_default (self->context); + + GST_TRACE_OBJECT (self, "Stopped main thread"); + + return NULL; +} + +void +gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper) +{ + g_signal_connect (self->player_skeleton, "handle-play", + G_CALLBACK (handle_play_cb), clapper); + g_signal_connect (self->player_skeleton, "handle-pause", + G_CALLBACK (handle_pause_cb), clapper); + g_signal_connect (self->player_skeleton, "handle-play-pause", + G_CALLBACK (handle_play_pause_cb), clapper); +} + +void +gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * status) +{ + g_mutex_lock (&self->lock); + + if (strcmp (self->playback_status, status) == 0) { + g_mutex_unlock (&self->lock); + return; + } + g_free (self->playback_status); + self->playback_status = g_strdup (status); + self->can_play = strcmp (status, "Stopped") != 0; + + g_mutex_unlock (&self->lock); + + mpris_dispatcher_update_dispatch (self); +} + +/** + * gst_clapper_mpris_new: + * @own_name: DBus own name + * @id_path: DBus id path used for prefix + * @identity: (allow-none): friendly name + * @desktop_entry: (allow-none): Desktop entry filename + * @default_art_url: (allow-none): filepath to default art + * + * Creates a new #GstClapperMpris instance. + * + * Returns: (transfer full): a new #GstClapperMpris instance + */ +GstClapperMpris * +gst_clapper_mpris_new (const gchar * own_name, const gchar * id_path, + const gchar * identity, const gchar * desktop_entry, + const gchar * default_art_url) +{ + GstClapperMpris *self; + + self = g_object_new (GST_TYPE_CLAPPER, + "own-name", own_name, "id_path", id_path, + "identity", identity, "desktop-entry", desktop_entry, + "default-art-url", default_art_url, NULL); + + return self; +} diff --git a/lib/gst/clapper/gstclapper-mpris.h b/lib/gst/clapper/gstclapper-mpris.h new file mode 100644 index 00000000..cf0e94b2 --- /dev/null +++ b/lib/gst/clapper/gstclapper-mpris.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_CLAPPER_MPRIS_H__ +#define __GST_CLAPPER_MPRIS_H__ + +#include +#include + +#include + +G_BEGIN_DECLS + +typedef struct _GstClapperMpris GstClapperMpris; +typedef struct _GstClapperMprisClass GstClapperMprisClass; + +#define GST_TYPE_CLAPPER_MPRIS (gst_clapper_mpris_get_type ()) +#define GST_IS_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_MPRIS)) +#define GST_IS_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_MPRIS)) +#define GST_CLAPPER_MPRIS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass)) +#define GST_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMpris)) +#define GST_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass)) +#define GST_CLAPPER_MPRIS_CAST(obj) ((GstClapperMpris*)(obj)) + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapperMpris, g_object_unref) +#endif + +GST_CLAPPER_API +GType gst_clapper_mpris_get_type (void); + +GST_CLAPPER_API +GstClapperMpris * gst_clapper_mpris_new (const gchar *own_name, const gchar *id_path, const gchar *identity, + const gchar *desktop_entry, const gchar *default_art_url); + +G_END_DECLS + +#endif /* __GST_CLAPPER_MPRIS_H__ */ diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c index b9c6f41f..1cd77591 100644 --- a/lib/gst/clapper/gstclapper.c +++ b/lib/gst/clapper/gstclapper.c @@ -46,6 +46,7 @@ #include "gstclapper-signal-dispatcher-private.h" #include "gstclapper-video-renderer-private.h" #include "gstclapper-media-info-private.h" +#include "gstclapper-mpris-private.h" GST_DEBUG_CATEGORY_STATIC (gst_clapper_debug); #define GST_CAT_DEFAULT gst_clapper_debug @@ -76,6 +77,7 @@ enum PROP_0, PROP_VIDEO_RENDERER, PROP_SIGNAL_DISPATCHER, + PROP_MPRIS, PROP_STATE, PROP_URI, PROP_SUBURI, @@ -127,6 +129,7 @@ struct _GstClapper GstClapperVideoRenderer *video_renderer; GstClapperSignalDispatcher *signal_dispatcher; + GstClapperMpris *mpris; gchar *uri; gchar *redirect_uri; @@ -305,6 +308,13 @@ gst_clapper_class_init (GstClapperClass * klass) G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + param_specs[PROP_MPRIS] = + g_param_spec_object ("mpris", + "MPRIS", "Clapper MPRIS for playback control over DBus", + GST_TYPE_CLAPPER_MPRIS, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + param_specs[PROP_STATE] = g_param_spec_enum ("state", "Clapper State", "Current player state", GST_TYPE_CLAPPER_STATE, DEFAULT_STATE, G_PARAM_READABLE | @@ -491,7 +501,7 @@ gst_clapper_finalize (GObject * object) { GstClapper *self = GST_CLAPPER (object); - GST_TRACE_OBJECT (self, "Finalizing"); + GST_TRACE_OBJECT (self, "Finalize"); g_free (self->uri); g_free (self->redirect_uri); @@ -507,6 +517,8 @@ gst_clapper_finalize (GObject * object) g_object_unref (self->video_renderer); if (self->signal_dispatcher) g_object_unref (self->signal_dispatcher); + if (self->mpris) + g_object_unref (self->mpris); if (self->current_vis_element) gst_object_unref (self->current_vis_element); if (self->collection) @@ -649,6 +661,9 @@ gst_clapper_set_property (GObject * object, guint prop_id, case PROP_SIGNAL_DISPATCHER: self->signal_dispatcher = g_value_dup_object (value); break; + case PROP_MPRIS: + self->mpris = g_value_dup_object (value); + break; case PROP_URI:{ g_mutex_lock (&self->lock); g_free (self->uri); @@ -741,6 +756,9 @@ gst_clapper_get_property (GObject * object, guint prop_id, GstClapper *self = GST_CLAPPER (object); switch (prop_id) { + case PROP_MPRIS: + g_value_set_object (value, self->mpris); + break; case PROP_STATE: g_mutex_lock (&self->lock); g_value_set_enum (value, self->app_state); @@ -980,6 +998,23 @@ change_state (GstClapper * self, GstClapperState state) state_changed_dispatch, data, (GDestroyNotify) state_changed_signal_data_free); } + + if (!self->mpris) + return; + + switch (state) { + case GST_CLAPPER_STATE_STOPPED: + gst_clapper_mpris_set_playback_status (self->mpris, "Stopped"); + break; + case GST_CLAPPER_STATE_PAUSED: + gst_clapper_mpris_set_playback_status (self->mpris, "Paused"); + break; + case GST_CLAPPER_STATE_PLAYING: + gst_clapper_mpris_set_playback_status (self->mpris, "Playing"); + break; + default: + break; + } } typedef struct @@ -2925,6 +2960,9 @@ gst_clapper_main (gpointer data) self->bus = bus = gst_element_get_bus (self->playbin); gst_bus_add_signal_watch (bus); + if (self->mpris) + gst_clapper_mpris_set_clapper (self->mpris, self); + g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb), self); g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb), @@ -3025,6 +3063,7 @@ gst_clapper_main (gpointer data) * gst_clapper_new: * @video_renderer: (transfer full) (allow-none): GstClapperVideoRenderer to use * @signal_dispatcher: (transfer full) (allow-none): GstClapperSignalDispatcher to use + * @mpris: (transfer full) (allow-none): GstClapperMpris to use * * Creates a new #GstClapper instance that uses @signal_dispatcher to dispatch * signals to some event loop system, or emits signals directly if NULL is @@ -3038,18 +3077,20 @@ gst_clapper_main (gpointer data) */ GstClapper * gst_clapper_new (GstClapperVideoRenderer * video_renderer, - GstClapperSignalDispatcher * signal_dispatcher) + GstClapperSignalDispatcher * signal_dispatcher, + GstClapperMpris * mpris) { GstClapper *self; - self = - g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer, - "signal-dispatcher", signal_dispatcher, NULL); + self = g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer, + "signal-dispatcher", signal_dispatcher, "mpris", mpris, NULL); if (video_renderer) g_object_unref (video_renderer); if (signal_dispatcher) g_object_unref (signal_dispatcher); + if (mpris) + g_object_unref (mpris); return self; } @@ -3701,6 +3742,28 @@ gst_clapper_get_pipeline (GstClapper * self) return val; } +/** + * gst_clapper_get_mpris: + * @clapper: #GstClapper instance + * + * A Function to get the #GstClapperMpris instance. + * + * Returns: (transfer full): mpris instance. + * + * The caller should free it with g_object_unref() + */ +GstClapperMpris * +gst_clapper_get_mpris (GstClapper * self) +{ + GstClapperMpris *val; + + g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); + + g_object_get (self, "mpris", &val, NULL); + + return val; +} + /** * gst_clapper_get_media_info: * @clapper: #GstClapper instance diff --git a/lib/gst/clapper/gstclapper.h b/lib/gst/clapper/gstclapper.h index 73e8f995..1150060f 100644 --- a/lib/gst/clapper/gstclapper.h +++ b/lib/gst/clapper/gstclapper.h @@ -30,6 +30,7 @@ #include #include #include +#include G_BEGIN_DECLS @@ -153,7 +154,8 @@ GST_CLAPPER_API GType gst_clapper_get_type (void); GST_CLAPPER_API -GstClapper * gst_clapper_new (GstClapperVideoRenderer *video_renderer, GstClapperSignalDispatcher *signal_dispatcher); +GstClapper * gst_clapper_new (GstClapperVideoRenderer *video_renderer, GstClapperSignalDispatcher *signal_dispatcher, + GstClapperMpris *mpris); GST_CLAPPER_API void gst_clapper_play (GstClapper *clapper); @@ -220,6 +222,10 @@ void gst_clapper_set_mute (GstClapper *clapper GST_CLAPPER_API GstElement * gst_clapper_get_pipeline (GstClapper *clapper); +GST_CLAPPER_API +GstClapperMpris * + gst_clapper_get_mpris (GstClapper *clapper); + GST_CLAPPER_API void gst_clapper_set_video_track_enabled (GstClapper *clapper, gboolean enabled); diff --git a/lib/gst/clapper/meson.build b/lib/gst/clapper/meson.build index 48b5cef9..517fc672 100644 --- a/lib/gst/clapper/meson.build +++ b/lib/gst/clapper/meson.build @@ -1,3 +1,5 @@ +gnome = import('gnome') + gstclapper_sources = [ 'gstclapper.c', 'gstclapper-signal-dispatcher.c', @@ -6,6 +8,7 @@ gstclapper_sources = [ 'gstclapper-g-main-context-signal-dispatcher.c', 'gstclapper-video-overlay-video-renderer.c', 'gstclapper-visualization.c', + 'gstclapper-mpris.c', 'gstclapper-gtk4-plugin.c', 'gtk4/gstclapperglsink.c', @@ -23,6 +26,7 @@ gstclapper_headers = [ 'gstclapper-g-main-context-signal-dispatcher.h', 'gstclapper-video-overlay-video-renderer.h', 'gstclapper-visualization.h', + 'gstclapper-mpris.h', 'gstclapper-gtk4-plugin.h', ] gstclapper_defines = [ @@ -67,15 +71,22 @@ if not have_gtk_gl_windowing error('GTK4 widget requires GL windowing') endif +gstclapper_mpris_gdbus = gnome.gdbus_codegen('gstclapper-mpris-gdbus', + sources: '../../../data/gstclapper-mpris-gdbus.xml', + interface_prefix: 'org.mpris.', + namespace: 'GstClapperMpris' +) + gstclapper = library('gstclapper-' + api_version, - gstclapper_sources, + gstclapper_sources + gstclapper_mpris_gdbus, c_args : gstclapper_defines, link_args : noseh_link_args, include_directories : [configinc, libsinc], version : libversion, install : true, install_dir : clapper_libdir, - dependencies : [gtk4_dep, gstbase_dep, gstvideo_dep, gstaudio_dep, + dependencies : [gtk4_dep, glib_dep, gio_dep, + gstbase_dep, gstvideo_dep, gstaudio_dep, gsttag_dep, gstpbutils_dep, libm] + gtk_deps, ) diff --git a/pkgs/flatpak/com.github.rafostar.Clapper.json b/pkgs/flatpak/com.github.rafostar.Clapper.json index 00988f26..6f3d2fd0 100644 --- a/pkgs/flatpak/com.github.rafostar.Clapper.json +++ b/pkgs/flatpak/com.github.rafostar.Clapper.json @@ -13,6 +13,7 @@ "--share=network", "--device=all", "--filesystem=xdg-videos", + "--own-name=org.mpris.MediaPlayer2.Clapper", "--talk-name=org.gnome.Shell", "--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0", "--env=GST_VAAPI_ALL_DRIVERS=1" diff --git a/src/misc.js b/src/misc.js index 94e238a5..b3ab2c96 100644 --- a/src/misc.js +++ b/src/misc.js @@ -1,4 +1,4 @@ -const { Gio, Gdk, Gtk } = imports.gi; +const { Gio, GLib, Gdk, Gtk } = imports.gi; const Debug = imports.src.debug; const { debug } = Debug; @@ -39,6 +39,57 @@ function getClapperVersion() : ''; } +function getClapperThemeIconUri() +{ + const display = Gdk.Display.get_default(); + if(!display) return null; + + const iconTheme = Gtk.IconTheme.get_for_display(display); + if(!iconTheme || !iconTheme.has_icon(appId)) + return null; + + const iconPaintable = iconTheme.lookup_icon(appId, null, 256, 1, + Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_REGULAR + ); + const iconFile = iconPaintable.get_file(); + if(!iconFile) return null; + + const iconPath = iconFile.get_path(); + if(!iconPath) return null; + + let substractName = iconPath.substring( + iconPath.indexOf('/icons/') + 7, iconPath.indexOf('/scalable/') + ); + if(!substractName || substractName.includes('/')) + return null; + + substractName = substractName.toLowerCase(); + const postFix = (substractName === iconTheme.theme_name.toLowerCase()) + ? substractName + : 'hicolor'; + const cacheIconName = `clapper-${postFix}.svg`; + + /* We need to have this icon placed in a folder + * accessible from both app runtime and gnome-shell */ + const expectedFile = Gio.File.new_for_path( + GLib.get_user_cache_dir() + `/${appId}/icons/${cacheIconName}` + ); + if(!expectedFile.query_exists(null)) { + debug('no cached icon file'); + + const dirPath = expectedFile.get_parent().get_path(); + GLib.mkdir_with_parents(dirPath, 493); // octal 755 + iconFile.copy(expectedFile, + Gio.FileCopyFlags.TARGET_DEFAULT_PERMS, null, null + ); + debug(`icon copied to cache dir: ${cacheIconName}`); + } + const iconUri = expectedFile.get_uri(); + debug(`using cached clapper icon uri: ${iconUri}`); + + return iconUri; +} + function loadCustomCss() { const clapperPath = getClapperPath(); diff --git a/src/player.js b/src/player.js index 7a80b809..bfd2ea20 100644 --- a/src/player.js +++ b/src/player.js @@ -20,14 +20,18 @@ class ClapperPlayer extends GstClapper.Clapper const glsinkbin = Gst.ElementFactory.make('glsinkbin', null); glsinkbin.sink = gtk4plugin.video_sink; - const dispatcher = new GstClapper.ClapperGMainContextSignalDispatcher(); - const renderer = new GstClapper.ClapperVideoOverlayVideoRenderer({ - video_sink: glsinkbin - }); - super._init({ - signal_dispatcher: dispatcher, - video_renderer: renderer + signal_dispatcher: new GstClapper.ClapperGMainContextSignalDispatcher(), + video_renderer: new GstClapper.ClapperVideoOverlayVideoRenderer({ + video_sink: glsinkbin, + }), + mpris: new GstClapper.ClapperMpris({ + own_name: `org.mpris.MediaPlayer2.${Misc.appName}`, + id_path: '/' + Misc.appId.replace(/\./g, '/'), + identity: Misc.appName, + desktop_entry: Misc.appId, + default_art_url: Misc.getClapperThemeIconUri(), + }), }); this.widget = gtk4plugin.video_sink.widget; From 68d7205ead1bf760fcf9f02df1f53aa0bdeaeb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Fri, 21 May 2021 16:06:37 +0200 Subject: [PATCH 2/7] mpris: Support metadata url, title and length --- lib/gst/clapper/gstclapper-mpris-private.h | 3 + lib/gst/clapper/gstclapper-mpris.c | 68 ++++++++++++++++++++++ lib/gst/clapper/gstclapper.c | 9 +++ 3 files changed, 80 insertions(+) diff --git a/lib/gst/clapper/gstclapper-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h index a44a51fc..a121de8f 100644 --- a/lib/gst/clapper/gstclapper-mpris-private.h +++ b/lib/gst/clapper/gstclapper-mpris-private.h @@ -31,6 +31,9 @@ void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstC G_GNUC_INTERNAL void gst_clapper_mpris_set_playback_status (GstClapperMpris *self, const gchar *status); +G_GNUC_INTERNAL +void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info); + G_END_DECLS #endif /* __GST_CLAPPER_MPRIS_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c index 5a495914..60968a4b 100644 --- a/lib/gst/clapper/gstclapper-mpris.c +++ b/lib/gst/clapper/gstclapper-mpris.c @@ -43,6 +43,8 @@ struct _GstClapperMpris GstClapperMprisMediaPlayer2 *base_skeleton; GstClapperMprisMediaPlayer2Player *player_skeleton; + GstClapperMediaInfo *media_info; + guint name_id; /* Properties */ @@ -52,6 +54,8 @@ struct _GstClapperMpris gchar *desktop_entry; gchar *default_art_url; + gboolean parse_media_info; + /* Current status */ gchar *playback_status; gboolean can_play; @@ -111,6 +115,9 @@ gst_clapper_mpris_init (GstClapperMpris * self) self->default_art_url = NULL; self->playback_status = g_strdup ("Stopped"); + self->can_play = FALSE; + self->parse_media_info = FALSE; + self->media_info = NULL; GST_TRACE_OBJECT (self, "Initialized"); } @@ -230,6 +237,8 @@ gst_clapper_mpris_finalize (GObject * object) g_object_unref (self->base_skeleton); if (self->player_skeleton) g_object_unref (self->player_skeleton); + if (self->media_info) + g_object_unref (self->media_info); g_mutex_clear (&self->lock); g_cond_clear (&self->cond); @@ -376,6 +385,51 @@ mpris_update_props_dispatch (gpointer user_data) GST_DEBUG_OBJECT (self, "Updating MPRIS props"); g_mutex_lock (&self->lock); + if (self->parse_media_info) { + GVariantBuilder builder; + guint64 duration; + const gchar *track_id, *uri, *title; + + GST_DEBUG_OBJECT (self, "Parsing media info"); + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + track_id = _get_mpris_trackid (self); + uri = gst_clapper_media_info_get_uri (self->media_info); + title = gst_clapper_media_info_get_title (self->media_info); + + if (track_id) { + g_variant_builder_add (&builder, "{sv}", "mpris:trackid", + g_variant_new_string (track_id)); + GST_DEBUG_OBJECT (self, "mpris:trackid: %s", track_id); + } + if (uri) { + g_variant_builder_add (&builder, "{sv}", "xesam:url", + g_variant_new_string (uri)); + GST_DEBUG_OBJECT (self, "xesam:url: %s", uri); + } + if (title) { + g_variant_builder_add (&builder, "{sv}", "xesam:title", + g_variant_new_string (title)); + GST_DEBUG_OBJECT (self, "xesam:title: %s", title); + } + + duration = gst_clapper_media_info_get_duration (self->media_info); + duration = (duration != GST_CLOCK_TIME_NONE) ? duration / GST_USECOND : 0; + g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (duration)); + GST_DEBUG_OBJECT (self, "mpris:length: %ld", duration); + + /* TODO: Check for image sample */ + if (self->default_art_url) { + g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url)); + GST_DEBUG_OBJECT (self, "mpris:artUrl: %s", self->default_art_url); + } + + GST_DEBUG_OBJECT (self, "Media info parsed"); + self->parse_media_info = FALSE; + + gst_clapper_mpris_media_player2_player_set_metadata ( + self->player_skeleton, g_variant_builder_end (&builder)); + } if (gst_clapper_mpris_media_player2_player_get_can_play ( self->player_skeleton) != self->can_play) { /* "can-play" is bound with "can-pause" */ @@ -391,6 +445,7 @@ mpris_update_props_dispatch (gpointer user_data) } g_mutex_unlock (&self->lock); + GST_DEBUG_OBJECT (self, "MPRIS props updated"); return G_SOURCE_REMOVE; } @@ -502,6 +557,19 @@ gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * sta mpris_dispatcher_update_dispatch (self); } +void +gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info) +{ + g_mutex_lock (&self->lock); + if (self->media_info) + g_object_unref (self->media_info); + self->media_info = info; + self->parse_media_info = TRUE; + g_mutex_unlock (&self->lock); + + mpris_dispatcher_update_dispatch (self); +} + /** * gst_clapper_mpris_new: * @own_name: DBus own name diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c index 1cd77591..8b6e14c6 100644 --- a/lib/gst/clapper/gstclapper.c +++ b/lib/gst/clapper/gstclapper.c @@ -1682,6 +1682,15 @@ state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, self->cached_duration = GST_CLOCK_TIME_NONE; } emit_media_info_updated (self); + if (self->mpris) { + GstClapperMediaInfo *info; + + g_mutex_lock (&self->lock); + info = gst_clapper_media_info_copy (self->media_info); + g_mutex_unlock (&self->lock); + + gst_clapper_mpris_set_media_info (self->mpris, info); + } } if (new_state == GST_STATE_PAUSED From f0475ee0557c96c7122d6025d25043f4bbbdad59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Fri, 21 May 2021 19:28:07 +0200 Subject: [PATCH 3/7] API: Support seeking by offset --- lib/gst/clapper/gstclapper.c | 25 +++++++++++++++++++++++++ lib/gst/clapper/gstclapper.h | 3 +++ 2 files changed, 28 insertions(+) diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c index 8b6e14c6..93708a1d 100644 --- a/lib/gst/clapper/gstclapper.c +++ b/lib/gst/clapper/gstclapper.c @@ -1066,6 +1066,8 @@ tick_cb (gpointer user_data) position_updated_dispatch, data, (GDestroyNotify) position_updated_signal_data_free); } + if (self->mpris) + gst_clapper_mpris_set_position (self->mpris, position); } return G_SOURCE_CONTINUE; @@ -3519,6 +3521,29 @@ gst_clapper_seek (GstClapper * self, GstClockTime position) g_mutex_unlock (&self->lock); } +/** + * gst_clapper_seek_offset: + * @clapper: #GstClapper instance + * @offset: offset from current position to seek to in nanoseconds + * + * Seeks the currently-playing stream to the @offset time + * in nanoseconds. + */ +void +gst_clapper_seek_offset (GstClapper * self, GstClockTime offset) +{ + GstClockTime position; + + g_return_if_fail (GST_IS_CLAPPER (self)); + g_return_if_fail (GST_CLOCK_TIME_IS_VALID (offset)); + + position = gst_clapper_get_position (self); + + /* TODO: Prevent negative values */ + + gst_clapper_seek (self, position + offset); +} + static void remove_seek_source (GstClapper * self) { diff --git a/lib/gst/clapper/gstclapper.h b/lib/gst/clapper/gstclapper.h index 1150060f..35895109 100644 --- a/lib/gst/clapper/gstclapper.h +++ b/lib/gst/clapper/gstclapper.h @@ -172,6 +172,9 @@ void gst_clapper_stop (GstClapper *clapper GST_CLAPPER_API void gst_clapper_seek (GstClapper *clapper, GstClockTime position); +GST_CLAPPER_API +void gst_clapper_seek_offset (GstClapper *clapper, GstClockTime offset); + GST_CLAPPER_API GstClapperState gst_clapper_get_state (GstClapper *clapper); From 7535c4e598dd963ab0aa70609f1e08813bb1d432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Fri, 21 May 2021 19:29:06 +0200 Subject: [PATCH 4/7] mpris: Support position reporting and seeking --- lib/gst/clapper/gstclapper-mpris-private.h | 5 +- lib/gst/clapper/gstclapper-mpris.c | 73 ++++++++++++++++++++-- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/gst/clapper/gstclapper-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h index a121de8f..efacd6b3 100644 --- a/lib/gst/clapper/gstclapper-mpris-private.h +++ b/lib/gst/clapper/gstclapper-mpris-private.h @@ -28,11 +28,14 @@ G_BEGIN_DECLS G_GNUC_INTERNAL void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper); +G_GNUC_INTERNAL +void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info); + G_GNUC_INTERNAL void gst_clapper_mpris_set_playback_status (GstClapperMpris *self, const gchar *status); G_GNUC_INTERNAL -void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info); +void gst_clapper_mpris_set_position (GstClapperMpris *self, gint64 position); G_END_DECLS diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c index 60968a4b..dcec44b2 100644 --- a/lib/gst/clapper/gstclapper-mpris.c +++ b/lib/gst/clapper/gstclapper-mpris.c @@ -59,6 +59,7 @@ struct _GstClapperMpris /* Current status */ gchar *playback_status; gboolean can_play; + guint64 position; GThread *thread; GMutex lock; @@ -114,11 +115,13 @@ gst_clapper_mpris_init (GstClapperMpris * self) self->desktop_entry = NULL; self->default_art_url = NULL; - self->playback_status = g_strdup ("Stopped"); - self->can_play = FALSE; self->parse_media_info = FALSE; self->media_info = NULL; + self->playback_status = g_strdup ("Stopped"); + self->can_play = FALSE; + self->position = 0; + GST_TRACE_OBJECT (self, "Initialized"); } @@ -283,6 +286,8 @@ handle_play_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, { GstClapper *clapper = GST_CLAPPER (user_data); + GST_DEBUG ("Handle Play"); + gst_clapper_play (clapper); gst_clapper_mpris_media_player2_player_complete_play (player_skeleton, invocation); @@ -295,6 +300,8 @@ handle_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, { GstClapper *clapper = GST_CLAPPER (user_data); + GST_DEBUG ("Handle Pause"); + gst_clapper_pause (clapper); gst_clapper_mpris_media_player2_player_complete_pause (player_skeleton, invocation); @@ -307,12 +314,43 @@ handle_play_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, { GstClapper *clapper = GST_CLAPPER (user_data); + GST_DEBUG ("Handle PlayPause"); + gst_clapper_toggle_play (clapper); gst_clapper_mpris_media_player2_player_complete_play_pause (player_skeleton, invocation); return TRUE; } +static gboolean +handle_seek_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, gint64 offset, gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + GST_DEBUG ("Handle Seek"); + + gst_clapper_seek_offset (clapper, offset * GST_USECOND); + gst_clapper_mpris_media_player2_player_complete_seek (player_skeleton, invocation); + + return TRUE; +} + +static gboolean +handle_set_position_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, const gchar * track_id, + gint64 position, gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + GST_DEBUG ("Handle SetPosition"); + + gst_clapper_seek (clapper, position * GST_USECOND); + gst_clapper_mpris_media_player2_player_complete_set_position (player_skeleton, invocation); + + return TRUE; +} + static void unregister (GstClapperMpris * self) { @@ -353,6 +391,7 @@ name_acquired_cb (GDBusConnection * connection, gst_clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, "Stopped"); gst_clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, 0.01); gst_clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, 2.0); + gst_clapper_mpris_media_player2_player_set_can_seek (self->player_skeleton, TRUE); gst_clapper_mpris_media_player2_player_set_can_control (self->player_skeleton, TRUE); g_object_bind_property (self->player_skeleton, "can-play", @@ -433,15 +472,21 @@ mpris_update_props_dispatch (gpointer user_data) if (gst_clapper_mpris_media_player2_player_get_can_play ( self->player_skeleton) != self->can_play) { /* "can-play" is bound with "can-pause" */ - GST_DEBUG_OBJECT (self, "CanPlay/CanPause: %s", self->can_play ? "yes" : "no"); gst_clapper_mpris_media_player2_player_set_can_play ( self->player_skeleton, self->can_play); + GST_DEBUG_OBJECT (self, "CanPlay/CanPause: %s", self->can_play ? "yes" : "no"); } if (strcmp (gst_clapper_mpris_media_player2_player_get_playback_status ( self->player_skeleton), self->playback_status) != 0) { - GST_DEBUG_OBJECT (self, "PlaybackStatus: %s", self->playback_status); gst_clapper_mpris_media_player2_player_set_playback_status ( self->player_skeleton, self->playback_status); + GST_DEBUG_OBJECT (self, "PlaybackStatus: %s", self->playback_status); + } + if (gst_clapper_mpris_media_player2_player_get_position ( + self->player_skeleton) != self->position) { + gst_clapper_mpris_media_player2_player_set_position ( + self->player_skeleton, self->position); + GST_DEBUG_OBJECT (self, "Position: %ld", self->position); } g_mutex_unlock (&self->lock); @@ -537,13 +582,16 @@ gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper) G_CALLBACK (handle_pause_cb), clapper); g_signal_connect (self->player_skeleton, "handle-play-pause", G_CALLBACK (handle_play_pause_cb), clapper); + g_signal_connect (self->player_skeleton, "handle-seek", + G_CALLBACK (handle_seek_cb), clapper); + g_signal_connect (self->player_skeleton, "handle-set-position", + G_CALLBACK (handle_set_position_cb), clapper); } void gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * status) { g_mutex_lock (&self->lock); - if (strcmp (self->playback_status, status) == 0) { g_mutex_unlock (&self->lock); return; @@ -551,7 +599,22 @@ gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * sta g_free (self->playback_status); self->playback_status = g_strdup (status); self->can_play = strcmp (status, "Stopped") != 0; + g_mutex_unlock (&self->lock); + mpris_dispatcher_update_dispatch (self); +} + +void +gst_clapper_mpris_set_position (GstClapperMpris * self, gint64 position) +{ + position /= GST_USECOND; + + g_mutex_lock (&self->lock); + if (self->position == position) { + g_mutex_unlock (&self->lock); + return; + } + self->position = position; g_mutex_unlock (&self->lock); mpris_dispatcher_update_dispatch (self); From edb799bafa4679e3cb9ac19c991f1f0903de8dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sat, 22 May 2021 09:37:47 +0200 Subject: [PATCH 5/7] API: Parse title from URI when no title in tags --- .../clapper/gstclapper-media-info-private.h | 2 +- lib/gst/clapper/gstclapper-media-info.c | 3 +- lib/gst/clapper/gstclapper.c | 41 +++++++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/gst/clapper/gstclapper-media-info-private.h b/lib/gst/clapper/gstclapper-media-info-private.h index 3e420586..87e1f4b6 100644 --- a/lib/gst/clapper/gstclapper-media-info-private.h +++ b/lib/gst/clapper/gstclapper-media-info-private.h @@ -108,7 +108,7 @@ struct _GstClapperMediaInfo GList *video_stream_list; GList *subtitle_stream_list; - GstClockTime duration; + GstClockTime duration; }; struct _GstClapperMediaInfoClass diff --git a/lib/gst/clapper/gstclapper-media-info.c b/lib/gst/clapper/gstclapper-media-info.c index 3f35215f..20aca105 100644 --- a/lib/gst/clapper/gstclapper-media-info.c +++ b/lib/gst/clapper/gstclapper-media-info.c @@ -767,7 +767,8 @@ gst_clapper_media_info_get_toc (const GstClapperMediaInfo * info) * gst_clapper_media_info_get_title: * @info: a #GstClapperMediaInfo * - * Returns: the media title. + * Returns: the media title. When metadata does not contain title, + * returns title parsed from URI. */ const gchar * gst_clapper_media_info_get_title (const GstClapperMediaInfo * info) diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c index 93708a1d..9b42eb40 100644 --- a/lib/gst/clapper/gstclapper.c +++ b/lib/gst/clapper/gstclapper.c @@ -1804,8 +1804,12 @@ request_state_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, static void media_info_update (GstClapper * self, GstClapperMediaInfo * info) { - g_free (info->title); - info->title = get_from_tags (self, info, get_title); + /* Update title from new tags or leave the title from URI */ + gchar *tags_title = get_from_tags (self, info, get_title); + if (tags_title) { + g_free (info->title); + info->title = tags_title; + } g_free (info->container); info->container = get_from_tags (self, info, get_container_format); @@ -2672,6 +2676,32 @@ subtitle_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data) g_mutex_unlock (&self->lock); } +static gchar * +get_title_from_uri (const gchar * uri) +{ + gchar *proto = gst_uri_get_protocol (uri); + gchar *title = NULL; + + if (strcmp (proto, "file") == 0) { + const gchar *ext = strrchr (uri, '.'); + if (ext && strlen (ext) < 8) { + gchar *filename = g_filename_from_uri (uri, NULL, NULL); + if (filename) { + gchar *base = g_path_get_basename (filename); + g_free (filename); + title = g_strndup (base, strlen (base) - strlen (ext)); + g_free (base); + } + } + } else if (strcmp (proto, "dvb") == 0) { + const gchar *channel = strrchr (uri, '/') + 1; + title = g_strdup (channel); + } + g_free (proto); + + return title; +} + static void * get_title (GstTagList * tags) { @@ -2788,12 +2818,15 @@ gst_clapper_media_info_create (GstClapper * self) } media_info->title = get_from_tags (self, media_info, get_title); + if (!media_info->title) + media_info->title = get_title_from_uri (self->uri); + media_info->container = get_from_tags (self, media_info, get_container_format); media_info->image_sample = get_from_tags (self, media_info, get_cover_sample); - GST_DEBUG_OBJECT (self, "uri: %s title: %s duration: %" GST_TIME_FORMAT - " seekable: %s live: %s container: %s image_sample %p", + GST_DEBUG_OBJECT (self, "uri: %s, title: %s, duration: %" GST_TIME_FORMAT + ", seekable: %s, live: %s, container: %s, image_sample %p", media_info->uri, media_info->title, GST_TIME_ARGS (media_info->duration), media_info->seekable ? "yes" : "no", media_info->is_live ? "yes" : "no", media_info->container, media_info->image_sample); From 9f776e9ecb10fef760d327d9a4b814bc510d38bf Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 22 May 2021 21:52:52 +0200 Subject: [PATCH 6/7] mpris: Support changing volume --- lib/gst/clapper/gstclapper-mpris-private.h | 3 +- lib/gst/clapper/gstclapper-mpris.c | 70 ++++++++++++++++++++-- lib/gst/clapper/gstclapper.c | 2 +- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/lib/gst/clapper/gstclapper-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h index efacd6b3..1a16d94c 100644 --- a/lib/gst/clapper/gstclapper-mpris-private.h +++ b/lib/gst/clapper/gstclapper-mpris-private.h @@ -26,7 +26,8 @@ G_BEGIN_DECLS G_GNUC_INTERNAL -void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper); +void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper, + GstClapperSignalDispatcher *signal_dispatcher); G_GNUC_INTERNAL void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info); diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c index dcec44b2..af70833c 100644 --- a/lib/gst/clapper/gstclapper-mpris.c +++ b/lib/gst/clapper/gstclapper-mpris.c @@ -24,6 +24,12 @@ #include "gstclapper-mpris-gdbus.h" #include "gstclapper-mpris.h" #include "gstclapper-mpris-private.h" +#include "gstclapper-signal-dispatcher-private.h" + +GST_DEBUG_CATEGORY_STATIC (gst_clapper_mpris_debug); +#define GST_CAT_DEFAULT gst_clapper_mpris_debug + +#define MPRIS_DEFAULT_VOLUME 1.0 enum { @@ -33,6 +39,7 @@ enum PROP_IDENTITY, PROP_DESKTOP_ENTRY, PROP_DEFAULT_ART_URL, + PROP_VOLUME, PROP_LAST }; @@ -43,6 +50,7 @@ struct _GstClapperMpris GstClapperMprisMediaPlayer2 *base_skeleton; GstClapperMprisMediaPlayer2Player *player_skeleton; + GstClapperSignalDispatcher *signal_dispatcher; GstClapperMediaInfo *media_info; guint name_id; @@ -73,9 +81,6 @@ struct _GstClapperMprisClass GObjectClass parent_class; }; -GST_DEBUG_CATEGORY_STATIC (gst_clapper_mpris_debug); -#define GST_CAT_DEFAULT gst_clapper_mpris_debug - #define parent_class gst_clapper_mpris_parent_class G_DEFINE_TYPE (GstClapperMpris, gst_clapper_mpris, G_TYPE_OBJECT); @@ -83,6 +88,8 @@ static GParamSpec *param_specs[PROP_LAST] = { NULL, }; static void gst_clapper_mpris_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); +static void gst_clapper_mpris_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); static void gst_clapper_mpris_dispose (GObject * object); static void gst_clapper_mpris_finalize (GObject * object); static void gst_clapper_mpris_constructed (GObject * object); @@ -115,8 +122,9 @@ gst_clapper_mpris_init (GstClapperMpris * self) self->desktop_entry = NULL; self->default_art_url = NULL; - self->parse_media_info = FALSE; + self->signal_dispatcher = NULL; self->media_info = NULL; + self->parse_media_info = FALSE; self->playback_status = g_strdup ("Stopped"); self->can_play = FALSE; @@ -131,6 +139,7 @@ gst_clapper_mpris_class_init (GstClapperMprisClass * klass) GObjectClass *gobject_class = (GObjectClass *) klass; gobject_class->set_property = gst_clapper_mpris_set_property; + gobject_class->get_property = gst_clapper_mpris_get_property; gobject_class->dispose = gst_clapper_mpris_dispose; gobject_class->finalize = gst_clapper_mpris_finalize; gobject_class->constructed = gst_clapper_mpris_constructed; @@ -165,6 +174,11 @@ gst_clapper_mpris_class_init (GstClapperMprisClass * klass) NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + param_specs[PROP_VOLUME] = + g_param_spec_double ("volume", "Volume", "Volume", + 0, 1.5, MPRIS_DEFAULT_VOLUME, G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); } @@ -190,6 +204,25 @@ gst_clapper_mpris_set_property (GObject * object, guint prop_id, case PROP_DEFAULT_ART_URL: self->default_art_url = g_value_dup_string (value); break; + case PROP_VOLUME: + g_object_set_property (G_OBJECT (self->player_skeleton), "volume", value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_clapper_mpris_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstClapperMpris *self = GST_CLAPPER_MPRIS (object); + + switch (prop_id) { + case PROP_VOLUME: + g_object_get_property (G_OBJECT (self->player_skeleton), "volume", value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -240,6 +273,8 @@ gst_clapper_mpris_finalize (GObject * object) g_object_unref (self->base_skeleton); if (self->player_skeleton) g_object_unref (self->player_skeleton); + if (self->signal_dispatcher) + g_object_unref (self->signal_dispatcher); if (self->media_info) g_object_unref (self->media_info); @@ -351,6 +386,23 @@ handle_set_position_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, return TRUE; } +static void +volume_notify_dispatch (gpointer user_data) +{ + GstClapperMpris *self = user_data; + + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_VOLUME]); +} + +static void +handle_volume_notify_cb (G_GNUC_UNUSED GObject * obj, + G_GNUC_UNUSED GParamSpec * pspec, GstClapperMpris * self) +{ + gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, NULL, + volume_notify_dispatch, g_object_ref (self), + (GDestroyNotify) g_object_unref); +} + static void unregister (GstClapperMpris * self) { @@ -574,8 +626,12 @@ done: } void -gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper) +gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper, + GstClapperSignalDispatcher * signal_dispatcher) { + if (signal_dispatcher) + self->signal_dispatcher = g_object_ref (signal_dispatcher); + g_signal_connect (self->player_skeleton, "handle-play", G_CALLBACK (handle_play_cb), clapper); g_signal_connect (self->player_skeleton, "handle-pause", @@ -586,6 +642,10 @@ gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper) G_CALLBACK (handle_seek_cb), clapper); g_signal_connect (self->player_skeleton, "handle-set-position", G_CALLBACK (handle_set_position_cb), clapper); + + g_object_bind_property (clapper, "volume", self, "volume", G_BINDING_BIDIRECTIONAL); + g_signal_connect (self->player_skeleton, "notify::volume", + G_CALLBACK (handle_volume_notify_cb), self); } void diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c index 9b42eb40..c65abacb 100644 --- a/lib/gst/clapper/gstclapper.c +++ b/lib/gst/clapper/gstclapper.c @@ -3005,7 +3005,7 @@ gst_clapper_main (gpointer data) gst_bus_add_signal_watch (bus); if (self->mpris) - gst_clapper_mpris_set_clapper (self->mpris, self); + gst_clapper_mpris_set_clapper (self->mpris, self, self->signal_dispatcher); g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb), self); From 5f259b28fefdc40f21e83f6d586e103a10b5be1e Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sun, 23 May 2021 15:18:44 +0200 Subject: [PATCH 7/7] mpris: Add "SupportedUriSchemes" and handle "OpenUri" method --- lib/gst/clapper/gstclapper-mpris.c | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c index af70833c..fff485f0 100644 --- a/lib/gst/clapper/gstclapper-mpris.c +++ b/lib/gst/clapper/gstclapper-mpris.c @@ -386,6 +386,22 @@ handle_set_position_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, return TRUE; } +static gboolean +handle_open_uri_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, + GDBusMethodInvocation * invocation, const gchar * uri, + gpointer user_data) +{ + GstClapper *clapper = GST_CLAPPER (user_data); + + GST_DEBUG ("Handle OpenUri"); + + /* FIXME: set one item playlist instead */ + gst_clapper_set_uri (clapper, uri); + gst_clapper_mpris_media_player2_player_complete_open_uri (player_skeleton, invocation); + + return TRUE; +} + static void volume_notify_dispatch (gpointer user_data) { @@ -423,6 +439,47 @@ _get_mpris_trackid (GstClapperMpris * self) return g_strdup_printf ("%s%s%i", self->id_path, "/Track/", 0); } +static void +_set_supported_uri_schemes (GstClapperMpris * self) +{ + const gchar *uri_schemes[96] = {}; + GList *elements, *el; + guint index = 0; + + elements = gst_element_factory_list_get_elements ( + GST_ELEMENT_FACTORY_TYPE_SRC, GST_RANK_NONE); + + for (el = elements; el != NULL; el = el->next) { + const gchar *const *protocols; + GstElementFactory *factory = GST_ELEMENT_FACTORY (el->data); + + if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC) + continue; + + protocols = gst_element_factory_get_uri_protocols (factory); + if (protocols == NULL || *protocols == NULL) + continue; + + while (*protocols != NULL) { + guint j = index; + + while (j--) { + if (strcmp (uri_schemes[j], *protocols) == 0) + goto next; + } + uri_schemes[index] = *protocols; + GST_DEBUG_OBJECT (self, "Added supported URI scheme: %s", *protocols); + ++index; +next: + ++protocols; + } + } + gst_plugin_feature_list_free (elements); + + gst_clapper_mpris_media_player2_set_supported_uri_schemes ( + self->base_skeleton, uri_schemes); +} + static void name_acquired_cb (GDBusConnection * connection, const gchar *name, gpointer user_data) @@ -440,6 +497,8 @@ name_acquired_cb (GDBusConnection * connection, if (self->desktop_entry) gst_clapper_mpris_media_player2_set_desktop_entry (self->base_skeleton, self->desktop_entry); + _set_supported_uri_schemes (self); + gst_clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, "Stopped"); gst_clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, 0.01); gst_clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, 2.0); @@ -642,6 +701,8 @@ gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper, G_CALLBACK (handle_seek_cb), clapper); g_signal_connect (self->player_skeleton, "handle-set-position", G_CALLBACK (handle_set_position_cb), clapper); + g_signal_connect (self->player_skeleton, "handle-open-uri", + G_CALLBACK (handle_open_uri_cb), clapper); g_object_bind_property (clapper, "volume", self, "volume", G_BINDING_BIDIRECTIONAL); g_signal_connect (self->player_skeleton, "notify::volume",