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-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-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h
new file mode 100644
index 00000000..1a16d94c
--- /dev/null
+++ b/lib/gst/clapper/gstclapper-mpris-private.h
@@ -0,0 +1,43 @@
+/*
+ * 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,
+ GstClapperSignalDispatcher *signal_dispatcher);
+
+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_position (GstClapperMpris *self, gint64 position);
+
+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..fff485f0
--- /dev/null
+++ b/lib/gst/clapper/gstclapper-mpris.c
@@ -0,0 +1,782 @@
+/*
+ * 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"
+#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
+{
+ PROP_0,
+ PROP_OWN_NAME,
+ PROP_ID_PATH,
+ PROP_IDENTITY,
+ PROP_DESKTOP_ENTRY,
+ PROP_DEFAULT_ART_URL,
+ PROP_VOLUME,
+ PROP_LAST
+};
+
+struct _GstClapperMpris
+{
+ GObject parent;
+
+ GstClapperMprisMediaPlayer2 *base_skeleton;
+ GstClapperMprisMediaPlayer2Player *player_skeleton;
+
+ GstClapperSignalDispatcher *signal_dispatcher;
+ GstClapperMediaInfo *media_info;
+
+ guint name_id;
+
+ /* Properties */
+ gchar *own_name;
+ gchar *id_path;
+ gchar *identity;
+ gchar *desktop_entry;
+ gchar *default_art_url;
+
+ gboolean parse_media_info;
+
+ /* Current status */
+ gchar *playback_status;
+ gboolean can_play;
+ guint64 position;
+
+ GThread *thread;
+ GMutex lock;
+ GCond cond;
+ GMainContext *context;
+ GMainLoop *loop;
+};
+
+struct _GstClapperMprisClass
+{
+ GObjectClass parent_class;
+};
+
+#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_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);
+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->signal_dispatcher = NULL;
+ self->media_info = NULL;
+ self->parse_media_info = FALSE;
+
+ self->playback_status = g_strdup ("Stopped");
+ self->can_play = FALSE;
+ self->position = 0;
+
+ 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->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;
+
+ 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);
+
+ 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);
+}
+
+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;
+ 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;
+ }
+}
+
+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);
+ if (self->signal_dispatcher)
+ g_object_unref (self->signal_dispatcher);
+ if (self->media_info)
+ g_object_unref (self->media_info);
+
+ 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_DEBUG ("Handle Play");
+
+ 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_DEBUG ("Handle Pause");
+
+ 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_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 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)
+{
+ 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)
+{
+ 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
+_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)
+{
+ 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);
+
+ _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);
+ 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",
+ 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 (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" */
+ 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_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);
+ GST_DEBUG_OBJECT (self, "MPRIS props updated");
+
+ 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,
+ 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",
+ 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);
+ 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",
+ G_CALLBACK (handle_volume_notify_cb), self);
+}
+
+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);
+}
+
+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);
+}
+
+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
+ * @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..c65abacb 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
@@ -1031,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;
@@ -1647,6 +1684,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
@@ -1758,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);
@@ -2626,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)
{
@@ -2742,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);
@@ -2925,6 +3004,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, self->signal_dispatcher);
+
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 +3107,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 +3121,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;
}
@@ -3469,6 +3554,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)
{
@@ -3701,6 +3809,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..35895109 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);
@@ -170,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);
@@ -220,6 +225,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;