/*
* 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