/* * This file is part of Shairport Sync. * Copyright (c) Mike Brady 2018--2025 * All rights reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include "config.h" #include "common.h" #include "player.h" #include "rtsp.h" #include "rtp.h" #ifdef CONFIG_DACP_CLIENT #include "dacp.h" #endif #include "metadata/hub.h" #include "mpris-service.h" #include "utilities/exit.h" static guint ownerID = 0; static GBusType mpris_bus_type = G_BUS_TYPE_SYSTEM; // default is the dbus system message bus MediaPlayer2 *mprisPlayerSkeleton; MediaPlayer2Player *mprisPlayerPlayerSkeleton; double airplay_volume_to_mpris_volume(double sp) { if (sp < -30.0) sp = -30.0; if (sp > 0.0) sp = 0.0; sp = (sp / 30.0) + 1; return sp; } double mpris_volume_to_airplay_volume(double sp) { sp = (sp - 1.0) * 30.0; if (sp < -30.0) sp = -30.0; if (sp > 0.0) sp = 0.0; return sp; } void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) void *userdata) { // debug(1, "MPRIS metadata watcher called"); char response[100]; media_player2_player_set_volume(mprisPlayerPlayerSkeleton, airplay_volume_to_mpris_volume(argc->airplay_volume)); switch (argc->repeat_status) { case RS_NOT_AVAILABLE: strcpy(response, "Not Available"); break; case RS_OFF: strcpy(response, "None"); break; case RS_ONE: strcpy(response, "Track"); break; case RS_ALL: strcpy(response, "Playlist"); break; } media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton, response); switch (argc->player_state) { case PS_NOT_AVAILABLE: strcpy(response, "Not Available"); break; case PS_STOPPED: strcpy(response, "Stopped"); break; case PS_PAUSED: strcpy(response, "Paused"); break; case PS_PLAYING: strcpy(response, "Playing"); break; } media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton, response); /* switch (argc->shuffle_state) { case SS_NOT_AVAILABLE: strcpy(response, "Not Available"); break; case SS_OFF: strcpy(response, "Off"); break; case SS_ON: strcpy(response, "On"); break; } media_player2_player_set_shuffle_status(mprisPlayerPlayerSkeleton, response); */ switch (argc->shuffle_status) { case SS_NOT_AVAILABLE: media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, FALSE); break; case SS_OFF: media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, FALSE); break; case SS_ON: media_player2_player_set_shuffle(mprisPlayerPlayerSkeleton, TRUE); break; default: debug(1, "This should never happen."); } /* // Add the TrackID if we have one // Build the Track ID from the 16-byte item_composite_id in hex prefixed by // /org/gnome/ShairportSync char st[33]; char *pt = st; int it; int non_zero = 0; for (it = 0; it < 16; it++) { if (argc->track_metadata->item_composite_id[it]) non_zero = 1; snprintf(pt, 3, "%02X", argc->track_metadata->item_composite_id[it]); pt += 2; } *pt = 0; if (non_zero) { // debug(1, "Set ID using composite ID: \"0x%s\".", st); char trackidstring[1024]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%s", st); GVariant *trackid = g_variant_new("o", trackidstring); g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } else if ((argc->track_metadata) && (argc->track_metadata->item_id)) { char trackidstring[128]; // debug(1, "Set ID using mper ID: \"%u\".",argc->item_id); snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u", argc->track_metadata->item_id); GVariant *trackid = g_variant_new("o", trackidstring); g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } */ // Build the metadata array debug(4, "Build metadata"); GVariantBuilder *dict_builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); // Add in the artwork URI if it exists. if (argc->cover_art_pathname) { GVariant *artUrl = g_variant_new("s", argc->cover_art_pathname); g_variant_builder_add(dict_builder, "{sv}", "mpris:artUrl", artUrl); } // Add in the Track ID based on the 'mper' metadata if it is non-zero if (argc->item_id_is_valid != 0) { char trackidstring[128]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); GVariant *trackid = g_variant_new("o", trackidstring); g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } // Add the track name if it exists if (argc->track_name) { GVariant *track_name = g_variant_new("s", argc->track_name); g_variant_builder_add(dict_builder, "{sv}", "xesam:title", track_name); } // Add the album name if it exists if (argc->album_name) { GVariant *album_name = g_variant_new("s", argc->album_name); g_variant_builder_add(dict_builder, "{sv}", "xesam:album", album_name); } // Add the artist name if it exists if (argc->artist_name) { GVariantBuilder *artist_as = g_variant_builder_new(G_VARIANT_TYPE("as")); g_variant_builder_add(artist_as, "s", argc->artist_name); GVariant *artists = g_variant_builder_end(artist_as); g_variant_builder_unref(artist_as); g_variant_builder_add(dict_builder, "{sv}", "xesam:artist", artists); } // Add the genre if it exists if (argc->genre) { GVariantBuilder *genre_as = g_variant_builder_new(G_VARIANT_TYPE("as")); g_variant_builder_add(genre_as, "s", argc->genre); GVariant *genre = g_variant_builder_end(genre_as); g_variant_builder_unref(genre_as); g_variant_builder_add(dict_builder, "{sv}", "xesam:genre", genre); } if (argc->songtime_in_milliseconds_is_valid) { uint64_t track_length_in_microseconds = argc->songtime_in_milliseconds; track_length_in_microseconds *= 1000; // to microseconds in 64-bit precision // Make up the track name and album name // debug(1, "Set tracklength to %lu.", track_length_in_microseconds); GVariant *tracklength = g_variant_new("x", track_length_in_microseconds); g_variant_builder_add(dict_builder, "{sv}", "mpris:length", tracklength); } GVariant *dict = g_variant_builder_end(dict_builder); g_variant_builder_unref(dict_builder); media_player2_player_set_metadata(mprisPlayerPlayerSkeleton, dict); } static gboolean on_handle_quit(MediaPlayer2 *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { debug(1, ">> quit request..."); media_player2_complete_quit(skeleton, invocation); exit_request(EXIT_SUCCESS); return TRUE; } static gboolean on_handle_next(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("nextitem"); #endif media_player2_player_complete_next(skeleton, invocation); return TRUE; } static gboolean on_handle_previous(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("previtem"); #endif media_player2_player_complete_previous(skeleton, invocation); return TRUE; } static gboolean on_handle_stop(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("stop"); #endif media_player2_player_complete_stop(skeleton, invocation); return TRUE; } static gboolean on_handle_pause(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("pause"); #endif media_player2_player_complete_pause(skeleton, invocation); return TRUE; } static gboolean on_handle_play_pause(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("playpause"); #endif media_player2_player_complete_play_pause(skeleton, invocation); return TRUE; } static gboolean on_handle_play(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT send_simple_dacp_command("play"); #endif media_player2_player_complete_play(skeleton, invocation); return TRUE; } static gboolean on_handle_set_volume(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation, const gdouble volume, __attribute__((unused)) gpointer user_data) { #ifdef CONFIG_DACP_CLIENT double ap_volume = mpris_volume_to_airplay_volume(volume); debug(2, "Set mpris volume to %.6f, i.e. airplay volume to %.6f.", volume, ap_volume); char command[256] = ""; snprintf(command, sizeof(command), "setproperty?dmcp.device-volume=%.6f", ap_volume); send_simple_dacp_command(command); #endif media_player2_player_complete_play(skeleton, invocation); return TRUE; } static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { const char *empty_string_array[] = {NULL}; debug(2, "MPRIS well-known interface name \"%s\" acquired on the %s bus.", name, (mpris_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system"); mprisPlayerSkeleton = media_player2_skeleton_new(); mprisPlayerPlayerSkeleton = media_player2_player_skeleton_new(); g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(mprisPlayerSkeleton), connection, "/org/mpris/MediaPlayer2", NULL); g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(mprisPlayerPlayerSkeleton), connection, "/org/mpris/MediaPlayer2", NULL); media_player2_set_desktop_entry(mprisPlayerSkeleton, "shairport-sync"); media_player2_set_identity(mprisPlayerSkeleton, "Shairport Sync"); media_player2_set_can_quit(mprisPlayerSkeleton, TRUE); media_player2_set_can_raise(mprisPlayerSkeleton, FALSE); media_player2_set_has_track_list(mprisPlayerSkeleton, FALSE); media_player2_set_supported_uri_schemes(mprisPlayerSkeleton, empty_string_array); media_player2_set_supported_mime_types(mprisPlayerSkeleton, empty_string_array); media_player2_player_set_playback_status(mprisPlayerPlayerSkeleton, "Stopped"); media_player2_player_set_loop_status(mprisPlayerPlayerSkeleton, "None"); media_player2_player_set_minimum_rate(mprisPlayerPlayerSkeleton, 1.0); media_player2_player_set_maximum_rate(mprisPlayerPlayerSkeleton, 1.0); media_player2_player_set_can_go_next(mprisPlayerPlayerSkeleton, TRUE); media_player2_player_set_can_go_previous(mprisPlayerPlayerSkeleton, TRUE); media_player2_player_set_can_play(mprisPlayerPlayerSkeleton, TRUE); media_player2_player_set_can_pause(mprisPlayerPlayerSkeleton, TRUE); media_player2_player_set_can_seek(mprisPlayerPlayerSkeleton, FALSE); media_player2_player_set_can_control(mprisPlayerPlayerSkeleton, TRUE); g_signal_connect(mprisPlayerSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-play", G_CALLBACK(on_handle_play), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-pause", G_CALLBACK(on_handle_pause), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-play-pause", G_CALLBACK(on_handle_play_pause), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-stop", G_CALLBACK(on_handle_stop), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-next", G_CALLBACK(on_handle_next), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-previous", G_CALLBACK(on_handle_previous), NULL); g_signal_connect(mprisPlayerPlayerSkeleton, "handle-set-volume", G_CALLBACK(on_handle_set_volume), NULL); add_metadata_watcher(mpris_metadata_watcher, NULL); debug(1, "MPRIS service started at \"%s\" on the %s bus.", name, (mpris_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system"); } static void on_mpris_name_lost(__attribute__((unused)) GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { warn("could not acquire an MPRIS interface named \"%s\" on the %s bus.", name, (mpris_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system"); ownerID = 0; } int start_mpris_service() { mprisPlayerSkeleton = NULL; mprisPlayerPlayerSkeleton = NULL; // set up default message bus if (config.dbus_default_message_bus == DBT_session) mpris_bus_type = G_BUS_TYPE_SESSION; // look for explicit overrides if (config.mpris_service_bus_type == DBT_system) mpris_bus_type = G_BUS_TYPE_SYSTEM; else if (config.mpris_service_bus_type == DBT_session) mpris_bus_type = G_BUS_TYPE_SESSION; debug(1, "Looking for an MPRIS interface \"org.mpris.MediaPlayer2.ShairportSync\" on the %s bus.", (mpris_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system"); ownerID = g_bus_own_name(mpris_bus_type, "org.mpris.MediaPlayer2.ShairportSync", G_BUS_NAME_OWNER_FLAGS_NONE, NULL, on_mpris_name_acquired, on_mpris_name_lost, NULL, NULL); return 0; // this is just to quieten a compiler warning } void stop_mpris_service() { if (ownerID) { debug(2, "stopping MPRIS service -- unowning ownerID %d.", ownerID); g_bus_unown_name(ownerID); } }