Initial MPRIS support

Implement a working MPRIS DBus connection with a separate API to control it. Right now only player playback state is reflected and Play/Pause/PlayPause calls work.
This commit is contained in:
Rafał Dzięgiel
2021-05-20 19:03:33 +02:00
parent ac7be5956c
commit f08ffad178
12 changed files with 826 additions and 16 deletions

1
.gitattributes vendored
View File

@@ -1,4 +1,5 @@
extras/**/* linguist-vendored extras/**/* linguist-vendored
lib/**/* linguist-vendored lib/**/* linguist-vendored
lib/**/**/* linguist-vendored lib/**/**/* linguist-vendored
lib/gst/clapper/gstclapper-mpris* linguist-vendored=false
lib/gst/clapper/gtk4/* linguist-vendored=false lib/gst/clapper/gtk4/* linguist-vendored=false

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<node>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
<property name="CanQuit" type="b" access="read"/>
<property name="Fullscreen" type="b" access="readwrite"/>
<property name="CanSetFullscreen" type="b" access="read"/>
<property name="CanRaise" type="b" access="read"/>
<property name="HasTrackList" type="b" access="read"/>
<property name="Identity" type="s" access="read"/>
<property name="DesktopEntry" type="s" access="read"/>
<property name="SupportedUriSchemes" type="as" access="read"/>
<property name="SupportedMimeTypes" type="as" access="read"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg name="Offset" type="x" direction="in"/>
</method>
<method name="SetPosition">
<arg name="TrackId" type="o" direction="in"/>
<arg name="Position" type="x" direction="in"/>
</method>
<method name="OpenUri">
<arg name="Uri" type="s" direction="in"/>
</method>
<signal name="Seeked">
<arg type="x" name="Position"/>
</signal>
<property name="PlaybackStatus" type="s" access="read"/>
<property name="LoopStatus" type="s" access="readwrite"/>
<property name="Rate" type="d" access="readwrite"/>
<property name="Shuffle" type="b" access="readwrite"/>
<property name="Metadata" type="a{sv}" access="read"/>
<property name="Volume" type="d" access="readwrite"/>
<property name="Position" type="x" access="read"/>
<property name="MinimumRate" type="d" access="read"/>
<property name="MaximumRate" type="d" access="read"/>
<property name="CanGoNext" type="b" access="read"/>
<property name="CanGoPrevious" type="b" access="read"/>
<property name="CanPlay" type="b" access="read"/>
<property name="CanPause" type="b" access="read"/>
<property name="CanSeek" type="b" access="read"/>
<property name="CanControl" type="b" access="read"/>
</interface>
</node>

View File

@@ -28,6 +28,7 @@
#include <gst/clapper/gstclapper-g-main-context-signal-dispatcher.h> #include <gst/clapper/gstclapper-g-main-context-signal-dispatcher.h>
#include <gst/clapper/gstclapper-video-overlay-video-renderer.h> #include <gst/clapper/gstclapper-video-overlay-video-renderer.h>
#include <gst/clapper/gstclapper-visualization.h> #include <gst/clapper/gstclapper-visualization.h>
#include <gst/clapper/gstclapper-mpris.h>
#include <gst/clapper/gstclapper-gtk4-plugin.h> #include <gst/clapper/gstclapper-gtk4-plugin.h>
#endif /* __CLAPPER_H__ */ #endif /* __CLAPPER_H__ */

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2021 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* 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 <gst/clapper/gstclapper-mpris.h>
#include <gst/clapper/gstclapper.h>
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__ */

View File

@@ -0,0 +1,530 @@
/*
* Copyright (C) 2021 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* 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;
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2021 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* 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 <glib.h>
#include <gio/gio.h>
#include <gst/clapper/clapper-prelude.h>
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__ */

View File

