/* * Copyright (c) 2011- Osmo Antero. * * 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"OK * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Library General Public License 3 for more details. * * You should have received a copy of the GNU Library General Public * License 3 along with this program; if not, see /usr/share/common-licenses/GPL file * or . */ #include #include #include #include #include "log.h" #include "utility.h" #include "support.h" // _(x) #include "dbus-player.h" #include "dbus-mpris2.h" // This is a MPRIS2 (org.mpris.MediaPlayer2) compliant media-player interface. // Client side implementation. // Please see: https://specifications.freedesktop.org/mpris-spec/latest/ // Glib/DBus connection static GDBusConnection *g_dbus_conn = NULL; static GDBusConnection *mpris2_connect_to_dbus(); static void mpris2_disconnect_from_dbus(); static gboolean mpris2_check_proxy(MediaPlayerRec *player); static GVariant *mpris2_get_property(MediaPlayerRec *player, gchar *prop_name); GVariant *mpris2_get_player_value(gpointer player_rec, gchar *variable); static gchar *get_string_val(GVariant *v); void mpris2_module_init() { LOG_DEBUG("Init dbus_mpris2.c.\n"); g_dbus_conn = NULL; } void mpris2_module_exit() { LOG_DEBUG("Clean up dbus_mpris2.c.\n"); mpris2_disconnect_from_dbus(); } GDBusConnection *mpris2_connect_to_dbus() { // Connect to glib/DBus GError *error = NULL; if (!G_IS_DBUS_CONNECTION(g_dbus_conn)) { g_dbus_conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); if (!G_IS_DBUS_CONNECTION(g_dbus_conn)) { LOG_ERROR("mpris2_connect_to_dbus: Cannot connect to DBus: %s\n", error ? error->message : ""); if (error) g_error_free(error); return NULL; } } return g_dbus_conn; } void mpris2_disconnect_from_dbus() { // Disconnect from glib/DBus if (G_IS_DBUS_CONNECTION(g_dbus_conn)) { g_object_unref(g_dbus_conn); } g_dbus_conn = NULL; } void debug_hash_table(MediaPlayerRec *player, GHashTable *table) { LOG_PLAYER("------------------------\n"); } MediaPlayerRec *mpris2_player_new(const gchar *service_name) { // New MediaPlayerRec record MediaPlayerRec *player = g_malloc0(sizeof(MediaPlayerRec)); player->service_name = g_strdup(service_name); player->app_name = NULL; return player; } static gboolean mpris2_check_proxy(MediaPlayerRec *player) { // Create and return proxy for player->service_name so we can read it's Player properties. // Please see: https://specifications.freedesktop.org/mpris-spec/latest/ if (!player) return FALSE; // Already created? if (G_IS_DBUS_PROXY(player->proxy)) { return TRUE; } GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); // Proxy that points to "org.mpris.MediaPlayer2.Player" object. GError *error = NULL; player->proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, player->service_name, /* name */ "/org/mpris/MediaPlayer2", /* object path */ "org.mpris.MediaPlayer2.Player", /* interface */ NULL, &error); if (error) { g_printerr("Cannot create proxy for %s. %s.\n", player->service_name, error->message); g_error_free(error); player->proxy = NULL; } return (G_IS_DBUS_PROXY(player->proxy)); } gchar *mpris2_get_property_str(MediaPlayerRec *player, gchar *prop_name) { // Read a string property from the player's "org.mpris.MediaPlayer2" interface. GVariant *result = mpris2_get_property(player, prop_name); if (!result) { return NULL; } GVariant *peek = result; // Is "(v)"? if (g_variant_is_container(result)) { // Peek to "v" g_variant_get(result, "(v)", &peek); } gchar *s = NULL; g_variant_get(peek, "s", &s); g_variant_unref(result); result = NULL; g_variant_unref(peek); peek = NULL; // Return a string. // Caller should g_free() this value. return s; } static GVariant *mpris2_get_property(MediaPlayerRec *player, gchar *prop_name) { // Read a property from the player's "org.mpris.MediaPlayer2" interface. if (!player) return FALSE; GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); // Proxy that points to "org.mpris.MediaPlayer2" object. GError *error = NULL; GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, player->service_name, /* service name */ "/org/mpris/MediaPlayer2", /* object path */ "org.mpris.MediaPlayer2", /* base interface */ NULL, &error); if (error) { g_printerr("Cannot create proxy for %s. %s.\n", player->service_name, error->message); g_error_free(error); return NULL; } // List of valid properties: // https://specifications.freedesktop.org/mpris-spec/latest/ // Read value for prop_name. // I have earlier used the g_dbus_proxy_get_cached_property() function, but it returns NULL // values for many media-players. // GVariant *result = g_dbus_proxy_get_cached_property(proxy, prop_name); // This should work right. error = NULL; GVariant *result = g_dbus_proxy_call_sync(proxy, "org.freedesktop.DBus.Properties.Get", g_variant_new("(ss)", "org.mpris.MediaPlayer2", prop_name), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (error) { g_error_free(error); result = NULL; } // Unref proxy g_object_unref(proxy); // Caller should unref this value. return result; } static void mpris2_signal(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data) { // Handle "g-signal" signal messages. MediaPlayerRec *player = (MediaPlayerRec*)user_data; if (!player) return; // Got "PropertiesChanged" signal? if (g_strcmp0(signal_name, "PropertiesChanged")) return; // Data or properties changed. /* *** Sample datasets for the PropertiesChanged signal: Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'PlaybackStatus': <'Playing'>}, @as []) Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'Volume': <1.0>}, @as []) Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'CanGoNext': , 'Metadata': < {'mpris:trackid': <'/org/mpris/MediaPlayer2/Track/9'>, 'xesam:url': <'file:///home/moma/Music/Bruce% 20Springsteen%20-%20%20Wrecking%20Ball%20%5Bmp3-256-2012%5D/03%20-%20Shackled%20And%20Drawn.mp3'>, 'xesam:title': <'Shackled And Drawn'>, 'xesam:artist': <['Bruce Springsteen']>, 'xesam:album': <'Wrecking Ball'>, 'xesam:genre': <['Rock']>, 'xesam:albumArtist': <['Bruce Springsteen']>, 'xesam:audioBitrate'<262144>, 'xesam:contentCreated': <'2012-01-01T00:00:00Z'>, 'mpris:length': , 'xesam:trackNumber': <3>, 'xesam:discNumber': <1>, 'xesam:useCount': <0>, 'xesam:userRating': <0.0>, 'mpris:artUrl': < 'file:///home/moma/.cache/rhythmbox/album-art/00000098'>}>, 'CanSeek': , 'CanGoPrevious': , 'PlaybackStatus': <'Playing'>}, @as []) Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'PlaybackStatus': <'Paused'>}, @as []) *** */ // Debug: #if defined(DEBUG_PLAYER) || defined(DEBUG_ALL) gchar *str = g_variant_print(parameters, TRUE); LOG_PLAYER("g-signal: Received %s signal from %s.\n", signal_name, player->service_name); LOG_PLAYER("Raw data is:%s\n\n", str); g_free(str); #endif // What kind of message (or messages) we got for the media player? gboolean got_metadata = FALSE; // "Metadata" message gboolean got_playbackstatus = FALSE; // "PlaybackStatus" message gchar *playbackstatus_val = NULL; // "Playing", "Stopped", "Paused" gboolean got_volume = FALSE; // "Volume" up/down message // For other messages, please see // https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html for (gint i = 0; i < g_variant_n_children(parameters); ++i) { GVariant *child = g_variant_get_child_value(parameters, i); GVariantIter *iter = NULL; GVariant *value = NULL; const gchar *skey = NULL; // Check if we got array of "Metadata" values (dictionary) of type "{sv}" if (g_variant_is_of_type (child, (const GVariantType *) "a{sv}")) { g_variant_get(child, "a{sv}", &iter); while (g_variant_iter_loop (iter, "{sv}", &skey, &value)) { gchar *sdown = g_utf8_strdown(skey, MPRIS_STRLEN); // Got "Metadata" ? if (!g_strcmp0(sdown, "metadata") ) { got_metadata = TRUE; // Got "PlaybackStatus" ? } else if (!g_strcmp0(sdown, "playbackstatus") ) { got_playbackstatus = TRUE; // Read its value playbackstatus_val = get_string_val(value); // Got "Volume" ? } else if (!g_strcmp0(sdown, "volume") ) { got_volume = TRUE; } g_free(sdown); } } else if (g_variant_is_of_type (child, (const GVariantType *) "as")) { // Special case: This is just in case someone (media player) sends an array of strings of type "as", instead of "a{sv}". g_variant_get(child, "as", &iter); while (g_variant_iter_loop (iter, "s", &skey, NULL)) { gchar *sdown = g_utf8_strdown(skey, MPRIS_STRLEN); // Got "Metadata" ? if (!g_strcmp0(sdown, "metadata") ) { got_metadata = TRUE; // Got "PlaybackStatus" ? } else if (!g_strcmp0(sdown, "playbackstatus") ) { got_playbackstatus = TRUE; //playbackstatus_val = ??? // Got "Volume" ? } else if (!g_strcmp0(sdown, "volume") ) { got_volume = TRUE; } g_free(sdown); } } if (child) { g_variant_unref(child); } if (iter) { g_variant_iter_free (iter); } } // Got sound Volume up/down signal? if (got_volume) { ; // ATM, we do nothing LOG_PLAYER("Received \"Volume\" data from the media player\n"); } // Got PlaybackStatus? if (got_playbackstatus) { LOG_PLAYER("Received \"PlaybackStatus\" message from the media player. Value=\"%s\"\n", playbackstatus_val); TrackInfo *tr = &player->track; memset(tr, '\0', sizeof(TrackInfo)); tr->status = PLAYER_STATUS_CLOSED; if (g_strcmp0(playbackstatus_val, "Stopped") == 0) { tr->status = PLAYER_STATUS_STOPPED; } else if (g_strcmp0(playbackstatus_val, "Paused") == 0) { tr->status = PLAYER_STATUS_PAUSED; } else if (str_length0(playbackstatus_val) > 0) { // playbackstatus_val == "Playing" // tr->status = PLAYER_STATUS_PLAYING; // Re-read data so we get all necessary values. mpris2_get_metadata(player); } // Send data to the queue (in rec-manager.c) dbus_player_process_data(player, (tr->status == PLAYER_STATUS_PLAYING /* restart the recording? */)); // No need to re-re-read Metdata below got_metadata = FALSE; } // Got Metadata? if (got_metadata) { LOG_PLAYER("Received \"Metadata\" data from the media player\n"); // Re-read data so we get all necessary values. mpris2_get_metadata(player); //Debug: //dbus_player_debug_print(player); // Send data to the queue (in rec-manager.c) dbus_player_process_data(player, (TRUE /* restart the recording? */)); } g_free(playbackstatus_val); } void mpris2_set_signals(gpointer player_rec, gboolean do_connect) { // Connect/disconnct signals for this player. MediaPlayerRec *player = (MediaPlayerRec*)player_rec; // Connect event signals if (do_connect) { GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); GError *error = NULL; // Proxy for org.freedesktop.DBus.Properties player->prop_proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, player->service_name, /* name */ "/org/mpris/MediaPlayer2", /* object path */ "org.freedesktop.DBus.Properties", /* interface */ NULL, &error); if (error) { g_printerr("Cannot create proxy for org.freedesktop.DBus.Properties. %s.\n", error->message); g_error_free(error); player->prop_proxy = NULL; return; } // Ref: https://developer.gnome.org/gio/2.28/GDBusProxy.html g_signal_connect(player->prop_proxy, "g-properties-changed", G_CALLBACK(mpris2_signal), (gpointer)player/*user data*/); g_signal_connect(player->prop_proxy, "g-signal", G_CALLBACK(mpris2_signal), (gpointer)player/*user data*/); // Disconnect signals } else { // Delete player->prop_proxy. This should unset the above signals. if (G_IS_DBUS_PROXY(player->prop_proxy)) { g_object_unref(player->prop_proxy); } player->prop_proxy = NULL; // Delete also player->proxy if (G_IS_DBUS_PROXY(player->proxy)) { g_object_unref(player->proxy); } player->proxy = NULL; } } gboolean mpris2_service_is_running(gpointer player_rec) { // Check if the application is running. MediaPlayerRec *player = (MediaPlayerRec*)player_rec; if (!player_rec) return FALSE; return mpris2_service_is_running_by_name(player->service_name); } gboolean mpris2_service_is_running_by_name(const char *service_name) { // Check if the service_name/application is running. // Return TRUE/FALSE. // Connect to glib/DBus GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); if (!dbus_conn) return FALSE; // Proxy for DBUS_SERVICE_DBUS GError *error = NULL; GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, NULL, &error); if (error) { g_printerr("Cannot create proxy for %s. %s.\n", DBUS_INTERFACE_DBUS, error->message); g_error_free(error); return FALSE; } // Call "NameHasOwner" method error = NULL; GVariant *result = g_dbus_proxy_call_sync(proxy, "NameHasOwner", g_variant_new("(s)", service_name), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (error) { g_printerr("Cannot get NameHasOwner for %s. %s\n", service_name, error->message); g_error_free(error); g_object_unref(proxy); return FALSE; } // The result has format "(boolean,)" // Debug: // gchar *str = g_variant_print(result, TRUE); // LOG_PLAYER("Received data for HasName:<%s>\n", str); // g_free(str); // Take the boolean value gboolean running = FALSE; g_variant_get_child(result, 0, "b", &running); g_variant_unref(result); g_object_unref(proxy); // Return TRUE/FALSE return running; } static gchar *get_array_as_string(GVariant *v, gchar delim) { // Is a simple string (not an array) if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) { // g_variant_get(v, "s", &s); // Duplicate the string and add to list gsize siz; gchar *s = g_variant_dup_string(v, &siz); //list = g_list_append (list, s); //return list; return s; } // Is it a string array "as"? if (!g_variant_is_of_type(v, G_VARIANT_TYPE_STRING_ARRAY)) { return NULL; } // Loop through array of strings // GVariant *value; guint i = 0; GString *ret = g_string_new(NULL); const gchar *s = NULL; GVariantIter iter; g_variant_iter_init(&iter, v); while (g_variant_iter_loop(&iter, "s", &s)) { //gsize siz; //gchar *s =g_variant_dup_string(value, &siz); //gchar *s2 = g_strdup(s); ret = g_string_append(ret, s); // No need to g_free() s i++; // Add delim char (normally '\t' or '\v')? if (i < g_variant_iter_n_children(&iter)) { ret = g_string_append_c(ret, delim); } } // Return the string return g_string_free(ret, FALSE); // Caller should g_free() this value } static gchar *get_string_val(GVariant *v) { // Read and return a string value. The v can be either "s" or "as". // If array then return the first string. gchar *s = NULL; // Is it a string array "as"? if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING_ARRAY)) { g_variant_get_child(v, 0, "s", &s); // Is it a string "s"? } if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) { g_variant_get(v, "s", &s); } return g_strndup(s, MPRIS_STRLEN); // Caller should g_free() this value } static glong get_longint_val(GVariant *v) { glong l = 0L; if (g_variant_is_of_type (v, G_VARIANT_TYPE_UINT64)) l = g_variant_get_uint64(v); else if (g_variant_is_of_type (v, G_VARIANT_TYPE_INT64)) l = g_variant_get_int64(v); else if (g_variant_is_of_type (v, G_VARIANT_TYPE_UINT32)) l = (long)g_variant_get_uint32(v); else if (g_variant_is_of_type (v, G_VARIANT_TYPE_INT32)) l = (long)g_variant_get_int32(v); else l = (long)g_variant_get_uint32(v); return l; } GVariant *mpris2_get_player_value(gpointer player_rec, gchar *variable) { // Read value (named by variable) from org.mpris.MediaPlayer2.Player object. // Ref: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html MediaPlayerRec *player = (MediaPlayerRec*)player_rec; if (!player) return NULL; // Connect to glib/DBus GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); if (!dbus_conn) return NULL; // Proxy that points to "org.mpris.MediaPlayer2.Player" // Ref: https://specifications.freedesktop.org/mpris-spec/latest/ if (!mpris2_check_proxy(player)) { return NULL; } GError *error = NULL; GVariant *res = g_dbus_proxy_call_sync(player->proxy, "org.freedesktop.DBus.Properties.Get", g_variant_new("(ss)", "org.mpris.MediaPlayer2.Player", variable), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (error) { g_error_free(error); return NULL; } return res; } #if 0 void debug_variant(const gchar *tag, GVariant *v) { if (!v) { g_print("%s is NULL.\n", tag); return; } gchar *sval = g_variant_print(v, TRUE); const gchar *stype = g_variant_get_type_string(v); g_print("%s has type:%s and value:%s\n", tag, stype, sval); g_free(sval); } #endif void mpris2_get_metadata(gpointer player_rec) { // Get track information (=metadata) and state for the given media player. // Ref: https://specifications.freedesktop.org/mpris-spec/2.1/Player_Interface.html#Property:Metadata MediaPlayerRec *player = (MediaPlayerRec*)player_rec; if (!player) return; // Reset track info TrackInfo *tr = &player->track; memset(tr, '\0', sizeof(TrackInfo)); tr->status = PLAYER_STATUS_STOPPED; tr->flags = 0; tr->trackLength = -1L; tr->trackPos = -1L; // Proxy that points to "org.mpris.MediaPlayer2.Player" // Ref: https://specifications.freedesktop.org/mpris-spec/2.1/ if (!mpris2_check_proxy(player)) { return; } // Read "PlaybackStatus" from player's "org.mpris.MediaPlayer2.Player" interface // // Test: // $ dbus-send --print-reply --session --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 // org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' // GVariant *result = mpris2_get_player_value(player, "PlaybackStatus"); // DEBUG: debug_variant("PlaybackStatus", result); if (!result) { // Cannot contact player (it has quit)? tr->status = PLAYER_STATUS_CLOSED; return; } // So result != NULL. // Notice: The MPRIS2-standard defines result as "v" (variant that contains one string). // I think some players return a container type "(v)". GVariant *peek = result; // Is "(v)"? if (g_variant_is_container(result)) { // Peek to "v" g_variant_get(result, "(v)", &peek); } gchar *s = NULL; g_variant_get(peek, "s", &s); // Set tr->status if (!g_ascii_strcasecmp(s, "Playing")) tr->status = PLAYER_STATUS_PLAYING; else if (!g_ascii_strcasecmp(s, "Paused")) tr->status = PLAYER_STATUS_PAUSED; else if (!g_ascii_strcasecmp(s, "Stopped")) tr->status = PLAYER_STATUS_STOPPED; g_variant_unref(result); result = NULL; g_variant_unref(peek); peek = NULL; g_free(s); // Should we continue? if (tr->status != PLAYER_STATUS_PLAYING) { return; } // Here tr->status is PLAYER_STATUS_PLAYING. // Get track info (Metadata) from player's "org.mpris.MediaPlayer2.Player" interface. // The dict has type "a{sv}". // Ref: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Metadata // // Test: // $ dbus-send --print-reply --session --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 // org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'Metadata' // GVariant *dict = mpris2_get_player_value(player, "Metadata"); // DEBUG: debug_variant("Metadata", dict); if (!dict) { // Cannot get Metadata (should we consider this as on error?) // 03.april.2015, commented out by Moma: Ambient Noise Player does not support "Metadata" yet. // tr->status = PLAYER_STATUS_CLOSED; return; } /* Some essential key names for the returned "a{sv}" dictionary: 'mpris:trackid' has type 's' 'xesam:url' has type 's' 'xesam:title' has type 's' 'xesam:artist' has type 'as' 'xesam:genre' has type 'as' 'rhythmbox:streamTitle' has type 's' 'xesam:audioBitrate' has type 'i' 'mpris:length' has type 'x' 'xesam:trackNumber' has type 'i' 'xesam:useCount' has type 'i' 'xesam:userRating' has type 'd' Here is example data from Rhythmbox: ('org.mpris.MediaPlayer2.Player', {'CanSeek': , 'Metadata': <{ 'mpris:trackid': <'/org/mpris/MediaPlayer2/Track/2'>, 'xesam:url': <'file:///home/moma/Music/Bruce Springsteen.mp3'>, 'xesam:title': <'Land Of Hope'>, 'xesam:artist': <['Bruce Springsteen']>, 'xesam:album': <'Wrecking Ball'>, 'xesam:genre': <['Rock']>, 'xesam:albumArtist': <['Bruce Springsteen']>, 'xesam:audioBitrate': <262144>, 'xesam:contentCreated': <'2012-01-01T00:00:00Z'>, 'mpris:length': , 'xesam:trackNumber': <10>, 'xesam:discNumber': <1>, 'xesam:useCount': <0>, 'xesam:userRating': <0.0>, 'mpris:artUrl': <'file:///home/moma/.cache/rhythmbox/album-art/00000098'>}>, 'Volume': <1.0>, 'PlaybackStatus': <'Playing'>}, @as [])" For streams, RhythmBox also sends: rhythmbox:streamTitle': <'Radio City - Oulu'>, Totem movie player sends this (when playing a local file:///): 'mpris:length' (type:x) value:int64 197172000 'mpris:trackid' (type:s) value:'file:///home/moma/Music/Believe%20%20mike%20newman%20mix.mp3' Data from VLC (when playing a local file:///): ('org.mpris.MediaPlayer2.Player', {'Metadata': <{'mpris:trackid': , 'xesam:url': <'file:///home/moma/Music/Believe%20%20mike%20newman%20mix.mp3'>, 'vlc:time': , 'mpris:length': , 'vlc:length': , 'vlc:publisher': <9>}>}, @as []) */ peek = dict; // Is it "(v)" ? if (g_variant_is_container(dict)) { // Peek to "v" g_variant_get(dict, "(v)", &peek); } GVariantIter iter; GVariant *value = NULL; gchar *key = NULL; gchar streamTitle[MPRIS_STRLEN+1]; streamTitle[0] = '\0'; gchar station[MPRIS_STRLEN+1]; station[0] = '\0'; // Ref: https://developer.gnome.org/glib/2.30/glib-GVariant.html#g-variant-get-va g_variant_iter_init(&iter, peek); while (g_variant_iter_next(&iter, "{sv}", &key, &value)) { // Debug #if defined(DEBUG_PLAYER) || defined(DEBUG_ALL) gchar *sval = g_variant_print(value, TRUE); const gchar *stype = g_variant_get_type_string(value); LOG_PLAYER("Metdata key \"%s\" has type:%s and value:%s\n", key, stype, sval); g_free(sval); #endif /* Metdata key "mpris:trackid" has type:s and value:'spotify:track:0WLJgHmiCindTn3QMjyzg6' Metdata key "mpris:length" has type:t and value:uint64 179471000 Metdata key "mpris:artUrl" has type:s and value:'https://open.spotify.com/image/7fba57d7a4de4d20fec9b24d5b2e578db3af80c5' Metdata key "xesam:album" has type:s and value:"The Karaoke Channel - Sing If I Can't Have You Like Yvonne Elliman" Metdata key "xesam:albumArtist" has type:as and value:['The Karaoke Channel'] Metdata key "xesam:artist" has type:as and value:['The Karaoke Channel'] Metdata key "xesam:autoRating" has type:d and value:0.0 Metdata key "xesam:discNumber" has type:i and value:1 Metdata key "xesam:title" has type:s and value:"If I Can't Have You" Metdata key "xesam:trackNumber" has type:i and value:2 Metdata key "xesam:url" has type:s and value:'https://open.spotify.com/track/0WLJgHmiCindTn3QMjyzg6' 'xesam:title': <'Land Of Hope'>, 'xesam:artist': <['Bruce Springsteen']>, 'xesam:album': <'Wrecking Ball'>, 'xesam:genre': <['Rock']>, 'mpris:length': , 'mpris:trackid': <'/org/mpris/MediaPlayer2/Track/2'>, 'xesam:trackNumber': <10>, 'xesam:url': <'file:///home/moma/Music/Bruce Springsteen.mp3'>, 'mpris:artUrl': string 'xesam:albumArtist': <['Bruce Springsteen']>, 'xesam:audioBitrate': <262144>, 'xesam:contentCreated': <'2012-01-01T00:00:00Z'>, 'xesam:discNumber': <1>, 'xesam:useCount': <0>, 'xesam:userRating': <0.0>, */ // xesam:title? if (g_str_has_suffix(key, ":title")) { gchar *s = get_string_val(value); str_copy(tr->title, s, MPRIS_STRLEN); g_free(s); } // xesam::artist? Notice: This has data type "as" (an array of strings) // I suspect that VLC media player sets this to "s" (a string) else if (g_str_has_suffix(key, ":artist")) { gchar *s = get_array_as_string(value, STR_DELIM_CH); str_copy(tr->artist, s, MPRIS_STRLEN); g_free(s); } // xesam:album? else if (g_str_has_suffix(key, ":album")) { gchar *s = get_string_val(value); str_copy(tr->album, s, MPRIS_STRLEN); g_free(s); } // xesam:albumArtist? Notice: This has data type "as" (an array of strings) else if (g_str_has_suffix(key, ":albumArtist")) { gchar *s = get_array_as_string(value, STR_DELIM_CH); str_copy(tr->albumArtist, s, MPRIS_STRLEN); g_free(s); } // xesam::genre? Notice: This has data type "as" (an array of strings) else if (g_str_has_suffix(key, ":genre")) { gchar *s = get_array_as_string(value, STR_DELIM_CH); str_copy(tr->genre, s, MPRIS_STRLEN); g_free(s); } // xesam:url? else if (g_str_has_suffix(key, ":url")) { gchar *s = get_string_val(value); str_copy(tr->url, s, MPRIS_STRLEN); g_free(s); } // mpris:artUrl? else if (g_str_has_suffix(key, ":artUrl")) { gchar *s = get_string_val(value); str_copy(tr->artUrl, s, MPRIS_STRLEN); g_free(s); } // mpris:trackid? else if (g_str_has_suffix(key, ":trackid")) { gchar *s = get_string_val(value); str_copy(tr->trackId, s, MPRIS_STRLEN); g_free(s); } // xesam:trackNumber? else if (g_str_has_suffix(key, ":trackNumber")) { tr->trackNumber = get_longint_val(value); } // mpris:length? (total length of content/stream in microseconds) else if (g_str_has_suffix(key, ":length")) { tr->trackLength = get_longint_val(value); } // xesam:audioBitrate? else if (g_str_has_suffix(key, ":audioBitrate")) { tr->audioBitrate = get_longint_val(value); } // xesam:discNumber? else if (g_str_has_suffix(key, ":discNumber")) { tr->discNumber = get_longint_val(value); } // xesam:contentCreated? else if (g_str_has_suffix(key, ":contentCreated")) { gchar *s = get_string_val(value); str_copy(tr->contentCreated, s, MPRIS_STRLEN); g_free(s); } // rhythmbox:streamTitle? else if (g_str_has_suffix(key, ":streamTitle")) { gchar *s = get_string_val(value); str_copy(streamTitle, s, MPRIS_STRLEN); g_free(s); } // goodvibes:station? else if (g_str_has_suffix(key, ":station")) { gchar *s = get_string_val(value); str_copy(station, s, MPRIS_STRLEN); g_free(s); } // Free value and key g_variant_unref(value); g_free(key); } // Got tr->title? Try streamTitle. if (str_length0(tr->title) < 1) { str_copy(tr->title, streamTitle, MPRIS_STRLEN); } // Got tr->title? Try station name (this could be in tr->album too) if (str_length0(tr->title) < 1) { str_copy(tr->title, station, MPRIS_STRLEN); } g_variant_unref(dict); g_variant_unref(peek); // Read current stream/file position from player's "org.mpris.MediaPlayer2.Player" interface. // Ref: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html // // Test: // $ dbus-send --print-reply --session --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 // org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'Position' // // method returns sender=:1.125 -> dest=:1.170 reply_serial=2 // variant int64 218000000 (in microseconds) result = mpris2_get_player_value(player, "Position"); // (,) if (!result) { // trackPos = -1L; return; } peek = result; // Is it "(v)"? if (g_variant_is_container(result)) { // Peek to "v" g_variant_get(result, "(v)", &peek); } tr->trackPos = g_variant_get_int64(peek); g_variant_unref(result); g_variant_unref(peek); } void mpris2_start_app(gpointer player_rec) { // Start player application /* I have tried StartSeviceByName command, but it does not work: $ dbus-send --session --dest="org.freedesktop.DBus" \ "/org/freedesktop/DBus" \ "org.freedesktop.DBus.StartServiceByName" \ "string:org.gnome.Rhythmbox3" \ "int32:0" // The following code works fine for "org.gnome.Rhythmbox3" but there is // no service file for "org.mpris.MediaPlayer2.rhythmbox". gchar *service_name = "org.gnome.Rhythmbox3"; GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, NULL, &error); // Call StartServiceByName method. error = NULL; GVariant *result = g_dbus_proxy_call_sync(proxy, "StartServiceByName", g_variant_new("(su)", service_name, 0), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); */ // Let's do it the hard way. MediaPlayerRec *player = (MediaPlayerRec*)player_rec; if (!player_rec) return; // Already running? if (mpris2_service_is_running(player_rec)) { return; } if (!player->exec_cmd) { g_printerr("Executable name for %s is not set. Start the application manually.\n", player->app_name); return; } // Run the application. It will return immediately because this spawn asynchronous. GError *error = NULL; g_spawn_command_line_async (player->exec_cmd, &error); if (error) { // Translators: This is an error message. LOG_ERROR(_("Exec error. Cannot start process %s.\n%s.\n"), player->exec_cmd, error->message); g_error_free(error); error = NULL; } } void mpris2_detect_players() { // Get list of MPRIS2 (org.mpris.MediaPlayer2.*) compliant programs. // Same listing as from this dbus-send command: /* $ dbus-send \ --session \ --dest=org.freedesktop.DBus \ --type=method_call \ --print-reply \ /org/freedesktop/DBus \ org.freedesktop.DBus.ListNames Note: Maybe better use this verb: org.freedesktop.DBus.ListActivatableNames */ // Connect to glib/DBus GDBusConnection *dbus_conn = mpris2_connect_to_dbus(); if (!dbus_conn) return; // Get player list GHashTable *player_list = dbus_player_get_list_ref(); #define DBUS_MPRIS2_NAMESPACE "org.mpris.MediaPlayer2." GError *error = NULL; // Ref: https://dbus.freedesktop.org/doc/api/html/group__DBusShared.html // Create a proxy for org.freedesktop.DBus and execute "ListNames" GVariant *result = g_dbus_connection_call_sync (dbus_conn, "org.freedesktop.DBus", /* name */ "/org/freedesktop/DBus", /* path */ "org.freedesktop.DBus", /* interface */ "ListNames", // You may also try: "ListActivatableNames", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); #if 0 // This does the same thing. GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn, G_DBUS_PROXY_FLAGS_NONE, NULL, DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, NULL, &error); if (error) { g_print("Cannot create proxy for ListNames. %s\n", error->message); g_error_free(error); return; } // Call ListNames method, wait for reply error = NULL; GVariant *result = g_dbus_proxy_call_sync(proxy, "ListNames", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); // Unref the proxy g_object_unref(proxy); #endif if (error) { g_printerr("Cannot read service names from org.freedesktop.DBus. %s\n", error->message); g_error_free(error); return; } // The result is an array of strings, "(as)". GVariantIter *iter = NULL; gchar *service_name = NULL; // Get iter to a string array g_variant_get(result, "(as)", &iter); // Iter over all service_names service_name = NULL; while (g_variant_iter_next(iter, "s", &service_name)) { // Drop names that begin with ':' if (service_name && *service_name == ':') { g_free(service_name); continue; } // LOG_DEBUG("Available service: %s\n", service_name); MediaPlayerRec *player = NULL; // Does this name to org.mpris.MediaPlayer2.* namespace? // // Notice! The media player must be up & running before we see its org.mpris.MediaPlayer2.xxx service name ! if (!strncmp(DBUS_MPRIS2_NAMESPACE, service_name, strlen(DBUS_MPRIS2_NAMESPACE))) { LOG_PLAYER("Detected service name %s.\n", service_name); // New MediaPlayer record player = mpris2_player_new(service_name); // EDIT: "Identity" property is no longer needed or read. // We are reading application name from its desktop file. // Ref: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html // player->app_name = mpris2_get_property_str(player, "Identity"); // Get player's desktop file. // Ref: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html player->desktop_file = mpris2_get_property_str(player, "DesktopEntry"); // FIXME: { // FIXME in future: GnomeMusic reports erroneus .desktop filename (on "DesktopEntry"). // It reports "gnome-music", but Linux-systems has no "gnome-music.desktop" file. // The right answer would be "org.gnome.Music" meaning /usr/share/applications/org.gnome.Music.desktop // Here is the bug-report: https://bugs.launchpad.net/bugs/1819037 // Spec: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:DesktopEntry if (str_compare(player->desktop_file, "gnome-music", TRUE) == 0) { g_free(player->desktop_file); player->desktop_file = g_strdup("org.gnome.Music"); } } if (str_length0(player->desktop_file) < 1) { g_printerr("Error: DBus-interface for %s should implement \"DesktopEntry\" property.\n", service_name); player->desktop_file = get_base_name(service_name); } // Load app name, executable and icon name from player's xxx.desktop file get_details_from_desktop_file(player, player->desktop_file); // Function to connect/disconnect event signals player->func_set_signals = mpris2_set_signals; // Function to check if this player is running player->func_check_is_running = mpris2_service_is_running; // Function to get track-info (track/song name/title/album/artist, length, etc.) player->func_get_info = mpris2_get_metadata; // Function to start/run the application player->func_start_app = mpris2_start_app; } // Did we got a valid player record? if (player && player->app_name) { // Add player to the g_player_list. // Lookup the record by its app_name (like: "Amarok 2.3.2") if (!dbus_player_lookup_app_name(player->app_name)) { // Add it to the list g_hash_table_insert(player_list, g_strdup(player->service_name), player); } else { // Duplicate or bad record. Free it. dbus_player_delete_item(player); } } g_free(service_name); } g_variant_iter_free(iter); g_variant_unref(result); } #if 0 gboolean mpris2_start_app_via_dbus(gpointer player_rec) { // Start (run) application by its service_name // Ref: https://linoxide.com/how-tos/d-bus-ipc-mechanism-linux/ // Good, but it did not work for all media players. // I have commented it out. // Same as this dbus_send command // Example for RhythmBox $ dbus-send \ --session \ --dest=org.freedesktop.DBus \ --type=method_call \ --print-reply \ /org/freedesktop/DBus \ org.freedesktop.DBus.StartServiceByName string:org.gnome.Rhythmbox3 uint32:0 dbus_uint32_t flag; DBusError error; MediaPlayerRec *player = (MediaPlayerRec*)player_rec; if (!player) return FALSE; dbus_error_init(&error); DBusConnection *connection = dbus_bus_get(DBUS_BUS_SESSION, &error); if ( dbus_error_is_set(&error) ){ g_print("Error getting dbus connection: %s\n",error.message); dbus_error_free(&error); dbus_connection_unref(connection); return FALSE; } DBusMessage *message = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "StartServiceByName"); if (!message){ gprint("Error creating DBus message\n"); dbus_connection_unref(connection); return FALSE; } dbus_message_set_no_reply(message, TRUE); // Append the argument to the message, must ends with DBUS_TYPE_UINT32 flag = 0; dbus_message_append_args(message, DBUS_TYPE_STRING, &player->service_name, DBUS_TYPE_UINT32, &flag, DBUS_TYPE_INVALID); dbus_bool_t result = dbus_connection_send(connection, message, NULL); if (result) { g_print("Successfully activating the %s service\n",player->service_name); } else { g_print("Failed to activate the %s service\n",player->service_name); } dbus_message_unref(message); dbus_connection_unref(connection); return result; } #endif