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;