diff options
author | Bastien Nocera <hadess@hadess.net> | 2021-10-18 12:22:14 +0200 |
---|---|---|
committer | Bastien Nocera <hadess@hadess.net> | 2022-02-21 18:27:05 +0100 |
commit | df1289cf4701a5219c7cb5d90862580bcb357710 (patch) | |
tree | a96d6790f7275c0c112fdc6149ef6d0487ddf3dc | |
parent | 449d6a2a94ce0ae592c40620c3c0fb14815e016a (diff) | |
download | totem-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.in | 2 | ||||
-rw-r--r-- | src/plugins/meson.build | 2 | ||||
-rw-r--r-- | src/plugins/mpris/meson.build | 26 | ||||
-rw-r--r-- | src/plugins/mpris/mpris-spec.h | 107 | ||||
-rw-r--r-- | src/plugins/mpris/mpris.plugin.desktop.in | 9 | ||||
-rw-r--r-- | src/plugins/mpris/totem-mpris.c | 717 |
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); +} |