diff --git a/.github/workflows/check_ap2_systemd_basic.yml b/.github/workflows/check_ap2_systemd_basic.yml index e4ddc504..3359bace 100644 --- a/.github/workflows/check_ap2_systemd_basic.yml +++ b/.github/workflows/check_ap2_systemd_basic.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 3461a4f9..780dd3a4 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_ap2_systemd_full_build_folder.yml b/.github/workflows/check_ap2_systemd_full_build_folder.yml index a877fc35..f3b743b5 100644 --- a/.github/workflows/check_ap2_systemd_full_build_folder.yml +++ b/.github/workflows/check_ap2_systemd_full_build_folder.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_ap2_systemv_full.yml b/.github/workflows/check_ap2_systemv_full.yml index 8397ecb0..d3ff26f8 100644 --- a/.github/workflows/check_ap2_systemv_full.yml +++ b/.github/workflows/check_ap2_systemv_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_classic_systemd_basic.yml b/.github/workflows/check_classic_systemd_basic.yml index dc2e9ec4..5988e965 100644 --- a/.github/workflows/check_classic_systemd_basic.yml +++ b/.github/workflows/check_classic_systemd_basic.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index bf3ad651..bbc7c02e 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/BUILD.md b/BUILD.md index ba85f5bb..bf666bb9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -23,8 +23,9 @@ Do this until no more copies of `shairport-sync` are found. You should also remove any of the following service files that may be present: * `/etc/systemd/system/shairport-sync.service` * `/etc/systemd/user/shairport-sync.service` +* `/lib/systemd/system/shairport-sync.service` * `/lib/systemd/user/shairport-sync.service` -* `/etc/init.d/shairport-sync` +* `/etc/init.d/shairport-sync` New service files will be installed if necessary at the `# make install` stage. #### Reboot after Cleaning Up diff --git a/activity_monitor.c b/activity_monitor.c index 896a79f9..49f99d13 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -56,7 +56,8 @@ pthread_mutex_t activity_monitor_mutex; pthread_cond_t activity_monitor_cv; void going_active(int block) { - // debug(1, "activity_monitor: state transitioning to \"active\" with%s blocking", block ? "" : "out"); + // debug(1, "activity_monitor: state transitioning to \"active\" with%s blocking", block ? "" : + // "out"); if (config.cmd_active_start) command_execute(config.cmd_active_start, "", block); #ifdef CONFIG_METADATA @@ -75,7 +76,8 @@ void going_active(int block) { } void going_inactive(int block) { - // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : "out"); + // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : + // "out"); if (config.cmd_active_stop) command_execute(config.cmd_active_stop, "", block); #ifdef CONFIG_METADATA diff --git a/audio_alsa.c b/audio_alsa.c index 62db843f..9a37f3f6 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -189,7 +189,9 @@ static int precision_delay_available() { // this is very crude -- if the device is a hardware device, then it's assumed the delay is // precise const char *output_device_name = snd_pcm_name(alsa_handle); - int is_a_real_hardware_device = (strstr(output_device_name, "hw:") == output_device_name); + int is_a_real_hardware_device = 0; + if (output_device_name != NULL) + is_a_real_hardware_device = (strstr(output_device_name, "hw:") == output_device_name); // The criteria as to whether precision delay is available // is whether the device driver returns non-zero update timestamps diff --git a/audio_pa.c b/audio_pa.c index c91b76f6..8f92ad39 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -155,12 +155,11 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * static void deinit(void) { pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); - // debug(1, "pa deinit done"); + debug(1, "pa deinit done"); } -static void start(__attribute__((unused)) int sample_rate, - __attribute__((unused)) int sample_format) { - +static void do_start() { + // debug(1, "pa_start"); uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); @@ -223,88 +222,128 @@ static void start(__attribute__((unused)) int sample_rate, pa_threaded_mainloop_unlock(mainloop); } -static int play(void *buf, int samples, __attribute__((unused)) int sample_type, - __attribute__((unused)) uint32_t timestamp, - __attribute__((unused)) uint64_t playtime) { - // debug(1,"pa_play of %d samples.",samples); - // copy the samples into the queue - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; - } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } - return 0; +static void start(__attribute__((unused)) int sample_rate, + __attribute__((unused)) int sample_format) { + do_start(); } -int pa_delay(long *the_delay) { - long result = 0; - int reply = -ENODEV; +static int stream_is_open() { + int response = 0; // default to no pa_usec_t latency; int negative; pa_threaded_mainloop_lock(mainloop); int gl = pa_stream_get_latency(stream, &latency, &negative); pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); - reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; + if (gl >= 0) + response = 1; + return response; +} + +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { + // debug(1,"pa_play of %d samples.",samples); + if (stream_is_open() == 0) { + // debug(1,"pa open stream before play."); + do_start(); + } + if (stream_is_open() != 0) { + // copy the samples into the queue + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + } else { + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); + } } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; + // debug(2, "could not open pa stream for play"); + } + return 0; +} + +int pa_delay(long *the_delay) { + // debug(1,"pa_delay"); + int reply = -ENODEV; + long result = 0; + if (stream_is_open() == 0) { + // debug(1,"pa open stream before delay."); + do_start(); + } + if (stream_is_open() != 0) { + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; + } else { + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; + } + } else { + // debug(2, "could not open pa stream for delay"); } *the_delay = result; return reply; } void flush(void) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); + // debug(1,"Flush."); + if (stream_is_open() != 0) { + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; } static void stop(void) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; + // debug(1,"Stop."); + if (stream_is_open() != 0) { + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; - // debug(1,"pa stop"); - pa_stream_disconnect(stream); + // debug(1,"pa stop"); + pa_stream_disconnect(stream); + } } audio_output audio_pa = {.name = "pa", diff --git a/audio_pipe.c b/audio_pipe.c index 001f1385..e76c7300 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -141,7 +141,9 @@ static void deinit(void) { close(fd); } -static void help(void) { printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name); } +static void help(void) { + printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name); +} audio_output audio_pipe = {.name = "pipe", .help = &help, diff --git a/common.h b/common.h index a3f21126..639c1c0f 100644 --- a/common.h +++ b/common.h @@ -141,6 +141,7 @@ typedef struct { int metadata_sockport; size_t metadata_sockmsglength; int get_coverart; + double metadata_progress_interval; // 0 means no progress reports #endif #ifdef CONFIG_MQTT int mqtt_enabled; @@ -159,6 +160,7 @@ typedef struct { int mqtt_enable_remote; char *mqtt_empty_payload_substitute; #endif + uint8_t ap1_prefix[6]; uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; int udp_port_base; diff --git a/configure.ac b/configure.ac index 0cd27627..c473c957 100644 --- a/configure.ac +++ b/configure.ac @@ -406,6 +406,8 @@ AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"]) AC_ARG_WITH(airplay-2, [AS_HELP_STRING([--with-airplay-2],[Build for AirPlay 2])]) if test "x$with_airplay_2" = "xyes" ; then AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2]) + AC_CHECK_PROG(XXD_CHECK,xxd,yes) + AS_IF([test x"$XXD_CHECK" != x"yes"], [AC_MSG_ERROR([xxd can not be found. Please install xxd for building for AirPlay 2.])]) PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ PKG_CHECK_MODULES([libplist], [libplist-2.0 >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!) @@ -420,7 +422,10 @@ if test "x$with_airplay_2" = "xyes" ; then PKG_CHECK_MODULES([libavutil], [libavutil],[CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"],[ AC_MSG_ERROR(AirPlay 2 support requires libavutil -- libavutil-dev suggested)]) PKG_CHECK_MODULES([libavcodec], [libavcodec],[CFLAGS="${libavcodec_CFLAGS} ${CFLAGS}" LIBS="${libavcodec_LIBS} ${LIBS}"],[ - AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested)]) + AC_CHECK_LIB([avcodec], [avcodec_find_decoder], [], [ + AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested) + ]) + ]) AC_CHECK_LIB([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])]) AC_CHECK_LIB([swresample],[swr_convert], [], [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])]) AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])]) diff --git a/dacp.c b/dacp.c index 73164ae2..50cf0e17 100644 --- a/dacp.c +++ b/dacp.c @@ -803,8 +803,9 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware // see reference above) debug(2, "DACP Composite ID seen"); - if (memcmp(metadata_store.item_composite_id, sp - item_size, - sizeof(metadata_store.item_composite_id)) != 0) { + if ((metadata_store.item_composite_id_is_valid == 0) || + (memcmp(metadata_store.item_composite_id, sp - item_size, + sizeof(metadata_store.item_composite_id)) != 0)) { memcpy(metadata_store.item_composite_id, sp - item_size, sizeof(metadata_store.item_composite_id)); char st[33]; @@ -817,6 +818,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { *pt = 0; debug(2, "Item composite ID changed to 0x%s.", st); metadata_store.item_composite_id_changed = 1; + metadata_store.item_composite_id_is_valid = 1; } break; case 'astm': @@ -826,6 +828,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { if (ui != metadata_store.songtime_in_milliseconds) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; + metadata_store.songtime_in_milliseconds_is_valid = 1; debug(2, "DACP Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); } @@ -1219,8 +1222,8 @@ int dacp_get_volume(int32_t *the_actual_volume) { http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); if (http_response == 200) { // get our machine number - uint16_t *hn = (uint16_t *)config.hw_addr; - uint32_t *ln = (uint32_t *)(config.hw_addr + 2); + uint16_t *hn = (uint16_t *)config.ap1_prefix; + uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); uint64_t t1 = ntohs(*hn); uint64_t t2 = ntohl(*ln); int64_t machine_number = (t1 << 32) + t2; // this form is useful @@ -1273,8 +1276,8 @@ int dacp_set_volume(int32_t vo) { http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); if (http_response == 200) { // get our machine number - uint16_t *hn = (uint16_t *)config.hw_addr; - uint32_t *ln = (uint32_t *)(config.hw_addr + 2); + uint16_t *hn = (uint16_t *)config.ap1_prefix; + uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); uint64_t t1 = ntohs(*hn); uint64_t t2 = ntohl(*ln); int64_t machine_number = (t1 << 32) + t2; // this form is useful diff --git a/dbus-service.c b/dbus-service.c index c6cc2ac7..4cfc8254 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -68,6 +68,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) argc->airplay_volume); shairport_sync_remote_control_set_client(shairportSyncRemoteControlSkeleton, argc->client_ip); + shairport_sync_remote_control_set_client_name(shairportSyncRemoteControlSkeleton, + argc->client_name); // although it's a DACP server, the server is in fact, part of the the AirPlay "client" (their // term). @@ -95,6 +97,34 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } } + if (argc->frame_position_string) { + // debug(1, "Check frame position string"); + th = shairport_sync_get_frame_position(shairportSyncSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->frame_position_string) != 0)) { + // debug(1, "Frame position string should be changed"); + shairport_sync_set_frame_position(shairportSyncSkeleton, argc->frame_position_string); + } + } + + if (argc->first_frame_position_string) { + // debug(1, "Check first frame position string"); + th = shairport_sync_get_first_frame_position(shairportSyncSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->first_frame_position_string) != 0)) { + // debug(1, "First frame position string should be changed"); + shairport_sync_set_first_frame_position(shairportSyncSkeleton, argc->first_frame_position_string); + } + } + + if (argc->stream_type) { + // debug(1, "Check stream type"); + th = shairport_sync_remote_control_get_stream_type(shairportSyncRemoteControlSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->stream_type) != 0)) { + // debug(1, "Stream type string should be changed"); + shairport_sync_remote_control_set_stream_type(shairportSyncRemoteControlSkeleton, + argc->stream_type); + } + } + switch (argc->player_state) { case PS_NOT_AVAILABLE: shairport_sync_remote_control_set_player_state(shairportSyncRemoteControlSkeleton, @@ -201,8 +231,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) 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 != 0) { + // Add in the Track ID based on the 'mper' metadata if it is valid + if (argc->item_id_is_valid != 0) { char trackidstring[128]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); @@ -210,6 +240,15 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } + // Add in the Song Data Kind based on the 'asdk' metadata if it is valid + // It seems that this is 0 for a timed play, e.g. a track or an album, but is 1 for an untimed + // play, such as a stream. + + if (argc->song_data_kind_is_valid != 0) { + GVariant *songdatakind = g_variant_new_uint32(argc->song_data_kind); + g_variant_builder_add(dict_builder, "{sv}", "sps:songdatakind", songdatakind); + } + // Add the track name if it exists if (argc->track_name) { GVariant *track_name = g_variant_new("s", argc->track_name); @@ -222,7 +261,7 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:album", album_name); } - // Add the artist name if it exists + // Add the artist name list 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); @@ -231,7 +270,25 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:artist", artists); } - // Add the genre if it exists + // Add the album artist list if it exists + if (argc->album_artist_name) { + GVariantBuilder *album_artist_as = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(album_artist_as, "s", argc->album_artist_name); + GVariant *album_artists = g_variant_builder_end(album_artist_as); + g_variant_builder_unref(album_artist_as); + g_variant_builder_add(dict_builder, "{sv}", "xesam:albumArtist", album_artists); + } + + // Add the composer list if it exists + if (argc->composer) { + GVariantBuilder *composer_as = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(composer_as, "s", argc->composer); + GVariant *composers = g_variant_builder_end(composer_as); + g_variant_builder_unref(composer_as); + g_variant_builder_add(dict_builder, "{sv}", "xesam:composer", composers); + } + + // Add the genre list 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); @@ -240,11 +297,11 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:genre", genre); } - if (argc->songtime_in_milliseconds) { + if (argc->songtime_in_milliseconds_is_valid != 0) { 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); + // debug(1, "Set tracklength to %" PRId64 ".", track_length_in_microseconds); GVariant *tracklength = g_variant_new("x", track_length_in_microseconds); g_variant_builder_add(dict_builder, "{sv}", "mpris:length", tracklength); } @@ -814,6 +871,17 @@ static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvoc return TRUE; } +static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skeleton, + GDBusMethodInvocation *invocation, + const gdouble seconds, + __attribute__((unused)) + gpointer user_data) { + debug(1, ">> set frame position update interval to %.6f.", seconds); + config.metadata_progress_interval = seconds; + shairport_sync_complete_set_frame_position_update_interval(skeleton, invocation); + return TRUE; +} + static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { @@ -872,6 +940,9 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name g_signal_connect(shairportSyncSkeleton, "handle-drop-session", G_CALLBACK(on_handle_drop_session), NULL); + g_signal_connect(shairportSyncSkeleton, "handle-set-frame-position-update-interval", + G_CALLBACK(on_handle_set_frame_position_update_interval), NULL); + g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity", G_CALLBACK(notify_verbosity_callback), NULL); @@ -1019,6 +1090,11 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name // NULL); #endif + shairport_sync_set_service_name(SHAIRPORT_SYNC(shairportSyncSkeleton), config.service_name); + shairport_sync_set_output_rate(SHAIRPORT_SYNC(shairportSyncSkeleton), config.output_rate); + shairport_sync_set_output_format(SHAIRPORT_SYNC(shairportSyncSkeleton), + sps_format_description_string(config.output_format)); + #ifdef CONFIG_AIRPLAY_2 shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2"); #else @@ -1132,7 +1208,7 @@ void stop_dbus_service() { if (ownerID) { g_bus_unown_name(ownerID); } else if (service_is_running != 0) { - debug(1, "Zero OwnerID for running \"org.gnome.ShairportSync\" dbus service."); + debug(1, "Zero OwnerID for running \"org.gnome.ShairportSync\" dbus service."); } service_is_running = 0; } diff --git a/docker/Dockerfile b/docker/Dockerfile index 11647acd..89308350 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -89,6 +89,8 @@ RUN apk -U add \ COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync COPY --from=builder /usr/local/bin/nqptp /usr/local/bin/nqptp COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf /etc/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ @@ -107,4 +109,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "s6-setuidgid", "shairport-sync", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 3010ca11..65ba957d 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -68,6 +68,8 @@ RUN apk -U add \ # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf /etc/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ @@ -86,4 +88,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "s6-setuidgid", "shairport-sync", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] diff --git a/mdns.c b/mdns.c index 9a5465c1..05366413 100644 --- a/mdns.c +++ b/mdns.c @@ -69,7 +69,7 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { char *p = ap1_service_name; int i; for (i = 0; i < 6; i++) { - snprintf(p, 3, "%02X", config.hw_addr[i]); + snprintf(p, 3, "%02X", config.ap1_prefix[i]); p += 2; } *p++ = '@'; diff --git a/metadata_hub.c b/metadata_hub.c index b66035a0..e39752fe 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -102,6 +102,7 @@ void run_metadata_watchers(void) { // turn off changed flags metadata_store.cover_art_pathname_changed = 0; metadata_store.client_ip_changed = 0; + metadata_store.client_name_changed = 0; metadata_store.server_ip_changed = 0; metadata_store.progress_string_changed = 0; metadata_store.item_id_changed = 0; @@ -109,6 +110,7 @@ void run_metadata_watchers(void) { metadata_store.artist_name_changed = 0; metadata_store.album_artist_name_changed = 0; metadata_store.album_name_changed = 0; + metadata_store.song_data_kind_changed = 0; metadata_store.track_name_changed = 0; metadata_store.genre_changed = 0; metadata_store.comment_changed = 0; @@ -335,7 +337,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin // Some metadata items are contained in one metadata packet. // The start of the metadata packet is signalled by an 'ssnc' 'mdst' item and - // the end of it by an 'ssnc 'mden' item. + // the end of it by an 'ssnc 'mden' item. // We don't set "changed" for them individually; instead we set it when the 'mden' token // comes in if the metadata_packet_item_changed item is set by parsed items // within the packet. @@ -347,6 +349,20 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin char *cs; if (type == 'core') { switch (code) { + case 'asdk': { + // get the one-byte number as an unsigned number + int song_data_kind = data[0]; // one byte + song_data_kind = song_data_kind & 0xFF; // unsigned + debug(2, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind, length); + if ((song_data_kind != metadata_store.song_data_kind) || + (metadata_store.song_data_kind_is_valid == 0)) { + metadata_store.song_data_kind = song_data_kind; + metadata_store.song_data_kind_changed = 1; + metadata_store.song_data_kind_is_valid = 1; + debug(2, "MH Song Data Kind set to: \"%d\"", metadata_store.song_data_kind); + metadata_packet_item_changed = 1; + } + } break; case 'mper': { // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them uint64_t vl = ntohl(*(uint32_t *)data); // get the high order 32 bits @@ -354,10 +370,10 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits vl = vl + ul; debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length); - if (vl != metadata_store.item_id) { + if ((vl != metadata_store.item_id) || (metadata_store.item_id_is_valid == 0)) { metadata_store.item_id = vl; metadata_store.item_id_changed = 1; - metadata_store.item_id_received = 1; + metadata_store.item_id_is_valid = 1; debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id); metadata_packet_item_changed = 1; } @@ -365,9 +381,11 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin case 'astm': { uint32_t ui = ntohl(*(uint32_t *)data); debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); - if (ui != metadata_store.songtime_in_milliseconds) { + if ((ui != metadata_store.songtime_in_milliseconds) || + (metadata_store.songtime_in_milliseconds_is_valid == 0)) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; + metadata_store.songtime_in_milliseconds_is_valid = 1; debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); metadata_packet_item_changed = 1; } @@ -549,6 +567,14 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } free(cs); break; + case 'snam': + cs = strndup(data, length); + if (string_update(&metadata_store.client_name, &metadata_store.client_name_changed, cs)) { + changed = 1; + debug(2, "MH Client Name set to: \"%s\"", metadata_store.client_name); + } + free(cs); + break; case 'prgr': cs = strndup(data, length); if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed, @@ -558,6 +584,32 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } free(cs); break; + case 'phbt': + cs = strndup(data, length); + if (string_update(&metadata_store.frame_position_string, + &metadata_store.frame_position_string_changed, cs)) { + changed = 1; + debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.frame_position_string); + } + free(cs); + break; + case 'phb0': + cs = strndup(data, length); + if (string_update(&metadata_store.first_frame_position_string, + &metadata_store.first_frame_position_string_changed, cs)) { + changed = 1; + debug(2, "MH First Frame Position String set to: \"%s\"", metadata_store.first_frame_position_string); + } + free(cs); + break; + case 'styp': + cs = strndup(data, length); + if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, cs)) { + changed = 1; + debug(2, "MH Stream Type set to: \"%s\"", metadata_store.stream_type); + } + free(cs); + break; case 'svip': cs = strndup(data, length); if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) { @@ -591,14 +643,14 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin changed = (metadata_store.player_state != PS_PAUSED); metadata_store.player_state = PS_PAUSED; break; -/* -// not using this anymore. - case 'pffr': // this is sent when the first frame has been received - case 'prsm': - changed = (metadata_store.player_state != PS_PLAYING); - metadata_store.player_state = PS_PLAYING; - break; -*/ + /* + // not using this anymore. + case 'pffr': // this is sent when the first frame has been received + case 'prsm': + changed = (metadata_store.player_state != PS_PLAYING); + metadata_store.player_state = PS_PLAYING; + break; + */ case 'pvol': { // Note: it's assumed that the config.airplay volume has already been correctly set. // int32_t actual_volume; diff --git a/metadata_hub.h b/metadata_hub.h index bdbbf321..8235c6e4 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -43,12 +43,24 @@ typedef struct metadata_bundle { // server int client_ip_changed; + char *client_name; // the name of the client device, if available + int client_name_changed; + char *server_ip; // IP number used by Shairport Sync int server_ip_changed; + + char *stream_type; // Realtime or Buffered + int stream_type_changed; char *progress_string; // progress string, emitted by the source from time to time int progress_string_changed; + char *frame_position_string; // frame position string emitted by SPS on request + int frame_position_string_changed; + + char *first_frame_position_string; // first frame position string emitted by SPS on request + int first_frame_position_string_changed; + int player_thread_active; // true if a play thread is running int dacp_server_active; // true if there's a reachable DACP server (assumed to be the Airplay // client) ; false otherwise @@ -67,15 +79,21 @@ typedef struct metadata_bundle { char *cover_art_pathname; int cover_art_pathname_changed; - + uint64_t item_id; // seems to be a track ID -- see itemid in DACP.c int item_id_changed; - int item_id_received; // important for deciding if the track information should be ignored. + int item_id_is_valid; unsigned char item_composite_id[16]; // seems to be nowplaying 4 ids: dbid, plid, playlistItem, itemid int item_composite_id_changed; + int item_composite_id_is_valid; + + int song_data_kind; + int song_data_kind_changed; + int song_data_kind_is_valid; + char *track_name; int track_name_changed; @@ -120,6 +138,8 @@ typedef struct metadata_bundle { uint32_t songtime_in_milliseconds; int songtime_in_milliseconds_changed; + int songtime_in_milliseconds_is_valid; + // end diff --git a/mpris-service.c b/mpris-service.c index 8416df95..b31d179d 100644 --- a/mpris-service.c +++ b/mpris-service.c @@ -174,7 +174,7 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused) } // Add in the Track ID based on the 'mper' metadata if it is non-zero - if (argc->item_id != 0) { + if (argc->item_id_is_valid != 0) { char trackidstring[128]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); @@ -212,7 +212,7 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused) g_variant_builder_add(dict_builder, "{sv}", "xesam:genre", genre); } - if (argc->songtime_in_milliseconds) { + 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 diff --git a/mqtt.c b/mqtt.c index a007cbc6..cf2e2388 100644 --- a/mqtt.c +++ b/mqtt.c @@ -178,6 +178,10 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'asal': mqtt_publish("songalbum", data, length); break; + case 'asdk': + mqtt_publish("songdatakind", data, + length); // 0 seem to be a timed item, 1 an untimed stream + break; case 'clip': mqtt_publish("client_ip", data, length); break; @@ -193,6 +197,12 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'daid': mqtt_publish("dacp_id", data, length); break; + case 'ofmt': + mqtt_publish("output_format", data, length); + break; + case 'ofps': + mqtt_publish("output_frame_rate", data, length); + break; case 'pbeg': mqtt_publish("play_start", data, length); break; @@ -216,9 +226,15 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'snam': mqtt_publish("client_name", data, length); break; + case 'styp': + mqtt_publish("stream_type", data, length); + break; case 'svip': mqtt_publish("server_ip", data, length); break; + case 'svna': + mqtt_publish("service_name", data, length); + break; } } } diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 1051bab6..722fda5b 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -24,6 +24,14 @@ + + + + + + + + @@ -49,6 +57,8 @@ + + diff --git a/player.c b/player.c index 5aad9153..9206e5da 100644 --- a/player.c +++ b/player.c @@ -363,20 +363,35 @@ static void terminate_decoders(rtsp_conn_info *conn) { #endif } +uint64_t buffers_allocated = 0; +uint64_t buffers_released = 0; static void init_buffer(rtsp_conn_info *conn) { // debug(1,"input_bytes_per_frame: %d.", conn->input_bytes_per_frame); // debug(1,"input_bit_depth: %d.", conn->input_bit_depth); int i; - for (i = 0; i < BUFFER_FRAMES; i++) + for (i = 0; i < BUFFER_FRAMES; i++) { // conn->audio_buffer[i].data = malloc(conn->input_bytes_per_frame * // conn->max_frames_per_packet); - conn->audio_buffer[i].data = malloc(8 * conn->max_frames_per_packet); // todo + void *allocation = malloc(8 * conn->max_frames_per_packet); + if (allocation == NULL) { + die("could not allocate memory for audio buffers. %" PRId64 " buffers allocated, %" PRId64 + " buffers released.", + buffers_allocated, buffers_released); + } else { + conn->audio_buffer[i].data = allocation; + buffers_allocated++; + } + } } static void free_audio_buffers(rtsp_conn_info *conn) { int i; - for (i = 0; i < BUFFER_FRAMES; i++) + for (i = 0; i < BUFFER_FRAMES; i++) { free(conn->audio_buffer[i].data); + buffers_released++; + } + debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, + buffers_released); } int first_possibly_missing_frame = -1; @@ -1718,14 +1733,14 @@ void player_thread_cleanup_handler(void *arg) { int64_t elapsedMin = (time_playing / 60) % 60; int64_t elapsedSec = time_playing % 60; if (conn->frame_rate_valid) - inform("Connection %d: Playback Stopped. Total playing time %02" PRId64 ":%02" PRId64 + inform("Connection %d: Playback stopped. Total playing time %02" PRId64 ":%02" PRId64 ":%02" PRId64 ". " "Output: %0.2f (raw), %0.2f (corrected) " "frames per second.", conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->raw_frame_rate, conn->corrected_frame_rate); else - inform("Connection %d: Playback Stopped. Total playing time %02" PRId64 ":%02" PRId64 + inform("Connection %d: Playback stopped. Total playing time %02" PRId64 ":%02" PRId64 ":%02" PRId64 ".", conn->connection_number, elapsedHours, elapsedMin, elapsedSec); } @@ -1738,8 +1753,8 @@ void player_thread_cleanup_handler(void *arg) { #endif // four possibilities - // 1 -- regular AirPlay 1 - // 2 -- AirPlay 2 in AirPlay 1 mode + // 1 -- Classic Airplay -- "AirPlay 1" + // 2 -- AirPlay 2 in Classic Airplay mode // 3 -- AirPlay 2 in Buffered Audio Mode // 4 -- AirPlay 3 in Realtime Audio Mode. @@ -1822,7 +1837,9 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; - +#ifdef CONFIG_METADATA + uint64_t time_of_last_metadata_progress_update; +#endif uint64_t previous_frames_played = 0; // initialised to avoid a "possibly uninitialised" warning uint64_t previous_raw_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning @@ -2182,7 +2199,9 @@ void *player_thread_func(void *arg) { debug(2, "Play begin"); while (1) { - +#ifdef CONFIG_METADATA + int this_is_the_first_frame = 0; // will be set if it is +#endif // check a few parameters to ensure they are non-zero if (config.output_rate == 0) debug(1, "config.output_rate is zero!"); @@ -2675,6 +2694,9 @@ void *player_thread_func(void *arg) { if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; +#ifdef CONFIG_METADATA + this_is_the_first_frame = 1; +#endif // debug(2,"first frame real sync error (positive --> late): %" PRId64 " frames.", // sync_error); @@ -2725,23 +2747,42 @@ void *player_thread_func(void *arg) { sync_error = 0; // say the error was fixed! } // since this is the first frame of audio, inform the user if requested... - if (config.statistics_requested) { #ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_stream_type == realtime_stream) { - if (conn->airplay_type == ap_1) - inform("Connection %d: Playback Started -- AirPlay 1 Compatible.", - conn->connection_number); - else - inform("Connection %d: Playback Started -- AirPlay 2 Realtime.", - conn->connection_number); - } else { - inform("Connection %d: Playback Started -- AirPlay 2 Buffered.", - conn->connection_number); - } -#else - inform("Connection %d: Playback Started -- AirPlay 1.", conn->connection_number); + if (conn->airplay_stream_type == realtime_stream) { + if (conn->airplay_type == ap_1) { +#ifdef CONFIG_METADATA + send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif + if (config.statistics_requested) + inform("Connection %d: Playback started at frame %" PRId64 + " -- Classic AirPlay (\"AirPlay 1\") Compatible.", + conn->connection_number, inframe->given_timestamp); + } else { +#ifdef CONFIG_METADATA + send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); +#endif + if (config.statistics_requested) + inform("Connection %d: Playback started at frame %" PRId64 + " -- AirPlay 2 Realtime.", + conn->connection_number, inframe->given_timestamp); + } + } else { +#ifdef CONFIG_METADATA + send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); +#endif + if (config.statistics_requested) + inform("Connection %d: Playback started at frame %" PRId64 + " -- AirPlay 2 Buffered.", + conn->connection_number, inframe->given_timestamp); } +#else +#ifdef CONFIG_METADATA + send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); +#endif + if (config.statistics_requested) + inform("Connection %d: Playback started at frame %" PRId64 " -- Classic AirPlay (\"AirPlay 1\").", + conn->connection_number, inframe->given_timestamp); +#endif } // not too sure if abs() is implemented for int64_t, so we'll do it manually int64_t abs_sync_error = sync_error; @@ -3007,8 +3048,36 @@ void *player_thread_func(void *arg) { } uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); + config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); +#ifdef CONFIG_METADATA + // debug(1,"config.metadata_progress_interval is %f.", + // config.metadata_progress_interval); + if (config.metadata_progress_interval != 0.0) { + char hb[128]; + if (this_is_the_first_frame != 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); + send_ssnc_metadata('phb0', hb, strlen(hb), 1); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } else { + uint64_t mx = 1000000000; + uint64_t iv = config.metadata_progress_interval * mx; + iv = iv + time_of_last_metadata_progress_update; + int64_t delta = iv - local_time_now; + if (delta <= 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } + } + } +#endif } } @@ -3040,6 +3109,9 @@ void *player_thread_func(void *arg) { // if this is the first frame, see if it's close to when it's supposed to be // release, which will be its time plus latency and any offset_time if (at_least_one_frame_seen_this_session == 0) { +#ifdef CONFIG_METADATA + this_is_the_first_frame = 1; +#endif at_least_one_frame_seen_this_session = 1; } @@ -3057,6 +3129,33 @@ void *player_thread_func(void *arg) { frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); +#ifdef CONFIG_METADATA + // debug(1,"config.metadata_progress_interval is %f.", + // config.metadata_progress_interval); + if (config.metadata_progress_interval != 0.0) { + char hb[128]; + if (this_is_the_first_frame != 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); + send_ssnc_metadata('phb0', hb, strlen(hb), 1); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } else { + uint64_t mx = 1000000000; + uint64_t iv = config.metadata_progress_interval * mx; + iv = iv + time_of_last_metadata_progress_update; + int64_t delta = iv - local_time_now; + if (delta <= 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } + } + } +#endif } } diff --git a/rtp.c b/rtp.c index 88e05d0d..e61d521c 100644 --- a/rtp.c +++ b/rtp.c @@ -2842,12 +2842,12 @@ void *rtp_buffered_audio_processor(void *arg) { // https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction // Note: the eight-byte nonce must be front-padded out to 12 bytes. - + response = crypto_aead_chacha20poly1305_ietf_decrypt( m + 7, // m &new_payload_length, // mlen_p NULL, // nsec, - packet + 12, // the ciphertext starts 12 bytes in and is followed by the MAC tag, + packet + 12, // the ciphertext starts 12 bytes in and is followed by the MAC tag, nread - (8 + 12), // clen -- the last 8 bytes are the nonce packet + 4, // authenticated additional data 8, // authenticated additional data length diff --git a/rtsp.c b/rtsp.c index 52add733..c68feb43 100644 --- a/rtsp.c +++ b/rtsp.c @@ -47,6 +47,7 @@ #include #include #include +#include #include @@ -1219,12 +1220,18 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time ssize_t result = 0; // closed if (conn->fd > 0) { int64_t remaining_time = 0; - uint64_t time_to_wait_to = get_absolute_time_in_ns();; + uint64_t time_to_wait_to = get_absolute_time_in_ns(); + ; time_to_wait_to = time_to_wait_to + wait_time; + + int flags = 1; + if (setsockopt(conn->fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { + debug(1,"can't enable keepalive checking on the RTSP socket"); + } // remaining_time will be zero if wait_time is zero if (wait_time != 0) { - remaining_time = time_to_wait_to - get_absolute_time_in_ns(); + remaining_time = time_to_wait_to - get_absolute_time_in_ns(); } do { struct timeval tv; @@ -1240,7 +1247,8 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time #ifdef CONFIG_AIRPLAY_2 if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; - result = read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); + result = + read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); } else { result = read(conn->fd, buf, count); } @@ -1250,11 +1258,13 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time if (wait_time != 0) remaining_time = time_to_wait_to - get_absolute_time_in_ns(); if (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)) - debug(1,"remaining time on a timed read is %" PRId64 " ns.", remaining_time); - } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)); + debug(1, "remaining time on a timed read is %" PRId64 " ns.", remaining_time); + } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && + (remaining_time > 0)); } else { - debug(1,"Connection %d: attempt to read from a closed RTSP connection.",conn->connection_number); + debug(1, "Connection %d: attempt to read from a closed RTSP connection.", + conn->connection_number); } return result; } @@ -1268,9 +1278,7 @@ void set_client_as_ptp_clock(rtsp_conn_info *conn) { ptp_send_control_message_string(timing_list_message); } -void clear_ptp_clock() { - ptp_send_control_message_string("T"); -} +void clear_ptp_clock() { ptp_send_control_message_string("T"); } #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { @@ -1333,7 +1341,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes while (msg_size < 0) { if (conn->stop != 0) { - debug(3, "Connection %d: shutdown requested.", conn->connection_number); + debug(3, "Connection %d: Shutdown requested by client.", conn->connection_number); reply = rtsp_read_request_response_immediate_shutdown_requested; goto shutdown; } @@ -1341,16 +1349,14 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); if (nread == 0) { - // a blocking read that returns zero means eof -- implies connection closed - - debug(1, "Connection %d: Closed by client: from %s:%u to self at %s:%u.", - conn->connection_number, - conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); - conn->fd = 0; + // a blocking read that returns zero means eof -- implies connection closed by client + debug(1, "Connection %d: Connection closed by client.", conn->connection_number); reply = rtsp_read_request_response_channel_closed; + // Note: the socket will be closed when the thread exits goto shutdown; } + + // An ETIMEDOUT error usually means keepalive has failed. if (nread < 0) { if (errno == EINTR) @@ -1672,7 +1678,6 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) #ifdef CONFIG_AIRPLAY_2 - void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug_log_rtsp_message(2, "GET /info:", req); @@ -1894,8 +1899,8 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message if ((conn->ap2_flush_requested != 0) && (conn->ap2_flush_from_valid != 0) && (flushFromValid != 0)) { // if there is a request already, and it's a deferred request, and the current request is also - // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de Morgan's - // Law, but this seems clearer + // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de + // Morgan's Law, but this seems clearer } else { conn->ap2_flush_from_sequence_number = flushFromSeq; conn->ap2_flush_from_rtp_timestamp = flushFromTS; @@ -1915,7 +1920,7 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(2, "Deferred Flush Requested"); else debug(2, "Immediate Flush Requested"); - + plist_free(messagePlist); // display_all_flush_requests(conn); } @@ -2014,7 +2019,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message #ifdef CONFIG_METADATA send_ssnc_metadata('paus', NULL, 0, 1); // pause -- contains cancellation points #endif - if (config.output->stop) { + if (config.output->stop) { debug(2, "Connection %d: Stop the output backend.", conn->connection_number); config.output->stop(); } @@ -2570,38 +2575,38 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp debug(2, "Connection %d: SETPEERS %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "SETPEERS request", req); -/* - char timing_list_message[4096]; - timing_list_message[0] = 'T'; - timing_list_message[1] = 0; + /* + char timing_list_message[4096]; + timing_list_message[0] = 'T'; + timing_list_message[1] = 0; - // ensure the client itself is first -- it's okay if it's duplicated later - strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, (const char *)&conn->client_ip_string, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // ensure the client itself is first -- it's okay if it's duplicated later + strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - + strlen(timing_list_message)); strncat(timing_list_message, (const char + *)&conn->client_ip_string, sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - plist_t addresses_array = NULL; - plist_from_memory(req->content, req->contentlength, &addresses_array); - uint32_t items = plist_array_get_size(addresses_array); - if (items) { - uint32_t item; - for (item = 0; item < items; item++) { - plist_t n = plist_array_get_item(addresses_array, item); - char *ip_address = NULL; - plist_get_string_val(n, &ip_address); - // debug(1,ip_address); - strncat(timing_list_message, " ", - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, ip_address, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - if (ip_address != NULL) - free(ip_address); + plist_t addresses_array = NULL; + plist_from_memory(req->content, req->contentlength, &addresses_array); + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + uint32_t item; + for (item = 0; item < items; item++) { + plist_t n = plist_array_get_item(addresses_array, item); + char *ip_address = NULL; + plist_get_string_val(n, &ip_address); + // debug(1,ip_address); + strncat(timing_list_message, " ", + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, ip_address, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + if (ip_address != NULL) + free(ip_address); + } + ptp_send_control_message_string(timing_list_message); } - ptp_send_control_message_string(timing_list_message); - } - plist_free(addresses_array); -*/ - //set_client_as_ptp_clock(conn); + plist_free(addresses_array); + */ + // set_client_as_ptp_clock(conn); resp->respcode = 200; } #endif @@ -2848,7 +2853,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t messagePlist = plist_from_rtsp_content(req); plist_t setupResponsePlist = plist_new_dict(); resp->respcode = 400; - + // see if we can get a name for the client char *clientNameString = NULL; plist_t nameItem = plist_dict_get_item(messagePlist, "name"); @@ -2867,6 +2872,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "and open a TCP port.", conn->connection_number); conn->airplay_stream_category = unspecified_stream_category; + // figure out what category of stream it is, by looking at the plist plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); if (timingProtocol != NULL) { @@ -2875,8 +2881,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { debug(1, "Connection %d: AP2 PTP connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; #ifdef CONFIG_METADATA @@ -2890,23 +2896,27 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) #endif } else if (strcmp(timingProtocolString, "NTP") == 0) { debug(1, "Connection %d: SETUP: NTP setup from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ntp_stream; conn->timing_type = ts_ntp; } else if (strcmp(timingProtocolString, "None") == 0) { - debug(3, "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + debug(3, + "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at " + "%s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); // now check to see if it's got the "isRemoteControlOnly" item and check it's true plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); if (isRemoteControlOnly != NULL) { uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + debug( + 1, + "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = remote_control_stream; } else { debug(1, @@ -3039,7 +3049,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) freeifaddrs(addrs); // debug(1,"initial timing peer command: \"%s\".", timing_list_message); - //ptp_send_control_message_string(timing_list_message); + // ptp_send_control_message_string(timing_list_message); set_client_as_ptp_clock(conn); plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); @@ -3160,7 +3170,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // get the session key -- it must have one plist_t item = plist_dict_get_item(stream0, "shk"); // session key - uint64_t item_value = 0; // the length + uint64_t item_value = 0; // the length plist_get_data_val(item, (char **)&conn->session_key, &item_value); // more stuff @@ -3338,7 +3348,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_array_append_item(streams_array, stream0dict); plist_dict_set_item(setupResponsePlist, "streams", streams_array); resp->respcode = 200; - + } else if (conn->airplay_stream_category == remote_control_stream) { debug(2, "Connection %d (RC): SETUP: Remote Control Stream received from %s.", conn->connection_number, conn->client_ip_string); @@ -3672,6 +3682,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // play session is stopped. // `svip` -- the payload is the IP number of the server, i.e. the player itself. // Can be an IPv4 or an IPv6 number. +// `svna` -- the payload is the service name of the player, i.e. the name by +// which it is seen in the AirPlay menu. // `disc` -- the payload is the IP number of the client, i.e. the sender of audio. // Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. // It is sent when a client has been disconnected. @@ -4452,8 +4464,8 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag conn->airplay_type = ap_1; conn->timing_type = ts_ntp; debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); #endif conn->stream.type = ast_unknown; @@ -4547,7 +4559,7 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag if ((paesiv == NULL) && (prsaaeskey == NULL)) { // debug(1,"Unencrypted session requested?"); conn->stream.encrypted = 0; - } else if ((paesiv != NULL) && (prsaaeskey != NULL)){ + } else if ((paesiv != NULL) && (prsaaeskey != NULL)) { conn->stream.encrypted = 1; // debug(1,"Encrypted session requested"); } else { @@ -4751,7 +4763,7 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { } for (i = 0; i < 6; i++) - *bp++ = config.hw_addr[i]; + *bp++ = config.ap1_prefix[i]; int buflen, resplen; buflen = bp - buf; @@ -4984,26 +4996,34 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { } #endif - debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", + debug(3, "Connection %d: terminating -- closing timing, control and audio sockets...", conn->connection_number); if (conn->control_socket) { + debug(3, "Connection %d: terminating -- closing control_socket %d.", conn->connection_number, + conn->control_socket); close(conn->control_socket); + conn->control_socket = 0; } if (conn->timing_socket) { + debug(3, "Connection %d: terminating -- closing timing_socket %d.", conn->connection_number, + conn->timing_socket); close(conn->timing_socket); + conn->timing_socket = 0; } if (conn->audio_socket) { + debug(3, "Connection %d: terminating -- closing audio_socket %d.", conn->connection_number, + conn->audio_socket); close(conn->audio_socket); + conn->audio_socket = 0; } - if (conn->fd > 0) { - debug(3, "Connection %d terminating: closing fd %d.", conn->connection_number, conn->fd); - close(conn->fd); - debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); - - debug(1, "Connection %d Closed by self: from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + debug(2, + "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at " + "%s:%u.", + conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); + close(conn->fd); + conn->fd = 0; } if (conn->auth_nonce) { free(conn->auth_nonce); @@ -5475,6 +5495,41 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { } else { size_of_reply = sizeof(SOCKADDR); if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { + + // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. + + // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up + // an ETIMEOUT error is returned if the keepalive check fails + + int keepAliveIdleTime = 35; // wait this many seconds before checking for a dropped client + int keepAliveCount = 5; // check this many times + int keepAliveInterval = 5; // wait this many seconds between checks + + +#if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX + #define SOL_OPTION IPPROTO_TCP +#else + #define SOL_OPTION SOL_TCP +#endif + +#ifdef COMPILE_FOR_OSX + #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE +#else + #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE +#endif + + + if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { + debug(1,"can't set the keepidle wait time"); + } + + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, sizeof(keepAliveCount))) { + debug(1,"can't set the keepidle missing count"); + } + if (setsockopt(conn->fd, SOL_OPTION , TCP_KEEPINTVL, (void *)&keepAliveInterval, sizeof(keepAliveInterval))) { + debug(1,"can't set the keepidle missing count interval"); + }; + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 730b4c1c..660d2b2a 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -216,6 +216,9 @@ metadata = // cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart"; // artwork will be stored in this directory if the dbus or MPRIS interfaces are enabled or if the MQTT client is in use. Set it to "" to prevent caching, which may be useful on some systems // pipe_name = "/tmp/shairport-sync-metadata"; // pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up +// progress_interval = 0.0; // if non-zero, progress 'phbt' messages will be sent at the interval specified in seconds. A 'phb0' message will also be sent when the first audio frame of a play session is about to be played. +// Each message consists of the RTPtime of a a frame of audio and the exact system time when it is to be played. The system time, in nanoseconds, is based the CLOCK_MONOTONIC_RAW of the machine -- if available -- or CLOCK_MONOTONIC otherwise. +// Messages are sent when the frame is placed in the output device's buffer, thus, they will be _approximately_ 'audio_backend_buffer_desired_length_in_seconds' (default 0.2 seconds) ahead of time. // socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes" // socket_port = 5555; // if socket_address is set, the port to send UDP packets to // socket_msglength = 65000; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500. diff --git a/shairport.c b/shairport.c index f8f85ad9..96fdcbec 100644 --- a/shairport.c +++ b/shairport.c @@ -999,6 +999,10 @@ int parse_options(int argc, char **argv) { config.metadata_pipename = (char *)str; } + if (config_lookup_float(config.cfg, "metadata.progress_interval", &dvalue)) { + config.metadata_progress_interval = dvalue; + } + if (config_lookup_string(config.cfg, "metadata.socket_address", &str)) { config.metadata_sockaddr = (char *)str; } @@ -1596,10 +1600,10 @@ void exit_function() { #ifdef CONFIG_SOXR // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down if (soxr_time_check_thread_started != 0) { - debug(1, "Waiting for SoXr timecheck to terminate..."); + debug(2, "Waiting for SoXr timecheck to terminate..."); pthread_join(soxr_time_check_thread, NULL); soxr_time_check_thread_started = 0; - debug(1, "Waiting for SoXr timecheck to terminate done"); + debug(2, "Waiting for SoXr timecheck to terminate done"); } #endif @@ -2454,34 +2458,43 @@ int main(int argc, char **argv) { soxr_time_check_thread_started = 1; #endif - /* - uint8_t ap_md5[16]; + // calculate the 12-hex-digit prefix by hashing the service name. + uint8_t ap_md5[16]; - #ifdef CONFIG_OPENSSL - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, config.service_name, strlen(config.service_name)); - MD5_Final(ap_md5, &ctx); - #endif + debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); +#ifdef CONFIG_OPENSSL + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, config.service_name, strlen(config.service_name)); + MD5_Update(&ctx, config.hw_addr, sizeof(config.hw_addr)); + MD5_Final(ap_md5, &ctx); +#endif - #ifdef CONFIG_MBEDTLS - #if MBEDTLS_VERSION_MINOR >= 7 - mbedtls_md5_context tctx; - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, - strlen(config.service_name)); mbedtls_md5_finish_ret(&tctx, ap_md5); #else mbedtls_md5_context - tctx; mbedtls_md5_starts(&tctx); mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, - strlen(config.service_name)); mbedtls_md5_finish(&tctx, ap_md5); #endif #endif +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_context tctx; + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish_ret(&tctx, ap_md5); +#else + mbedtls_md5_context tctx; + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish(&tctx, ap_md5); +#endif +#endif - #ifdef CONFIG_POLARSSL - md5_context tctx; - md5_starts(&tctx); - md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); - md5_finish(&tctx, ap_md5); - #endif +#ifdef CONFIG_POLARSSL + md5_context tctx; + md5_starts(&tctx); + md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + md5_finish(&tctx, ap_md5); +#endif - memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr)); - */ + memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix)); #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary @@ -2537,6 +2550,15 @@ int main(int argc, char **argv) { } #endif +#ifdef CONFIG_METADATA + send_ssnc_metadata('svna', config.service_name, strlen(config.service_name), 1); + char buffer[256] = ""; + snprintf(buffer, sizeof(buffer), "%d", config.output_rate); + send_ssnc_metadata('ofps', buffer, strlen(buffer), 1); + snprintf(buffer, sizeof(buffer), "%s", sps_format_description_string(config.output_format)); + send_ssnc_metadata('ofmt', buffer, strlen(buffer), 1); +#endif + activity_monitor_start(); // not yet for AP2 pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL); atexit(exit_rtsp_listener);