2 * This file is part of Shairport Sync.
3 * Copyright (c) Mike Brady 2018 -- 2020
6 * Permission is hereby granted, free of charge, to any person
7 * obtaining a copy of this software and associated documentation
8 * files (the "Software"), to deal in the Software without
9 * restriction, including without limitation the rights to use,
10 * copy, modify, merge, publish, distribute, sublicense, and/or
11 * sell copies of the Software, and to permit persons to whom the
12 * Software is furnished to do so, subject to the following conditions:
14 * The above copyright notice and this permission notice shall be
15 * included in all copies or substantial portions of the Software.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 * OTHER DEALINGS IN THE SOFTWARE.
40 #include "metadata_hub.h"
41 #include "mpris-service.h"
43 MediaPlayer2
*mprisPlayerSkeleton
;
44 MediaPlayer2Player
*mprisPlayerPlayerSkeleton
;
46 double airplay_volume_to_mpris_volume(double sp
) {
55 double mpris_volume_to_airplay_volume(double sp
) {
56 sp
= (sp
- 1.0) * 30.0;
64 void mpris_metadata_watcher(struct metadata_bundle
*argc
, __attribute__((unused
)) void *userdata
) {
65 // debug(1, "MPRIS metadata watcher called");
67 media_player2_player_set_volume(mprisPlayerPlayerSkeleton
,
68 airplay_volume_to_mpris_volume(argc
->airplay_volume
));
69 switch (argc
->repeat_status
) {
70 case RS_NOT_AVAILABLE
:
71 strcpy(response
, "Not Available");
74 strcpy(response
, "None");
77 strcpy(response
, "Track");
80 strcpy(response
, "Playlist");
84 media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton
, response
);
86 switch (argc
->player_state
) {
87 case PS_NOT_AVAILABLE
:
88 strcpy(response
, "Not Available");
91 strcpy(response
, "Stopped");
94 strcpy(response
, "Paused");
97 strcpy(response
, "Playing");
101 media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton
, response
);
104 switch (argc->shuffle_state) {
105 case SS_NOT_AVAILABLE:
106 strcpy(response, "Not Available");
109 strcpy(response, "Off");
112 strcpy(response, "On");
116 media_player2_player_set_shuffle_status(mprisPlayerPlayerSkeleton, response);
119 switch (argc
->shuffle_status
) {
120 case SS_NOT_AVAILABLE
:
121 media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton
, FALSE
);
124 media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton
, FALSE
);
127 media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton
, TRUE
);
130 debug(1, "This should never happen.");
134 // Add the TrackID if we have one
135 // Build the Track ID from the 16-byte item_composite_id in hex prefixed by
136 // /org/gnome/ShairportSync
141 for (it = 0; it < 16; it++) {
142 if (argc->track_metadata->item_composite_id[it])
144 snprintf(pt, 3, "%02X", argc->track_metadata->item_composite_id[it]);
150 // debug(1, "Set ID using composite ID: \"0x%s\".", st);
151 char trackidstring[1024];
152 snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%s", st);
153 GVariant *trackid = g_variant_new("o", trackidstring);
154 g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid);
155 } else if ((argc->track_metadata) && (argc->track_metadata->item_id)) {
156 char trackidstring[128];
157 // debug(1, "Set ID using mper ID: \"%u\".",argc->item_id);
158 snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u",
159 argc->track_metadata->item_id);
160 GVariant *trackid = g_variant_new("o", trackidstring);
161 g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid);
166 // Build the metadata array
167 debug(2, "Build metadata");
168 GVariantBuilder
*dict_builder
= g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
170 // Add in the artwork URI if it exists.
171 if (argc
->cover_art_pathname
) {
172 GVariant
*artUrl
= g_variant_new("s", argc
->cover_art_pathname
);
173 g_variant_builder_add(dict_builder
, "{sv}", "mpris:artUrl", artUrl
);
176 // Add in the Track ID based on the 'mper' metadata if it is non-zero
177 if (argc
->item_id_is_valid
!= 0) {
178 char trackidstring
[128];
179 snprintf(trackidstring
, sizeof(trackidstring
), "/org/gnome/ShairportSync/%" PRIX64
"",
181 GVariant
*trackid
= g_variant_new("o", trackidstring
);
182 g_variant_builder_add(dict_builder
, "{sv}", "mpris:trackid", trackid
);
185 // Add the track name if it exists
186 if (argc
->track_name
) {
187 GVariant
*track_name
= g_variant_new("s", argc
->track_name
);
188 g_variant_builder_add(dict_builder
, "{sv}", "xesam:title", track_name
);
191 // Add the album name if it exists
192 if (argc
->album_name
) {
193 GVariant
*album_name
= g_variant_new("s", argc
->album_name
);
194 g_variant_builder_add(dict_builder
, "{sv}", "xesam:album", album_name
);
197 // Add the artist name if it exists
198 if (argc
->artist_name
) {
199 GVariantBuilder
*artist_as
= g_variant_builder_new(G_VARIANT_TYPE("as"));
200 g_variant_builder_add(artist_as
, "s", argc
->artist_name
);
201 GVariant
*artists
= g_variant_builder_end(artist_as
);
202 g_variant_builder_unref(artist_as
);
203 g_variant_builder_add(dict_builder
, "{sv}", "xesam:artist", artists
);
206 // Add the genre if it exists
208 GVariantBuilder
*genre_as
= g_variant_builder_new(G_VARIANT_TYPE("as"));
209 g_variant_builder_add(genre_as
, "s", argc
->genre
);
210 GVariant
*genre
= g_variant_builder_end(genre_as
);
211 g_variant_builder_unref(genre_as
);
212 g_variant_builder_add(dict_builder
, "{sv}", "xesam:genre", genre
);
215 if (argc
->songtime_in_milliseconds_is_valid
) {
216 uint64_t track_length_in_microseconds
= argc
->songtime_in_milliseconds
;
217 track_length_in_microseconds
*= 1000; // to microseconds in 64-bit precision
218 // Make up the track name and album name
219 // debug(1, "Set tracklength to %lu.", track_length_in_microseconds);
220 GVariant
*tracklength
= g_variant_new("x", track_length_in_microseconds
);
221 g_variant_builder_add(dict_builder
, "{sv}", "mpris:length", tracklength
);
224 GVariant
*dict
= g_variant_builder_end(dict_builder
);
225 g_variant_builder_unref(dict_builder
);
226 media_player2_player_set_metadata(mprisPlayerPlayerSkeleton
, dict
);
229 static gboolean
on_handle_quit(MediaPlayer2
*skeleton
, GDBusMethodInvocation
*invocation
,
230 __attribute__((unused
)) gpointer user_data
) {
231 debug(1, "quit requested (MPRIS interface).");
232 type_of_exit_cleanup
= TOE_dbus
; // request an exit cleanup that is compatible with dbus
234 media_player2_complete_quit(skeleton
, invocation
);
238 static gboolean
on_handle_next(MediaPlayer2Player
*skeleton
, GDBusMethodInvocation
*invocation
,
239 __attribute__((unused
)) gpointer user_data
) {
240 send_simple_dacp_command("nextitem");
241 media_player2_player_complete_next(skeleton
, invocation
);
245 static gboolean
on_handle_previous(MediaPlayer2Player
*skeleton
, GDBusMethodInvocation
*invocation
,
246 __attribute__((unused
)) gpointer user_data
) {
247 send_simple_dacp_command("previtem");
248 media_player2_player_complete_previous(skeleton
, invocation
);
252 static gboolean
on_handle_stop(MediaPlayer2Player
*skeleton
, GDBusMethodInvocation
*invocation
,
253 __attribute__((unused
)) gpointer user_data
) {
254 send_simple_dacp_command("stop");
255 media_player2_player_complete_stop(skeleton
, invocation
);
259 static gboolean
on_handle_pause(MediaPlayer2Player
*skeleton
, GDBusMethodInvocation
*invocation
,
260 __attribute__((unused
)) gpointer user_data
) {
261 send_simple_dacp_command("pause");
262 media_player2_player_complete_pause(skeleton
, invocation
);
266 static gboolean
on_handle_play_pause(MediaPlayer2Player
*skeleton
,
267 GDBusMethodInvocation
*invocation
,
268 __attribute__((unused
)) gpointer user_data
) {
269 send_simple_dacp_command("playpause");
270 media_player2_player_complete_play_pause(skeleton
, invocation
);
274 static gboolean
on_handle_play(MediaPlayer2Player
*skeleton
, GDBusMethodInvocation
*invocation
,
275 __attribute__((unused
)) gpointer user_data
) {
276 send_simple_dacp_command("play");
277 media_player2_player_complete_play(skeleton
, invocation
);
281 static gboolean
on_handle_set_volume(MediaPlayer2Player
*skeleton
,
282 GDBusMethodInvocation
*invocation
, const gdouble volume
,
283 __attribute__((unused
)) gpointer user_data
) {
284 double ap_volume
= mpris_volume_to_airplay_volume(volume
);
285 debug(2, "Set mpris volume to %.6f, i.e. airplay volume to %.6f.", volume
, ap_volume
);
286 char command
[256] = "";
287 snprintf(command
, sizeof(command
), "setproperty?dmcp.device-volume=%.6f", ap_volume
);
288 send_simple_dacp_command(command
);
289 media_player2_player_complete_play(skeleton
, invocation
);
293 static void on_mpris_name_acquired(GDBusConnection
*connection
, const gchar
*name
,
294 __attribute__((unused
)) gpointer user_data
) {
296 const char *empty_string_array
[] = {NULL
};
298 // debug(1, "MPRIS well-known interface name \"%s\" acquired on the %s bus.", name,
299 // (config.mpris_service_bus_type == DBT_session) ? "session" : "system");
300 mprisPlayerSkeleton
= media_player2_skeleton_new();
301 mprisPlayerPlayerSkeleton
= media_player2_player_skeleton_new();
303 g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(mprisPlayerSkeleton
), connection
,
304 "/org/mpris/MediaPlayer2", NULL
);
305 g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(mprisPlayerPlayerSkeleton
), connection
,
306 "/org/mpris/MediaPlayer2", NULL
);
308 media_player2_set_desktop_entry(mprisPlayerSkeleton
, "shairport-sync");
309 media_player2_set_identity(mprisPlayerSkeleton
, "Shairport Sync");
310 media_player2_set_can_quit(mprisPlayerSkeleton
, TRUE
);
311 media_player2_set_can_raise(mprisPlayerSkeleton
, FALSE
);
312 media_player2_set_has_track_list(mprisPlayerSkeleton
, FALSE
);
313 media_player2_set_supported_uri_schemes(mprisPlayerSkeleton
, empty_string_array
);
314 media_player2_set_supported_mime_types(mprisPlayerSkeleton
, empty_string_array
);
316 media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton
, "Stopped");
317 media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton
, "None");
318 media_player2_player_set_minimum_rate(mprisPlayerPlayerSkeleton
, 1.0);
319 media_player2_player_set_maximum_rate(mprisPlayerPlayerSkeleton
, 1.0);
320 media_player2_player_set_can_go_next(mprisPlayerPlayerSkeleton
, TRUE
);
321 media_player2_player_set_can_go_previous(mprisPlayerPlayerSkeleton
, TRUE
);
322 media_player2_player_set_can_play(mprisPlayerPlayerSkeleton
, TRUE
);
323 media_player2_player_set_can_pause(mprisPlayerPlayerSkeleton
, TRUE
);
324 media_player2_player_set_can_seek(mprisPlayerPlayerSkeleton
, FALSE
);
325 media_player2_player_set_can_control(mprisPlayerPlayerSkeleton
, TRUE
);
327 g_signal_connect(mprisPlayerSkeleton
, "handle-quit", G_CALLBACK(on_handle_quit
), NULL
);
329 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-play", G_CALLBACK(on_handle_play
), NULL
);
330 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-pause", G_CALLBACK(on_handle_pause
), NULL
);
331 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-play-pause", G_CALLBACK(on_handle_play_pause
),
333 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-stop", G_CALLBACK(on_handle_stop
), NULL
);
334 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-next", G_CALLBACK(on_handle_next
), NULL
);
335 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-previous", G_CALLBACK(on_handle_previous
),
337 g_signal_connect(mprisPlayerPlayerSkeleton
, "handle-set-volume", G_CALLBACK(on_handle_set_volume
),
340 add_metadata_watcher(mpris_metadata_watcher
, NULL
);
342 debug(1, "MPRIS service started at \"%s\" on the %s bus.", name
,
343 (config
.mpris_service_bus_type
== DBT_session
) ? "session" : "system");
346 static void on_mpris_name_lost_again(__attribute__((unused
)) GDBusConnection
*connection
,
348 __attribute__((unused
)) gpointer user_data
) {
349 warn("could not acquire an MPRIS interface named \"%s\" on the %s bus.", name
,
350 (config
.mpris_service_bus_type
== DBT_session
) ? "session" : "system");
353 static void on_mpris_name_lost(__attribute__((unused
)) GDBusConnection
*connection
,
354 __attribute__((unused
)) const gchar
*name
,
355 __attribute__((unused
)) gpointer user_data
) {
356 // debug(1, "Could not acquire MPRIS interface \"%s\" on the %s bus -- will try adding the process
358 // "number to the end of it.",
359 // name,(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
360 pid_t pid
= getpid();
361 char interface_name
[256] = "";
362 snprintf(interface_name
, sizeof(interface_name
), "org.mpris.MediaPlayer2.ShairportSync.i%d", pid
);
363 GBusType mpris_bus_type
= G_BUS_TYPE_SYSTEM
;
364 if (config
.mpris_service_bus_type
== DBT_session
)
365 mpris_bus_type
= G_BUS_TYPE_SESSION
;
366 // debug(1, "Looking for an MPRIS interface \"%s\" on the %s bus.",interface_name,
367 // (mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
368 g_bus_own_name(mpris_bus_type
, interface_name
, G_BUS_NAME_OWNER_FLAGS_NONE
, NULL
,
369 on_mpris_name_acquired
, on_mpris_name_lost_again
, NULL
, NULL
);
372 int start_mpris_service() {
373 mprisPlayerSkeleton
= NULL
;
374 mprisPlayerPlayerSkeleton
= NULL
;
375 GBusType mpris_bus_type
= G_BUS_TYPE_SYSTEM
;
376 if (config
.mpris_service_bus_type
== DBT_session
)
377 mpris_bus_type
= G_BUS_TYPE_SESSION
;
378 // debug(1, "Looking for an MPRIS interface \"org.mpris.MediaPlayer2.ShairportSync\" on the %s
379 // bus.",(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
380 g_bus_own_name(mpris_bus_type
, "org.mpris.MediaPlayer2.ShairportSync",
381 G_BUS_NAME_OWNER_FLAGS_NONE
, NULL
, on_mpris_name_acquired
, on_mpris_name_lost
,
383 return 0; // this is just to quieten a compiler warning