2
* Copyright (c) Linux community.
4
* This library is free software; you can redistribute it and/or
5
* modify it under the terms of the GNU Library General Public
6
* License as published by the Free Software Foundation; either
7
* version 2 of the License, or (at your option) any later version.
9
* This library is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
* Library General Public License for more details.
14
* You should have received a copy of the GNU Library General Public
15
* License along with this library; if not, write to the
16
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17
* Boston, MA 02111-1307, USA.
22
#include <dbus/dbus.h>
25
#include "dbus-player.h"
26
#include "dbus-mpris2.h"
28
// This is a MPRIS (org.mpris.MediaPlayer2) interface to various media-players.
29
// Please see: http://www.mpris.org/2.1/spec/
31
// Glib/DBus connection
32
static GDBusConnection *g_dbus_conn = NULL;
34
static GDBusConnection *mpris2_connect_to_dbus();
35
static void mpris2_disconnect_from_dbus();
37
static gboolean mpris2_check_proxy(MediaPlayerRec *player);
38
static GVariant *mpris2_get_property(MediaPlayerRec *player, gchar *prop_name);
40
void mpris2_module_init() {
41
LOG_DEBUG("Init dbus_mpris2.c.\n");
46
void mpris2_module_exit() {
47
LOG_DEBUG("Clean up dbus_mpris2.c.\n");
49
mpris2_disconnect_from_dbus();
52
GDBusConnection *mpris2_connect_to_dbus() {
53
// Connect to glib/DBus
56
if (!G_IS_DBUS_CONNECTION(g_dbus_conn)) {
57
g_dbus_conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
59
if (!G_IS_DBUS_CONNECTION(g_dbus_conn)) {
60
LOG_ERROR("mpris2_connect_to_dbus: Cannot connect to DBus: %s\n",
61
error ? error->message : "");
72
void mpris2_disconnect_from_dbus() {
73
// Disconnect from glib/DBus
74
if (G_IS_DBUS_CONNECTION(g_dbus_conn)) {
75
g_object_unref(g_dbus_conn);
80
void mpris2_player_track_changed(gpointer player_rec) {
81
// Sound/audio track changed.
82
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
85
// Stop current recording first. Send PLAYER_STATUS_STOPPED to the rec-manager.c
86
// Notice: Some players send NEITHER "{'PlaybackStatus': 'Stopped'}" NOR
87
// "{'PlaybackStatus': 'Playing'}" signals before/after the track change.
88
// We have to stop/start recording ourselves.
89
TrackInfo *tr = &player->track;
90
tr->status = PLAYER_STATUS_STOPPED;
91
dbus_player_process_data(player);
93
// Re-read track-data. Status should be PLAYER_STATUS_PLAYING.
94
mpris2_get_metadata(player_rec);
97
// dbus_player_debug_print(player);
99
// Send data to the queue (rec-manager.c)
100
dbus_player_process_data(player);
103
void mpris2_player_state_changed(gpointer player_rec) {
104
// Player's state changed (Palying/Paused/Stopped).
105
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
108
// Re-read status data.
109
mpris2_get_metadata(player_rec);
112
// dbus_player_debug_print(player);
114
// Send data to the queue (rec-manager.c)
115
dbus_player_process_data(player);
118
void debug_hash_table(MediaPlayerRec *player, GHashTable *table) {
119
LOG_PLAYER("------------------------\n");
122
MediaPlayerRec *mpris2_player_new(const gchar *service_name) {
123
// New MediaPlayerRec record
124
MediaPlayerRec *player = g_malloc0(sizeof(MediaPlayerRec));
125
player->service_name = g_strdup(service_name);
126
player->app_name = NULL;
130
static gboolean mpris2_check_proxy(MediaPlayerRec *player) {
131
// Create and return proxy for player->service_name.
132
// Noice: This proxy points to the base interface "org.mpris.MediaPlayer2".
133
// Please see: http://www.mpris.org/2.1/spec/
135
if (!player) return FALSE;
138
if (G_IS_DBUS_PROXY(player->proxy)) {
142
GDBusConnection *dbus_conn = mpris2_connect_to_dbus();
144
GError *error = NULL;
145
player->proxy = g_dbus_proxy_new_sync(dbus_conn,
146
G_DBUS_PROXY_FLAGS_NONE,
148
player->service_name, /* service name */
149
"/org/mpris/MediaPlayer2", /* object path */
150
"org.mpris.MediaPlayer2", /* interface */
155
g_printerr("Cannot create proxy for %s. %s.\n", player->service_name, error->message);
157
player->proxy = NULL;
160
return (G_IS_DBUS_PROXY(player->proxy));
163
gchar *mpris2_get_property_str(MediaPlayerRec *player, gchar *prop_name) {
164
// Return (string) value for the given property name.
165
GVariant *result = mpris2_get_property(player, prop_name);
166
if (!result) return NULL;
169
g_variant_get(result, "s", &str);
171
g_variant_unref(result);
173
// Caller should g_free() this value
177
GVariant *mpris2_get_property(MediaPlayerRec *player, gchar *prop_name) {
178
// Return (GVariant) value for the given property name.
180
if (!mpris2_check_proxy(player)) return NULL;
182
// List of valid properties: http://www.mpris.org/2.1/spec/
183
GVariant *result = g_dbus_proxy_get_cached_property(player->proxy, prop_name);
185
// Caller should unref this value
189
static void mpris2_prop_signal(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name,
190
GVariant *parameters, gpointer user_data) {
191
// Handle "PropertiesChanged" signal.
193
MediaPlayerRec *player = (MediaPlayerRec*)user_data;
196
// Got "PropertiesChanged" signal?
197
if (g_strcmp0(signal_name, "PropertiesChanged")) return;
200
gchar *str = g_variant_print(parameters, TRUE);
201
LOG_PLAYER("Received %s signal from %s.\n", signal_name, player->service_name);
202
LOG_PLAYER("Data is:%s\n\n", str);
205
// The data must have format "(sa{sv}as)"
206
if (g_strcmp0(g_variant_get_type_string(parameters), "(sa{sv}as)")) {
210
GVariantIter *iter = NULL;
211
GVariantIter *invalidated_iter = NULL;
214
// Ref: http://developer.gnome.org/gio/2.26/ch27s05.html
215
g_variant_get(parameters, "(sa{sv}as)", &iface, &iter, &invalidated_iter);
218
// LOG_DEBUG("The interface is %s.\n", iface);
220
// Check if Metadata(sound track) or PlaybackStatus has changed.
221
// We are NOT interested in the data itself, just which properties changed.
222
gboolean track_changed = FALSE;
223
gboolean player_state_changed = FALSE;
226
GVariant *value = NULL;
227
while (g_variant_iter_next(iter, "{sv}", &key, &value)) {
229
// Sound/audio track changed?
230
if (!g_ascii_strcasecmp(key, "Metadata")) {
231
track_changed = TRUE;
233
// Player's state changed; Playing/Stopped/Paused?
234
} else if (!g_ascii_strcasecmp(key, "PlaybackStatus")) {
235
player_state_changed = TRUE;
239
g_variant_unref(value);
243
// Audio track changed.
244
// Stop current recording. Then re-read data and re-start recording from a new track.
245
mpris2_player_track_changed(player);
247
} else if (player_state_changed) {
248
// Player's state changed.
249
// Send status change to the recorder.
250
mpris2_player_state_changed(player);
254
Typical data for the PropertiesChanged signal:
256
Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'PlaybackStatus': <'Playing'>}, @as [])
258
Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'Volume': <1.0>}, @as [])
260
Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'CanGoNext': <true>, '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': <int64 226000000>, 'xesam:trackNumber': <3>, 'xesam:discNumber': <1>, 'xesam:useCount': <0>, 'xesam:userRating': <0.0>, 'mpris:artUrl': <'file:///home/moma/.cache/rhythmbox/album-art/00000098'>}>, 'CanSeek': <true>, 'CanGoPrevious': <true>, 'PlaybackStatus': <'Playing'>}, @as [])
262
Signal PropertiesChanged: ('org.mpris.MediaPlayer2.Player', {'PlaybackStatus': <'Paused'>}, @as [])
267
void mpris2_prop_changed(GDBusProxy *proxy, GVariant *changed_properties,
268
GStrv invalidated_properties, gpointer user_data) {
270
//Ref: http://developer.gnome.org/gio/unstable/GDBusProxy.html#GDBusProxy-g-properties-changd
274
void mpris2_set_signals(gpointer player_rec, gboolean do_connect) {
275
// Connect/disconnct signals for this player.
277
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
279
// Connect event signals
282
GDBusConnection *dbus_conn = mpris2_connect_to_dbus();
284
GError *error = NULL;
285
// This is a proxy for org.freedesktop.DBus.Properties
286
player->prop_proxy = g_dbus_proxy_new_sync(dbus_conn,
287
G_DBUS_PROXY_FLAGS_NONE,
289
player->service_name, /* name */
290
"/org/mpris/MediaPlayer2", /* object path */
291
"org.freedesktop.DBus.Properties", /* interface */
296
g_printerr("Cannot create proxy for org.freedesktop.DBus.Properties. %s.\n", error->message);
298
player->prop_proxy = NULL;
302
// Ref: http://developer.gnome.org/gio/2.28/GDBusProxy.html
303
g_signal_connect(player->prop_proxy, "g-properties-changed",
304
G_CALLBACK(mpris2_prop_changed), (gpointer)player/*user data*/);
306
g_signal_connect(player->prop_proxy, "g-signal",
307
G_CALLBACK(mpris2_prop_signal), (gpointer)player/*user data*/);
309
// Disconnect signals
313
if (G_IS_DBUS_PROXY(player->prop_proxy)) {
314
g_object_unref(player->prop_proxy);
316
player->prop_proxy = NULL;
320
gboolean mpris2_service_is_running(gpointer player_rec) {
321
// Check if the application is running.
322
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
323
if (!player_rec) return FALSE;
325
return mpris2_service_is_running_by_name(player->service_name);
328
gboolean mpris2_service_is_running_by_name(const char *service_name) {
329
// Check if the service_name/application is running.
331
// Connect to glib/DBus
332
GDBusConnection *dbus_conn = mpris2_connect_to_dbus();
333
if (!dbus_conn) return FALSE;
335
// Create proxy for DBUS_SERVICE_DBUS
336
GError *error = NULL;
337
GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn,
338
G_DBUS_PROXY_FLAGS_NONE,
347
g_printerr("Cannot create proxy for %s. %s.\n", DBUS_INTERFACE_DBUS, error->message);
352
// Call "NameHasOwner" method
354
GVariant *result = g_dbus_proxy_call_sync(proxy, "NameHasOwner",
355
g_variant_new("(s)", service_name),
356
G_DBUS_CALL_FLAGS_NONE,
362
g_printerr("Cannot get NameHasOwner for %s. %s\n", service_name, error->message);
364
g_object_unref(proxy);
368
// The result has format "(boolean,)"
371
// gchar *str = g_variant_print(result, TRUE);
372
// g_print("Received data for HasName:<%s>\n", str);
375
// Take the boolean value
376
gboolean running = FALSE;
377
g_variant_get_child(result, 0, "b", &running);
379
g_variant_unref(result);
380
g_object_unref(proxy);
386
gchar *get_string_val(GVariant *v) {
387
// Read and return string
390
// Is of string type?
391
if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) {
392
g_variant_get(v, "s", &s);
397
void mpris2_get_metadata(gpointer player_rec) {
398
// Get track information (=metadata) and state for the given media player.
399
// Ref: http://www.mpris.org/2.1/spec/Player_Node.html#Property:Metadata
401
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
405
TrackInfo *tr = &player->track;
406
tr->status = PLAYER_STATUS_STOPPED;
408
tr->track[0] = tr->artist[0] = tr->album[0] = '\0';
410
// Connect to glib/DBus
411
GDBusConnection *dbus_conn = mpris2_connect_to_dbus();
412
if (!dbus_conn) return;
414
// Create proxy for the Player object, "org.mpris.MediaPlayer2.Player"
415
// Ref: http://www.mpris.org/2.1/spec/
416
GError *error = NULL;
417
GDBusProxy *proxy = g_dbus_proxy_new_sync (dbus_conn,
418
G_DBUS_PROXY_FLAGS_NONE,
420
player->service_name, /* name */
421
"/org/mpris/MediaPlayer2", /* object path */
422
"org.mpris.MediaPlayer2.Player", /* .Player interface */
426
g_printerr("Cannot create proxy for %s. %s.\n", player->service_name, error->message);
431
// Read playback status
432
// Ref: http://www.mpris.org/2.1/spec/Player_Node.html
433
GVariant *ret = g_dbus_proxy_get_cached_property(proxy, "PlaybackStatus");
435
// Cannot contact player (it has quit)?
436
tr->status = PLAYER_STATUS_CLOSED;
437
g_object_unref(proxy);
443
g_variant_get(ret, "s", &s);
446
if (!g_ascii_strcasecmp(s, "Playing"))
447
tr->status = PLAYER_STATUS_PLAYING;
449
else if (!g_ascii_strcasecmp(s, "Paused"))
450
tr->status = PLAYER_STATUS_PAUSED;
452
else if (!g_ascii_strcasecmp(s, "Stopped"))
453
tr->status = PLAYER_STATUS_STOPPED;
455
g_variant_unref(ret);
458
// Should we continue?
459
if (tr->status != PLAYER_STATUS_PLAYING) {
460
g_object_unref(proxy);
464
// The state is PLAYER_STATUS_PLAYING.
465
// Read track information (Metadata). The dict has type "a{sv}"
466
GVariant *dict = g_dbus_proxy_get_cached_property(proxy, "Metadata");
468
/* Some key names for the returned "a{sv}" dictionary:
469
'mpris:trackid' has type 's'
470
'xesam:url' has type 's'
471
'xesam:title' has type 's'
472
'xesam:artist' has type 'as'
473
'xesam:genre' has type 'as'
474
'rhythmbox:streamTitle' has type 's'
475
'xesam:audioBitrate' has type 'i'
476
'mpris:length' has type 'x'
477
'xesam:trackNumber' has type 'i'
478
'xesam:useCount' has type 'i'
479
'xesam:userRating' has type 'd'
481
Example data from Rhythmbox:
482
('org.mpris.MediaPlayer2.Player',
485
'mpris:trackid': <'/org/mpris/MediaPlayer2/Track/2'>,
486
'xesam:url': <'file:///home/moma/Music/Bruce Springsteen.mp3'>,
487
'xesam:title': <'Land Of Hope'>,
488
'xesam:artist': <['Bruce Springsteen']>,
489
'xesam:album': <'Wrecking Ball'>,
490
'xesam:genre': <['Rock']>,
491
'xesam:albumArtist': <['Bruce Springsteen']>,
492
'xesam:audioBitrate': <262144>,
493
'xesam:contentCreated': <'2012-01-01T00:00:00Z'>,
494
'mpris:length': <int64 418000000>,
495
'xesam:trackNumber': <10>,
496
'xesam:discNumber': <1>,
497
'xesam:useCount': <0>,
498
'xesam:userRating': <0.0>,
499
'mpris:artUrl': <'file:///home/moma/.cache/rhythmbox/album-art/00000098'>}>,
501
'PlaybackStatus': <'Playing'>}, @as [])"
505
GVariant *value = NULL;
508
// Ref: http://developer.gnome.org/glib/2.30/glib-GVariant.html#g-variant-get-va
509
g_variant_iter_init(&iter, dict);
510
while (g_variant_iter_next (&iter, "{sv}", &key, &value)) {
513
GVariantIter *iter = NULL;
516
if (g_str_has_suffix(key, ":title")) {
517
str = get_string_val(value);
518
str_copy(tr->track, str, MPRIS_STRLEN-1);
521
// xesam::artist? Notice: This has data type "as".
522
else if (g_str_has_suffix(key, ":artist")) {
523
// Take 1.st string of the array
524
g_variant_get(value, "as", &iter);
527
g_variant_iter_next(iter, "s", &str);
529
str_copy(tr->artist, str, MPRIS_STRLEN-1);
532
// xesam:albumArtist? Notice: This has data type "as".
533
else if (g_str_has_suffix(key, ":albumArtist")) {
534
// Already set by ":artist"?
535
if (tr->artist[0] == '\0') {
536
// Take 1.st string of the array
537
g_variant_get(value, "as", &iter);
540
g_variant_iter_next(iter, "s", &str);
542
str_copy(tr->artist, str, MPRIS_STRLEN-1);
547
else if (g_str_has_suffix(key, ":album")) {
548
str = get_string_val(value);
549
str_copy(tr->album, str, MPRIS_STRLEN-1);
553
g_variant_iter_free(iter);
557
g_variant_unref(value);
561
g_variant_unref(dict);
562
g_object_unref(proxy);
565
void mpris2_start_app(gpointer player_rec) {
566
// Start player application
568
MediaPlayerRec *player = (MediaPlayerRec*)player_rec;
569
if (!player_rec) return;
572
if (mpris2_service_is_running(player_rec)) {
576
if (!player->exec_name) {
577
g_printerr("Executable name for %s is not set. Start the application manually.\n",
582
// Find path for exec_name
583
gchar *path = find_command_path(player->exec_name);
585
g_printerr("Cannot run %s. Start the application %s manually.\n",
586
player->exec_name, player->app_name);
590
// Run the application.
592
// Build argv[] list.
593
gchar **argv = g_new(gchar*, 2);
594
argv[0] = g_strdup(path);
597
// Run the command. It will return immediately because it's asynchronous.
598
GError *error = NULL;
599
GPid __attribute__((unused)) pid = exec_command_async(argv, &error);
609
void mpris2_detect_players() {
610
// Get list of MPRIS2 (org.mpris.MediaPlayer2.*) compliant programs.
612
// Connect to glib/DBus
613
GDBusConnection *dbus_conn = mpris2_connect_to_dbus();
614
if (!dbus_conn) return;
617
GHashTable *player_list = dbus_player_get_list_ref();
619
#define DBUS_MPRIS2_NAMESPACE "org.mpris.MediaPlayer2."
621
GError *error = NULL;
623
// Ref: http://dbus.freedesktop.org/doc/api/html/group__DBusShared.html
624
// Create proxy for org.freedesktop.DBus.
625
// And execute "ListNames"
626
GVariant *result = g_dbus_connection_call_sync (dbus_conn,
627
"org.freedesktop.DBus", /* name */
628
"/org/freedesktop/DBus", /* path */
629
"org.freedesktop.DBus", /* interface */
633
G_DBUS_CALL_FLAGS_NONE,
639
// This does the same thing.
640
GDBusProxy *proxy = g_dbus_proxy_new_sync(dbus_conn,
641
G_DBUS_PROXY_FLAGS_NONE,
650
g_print("Cannot create proxy for ListNames. %s\n", error->message);
655
// Call ListNames method, wait for reply
657
GVariant *result = g_dbus_proxy_call_sync(proxy, "ListNames",
659
G_DBUS_CALL_FLAGS_NONE,
665
g_object_unref(proxy);
670
g_printerr("Cannot read service names from org.freedesktop.DBus. %s\n", error->message);
675
// The result is an array of strings, "(as)".
677
GVariantIter *iter = NULL;
678
gchar *service_name = NULL;
680
// Get iter to the string array
681
g_variant_get(result, "(as)", &iter);
683
// Iter over all service_names
685
while (g_variant_iter_next(iter, "s", &service_name)) {
687
// Drop names that begin with ':'
688
if (service_name && *service_name == ':') {
689
g_free(service_name);
693
MediaPlayerRec *player = NULL;
695
// Belongs to org.mpris.MediaPlayer2.* namespace?
696
if (!strncmp(DBUS_MPRIS2_NAMESPACE, service_name, strlen(DBUS_MPRIS2_NAMESPACE))) {
698
LOG_DEBUG("Detected service name %s.\n", service_name);
700
// New MediaPlayer record
701
player = mpris2_player_new(service_name);
703
// Set application name
704
// Ref: http://specifications.freedesktop.org/mpris-spec/latest/Root_Node.html
705
player->app_name = mpris2_get_property_str(player, "Identity");
707
// Set executable name (from it's .desktop file)
708
player->exec_name = mpris2_get_property_str(player, "DesktopEntry");
710
// Function to connect/disconnect event signals
711
player->func_set_signals = mpris2_set_signals;
713
// Function to check if this player is running
714
player->func_check_is_running = mpris2_service_is_running;
716
// Function to get track-info (track/song name/title/album/artist, length, etc.)
717
player->func_get_info = mpris2_get_metadata;
719
// Function to start/run the application
720
player->func_start_app = mpris2_start_app;
722
// Set icon name (this is not a perfect solution!).
723
// Find last "." in the service_name.
724
gchar *pos = g_strrstr(player->service_name, ".");
727
player->icon_name = g_strdup_printf("%s.png", pos+1);
729
player->icon_name = g_strdup("mediaplayer.png");
734
// Has a player record?
735
if (player && player->app_name) {
737
// Add player to the g_player_list.
739
// Lookup the record by its app_name (like: "Amarok 2.3.2")
740
if (!dbus_player_lookup_app_name(player->app_name)) {
741
// Add it to the list
742
g_hash_table_insert(player_list, g_strdup(player->service_name), player);
744
// Duplicate or bad record. Free it.
745
dbus_player_delete_item(player);
750
g_free(service_name);
753
g_variant_iter_free(iter);
754
g_variant_unref(result);