summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBastien Nocera <hadess@hadess.net>2021-10-18 12:22:14 +0200
committerBastien Nocera <hadess@hadess.net>2022-02-21 18:27:05 +0100
commitdf1289cf4701a5219c7cb5d90862580bcb357710 (patch)
treea96d6790f7275c0c112fdc6149ef6d0487ddf3dc
parent449d6a2a94ce0ae592c40620c3c0fb14815e016a (diff)
downloadtotem-df1289cf4701a5219c7cb5d90862580bcb357710.tar.gz
mpris: Add new native MPRIS plugin
This version is implemented in C so as to be able to enable it by default, without dragging in Python, whether at compile-time, or at run-time. Closes: #59
-rw-r--r--po/POTFILES.in2
-rw-r--r--src/plugins/meson.build2
-rw-r--r--src/plugins/mpris/meson.build26
-rw-r--r--src/plugins/mpris/mpris-spec.h107
-rw-r--r--src/plugins/mpris/mpris.plugin.desktop.in9
-rw-r--r--src/plugins/mpris/totem-mpris.c717
6 files changed, 863 insertions, 0 deletions
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d60bb7510..1e94daa61 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -34,6 +34,8 @@ src/plugins/dbusservice/dbusservice.plugin.desktop.in
src/plugins/dbusservice/dbusservice.py
src/plugins/im-status/totem-im-status.c
src/plugins/im-status/totem-im-status.plugin.desktop.in
+src/plugins/mpris/mpris.plugin.desktop.in
+src/plugins/mpris/totem-mpris.c
src/plugins/open-directory/open-directory.plugin.desktop.in
src/plugins/open-directory/totem-open-directory.c
src/plugins/opensubtitles/opensubtitles.plugin.desktop.in
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 268ac5ab5..fc28a24c9 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -17,6 +17,7 @@ allowed_plugins = [
'autoload-subtitles',
'dbusservice',
'im-status',
+ 'mpris',
'open-directory',
'opensubtitles',
'properties',
@@ -40,6 +41,7 @@ if plugins_option != 'none'
'apple-trailers',
'autoload-subtitles',
'im-status',
+ 'mpris',
'open-directory',
'properties',
'recent',
diff --git a/src/plugins/mpris/meson.build b/src/plugins/mpris/meson.build
new file mode 100644
index 000000000..5de3f4b32
--- /dev/null
+++ b/src/plugins/mpris/meson.build
@@ -0,0 +1,26 @@
+plugin_name = 'mpris'
+
+plugin_dir = join_paths(totem_pluginsdir, plugin_name)
+
+shared_module(
+ plugin_name,
+ sources: 'totem-' + plugin_name + '.c',
+ include_directories: plugins_incs,
+ dependencies: plugins_deps + [
+ gio_dep
+ ],
+ c_args: plugins_cflags,
+ install: true,
+ install_dir: plugin_dir
+)
+
+plugin_data = plugin_name + '.plugin'
+
+custom_target(
+ plugin_data,
+ input: plugin_data + '.desktop.in',
+ output: plugin_data,
+ command: msgfmt_plugin_cmd,
+ install: true,
+ install_dir: plugin_dir
+)
diff --git a/src/plugins/mpris/mpris-spec.h b/src/plugins/mpris/mpris-spec.h
new file mode 100644
index 000000000..2b5fe03df
--- /dev/null
+++ b/src/plugins/mpris/mpris-spec.h
@@ -0,0 +1,107 @@
+#define MPRIS_BUS_NAME_PREFIX "org.mpris.MediaPlayer2"
+#define MPRIS_OBJECT_NAME "/org/mpris/MediaPlayer2"
+
+#define MPRIS_ROOT_INTERFACE "org.mpris.MediaPlayer2"
+#define MPRIS_PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
+#define MPRIS_TRACKLIST_INTERFACE "org.mpris.MediaPlayer2.TrackList"
+#define MPRIS_PLAYLISTS_INTERFACE "org.mpris.MediaPlayer2.Playlists"
+
+const char *mpris_introspection_xml =
+ "<node>"
+ " <interface name='org.mpris.MediaPlayer2'>"
+ " <method name='Raise'/>"
+ " <method name='Quit'/>"
+ " <property name='CanQuit' 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 direction='in' name='Offset' type='x'/>"
+ " </method>"
+ " <method name='SetPosition'>"
+ " <arg direction='in' name='TrackId' type='o'/>"
+ " <arg direction='in' name='Position' type='x'/>"
+ " </method>"
+ " <method name='OpenUri'>"
+ " <arg direction='in' name='Uri' type='s'/>"
+ " </method>"
+ " <signal name='Seeked'>"
+ " <arg name='Position' type='x'/>"
+ " </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>"
+ " <interface name='org.mpris.MediaPlayer2.TrackList'>"
+ " <method name='GetTracksMetadata'>"
+ " <arg direction='in' name='TrackIds' type='ao'/>"
+ " <arg direction='out' name='Metadata' type='aa{sv}'/>"
+ " </method>"
+ " <method name='AddTrack'>"
+ " <arg direction='in' name='Uri' type='s'/>"
+ " <arg direction='in' name='AfterTrack' type='o'/>"
+ " <arg direction='in' name='SetAsCurrent' type='b'/>"
+ " </method>"
+ " <method name='RemoveTrack'>"
+ " <arg direction='in' name='TrackId' type='o'/>"
+ " </method>"
+ " <method name='GoTo'>"
+ " <arg direction='in' name='TrackId' type='o'/>"
+ " </method>"
+ " <signal name='TrackListReplaced'>"
+ " <arg name='Tracks' type='ao'/>"
+ " <arg name='CurrentTrack' type='o'/>"
+ " </signal>"
+ " <signal name='TrackAdded'>"
+ " <arg name='Metadata' type='a{sv}'/>"
+ " <arg name='AfterTrack' type='o'/>"
+ " </signal>"
+ " <signal name='TrackRemoved'>"
+ " <arg name='TrackId' type='o'/>"
+ " </signal>"
+ " <signal name='TrackMetadataChanged'>"
+ " <arg name='TrackId' type='o'/>"
+ " <arg name='Metadata' type='a{sv}'/>"
+ " </signal>"
+ " <property name='Tracks' type='ao' access='read'/>"
+ " <property name='CanEditTracks' type='b' access='read'/>"
+ " </interface>"
+ " <interface name='org.mpris.MediaPlayer2.Playlists'>"
+ " <method name='ActivatePlaylist'>"
+ " <arg direction='in' name='PlaylistId' type='o'/>"
+ " </method>"
+ " <method name='GetPlaylists'>"
+ " <arg direction='in' name='Index' type='u'/>"
+ " <arg direction='in' name='MaxCount' type='u'/>"
+ " <arg direction='in' name='Order' type='s'/>"
+ " <arg direction='in' name='ReverseOrder' type='b'/>"
+ " <arg direction='out' type='a(oss)'/>"
+ " </method>"
+ " <property name='PlaylistCount' type='u' access='read'/>"
+ " <property name='Orderings' type='as' access='read'/>"
+ " <property name='ActivePlaylist' type='(b(oss))' access='read'/>"
+ " </interface>"
+ "</node>";
diff --git a/src/plugins/mpris/mpris.plugin.desktop.in b/src/plugins/mpris/mpris.plugin.desktop.in
new file mode 100644
index 000000000..e50c3991d
--- /dev/null
+++ b/src/plugins/mpris/mpris.plugin.desktop.in
@@ -0,0 +1,9 @@
+[Plugin]
+Module=mpris
+IAge=1
+Name=MPRIS D-Bus Interface
+Description=Send notifications of currently-playing videos and allow remote control using MPRIS.
+Builtin=true
+Authors=Bastien Nocera
+Copyright=Copyright © 2022 Bastien Nocera
+Website=https://wiki.gnome.org/Apps/Videos
diff --git a/src/plugins/mpris/totem-mpris.c b/src/plugins/mpris/totem-mpris.c
new file mode 100644
index 000000000..529b1443f
--- /dev/null
+++ b/src/plugins/mpris/totem-mpris.c
@@ -0,0 +1,717 @@
+/*
+ * Copyright (C) 2010-2014, 2016, 2020-2021 Jonathan Matthew <jonathan@d14n.org>
+ * Copyright (C) 2022 Bastien Nocera <hadess@hadess.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ *
+ * See license_change file for details.
+ *
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n-lib.h>
+#include <gmodule.h>
+#include <libpeas/peas-extension-base.h>
+#include <libpeas/peas-object-module.h>
+#include <libpeas/peas-activatable.h>
+#include <string.h>
+
+#include "totem-plugin.h"
+#include "totem.h"
+#include "mpris-spec.h"
+
+#define TOTEM_TYPE_MPRIS_PLUGIN (totem_mpris_plugin_get_type ())
+#define TOTEM_MPRIS_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TOTEM_TYPE_MPRIS_PLUGIN, TotemMprisPlugin))
+
+typedef struct {
+ PeasExtensionBase parent;
+
+ GDBusConnection *connection;
+ GDBusNodeInfo *node_info;
+ guint name_own_id;
+ guint root_id;
+ guint player_id;
+
+ TotemObject *totem;
+
+ GHashTable *player_property_changes;
+ gboolean emit_seeked;
+ guint property_emit_id;
+
+ char *current_mrl;
+ gint64 last_position;
+
+ GHashTable *metadata; /* key: str, value: str */
+ guint32 track_number;
+} TotemMprisPlugin;
+
+TOTEM_PLUGIN_REGISTER(TOTEM_TYPE_MPRIS_PLUGIN, TotemMprisPlugin, totem_mpris_plugin);
+
+static void
+emit_property_changes (TotemMprisPlugin *pi, GHashTable *changes, const char *interface)
+{
+ GError *error = NULL;
+ GVariantBuilder *properties;
+ GVariantBuilder *invalidated;
+ GVariant *parameters;
+ gpointer propname, propvalue;
+ GHashTableIter iter;
+
+ properties = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
+ invalidated = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+ g_hash_table_iter_init (&iter, changes);
+ while (g_hash_table_iter_next (&iter, &propname, &propvalue)) {
+ if (propvalue != NULL) {
+ g_variant_builder_add (properties,
+ "{sv}",
+ propname,
+ propvalue);
+ } else {
+ g_variant_builder_add (invalidated, "s", propname);
+ }
+
+ }
+
+ parameters = g_variant_new ("(sa{sv}as)",
+ interface,
+ properties,
+ invalidated);
+ g_variant_builder_unref (properties);
+ g_variant_builder_unref (invalidated);
+ g_dbus_connection_emit_signal (pi->connection,
+ NULL,
+ MPRIS_OBJECT_NAME,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ parameters,
+ &error);
+ if (error != NULL) {
+ g_warning ("Unable to send MPRIS property changes for %s: %s",
+ interface, error->message);
+ g_clear_error (&error);
+ }
+
+}
+
+static gboolean
+emit_properties_idle (TotemMprisPlugin *pi)
+{
+ if (pi->player_property_changes != NULL) {
+ emit_property_changes (pi, pi->player_property_changes, MPRIS_PLAYER_INTERFACE);
+ g_hash_table_destroy (pi->player_property_changes);
+ pi->player_property_changes = NULL;
+ }
+
+ if (pi->emit_seeked) {
+ GError *error = NULL;
+ g_debug ("emitting Seeked; new time %" G_GINT64_FORMAT, pi->last_position/1000);
+ g_dbus_connection_emit_signal (pi->connection,
+ NULL,
+ MPRIS_OBJECT_NAME,
+ MPRIS_PLAYER_INTERFACE,
+ "Seeked",
+ g_variant_new ("(x)", pi->last_position / 1000),
+ &error);
+ if (error != NULL) {
+ g_warning ("Unable to set MPRIS Seeked signal: %s", error->message);
+ g_clear_error (&error);
+ }
+ pi->emit_seeked = 0;
+ }
+ pi->property_emit_id = 0;
+ return FALSE;
+}
+
+static void
+add_player_property_change (TotemMprisPlugin *pi,
+ const char *property,
+ GVariant *value)
+{
+ if (pi->player_property_changes == NULL) {
+ pi->player_property_changes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
+ }
+ g_hash_table_insert (pi->player_property_changes, g_strdup (property), g_variant_ref_sink (value));
+
+ if (pi->property_emit_id == 0) {
+ pi->property_emit_id = g_idle_add ((GSourceFunc)emit_properties_idle, pi);
+ }
+}
+
+/* MPRIS root interface */
+
+static void
+handle_root_method_call (GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ TotemMprisPlugin *pi)
+{
+ if (g_strcmp0 (object_path, MPRIS_OBJECT_NAME) != 0 ||
+ g_strcmp0 (interface_name, MPRIS_ROOT_INTERFACE) != 0) {
+ g_dbus_method_invocation_return_error (invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Method %s.%s not supported",
+ interface_name,
+ method_name);
+ return;
+ }
+
+ if (g_strcmp0 (method_name, "Raise") == 0) {
+ GtkWindow *window = totem_object_get_main_window (pi->totem);
+ gtk_window_present (window);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Quit") == 0) {
+ totem_object_exit (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else {
+ g_dbus_method_invocation_return_error (invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Method %s.%s not supported",
+ interface_name,
+ method_name);
+ }
+}
+
+static GVariant *
+get_root_property (GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *property_name,
+ GError **error,
+ TotemMprisPlugin *pi)
+{
+ if (g_strcmp0 (object_path, MPRIS_OBJECT_NAME) != 0 ||
+ g_strcmp0 (interface_name, MPRIS_ROOT_INTERFACE) != 0) {
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Property %s.%s not supported",
+ interface_name,
+ property_name);
+ return NULL;
+ }
+
+ if (g_strcmp0 (property_name, "CanQuit") == 0) {
+ return g_variant_new_boolean (TRUE);
+ } else if (g_strcmp0 (property_name, "CanRaise") == 0) {
+ return g_variant_new_boolean (TRUE);
+ } else if (g_strcmp0 (property_name, "HasTrackList") == 0) {
+ return g_variant_new_boolean (FALSE);
+ } else if (g_strcmp0 (property_name, "Identity") == 0) {
+ return g_variant_new_string ("Videos");
+ } else if (g_strcmp0 (property_name, "DesktopEntry") == 0) {
+ return g_variant_new_string ("org.gnome.Totem");
+ } else if (g_strcmp0 (property_name, "SupportedUriSchemes") == 0) {
+ return g_variant_new_strv (totem_object_get_supported_uri_schemes (), -1);
+ } else if (g_strcmp0 (property_name, "SupportedMimeTypes") == 0) {
+ return g_variant_new_strv (totem_object_get_supported_content_types (), -1);
+ }
+
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Property %s.%s not supported",
+ interface_name,
+ property_name);
+ return NULL;
+}
+
+static const GDBusInterfaceVTable root_vtable =
+{
+ (GDBusInterfaceMethodCallFunc) handle_root_method_call,
+ (GDBusInterfaceGetPropertyFunc) get_root_property,
+ NULL
+};
+
+/* MPRIS player interface */
+
+const char *str_metadata[] = {
+ "xesam:title",
+ "xesam:artist",
+ "xesam:album",
+};
+
+static void
+calculate_metadata (TotemMprisPlugin *pi,
+ GVariantBuilder *builder)
+{
+ guint i;
+ gint64 stream_length;
+
+ g_object_get (G_OBJECT (pi->totem), "stream-length", &stream_length, NULL);
+
+ g_variant_builder_add (builder,
+ "{sv}",
+ "mpris:length",
+ g_variant_new_int64 (stream_length));
+ g_variant_builder_add (builder,
+ "{sv}",
+ "xesam:trackNumber",
+ g_variant_new_uint32 (pi->track_number));
+ for (i = 0; i < G_N_ELEMENTS (str_metadata); i++) {
+ const char *str;
+
+ str = g_hash_table_lookup (pi->metadata, str_metadata[i]);
+ if (!str)
+ continue;
+ g_variant_builder_add (builder,
+ "{sv}",
+ str_metadata[i],
+ g_variant_new_string (str));
+ }
+}
+
+static void
+handle_player_method_call (GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ TotemMprisPlugin *pi)
+
+{
+ if (g_strcmp0 (object_path, MPRIS_OBJECT_NAME) != 0 ||
+ g_strcmp0 (interface_name, MPRIS_PLAYER_INTERFACE) != 0) {
+ g_dbus_method_invocation_return_error (invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Method %s.%s not supported",
+ interface_name,
+ method_name);
+ return;
+ }
+
+ if (g_strcmp0 (method_name, "Next") == 0) {
+ totem_object_seek_next (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Previous") == 0) {
+ totem_object_seek_previous (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Pause") == 0) {
+ totem_object_pause (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "PlayPause") == 0) {
+ totem_object_play_pause (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Stop") == 0) {
+ totem_object_stop (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Play") == 0) {
+ totem_object_play (pi->totem);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "Seek") == 0) {
+ gint64 offset;
+ g_variant_get (parameters, "(x)", &offset);
+ totem_object_seek_relative (pi->totem, offset / 1000, FALSE);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "SetPosition") == 0) {
+ gint64 position, stream_length;
+ const char *client_entry_path;
+
+ g_variant_get (parameters, "(&ox)", &client_entry_path, &position);
+ position /= 1000;
+ g_object_get (G_OBJECT (pi->totem), "stream-length", &stream_length, NULL);
+
+ if (position < 0 || position > stream_length) {
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ return;
+ }
+
+ totem_object_seek_time (pi->totem, position, FALSE);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else if (g_strcmp0 (method_name, "OpenUri") == 0) {
+ const char *uri;
+
+ g_variant_get (parameters, "(&s)", &uri);
+ totem_object_add_to_playlist (pi->totem, uri, NULL, TRUE);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else {
+ g_dbus_method_invocation_return_error (invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Method %s.%s not supported",
+ interface_name,
+ method_name);
+ }
+}
+
+static GVariant *
+calculate_playback_status (TotemMprisPlugin *pi)
+{
+ if (totem_object_is_playing (pi->totem))
+ return g_variant_new_string ("Playing");
+ else if (totem_object_is_paused (pi->totem))
+ return g_variant_new_string ("Paused");
+ return g_variant_new_string ("Stopped");
+}
+
+static GVariant *
+calculate_loop_status (TotemMprisPlugin *pi)
+{
+ if (totem_object_remote_get_setting (pi->totem, TOTEM_REMOTE_SETTING_REPEAT))
+ return g_variant_new_string ("Playlist");
+ return g_variant_new_string ("None");
+}
+
+static GVariant *
+calculate_can_seek (TotemMprisPlugin *pi)
+{
+ return g_variant_new_boolean (pi->current_mrl != NULL &&
+ totem_object_is_seekable (pi->totem));
+}
+
+static GVariant *
+get_player_property (GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *property_name,
+ GError **error,
+ TotemMprisPlugin *pi)
+{
+ if (g_strcmp0 (object_path, MPRIS_OBJECT_NAME) != 0 ||
+ g_strcmp0 (interface_name, MPRIS_PLAYER_INTERFACE) != 0) {
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Property %s.%s not supported",
+ interface_name,
+ property_name);
+ return NULL;
+ }
+
+ if (g_strcmp0 (property_name, "PlaybackStatus") == 0) {
+ return calculate_playback_status (pi);
+ } else if (g_strcmp0 (property_name, "LoopStatus") == 0) {
+ return calculate_loop_status (pi);
+ } else if (g_strcmp0 (property_name, "Rate") == 0) {
+ return g_variant_new_double (totem_object_get_rate (pi->totem));
+ } else if (g_strcmp0 (property_name, "Metadata") == 0) {
+ GVariantBuilder *builder;
+ GVariant *v;
+
+ builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
+ calculate_metadata (pi, builder);
+ v = g_variant_builder_end (builder);
+ g_variant_builder_unref (builder);
+ return v;
+ } else if (g_strcmp0 (property_name, "Volume") == 0) {
+ return g_variant_new_double (totem_object_get_volume (pi->totem));
+ } else if (g_strcmp0 (property_name, "Position") == 0) {
+ return g_variant_new_int64 (totem_object_get_current_time (pi->totem) * 1000);
+ } else if (g_strcmp0 (property_name, "MinimumRate") == 0) {
+ return g_variant_new_double (0.75);
+ } else if (g_strcmp0 (property_name, "MaximumRate") == 0) {
+ return g_variant_new_double (1.75);
+ } else if (g_strcmp0 (property_name, "CanGoNext") == 0) {
+ return g_variant_new_boolean (totem_object_can_seek_next (pi->totem));
+ } else if (g_strcmp0 (property_name, "CanGoPrevious") == 0) {
+ return g_variant_new_boolean (totem_object_can_seek_previous (pi->totem));
+ } else if (g_strcmp0 (property_name, "CanPlay") == 0) {
+ return g_variant_new_boolean (pi->current_mrl != NULL);
+ } else if (g_strcmp0 (property_name, "CanPause") == 0) {
+ return g_variant_new_boolean (pi->current_mrl != NULL);
+ } else if (g_strcmp0 (property_name, "CanSeek") == 0) {
+ return calculate_can_seek (pi);
+ } else if (g_strcmp0 (property_name, "CanControl") == 0) {
+ return g_variant_new_boolean (TRUE);
+ }
+
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Property %s.%s not supported",
+ interface_name,
+ property_name);
+ return NULL;
+}
+
+static gboolean
+set_player_property (GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *property_name,
+ GVariant *value,
+ GError **error,
+ TotemMprisPlugin *pi)
+{
+ if (g_strcmp0 (object_path, MPRIS_OBJECT_NAME) != 0 ||
+ g_strcmp0 (interface_name, MPRIS_PLAYER_INTERFACE) != 0) {
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "%s:%s not supported",
+ object_path,
+ interface_name);
+ return FALSE;
+ }
+
+ if (g_strcmp0 (property_name, "LoopStatus") == 0) {
+ const char *status;
+
+ status = g_variant_get_string (value, NULL);
+ totem_object_remote_set_setting (pi->totem, TOTEM_REMOTE_SETTING_REPEAT,
+ g_strcmp0 (status, "Playlist") == 0);
+ return TRUE;
+ } else if (g_strcmp0 (property_name, "Rate") == 0) {
+ totem_object_set_rate (pi->totem, g_variant_get_double (value));
+ return TRUE;
+ } else if (g_strcmp0 (property_name, "Volume") == 0) {
+ totem_object_set_volume (pi->totem, g_variant_get_double (value));
+ return TRUE;
+ }
+
+ g_set_error (error,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NOT_SUPPORTED,
+ "Property %s.%s not supported",
+ interface_name,
+ property_name);
+ return FALSE;
+}
+
+static const GDBusInterfaceVTable player_vtable =
+{
+ (GDBusInterfaceMethodCallFunc) handle_player_method_call,
+ (GDBusInterfaceGetPropertyFunc) get_player_property,
+ (GDBusInterfaceSetPropertyFunc) set_player_property,
+};
+
+static void
+playing_changed_cb (TotemObject *totem, GParamSpec *pspec, TotemMprisPlugin *pi)
+{
+ g_debug ("emitting PlaybackStatus change");
+ add_player_property_change (pi, "PlaybackStatus", calculate_playback_status (pi));
+}
+
+static void
+seekable_changed_cb (TotemObject *totem, GParamSpec *pspec, TotemMprisPlugin *pi)
+{
+ g_debug ("emitting CanSeek change");
+ add_player_property_change (pi, "CanSeek", calculate_can_seek (pi));
+}
+
+static void
+metadata_updated_cb (TotemObject *totem,
+ const char *artist,
+ const char *title,
+ const char *album,
+ guint32 track_number,
+ TotemMprisPlugin *pi)
+{
+ GVariantBuilder *builder;
+
+ g_hash_table_insert (pi->metadata, "xesam:artist", g_strdup (artist));
+ g_hash_table_insert (pi->metadata, "xesam:title", g_strdup (title));
+ g_hash_table_insert (pi->metadata, "xesam:album", g_strdup (album));
+ pi->track_number = track_number;
+
+ builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
+ calculate_metadata (pi, builder);
+ add_player_property_change (pi, "Metadata", g_variant_builder_end (builder));
+ g_variant_builder_unref (builder);
+}
+
+static void
+time_changed_cb (TotemObject *totem, GParamSpec *pspec, TotemMprisPlugin *pi)
+{
+ gint64 position;
+
+ position = totem_object_get_current_time (pi->totem);
+ /* Only notify of seeks if we've skipped more than 3 seconds */
+ if (ABS (position - pi->last_position) < 3) {
+ pi->last_position = position;
+ return;
+ }
+
+ if (pi->property_emit_id == 0) {
+ pi->property_emit_id = g_idle_add ((GSourceFunc)emit_properties_idle, pi);
+ }
+ pi->emit_seeked = TRUE;
+ pi->last_position = position;
+}
+
+static void
+mrl_changed_cb (TotemObject *totem, GParamSpec *pspec, TotemMprisPlugin *pi)
+{
+ g_clear_pointer (&pi->current_mrl, g_free);
+ pi->current_mrl = totem_object_get_current_mrl (totem);
+
+ add_player_property_change (pi, "CanPlay",
+ g_variant_new_boolean (pi->current_mrl != NULL));
+ add_player_property_change (pi, "CanPause",
+ g_variant_new_boolean (pi->current_mrl != NULL));
+ add_player_property_change (pi, "CanSeek", calculate_can_seek (pi));
+ add_player_property_change (pi, "CanGoNext",
+ g_variant_new_boolean (totem_object_can_seek_next (pi->totem)));
+ add_player_property_change (pi, "CanGoPrevious",
+ g_variant_new_boolean (totem_object_can_seek_previous (pi->totem)));
+}
+
+static void
+name_acquired_cb (GDBusConnection *connection, const char *name, TotemMprisPlugin *pi)
+{
+ g_debug ("successfully acquired dbus name %s", name);
+}
+
+static void
+name_lost_cb (GDBusConnection *connection, const char *name, TotemMprisPlugin *pi)
+{
+ g_debug ("lost dbus name %s", name);
+}
+
+static void
+impl_activate (PeasActivatable *plugin)
+{
+ TotemMprisPlugin *pi = TOTEM_MPRIS_PLUGIN (plugin);
+ GDBusInterfaceInfo *ifaceinfo;
+ g_autoptr(GError) error = NULL;
+
+ pi->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (!pi->connection) {
+ g_warning ("Unable to connect to D-Bus session bus: %s", error->message);
+ return;
+ }
+
+ pi->node_info = g_dbus_node_info_new_for_xml (mpris_introspection_xml, &error);
+ if (error != NULL) {
+ g_warning ("Unable to read MPRIS interface specificiation: %s", error->message);
+ return;
+ }
+
+ /* register root interface */
+ ifaceinfo = g_dbus_node_info_lookup_interface (pi->node_info, MPRIS_ROOT_INTERFACE);
+ pi->root_id = g_dbus_connection_register_object (pi->connection,
+ MPRIS_OBJECT_NAME,
+ ifaceinfo,
+ &root_vtable,
+ plugin,
+ NULL,
+ &error);
+ if (error != NULL) {
+ g_warning ("unable to register MPRIS root interface: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ /* register player interface */
+ ifaceinfo = g_dbus_node_info_lookup_interface (pi->node_info, MPRIS_PLAYER_INTERFACE);
+ pi->player_id = g_dbus_connection_register_object (pi->connection,
+ MPRIS_OBJECT_NAME,
+ ifaceinfo,
+ &player_vtable,
+ plugin,
+ NULL,
+ &error);
+ if (error != NULL) {
+ g_warning ("Unable to register MPRIS player interface: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ pi->totem = g_object_get_data (G_OBJECT (plugin), "object");
+
+ /* connect signal handlers for stuff */
+ g_signal_connect_object (pi->totem,
+ "metadata-updated",
+ G_CALLBACK (metadata_updated_cb),
+ plugin, 0);
+ g_signal_connect_object (pi->totem,
+ "notify::playing",
+ G_CALLBACK (playing_changed_cb),
+ plugin, 0);
+ g_signal_connect_object (pi->totem,
+ "notify::seekable",
+ G_CALLBACK (seekable_changed_cb),
+ plugin, 0);
+ g_signal_connect_object (pi->totem,
+ "notify::current-mrl",
+ G_CALLBACK (mrl_changed_cb),
+ plugin, 0);
+ g_signal_connect_object (pi->totem,
+ "notify::current-time",
+ G_CALLBACK (time_changed_cb),
+ plugin, 0);
+
+ pi->name_own_id = g_bus_own_name (G_BUS_TYPE_SESSION,
+ MPRIS_BUS_NAME_PREFIX ".totem",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ NULL,
+ (GBusNameAcquiredCallback) name_acquired_cb,
+ (GBusNameLostCallback) name_lost_cb,
+ g_object_ref (pi),
+ g_object_unref);
+
+ pi->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+ pi->current_mrl = totem_object_get_current_mrl (pi->totem);
+}
+
+static void
+impl_deactivate (PeasActivatable *plugin)
+{
+ TotemMprisPlugin *pi = TOTEM_MPRIS_PLUGIN (plugin);
+ TotemObject *totem;
+
+ if (pi->root_id != 0) {
+ g_dbus_connection_unregister_object (pi->connection, pi->root_id);
+ pi->root_id = 0;
+ }
+ if (pi->player_id != 0) {
+ g_dbus_connection_unregister_object (pi->connection, pi->player_id);
+ pi->player_id = 0;
+ }
+
+ g_clear_handle_id (&pi->property_emit_id, g_source_remove);
+ g_clear_pointer (&pi->player_property_changes, g_hash_table_destroy);
+ g_clear_pointer (&pi->current_mrl, g_free);
+ g_clear_pointer (&pi->metadata, g_hash_table_destroy);
+
+ totem = g_object_get_data (G_OBJECT (plugin), "object");
+ if (totem != NULL) {
+ g_signal_handlers_disconnect_by_func (totem,
+ G_CALLBACK (metadata_updated_cb),
+ plugin);
+ g_signal_handlers_disconnect_by_func (totem,
+ G_CALLBACK (playing_changed_cb),
+ plugin);
+ g_signal_handlers_disconnect_by_func (totem,
+ G_CALLBACK (seekable_changed_cb),
+ plugin);
+ g_signal_handlers_disconnect_by_func (totem,
+ G_CALLBACK (mrl_changed_cb),
+ plugin);
+ g_signal_handlers_disconnect_by_func (totem,
+ G_CALLBACK (time_changed_cb),
+ plugin);
+ }
+ g_clear_handle_id (&pi->name_own_id, g_bus_unown_name);
+ g_clear_pointer (&pi->node_info, g_dbus_node_info_unref);
+ g_clear_object (&pi->connection);
+}