@@ -46,6 +46,7 @@
#include "gstclapper-signal-dispatcher-private.h" #include "gstclapper-signal-dispatcher-private.h"
#include "gstclapper-video-renderer-private.h" #include "gstclapper-video-renderer-private.h"
#include "gstclapper-media-info-private.h" #include "gstclapper-media-info-private.h"
#include "gstclapper-mpris-private.h"
GST_DEBUG_CATEGORY_STATIC (gst_clapper_debug); GST_DEBUG_CATEGORY_STATIC (gst_clapper_debug);
#define GST_CAT_DEFAULT gst_clapper_debug #define GST_CAT_DEFAULT gst_clapper_debug
@@ -76,6 +77,7 @@ enum
PROP_0, PROP_0,
PROP_VIDEO_RENDERER, PROP_VIDEO_RENDERER,
PROP_SIGNAL_DISPATCHER, PROP_SIGNAL_DISPATCHER,
PROP_MPRIS,
PROP_STATE, PROP_STATE,
PROP_URI, PROP_URI,
PROP_SUBURI, PROP_SUBURI,
@@ -127,6 +129,7 @@ struct _GstClapper
GstClapperVideoRenderer *video_renderer; GstClapperVideoRenderer *video_renderer;
GstClapperSignalDispatcher *signal_dispatcher; GstClapperSignalDispatcher *signal_dispatcher;
GstClapperMpris *mpris;
gchar *uri; gchar *uri;
gchar *redirect_uri; gchar *redirect_uri;
@@ -305,6 +308,13 @@ gst_clapper_class_init (GstClapperClass * klass)
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); 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] = param_specs[PROP_STATE] =
g_param_spec_enum ("state", "Clapper State", "Current player state", g_param_spec_enum ("state", "Clapper State", "Current player state",
GST_TYPE_CLAPPER_STATE, DEFAULT_STATE, G_PARAM_READABLE | GST_TYPE_CLAPPER_STATE, DEFAULT_STATE, G_PARAM_READABLE |
@@ -491,7 +501,7 @@ gst_clapper_finalize (GObject * object)
{ {
GstClapper *self = GST_CLAPPER (object); GstClapper *self = GST_CLAPPER (object);
GST_TRACE_OBJECT (self, "Finalizing"); GST_TRACE_OBJECT (self, "Finalize");
g_free (self->uri); g_free (self->uri);
g_free (self->redirect_uri); g_free (self->redirect_uri);
@@ -507,6 +517,8 @@ gst_clapper_finalize (GObject * object)
g_object_unref (self->video_renderer); g_object_unref (self->video_renderer);
if (self->signal_dispatcher) if (self->signal_dispatcher)
g_object_unref (self->signal_dispatcher); g_object_unref (self->signal_dispatcher);
if (self->mpris)
g_object_unref (self->mpris);
if (self->current_vis_element) if (self->current_vis_element)
gst_object_unref (self->current_vis_element); gst_object_unref (self->current_vis_element);
if (self->collection) if (self->collection)
@@ -649,6 +661,9 @@ gst_clapper_set_property (GObject * object, guint prop_id,
case PROP_SIGNAL_DISPATCHER: case PROP_SIGNAL_DISPATCHER:
self->signal_dispatcher = g_value_dup_object (value); self->signal_dispatcher = g_value_dup_object (value);
break; break;
case PROP_MPRIS:
self->mpris = g_value_dup_object (value);
break;
case PROP_URI:{ case PROP_URI:{
g_mutex_lock (&self->lock); g_mutex_lock (&self->lock);
g_free (self->uri); g_free (self->uri);
@@ -741,6 +756,9 @@ gst_clapper_get_property (GObject * object, guint prop_id,
GstClapper *self = GST_CLAPPER (object); GstClapper *self = GST_CLAPPER (object);
switch (prop_id) { switch (prop_id) {
case PROP_MPRIS:
g_value_set_object (value, self->mpris);
break;
case PROP_STATE: case PROP_STATE:
g_mutex_lock (&self->lock); g_mutex_lock (&self->lock);
g_value_set_enum (value, self->app_state); g_value_set_enum (value, self->app_state);
@@ -980,6 +998,23 @@ change_state (GstClapper * self, GstClapperState state)
state_changed_dispatch, data, state_changed_dispatch, data,
(GDestroyNotify) state_changed_signal_data_free); (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 typedef struct
@@ -2925,6 +2960,9 @@ gst_clapper_main (gpointer data)
self->bus = bus = gst_element_get_bus (self->playbin); self->bus = bus = gst_element_get_bus (self->playbin);
gst_bus_add_signal_watch (bus); 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), g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb),
self); self);
g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb), g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb),
@@ -3025,6 +3063,7 @@ gst_clapper_main (gpointer data)
* gst_clapper_new: * gst_clapper_new:
* @video_renderer: (transfer full) (allow-none): GstClapperVideoRenderer to use * @video_renderer: (transfer full) (allow-none): GstClapperVideoRenderer to use
* @signal_dispatcher: (transfer full) (allow-none): GstClapperSignalDispatcher 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 * Creates a new #GstClapper instance that uses @signal_dispatcher to dispatch
* signals to some event loop system, or emits signals directly if NULL is * signals to some event loop system, or emits signals directly if NULL is
@@ -3038,18 +3077,20 @@ gst_clapper_main (gpointer data)
*/ */
GstClapper * GstClapper *
gst_clapper_new (GstClapperVideoRenderer * video_renderer, gst_clapper_new (GstClapperVideoRenderer * video_renderer,
GstClapperSignalDispatcher * signal_dispatcher) GstClapperSignalDispatcher * signal_dispatcher,
GstClapperMpris * mpris)
{ {
GstClapper *self; GstClapper *self;
self = self = g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer,
g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer, "signal-dispatcher", signal_dispatcher, "mpris", mpris, NULL);
"signal-dispatcher", signal_dispatcher, NULL);
if (video_renderer) if (video_renderer)
g_object_unref (video_renderer); g_object_unref (video_renderer);
if (signal_dispatcher) if (signal_dispatcher)
g_object_unref (signal_dispatcher); g_object_unref (signal_dispatcher);
if (mpris)
g_object_unref (mpris);
return self; return self;
} }
@@ -3701,6 +3742,28 @@ gst_clapper_get_pipeline (GstClapper * self)
return val; 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: * gst_clapper_get_media_info:
* @clapper: #GstClapper instance * @clapper: #GstClapper instance

View File

@@ -30,6 +30,7 @@
#include <gst/clapper/gstclapper-signal-dispatcher.h> #include <gst/clapper/gstclapper-signal-dispatcher.h>
#include <gst/clapper/gstclapper-video-renderer.h> #include <gst/clapper/gstclapper-video-renderer.h>
#include <gst/clapper/gstclapper-media-info.h> #include <gst/clapper/gstclapper-media-info.h>
#include <gst/clapper/gstclapper-mpris.h>
G_BEGIN_DECLS G_BEGIN_DECLS
@@ -153,7 +154,8 @@ GST_CLAPPER_API
GType gst_clapper_get_type (void); GType gst_clapper_get_type (void);
GST_CLAPPER_API 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 GST_CLAPPER_API
void gst_clapper_play (GstClapper *clapper); void gst_clapper_play (GstClapper *clapper);
@@ -220,6 +222,10 @@ void gst_clapper_set_mute (GstClapper *clapper
GST_CLAPPER_API GST_CLAPPER_API
GstElement * gst_clapper_get_pipeline (GstClapper *clapper); GstElement * gst_clapper_get_pipeline (GstClapper *clapper);
GST_CLAPPER_API
GstClapperMpris *
gst_clapper_get_mpris (GstClapper *clapper);
GST_CLAPPER_API GST_CLAPPER_API
void gst_clapper_set_video_track_enabled (GstClapper *clapper, gboolean enabled); void gst_clapper_set_video_track_enabled (GstClapper *clapper, gboolean enabled);

View File

@@ -1,3 +1,5 @@
gnome = import('gnome')
gstclapper_sources = [ gstclapper_sources = [
'gstclapper.c', 'gstclapper.c',
'gstclapper-signal-dispatcher.c', 'gstclapper-signal-dispatcher.c',
@@ -6,6 +8,7 @@ gstclapper_sources = [
'gstclapper-g-main-context-signal-dispatcher.c', 'gstclapper-g-main-context-signal-dispatcher.c',
'gstclapper-video-overlay-video-renderer.c', 'gstclapper-video-overlay-video-renderer.c',
'gstclapper-visualization.c', 'gstclapper-visualization.c',
'gstclapper-mpris.c',
'gstclapper-gtk4-plugin.c', 'gstclapper-gtk4-plugin.c',
'gtk4/gstclapperglsink.c', 'gtk4/gstclapperglsink.c',
@@ -23,6 +26,7 @@ gstclapper_headers = [
'gstclapper-g-main-context-signal-dispatcher.h', 'gstclapper-g-main-context-signal-dispatcher.h',
'gstclapper-video-overlay-video-renderer.h', 'gstclapper-video-overlay-video-renderer.h',
'gstclapper-visualization.h', 'gstclapper-visualization.h',
'gstclapper-mpris.h',
'gstclapper-gtk4-plugin.h', 'gstclapper-gtk4-plugin.h',
] ]
gstclapper_defines = [ gstclapper_defines = [
@@ -67,15 +71,22 @@ if not have_gtk_gl_windowing
error('GTK4 widget requires GL windowing') error('GTK4 widget requires GL windowing')
endif 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 = library('gstclapper-' + api_version,
gstclapper_sources, gstclapper_sources + gstclapper_mpris_gdbus,
c_args : gstclapper_defines, c_args : gstclapper_defines,
link_args : noseh_link_args, link_args : noseh_link_args,
include_directories : [configinc, libsinc], include_directories : [configinc, libsinc],
version : libversion, version : libversion,
install : true, install : true,
install_dir : clapper_libdir, 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, gsttag_dep, gstpbutils_dep, libm] + gtk_deps,
) )

View File

@@ -13,6 +13,7 @@
"--share=network", "--share=network",
"--device=all", "--device=all",
"--filesystem=xdg-videos", "--filesystem=xdg-videos",
"--own-name=org.mpris.MediaPlayer2.Clapper",
"--talk-name=org.gnome.Shell", "--talk-name=org.gnome.Shell",
"--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0", "--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0",
"--env=GST_VAAPI_ALL_DRIVERS=1" "--env=GST_VAAPI_ALL_DRIVERS=1"

View File

@@ -1,4 +1,4 @@
const { Gio, Gdk, Gtk } = imports.gi; const { Gio, GLib, Gdk, Gtk } = imports.gi;
const Debug = imports.src.debug; const Debug = imports.src.debug;
const { debug } = 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() function loadCustomCss()
{ {
const clapperPath = getClapperPath(); const clapperPath = getClapperPath();

View File

@@ -20,14 +20,18 @@ class ClapperPlayer extends GstClapper.Clapper
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null); const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtk4plugin.video_sink; glsinkbin.sink = gtk4plugin.video_sink;
const dispatcher = new GstClapper.ClapperGMainContextSignalDispatcher();
const renderer = new GstClapper.ClapperVideoOverlayVideoRenderer({
video_sink: glsinkbin
});
super._init({ super._init({
signal_dispatcher: dispatcher, signal_dispatcher: new GstClapper.ClapperGMainContextSignalDispatcher(),
video_renderer: renderer 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; this.widget = gtk4plugin.video_sink.widget;