]>
Commit | Line | Data |
---|---|---|
1f75fe4e MB |
1 | /* |
2 | * This file is part of Shairport Sync. | |
249a6def | 3 | * Copyright (c) Mike Brady 2018 -- 2020 |
1f75fe4e MB |
4 | * All rights reserved. |
5 | * | |
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: | |
13 | * | |
14 | * The above copyright notice and this permission notice shall be | |
15 | * included in all copies or substantial portions of the Software. | |
16 | * | |
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. | |
25 | */ | |
54d761ff | 26 | #include <inttypes.h> |
df7c48f0 MB |
27 | #include <stdio.h> |
28 | #include <string.h> | |
29 | ||
30 | #include "config.h" | |
31 | ||
32 | #include "common.h" | |
33 | #include "player.h" | |
34 | #include "rtsp.h" | |
35 | ||
36 | #include "rtp.h" | |
37 | ||
38 | #include "dacp.h" | |
39 | ||
aaee366f | 40 | #include "metadata_hub.h" |
df7c48f0 | 41 | #include "mpris-service.h" |
b98e2a08 | 42 | |
85ba78b6 MB |
43 | MediaPlayer2 *mprisPlayerSkeleton; |
44 | MediaPlayer2Player *mprisPlayerPlayerSkeleton; | |
45 | ||
fe8198a2 MB |
46 | double airplay_volume_to_mpris_volume(double sp) { |
47 | if (sp < -30.0) | |
48 | sp = -30.0; | |
49 | if (sp > 0.0) | |
50 | sp = 0.0; | |
54d761ff | 51 | sp = (sp / 30.0) + 1; |
fe8198a2 MB |
52 | return sp; |
53 | } | |
54 | ||
55 | double mpris_volume_to_airplay_volume(double sp) { | |
54d761ff | 56 | sp = (sp - 1.0) * 30.0; |
fe8198a2 MB |
57 | if (sp < -30.0) |
58 | sp = -30.0; | |
59 | if (sp > 0.0) | |
60 | sp = 0.0; | |
61 | return sp; | |
62 | } | |
63 | ||
8991f342 | 64 | void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) void *userdata) { |
3a4e29c5 | 65 | // debug(1, "MPRIS metadata watcher called"); |
aaee366f | 66 | char response[100]; |
54d761ff MB |
67 | media_player2_player_set_volume(mprisPlayerPlayerSkeleton, |
68 | airplay_volume_to_mpris_volume(argc->airplay_volume)); | |
aaee366f | 69 | switch (argc->repeat_status) { |
cbbe84d8 MB |
70 | case RS_NOT_AVAILABLE: |
71 | strcpy(response, "Not Available"); | |
72 | break; | |
73 | case RS_OFF: | |
aaee366f MB |
74 | strcpy(response, "None"); |
75 | break; | |
cbbe84d8 | 76 | case RS_ONE: |
aaee366f MB |
77 | strcpy(response, "Track"); |
78 | break; | |
79 | case RS_ALL: | |
80 | strcpy(response, "Playlist"); | |
81 | break; | |
82 | } | |
83 | ||
aaee366f | 84 | media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton, response); |
c36a7822 | 85 | |
64d36693 | 86 | switch (argc->player_state) { |
cbbe84d8 MB |
87 | case PS_NOT_AVAILABLE: |
88 | strcpy(response, "Not Available"); | |
89 | break; | |
64d36693 MB |
90 | case PS_STOPPED: |
91 | strcpy(response, "Stopped"); | |
92 | break; | |
93 | case PS_PAUSED: | |
94 | strcpy(response, "Paused"); | |
95 | break; | |
96 | case PS_PLAYING: | |
97 | strcpy(response, "Playing"); | |
98 | break; | |
99 | } | |
100 | ||
c36a7822 | 101 | media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton, response); |
aaee366f | 102 | |
cbbe84d8 MB |
103 | /* |
104 | switch (argc->shuffle_state) { | |
105 | case SS_NOT_AVAILABLE: | |
106 | strcpy(response, "Not Available"); | |
107 | break; | |
108 | case SS_OFF: | |
109 | strcpy(response, "Off"); | |
110 | break; | |
111 | case SS_ON: | |
112 | strcpy(response, "On"); | |
113 | break; | |
114 | } | |
115 | ||
116 | media_player2_player_set_shuffle_status(mprisPlayerPlayerSkeleton, response); | |
117 | */ | |
118 | ||
119 | switch (argc->shuffle_status) { | |
120 | case SS_NOT_AVAILABLE: | |
121 | media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, FALSE); | |
122 | break; | |
123 | case SS_OFF: | |
124 | media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, FALSE); | |
125 | break; | |
126 | case SS_ON: | |
127 | media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, TRUE); | |
128 | break; | |
129 | default: | |
130 | debug(1, "This should never happen."); | |
131 | } | |
132 | ||
86b6d7bf MB |
133 | /* |
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 | |
137 | char st[33]; | |
138 | char *pt = st; | |
139 | int it; | |
140 | int non_zero = 0; | |
4aab0a6f MB |
141 | for (it = 0; it < 16; it++) { |
142 | if (argc->track_metadata->item_composite_id[it]) | |
143 | non_zero = 1; | |
08a7d4ff | 144 | snprintf(pt, 3, "%02X", argc->track_metadata->item_composite_id[it]); |
4aab0a6f MB |
145 | pt += 2; |
146 | } | |
86b6d7bf MB |
147 | *pt = 0; |
148 | ||
149 | if (non_zero) { | |
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); | |
162 | } | |
163 | ||
164 | */ | |
165 | ||
166 | // Build the metadata array | |
167 | debug(2, "Build metadata"); | |
168 | GVariantBuilder *dict_builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); | |
169 | ||
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); | |
2cfbeca6 | 174 | } |
4aab0a6f | 175 | |
86b6d7bf | 176 | // Add in the Track ID based on the 'mper' metadata if it is non-zero |
d67909b8 | 177 | if (argc->item_id_is_valid != 0) { |
64d36693 | 178 | char trackidstring[128]; |
54d761ff MB |
179 | snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", |
180 | argc->item_id); | |
c36a7822 MB |
181 | GVariant *trackid = g_variant_new("o", trackidstring); |
182 | g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); | |
3a4e29c5 | 183 | } |
c36a7822 | 184 | |
86b6d7bf MB |
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); | |
352f6d5c | 189 | } |
6b5a1b39 | 190 | |
86b6d7bf MB |
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); | |
3a4e29c5 | 195 | } |
c36a7822 | 196 | |
86b6d7bf MB |
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); | |
3a4e29c5 | 204 | } |
c36a7822 | 205 | |
86b6d7bf MB |
206 | // Add the genre if it exists |
207 | if (argc->genre) { | |
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); | |
3a4e29c5 | 213 | } |
c36a7822 | 214 | |
d67909b8 | 215 | if (argc->songtime_in_milliseconds_is_valid) { |
86b6d7bf MB |
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); | |
3a4e29c5 | 222 | } |
c36a7822 | 223 | |
aaee366f MB |
224 | GVariant *dict = g_variant_builder_end(dict_builder); |
225 | g_variant_builder_unref(dict_builder); | |
aaee366f MB |
226 | media_player2_player_set_metadata(mprisPlayerPlayerSkeleton, dict); |
227 | } | |
228 | ||
a4eaace7 MB |
229 | static gboolean on_handle_quit(MediaPlayer2 *skeleton, GDBusMethodInvocation *invocation, |
230 | __attribute__((unused)) gpointer user_data) { | |
c8c70b60 | 231 | debug(1, "quit requested (MPRIS interface)."); |
f775267d MB |
232 | type_of_exit_cleanup = TOE_dbus; // request an exit cleanup that is compatible with dbus |
233 | exit(EXIT_SUCCESS); | |
a4eaace7 MB |
234 | media_player2_complete_quit(skeleton, invocation); |
235 | return TRUE; | |
236 | } | |
237 | ||
03321799 | 238 | static gboolean on_handle_next(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, |
8991f342 | 239 | __attribute__((unused)) gpointer user_data) { |
03321799 MB |
240 | send_simple_dacp_command("nextitem"); |
241 | media_player2_player_complete_next(skeleton, invocation); | |
242 | return TRUE; | |
243 | } | |
244 | ||
245 | static gboolean on_handle_previous(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, | |
8991f342 | 246 | __attribute__((unused)) gpointer user_data) { |
03321799 MB |
247 | send_simple_dacp_command("previtem"); |
248 | media_player2_player_complete_previous(skeleton, invocation); | |
249 | return TRUE; | |
250 | } | |
251 | ||
b98e2a08 | 252 | static gboolean on_handle_stop(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, |
8991f342 | 253 | __attribute__((unused)) gpointer user_data) { |
b98e2a08 | 254 | send_simple_dacp_command("stop"); |
df7c48f0 MB |
255 | media_player2_player_complete_stop(skeleton, invocation); |
256 | return TRUE; | |
257 | } | |
258 | ||
b98e2a08 | 259 | static gboolean on_handle_pause(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, |
8991f342 | 260 | __attribute__((unused)) gpointer user_data) { |
b98e2a08 MB |
261 | send_simple_dacp_command("pause"); |
262 | media_player2_player_complete_pause(skeleton, invocation); | |
263 | return TRUE; | |
264 | } | |
265 | ||
bafdd94e | 266 | static gboolean on_handle_play_pause(MediaPlayer2Player *skeleton, |
8991f342 MB |
267 | GDBusMethodInvocation *invocation, |
268 | __attribute__((unused)) gpointer user_data) { | |
b98e2a08 MB |
269 | send_simple_dacp_command("playpause"); |
270 | media_player2_player_complete_play_pause(skeleton, invocation); | |
271 | return TRUE; | |
272 | } | |
273 | ||
274 | static gboolean on_handle_play(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, | |
8991f342 | 275 | __attribute__((unused)) gpointer user_data) { |
b98e2a08 MB |
276 | send_simple_dacp_command("play"); |
277 | media_player2_player_complete_play(skeleton, invocation); | |
278 | return TRUE; | |
279 | } | |
df7c48f0 | 280 | |
fe8198a2 | 281 | static gboolean on_handle_set_volume(MediaPlayer2Player *skeleton, |
54d761ff MB |
282 | GDBusMethodInvocation *invocation, const gdouble volume, |
283 | __attribute__((unused)) gpointer user_data) { | |
fe8198a2 MB |
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); | |
290 | return TRUE; | |
291 | } | |
292 | ||
bafdd94e | 293 | static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *name, |
8991f342 | 294 | __attribute__((unused)) gpointer user_data) { |
df7c48f0 | 295 | |
bafdd94e | 296 | const char *empty_string_array[] = {NULL}; |
df7c48f0 | 297 | |
c36a7822 MB |
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"); | |
df7c48f0 MB |
300 | mprisPlayerSkeleton = media_player2_skeleton_new(); |
301 | mprisPlayerPlayerSkeleton = media_player2_player_skeleton_new(); | |
302 | ||
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); | |
bafdd94e | 307 | |
df7c48f0 MB |
308 | media_player2_set_desktop_entry(mprisPlayerSkeleton, "shairport-sync"); |
309 | media_player2_set_identity(mprisPlayerSkeleton, "Shairport Sync"); | |
a4eaace7 | 310 | media_player2_set_can_quit(mprisPlayerSkeleton, TRUE); |
df7c48f0 MB |
311 | media_player2_set_can_raise(mprisPlayerSkeleton, FALSE); |
312 | media_player2_set_has_track_list(mprisPlayerSkeleton, FALSE); | |
bafdd94e MB |
313 | media_player2_set_supported_uri_schemes(mprisPlayerSkeleton, empty_string_array); |
314 | media_player2_set_supported_mime_types(mprisPlayerSkeleton, empty_string_array); | |
315 | ||
aaee366f MB |
316 | media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton, "Stopped"); |
317 | media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton, "None"); | |
bafdd94e MB |
318 | media_player2_player_set_minimum_rate(mprisPlayerPlayerSkeleton, 1.0); |
319 | media_player2_player_set_maximum_rate(mprisPlayerPlayerSkeleton, 1.0); | |
03321799 MB |
320 | media_player2_player_set_can_go_next(mprisPlayerPlayerSkeleton, TRUE); |
321 | media_player2_player_set_can_go_previous(mprisPlayerPlayerSkeleton, TRUE); | |
bafdd94e MB |
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); | |
326 | ||
a4eaace7 MB |
327 | g_signal_connect(mprisPlayerSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL); |
328 | ||
b98e2a08 MB |
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); | |
bafdd94e MB |
331 | g_signal_connect(mprisPlayerPlayerSkeleton, "handle-play-pause", G_CALLBACK(on_handle_play_pause), |
332 | NULL); | |
df7c48f0 | 333 | g_signal_connect(mprisPlayerPlayerSkeleton, "handle-stop", G_CALLBACK(on_handle_stop), NULL); |
03321799 | 334 | g_signal_connect(mprisPlayerPlayerSkeleton, "handle-next", G_CALLBACK(on_handle_next), NULL); |
1637a79d MB |
335 | g_signal_connect(mprisPlayerPlayerSkeleton, "handle-previous", G_CALLBACK(on_handle_previous), |
336 | NULL); | |
fe8198a2 MB |
337 | g_signal_connect(mprisPlayerPlayerSkeleton, "handle-set-volume", G_CALLBACK(on_handle_set_volume), |
338 | NULL); | |
249a6def | 339 | |
aaee366f MB |
340 | add_metadata_watcher(mpris_metadata_watcher, NULL); |
341 | ||
c36a7822 MB |
342 | debug(1, "MPRIS service started at \"%s\" on the %s bus.", name, |
343 | (config.mpris_service_bus_type == DBT_session) ? "session" : "system"); | |
df7c48f0 MB |
344 | } |
345 | ||
8991f342 MB |
346 | static void on_mpris_name_lost_again(__attribute__((unused)) GDBusConnection *connection, |
347 | const gchar *name, | |
348 | __attribute__((unused)) gpointer user_data) { | |
dfd90bac | 349 | warn("could not acquire an MPRIS interface named \"%s\" on the %s bus.", name, |
c36a7822 | 350 | (config.mpris_service_bus_type == DBT_session) ? "session" : "system"); |
df7c48f0 MB |
351 | } |
352 | ||
8991f342 MB |
353 | static void on_mpris_name_lost(__attribute__((unused)) GDBusConnection *connection, |
354 | __attribute__((unused)) const gchar *name, | |
355 | __attribute__((unused)) gpointer user_data) { | |
c36a7822 MB |
356 | // debug(1, "Could not acquire MPRIS interface \"%s\" on the %s bus -- will try adding the process |
357 | // " | |
f3ef6b5e MB |
358 | // "number to the end of it.", |
359 | // name,(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system"); | |
df7c48f0 MB |
360 | pid_t pid = getpid(); |
361 | char interface_name[256] = ""; | |
c2e3fa5a | 362 | snprintf(interface_name, sizeof(interface_name), "org.mpris.MediaPlayer2.ShairportSync.i%d", pid); |
df7c48f0 | 363 | GBusType mpris_bus_type = G_BUS_TYPE_SYSTEM; |
bafdd94e | 364 | if (config.mpris_service_bus_type == DBT_session) |
df7c48f0 | 365 | mpris_bus_type = G_BUS_TYPE_SESSION; |
c36a7822 MB |
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"); | |
df7c48f0 | 368 | g_bus_own_name(mpris_bus_type, interface_name, G_BUS_NAME_OWNER_FLAGS_NONE, NULL, |
bafdd94e | 369 | on_mpris_name_acquired, on_mpris_name_lost_again, NULL, NULL); |
df7c48f0 MB |
370 | } |
371 | ||
372 | int start_mpris_service() { | |
373 | mprisPlayerSkeleton = NULL; | |
374 | mprisPlayerPlayerSkeleton = NULL; | |
375 | GBusType mpris_bus_type = G_BUS_TYPE_SYSTEM; | |
bafdd94e | 376 | if (config.mpris_service_bus_type == DBT_session) |
df7c48f0 | 377 | mpris_bus_type = G_BUS_TYPE_SESSION; |
c36a7822 MB |
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"); | |
bafdd94e MB |
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, | |
382 | NULL, NULL); | |
df7c48f0 MB |
383 | return 0; // this is just to quieten a compiler warning |
384 | } |