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;