From 106d0b327aa1c65b7c08e53ff31842d88eaf07cb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Jan 2021 18:01:39 +0000 Subject: [PATCH 001/878] This is 3.3.8rc3 --- configure.ac | 10 +++++----- shairport-sync.spec | 4 +++- shairport.c | 4 ++-- tests/configure_test.sh | 20 +++++++++++++------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/configure.ac b/configure.ac index 341bb389..62a37e69 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3.8d4], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [3.3.8d5], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -124,9 +124,9 @@ AC_ARG_WITH(apple-alac,[AS_HELP_STRING([--with-apple-alac],[include support for if test "x${with_apple_alac}" = "xyes" ; then AC_DEFINE([CONFIG_APPLE_ALAC], 1, [Include support for using the Apple ALAC Decoder]) if test "x${with_pkg_config}" = xyes ; then - PKG_CHECK_MODULES([alac], [alac], [LIBS="${alac_LIBS} ${LIBS}"], AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library.)) + PKG_CHECK_MODULES([alac], [alac], [LIBS="${alac_LIBS} ${LIBS}"], AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.)) else - AC_CHECK_LIB([alac], [BitBufferInit], , AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library.)) + AC_CHECK_LIB([alac], [BitBufferInit], , AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.)) fi fi AM_CONDITIONAL([USE_APPLE_ALAC], [test "x${with_apple_alac}" = "xyes"]) @@ -182,7 +182,7 @@ elif test "x${with_ssl}" = xmbedtls ; then [AC_MSG_ERROR([mbed tls support requires the mbedx509 library -- libmbedx509-0 suggested],1)]) elif test "x${with_ssl}" = xpolarssl ; then AC_DEFINE([CONFIG_POLARSSL], 1, [Use the PolarSSL libraries for encryption and encoding and decoding]) - AC_CHECK_LIB([polarssl],[ssl_init], , AC_MSG_ERROR(PolarSSL is selected but the library cannot be found and is deprecated. Consider selecting mbed TLS instead using --with-ssl=mbedtls.)) + AC_CHECK_LIB([polarssl],[ssl_init], , AC_MSG_ERROR(PolarSSL is selected but the library libpolarssl-dev does not seem to be installed and is deprecated. Consider selecting mbedtls instead using --with-ssl=mbedtls.)) else AC_MSG_ERROR(specify one of --with-ssl=openssl or --with-ssl=mbedtls or --with-ssl=polarssl) fi @@ -311,7 +311,7 @@ AM_CONDITIONAL([USE_CONVOLUTION], [test "x$with_convolution" = "xyes"]) AC_ARG_WITH(dns_sd, [AS_HELP_STRING([--with-dns_sd],[choose dns_sd mDNS support])]) if test "x$with_dns_sd" = "xyes" ; then AC_DEFINE([CONFIG_DNS_SD], 1, [Include dns_sd mDNS support.]) - AC_SEARCH_LIBS([DNSServiceRefDeallocate], [dns_sd], , [AC_MSG_ERROR(dns_sd support requires the dns_sd library!)]) + AC_SEARCH_LIBS([DNSServiceRefDeallocate], [dns_sd], , [AC_MSG_ERROR(dns_sd support requires the dns_sd library. Originally in Apple's mDNSResponder but also in libavahi-compat-libdnssd-dev.)]) fi AM_CONDITIONAL([USE_DNS_SD], [test "x$with_dns_sd" = "xyes"]) diff --git a/shairport-sync.spec b/shairport-sync.spec index 7755c8fe..215b303c 100644 --- a/shairport-sync.spec +++ b/shairport-sync.spec @@ -1,5 +1,5 @@ Name: shairport-sync -Version: 3.3.5 +Version: 3.3.7 Release: 1%{?dist} Summary: AirTunes emulator. Multi-Room with Audio Synchronisation # MIT licensed except for tinysvcmdns under BSD, @@ -66,6 +66,8 @@ getent passwd %{name} &> /dev/null || useradd --system -c "%{name} User" \ %license LICENSES %changelog +* Tue Nov 1 2020 Mike Brady > $LOGFILE make -j $((`nproc`*2)) >> $LOGFILE 2>&1 if [ "$?" -ne "0" ] ; then - echo "\nError at build step with arg \"$A2\"." + echo "\nError at build step with arg \"$A2\". See \"../configure_test.log\"." exit 1 fi echo -n "ok" else - echo "\nError at configure step with arg \"$A2\"." + echo "\nError at configure step with arg \"$A2\". See \"../configure_test.log\"." exit 1 fi if [ "$A4" != "" ] ; then @@ -88,7 +93,7 @@ check_for_configuration_fail() TESTCOUNT="$(expr "$TESTCOUNT" '+' '1')" ./configure $3 $2 > $LOGFILE 2>&1 if [ "$?" -eq "0" ] ; then - echo "\nError: configuration did not fail with arg \"$2\"." + echo "\nError: configuration did not fail with arg \"$2\". See \"../configure_test.log\"." exit 1 fi echo " done." @@ -102,7 +107,7 @@ CWD=`pwd` cd .. autoreconf -fi > $LOGFILE 2>&1 if [ "$?" -ne "0" ] ; then - echo "\Error running \"autoreconf -fi\"" + echo " error running \"autoreconf -fi\" -- see \"../configure_test.log\"." exit 1 fi echo "ok." @@ -198,4 +203,5 @@ check_for_success x$1 --with-systemv '--sysconfdir=/etc --with-libdaemon --with- check_for_success x$1 --without-systemv '--sysconfdir=/etc --with-libdaemon --with-alsa --with-soxr --with-avahi --with-ssl=openssl' OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc cd $CWD -echo "$TESTCOUNT tests completed." \ No newline at end of file +echo "$TESTCOUNT tests completed." + From 18c9e13a026a4ab81acfcb551aeae0941e08b236 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Jan 2021 21:32:57 +0000 Subject: [PATCH 002/878] Clean up with clang-format --- common.c | 2 +- dacp.c | 8 ++--- rtsp.c | 96 +++++++++++++++++++++++++++++--------------------------- 3 files changed, 54 insertions(+), 52 deletions(-) diff --git a/common.c b/common.c index 8520c714..6e0f12ab 100644 --- a/common.c +++ b/common.c @@ -834,7 +834,7 @@ void command_set_volume(double volume) { _exit(EXIT_FAILURE); /* only if execv fails */ } } - _exit(EXIT_SUCCESS); + _exit(EXIT_SUCCESS); } else { if (config.cmd_blocking) { /* pid!=0 means parent process and if blocking is true, wait for process to finish */ diff --git a/dacp.c b/dacp.c index effbe5ec..0230ac1c 100644 --- a/dacp.c +++ b/dacp.c @@ -446,11 +446,11 @@ void set_dacp_server_information(rtsp_conn_info *conn) { if (dacp_server.active_remote_id) free(dacp_server.active_remote_id); if (conn->dacp_active_remote) - dacp_server.active_remote_id = - strdup(conn->dacp_active_remote); // even if the dacp_id remains the same, - // the active remote will change. + dacp_server.active_remote_id = + strdup(conn->dacp_active_remote); // even if the dacp_id remains the same, + // the active remote will change. else - dacp_server.active_remote_id = NULL; + dacp_server.active_remote_id = NULL; debug(3, "set_dacp_server_information set active-remote id to %s.", dacp_server.active_remote_id); pthread_cond_signal(&dacp_server_information_cv); diff --git a/rtsp.c b/rtsp.c index f3524ae2..2661ccec 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1652,40 +1652,41 @@ void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) { #endif void metadata_init(void) { - int ret; - if (config.metadata_enabled) { - // create the metadata pipe, if necessary - size_t pl = strlen(config.metadata_pipename) + 1; - char *path = malloc(pl + 1); - snprintf(path, pl + 1, "%s", config.metadata_pipename); - mode_t oldumask = umask(000); - if (mkfifo(path, 0666) && errno != EEXIST) - die("Could not create metadata pipe \"%s\".", path); - umask(oldumask); - debug(1, "metadata pipe name is \"%s\".", path); + int ret; + if (config.metadata_enabled) { + // create the metadata pipe, if necessary + size_t pl = strlen(config.metadata_pipename) + 1; + char *path = malloc(pl + 1); + snprintf(path, pl + 1, "%s", config.metadata_pipename); + mode_t oldumask = umask(000); + if (mkfifo(path, 0666) && errno != EEXIST) + die("Could not create metadata pipe \"%s\".", path); + umask(oldumask); + debug(1, "metadata pipe name is \"%s\".", path); - // try to open it - fd = try_to_open_pipe_for_writing(path); - // we check that it's not a "real" error. From the "man 2 open" page: - // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO - // open for reading." Which is okay. - if ((fd == -1) && (errno != ENXIO)) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "metadata_hub_thread_function -- error %d (\"%s\") opening pipe: \"%s\".", errno, - (char *)errorstring, path); - warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, - (char *)errorstring, path); - } - free(path); - int ret; - ret = pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL); - if (ret) - debug(1, "Failed to create metadata thread!"); + // try to open it + fd = try_to_open_pipe_for_writing(path); + // we check that it's not a "real" error. From the "man 2 open" page: + // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO + // open for reading." Which is okay. + if ((fd == -1) && (errno != ENXIO)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "metadata_hub_thread_function -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + } + free(path); + int ret; + ret = pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL); + if (ret) + debug(1, "Failed to create metadata thread!"); - ret = pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL); - if (ret) - debug(1, "Failed to create metadata multicast thread!"); + ret = + pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL); + if (ret) + debug(1, "Failed to create metadata multicast thread!"); } #ifdef CONFIG_METADATA_HUB ret = pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL); @@ -1715,19 +1716,19 @@ void metadata_stop(void) { pthread_join(metadata_hub_thread, NULL); // debug(2, "metadata stop hub done."); #endif - if (config.metadata_enabled) { - // debug(2, "metadata stop multicast thread."); - if (metadata_multicast_thread) { - pthread_cancel(metadata_multicast_thread); - pthread_join(metadata_multicast_thread, NULL); - // debug(2, "metadata stop multicast done."); - } - if (metadata_thread) { - // debug(2, "metadata stop metadata_thread thread."); - pthread_cancel(metadata_thread); - pthread_join(metadata_thread, NULL); - // debug(2, "metadata_stop finished successfully."); - } + if (config.metadata_enabled) { + // debug(2, "metadata stop multicast thread."); + if (metadata_multicast_thread) { + pthread_cancel(metadata_multicast_thread); + pthread_join(metadata_multicast_thread, NULL); + // debug(2, "metadata stop multicast done."); + } + if (metadata_thread) { + // debug(2, "metadata stop metadata_thread thread."); + pthread_cancel(metadata_thread); + pthread_join(metadata_thread, NULL); + // debug(2, "metadata_stop finished successfully."); + } } } } @@ -1799,8 +1800,9 @@ int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rts int block) { int rc; if (config.metadata_enabled) { - rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); - rc = send_metadata_to_queue(&metadata_multicast_queue, type, code, data, length, carrier, block); + rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); + rc = + send_metadata_to_queue(&metadata_multicast_queue, type, code, data, length, carrier, block); } #ifdef CONFIG_METADATA_HUB From 23ac1e31e12b1c1778fb1831ca4275b49ce7727f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:45:23 +0000 Subject: [PATCH 003/878] Fix omission of libdaemon when it is requested. --- configure.ac | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 62a37e69..36697d7a 100644 --- a/configure.ac +++ b/configure.ac @@ -147,17 +147,18 @@ AM_CONDITIONAL([USE_CUSTOMPIDDIR], [ test "x${with_piddir}" != "x" ]) # Look for libdaemon AC_ARG_WITH(libdaemon,[AS_HELP_STRING([--with-libdaemon],[include support for daemonising in non-systemd systems])]) -if test "x$with_libdaemon" = "x1"; then +if test "x$with_libdaemon" = "xyes"; then AC_DEFINE([CONFIG_LIBDAEMON], 1, [Include libdaemon for daemonising in non-systemd systems]) if test "x${with_pkg_config}" = xyes ; then PKG_CHECK_MODULES( [DAEMON], [libdaemon], - [LIBS="${DAEMON_LIBS} ${LIBS}"]) + [LIBS="${DAEMON_LIBS} ${LIBS}"], + [AC_MSG_ERROR(the libdaemon library has been selected but is missing -- libdaemon-dev suggested!)]) else - AC_CHECK_LIB([daemon],[daemon_fork], , AC_MSG_ERROR(libdaemon needed)) + AC_CHECK_LIB([daemon],[daemon_fork], , AC_MSG_ERROR(the libdaemon library has been selected but is missing -- libdaemon-dev suggested!)) fi fi -AM_CONDITIONAL([USE_LIBDAEMON], [test "x$with_libdaemon" = "x1"]) +AM_CONDITIONAL([USE_LIBDAEMON], [test "x$with_libdaemon" = "xyes"]) # Check --with-ssl=argument AC_ARG_WITH(ssl, [AS_HELP_STRING([--with-ssl=],[choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=polarssl (deprecated) for encryption services ])]) @@ -241,7 +242,7 @@ if test "x$with_alsa" = "xyes" ; then else AC_CHECK_LIB([asound], [snd_pcm_open], , AC_MSG_ERROR(ALSA support requires the asound library!)) fi -fi +fi AM_CONDITIONAL([USE_ALSA], [test "x$with_alsa" = "xyes"]) # Look for jack flag @@ -317,7 +318,7 @@ AM_CONDITIONAL([USE_DNS_SD], [test "x$with_dns_sd" = "xyes"]) # Look for dbus flag AC_ARG_WITH(dbus-interface, [AS_HELP_STRING([--with-dbus-interface],[include support for the native Shairport Sync D-Bus interface])]) -if test "x$with_dbus_interface" = "xyes" ; then +if test "x$with_dbus_interface" = "xyes" ; then AC_DEFINE([CONFIG_DBUS_INTERFACE], 1, [Support the native Shairport Sync D-Bus interface]) # remember to include glib, below fi From 91d1c26636368c7f25b9604b45dd69105de52e5c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:48:29 +0000 Subject: [PATCH 004/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 618edea6..8bb26f4c 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 3.3.8d5 +==== +**Bug Fixes** +* Fix a fault in the configuration script `configure.ac` that caused the `libdaemon` library to be omitted even when requested. Thanks to [aleszczynskig](https://github.com/aleszczynskig) and [xska2](https://github.com/xska2)for the [report](https://github.com/mikebrady/shairport-sync/issues/1137). + Version 3.3.8d4 ==== **Bug Fixes** From 53770b0a8abdcbb3b102fdf6f83431c9cfe2d176 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:48:47 +0000 Subject: [PATCH 005/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 8bb26f4c..b1eab80b 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 3.3.8d5 ==== **Bug Fixes** -* Fix a fault in the configuration script `configure.ac` that caused the `libdaemon` library to be omitted even when requested. Thanks to [aleszczynskig](https://github.com/aleszczynskig) and [xska2](https://github.com/xska2)for the [report](https://github.com/mikebrady/shairport-sync/issues/1137). +* Fix a fault in the configuration script `configure.ac` that caused the `libdaemon` library to be omitted even when requested. Thanks to [aleszczynskig](https://github.com/aleszczynskig) and [xska2](https://github.com/xska2) for the [report](https://github.com/mikebrady/shairport-sync/issues/1137). Version 3.3.8d4 ==== From c5a3b438ffeaa44484ab1668ea3082834b9b476c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 7 Mar 2021 11:17:06 +0000 Subject: [PATCH 006/878] Update shairport-sync.conf --- scripts/shairport-sync.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 6444b8a5..724e76c1 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -134,6 +134,7 @@ sndio = pa = { // server = "host"; // Set this to override the default pulseaudio server that should be used. +// sink = "SinkName"; // Set this to override the default pulseaudio sink that should be used. (Untested" // application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active. }; From 91adfedfd202225f8f29437d34325528cb462a71 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 7 Mar 2021 11:18:56 +0000 Subject: [PATCH 007/878] Update shairport-sync.conf --- scripts/shairport-sync.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 724e76c1..d5b034ea 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -134,7 +134,7 @@ sndio = pa = { // server = "host"; // Set this to override the default pulseaudio server that should be used. -// sink = "SinkName"; // Set this to override the default pulseaudio sink that should be used. (Untested" +// sink = "Sink Name"; // Set this to override the default pulseaudio sink that should be used. (Untested" // application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active. }; From 8de7f34611e942a2f7ad821925ca41c5e88c40e7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 27 Mar 2021 14:20:22 +0000 Subject: [PATCH 008/878] fix typo --- scripts/shairport-sync.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index d5b034ea..e7f9e748 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -134,7 +134,7 @@ sndio = pa = { // server = "host"; // Set this to override the default pulseaudio server that should be used. -// sink = "Sink Name"; // Set this to override the default pulseaudio sink that should be used. (Untested" +// sink = "Sink Name"; // Set this to override the default pulseaudio sink that should be used. (Untested) // application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active. }; From fd4c429b5a8126c0fff276f41a41af771bec247e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 27 Mar 2021 15:37:29 +0000 Subject: [PATCH 009/878] Be more careful with changing precision. Stop using snd_pcm_status_get_driver_htstamp. --- audio_alsa.c | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 69722d12..45a51321 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1456,18 +1456,23 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, if (ret == 0) { // must be 1.1 or later to use snd_pcm_status_get_driver_htstamp -#if SND_LIB_MINOR == 0 +//#if SND_LIB_MINOR == 0 snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); -#else - snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &update_timestamp); -#endif +//#else +// snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &update_timestamp); +//#endif *state = snd_pcm_status_get_state(alsa_snd_pcm_status); if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) { - uint64_t update_timestamp_ns = - update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; + // uint64_t update_timestamp_ns = + // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; + + uint64_t update_timestamp_ns = update_timestamp.tv_sec; + update_timestamp_ns = update_timestamp_ns * 1000000000; + update_timestamp_ns = update_timestamp_ns + update_timestamp.tv_nsec; + // if the update_timestamp is zero, we take this to mean that the device doesn't report // interrupt timings. (It could be that it's not a real hardware device.) @@ -1497,7 +1502,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, if (update_timestamp_ns == 0) { ret = snd_pcm_delay(alsa_handle, delay); } else { - *delay = snd_pcm_status_get_delay(alsa_snd_pcm_status); + snd_pcm_sframes_t delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status); /* // It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though @@ -1514,11 +1519,15 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, else clock_gettime(CLOCK_REALTIME, &tn); - uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; + // uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; + uint64_t time_now_ns = tn.tv_sec; + time_now_ns = time_now_ns * 1000000000; + time_now_ns = time_now_ns + tn.tv_nsec; + // see if it's stalled - if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == *delay)) { + if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == delay_temp)) { // hasn't outputted anything since the last call to delay() if (((update_timestamp_ns - stall_monitor_start_time) > stall_monitor_error_threshold) || @@ -1539,19 +1548,27 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } } else { stall_monitor_start_time = update_timestamp_ns; - stall_monitor_frame_count = *delay; + stall_monitor_frame_count = delay_temp; } if (ret == 0) { uint64_t delta = time_now_ns - update_timestamp_ns; - uint64_t frames_played_since_last_interrupt = - ((uint64_t)config.output_rate * delta) / 1000000000; +// uint64_t frames_played_since_last_interrupt = +// ((uint64_t)config.output_rate * delta) / 1000000000; + + uint64_t frames_played_since_last_interrupt = config.output_rate; + frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta; + frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000; + + snd_pcm_sframes_t frames_played_since_last_interrupt_sized = frames_played_since_last_interrupt; - - *delay = *delay - frames_played_since_last_interrupt_sized; + if ((frames_played_since_last_interrupt_sized < 0) || ((uint64_t)frames_played_since_last_interrupt_sized != frames_played_since_last_interrupt)) + debug(1,"overflow resizing frames_played_since_last_interrupt % " PRIx64 " to frames_played_since_last_interrupt %lx.", frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); + delay_temp = delay_temp - frames_played_since_last_interrupt_sized; } + *delay = delay_temp; } } else { // not running, thus no delay information, thus can't check for // stall From 6a42c7656927dca6103abe9a5418394d5af28ea4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 27 Mar 2021 16:12:42 +0000 Subject: [PATCH 010/878] Stop using snd_pcm_status_get_driver_htstamp in the alsa precision timing function. It seems to have changed (?). --- audio_alsa.c | 17 ++++++++++++----- configure.ac | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 45a51321..1360619d 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1455,12 +1455,19 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, int ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status); if (ret == 0) { -// must be 1.1 or later to use snd_pcm_status_get_driver_htstamp -//#if SND_LIB_MINOR == 0 snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); -//#else -// snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &update_timestamp); -//#endif + +/* +// must be 1.1 or later to use snd_pcm_status_get_driver_htstamp +#if SND_LIB_MINOR != 0 + snd_htimestamp_t driver_htstamp; + snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp); + uint64_t driver_htstamp_ns = driver_htstamp.tv_sec; + driver_htstamp_ns = driver_htstamp_ns * 1000000000; + driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec; + debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001); +#endif +*/ *state = snd_pcm_status_get_state(alsa_snd_pcm_status); diff --git a/configure.ac b/configure.ac index 36697d7a..21ba3cec 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3.8d5], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [3.3.8d6], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From 07d46787210b10a4e49d0d486af2604d3c9b772e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 27 Mar 2021 16:20:29 +0000 Subject: [PATCH 011/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b1eab80b..62563c11 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 3.3.8d6 +==== +**Bug Fix** +* Fix a bug in the `alsa` precision timing code. Thanks to [durwin99](https://github.com/durwin99), [Nicolas Da Mutten](https://github.com/cleverer), [mistakenideas](https://github.com/mistakenideas), [Ben Willmore](https://github.com/ben-willmore) and [giggywithit](https://github.com/giggywithit) for the [report](https://github.com/mikebrady/shairport-sync/issues/1158). + Version 3.3.8d5 ==== **Bug Fixes** From ec2323637072ad8358b0d7d55a708e3056b0047d Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Thu, 1 Apr 2021 08:50:48 -0700 Subject: [PATCH 012/878] pipewire: add initial support --- Makefile.am | 4 + audio.c | 6 + audio_pw.c | 501 +++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 15 ++ 4 files changed, 526 insertions(+) create mode 100644 audio_pw.c diff --git a/Makefile.am b/Makefile.am index c9ccb75e..0de2c4ba 100644 --- a/Makefile.am +++ b/Makefile.am @@ -80,6 +80,10 @@ if USE_PA shairport_sync_SOURCES += audio_pa.c endif +if USE_PW +shairport_sync_SOURCES += audio_pw.c +endif + if USE_CONVOLUTION shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp AM_CXXFLAGS += -std=c++11 diff --git a/audio.c b/audio.c index 0fe7b04f..10efbd9e 100644 --- a/audio.c +++ b/audio.c @@ -43,6 +43,9 @@ extern audio_output audio_ao; #ifdef CONFIG_SOUNDIO extern audio_output audio_soundio; #endif +#ifdef CONFIG_PW +extern audio_output audio_pw; +#endif #ifdef CONFIG_PA extern audio_output audio_pa; #endif @@ -66,6 +69,9 @@ static audio_output *outputs[] = { #ifdef CONFIG_SNDIO &audio_sndio, #endif +#ifdef CONFIG_PW + &audio_pw, +#endif #ifdef CONFIG_PA &audio_pa, #endif diff --git a/audio_pw.c b/audio_pw.c new file mode 100644 index 00000000..58a5fd72 --- /dev/null +++ b/audio_pw.c @@ -0,0 +1,501 @@ +/* + * Asynchronous Pipewire Backend. This file is part of Shairport Sync. + * Copyright (c) Shairport Sync 2021 + * 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 "audio.h" +#include "common.h" + +#include +#include +#include +#include +#include + +#include + +struct pw_data { + struct pw_thread_loop *mainloop; + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct pw_buffer *pw_buffer; + + struct pw_properties *props; + int sync; + + enum spa_audio_format format; + uint32_t rate; + uint32_t channels; + uint32_t stride; + uint32_t latency; + +} data; + +static void on_core_info(__attribute__((unused)) void *userdata, const struct pw_core_info *info) +{ + debug(1, "pw: remote %"PRIu32" is named \"%s\"", info->id, info->name); +} + +static void on_core_error(__attribute__((unused)) void *userdata, uint32_t id, int seq, int res, const char *message) +{ + warn("pw: remote error: id=%"PRIu32" seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info, + .error = on_core_error, +}; + +static void registry_event_global(__attribute__((unused)) void *userdata, uint32_t id, + __attribute__((unused)) uint32_t permissions, const char *type, __attribute__((unused)) uint32_t version, + const struct spa_dict *props) +{ + const struct spa_dict_item *item; + const char *name, *media_class; + + if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + + if (!name || !media_class) + return; + + debug(1, "pw: registry: id=%"PRIu32" type=%s name=\"%s\" media_class=\"%s\"", id, type, name, media_class); + + spa_dict_for_each(item, props) { + debug(1, "pw: \t\t%s = \"%s\"", item->key, item->value); + } + } +} + +static void registry_event_global_remove(__attribute__((unused)) void *userdata, uint32_t id) +{ + debug(1, "pw: registry: remove id=%"PRIu32"", id); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + struct pw_data *pipewire = userdata; + + debug(1, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old), pw_stream_state_as_string(state)); + + if (state == PW_STREAM_STATE_STREAMING) + debug(1, "pw: stream node %"PRIu32"", pw_stream_get_node_id(pipewire->stream)); + + if (state == PW_STREAM_STATE_ERROR) + debug(1, "pw: stream node %"PRIu32" error: %s", pw_stream_get_node_id(pipewire->stream), error); + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static void on_process(void *userdata) +{ + struct pw_data *pipewire = userdata; + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static void on_drained(void *userdata) +{ + struct pw_data *pipewire = userdata; + + pw_stream_set_active(pipewire->stream, false); + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed, + .process = on_process, + .drained = on_drained, +}; + +static void deinit() { + pw_thread_loop_stop(data.mainloop); + + if (data.stream) { + pw_stream_destroy(data.stream); + data.stream = NULL; + } + + if (data.registry) { + pw_proxy_destroy((struct pw_proxy*)data.registry); + data.registry = NULL; + } + + if (data.core) { + pw_core_disconnect(data.core); + data.core = NULL; + } + + if (data.context) { + pw_context_destroy(data.context); + data.context = NULL; + } + + if (data.mainloop) { + pw_thread_loop_destroy(data.mainloop); + data.mainloop = NULL; + } + + if (data.props) { + pw_properties_free(data.props); + data.props = NULL; + } +} + +static int init(__attribute__((unused)) int argc, __attribute__((unused)) char** argv) { + + struct pw_loop *loop; + struct pw_properties* props; + + // set up default values first + config.audio_backend_buffer_desired_length = 0.35; + config.audio_backend_buffer_interpolation_threshold_in_seconds = 0.02; + config.audio_backend_latency_offset = 0; + + pw_init(NULL, NULL); + + debug(1, "pw: compiled with libpipewire %s", pw_get_headers_version()); + debug(1, "pw: linked with libpipewire: %s", pw_get_library_version()); + + data.props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + PW_KEY_APP_NAME, "shairport-sync", + PW_KEY_NODE_NAME, "shairport-sync", + NULL); + + if (!data.props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + data.mainloop = pw_thread_loop_new("pipewire", NULL); + if (!data.mainloop) { + deinit(); + die("pw: pw_thread_loop_new_full() failed: %m"); + } + + props = pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL); + if (!props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + loop = pw_thread_loop_get_loop(data.mainloop); + + data.context = pw_context_new(loop, props, 0); + if (!data.context) { + deinit(); + die("pw: pw_context_new() failed: %m"); + } + + props = pw_properties_new(PW_KEY_REMOTE_NAME, NULL, NULL); + if (!props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + pw_thread_loop_lock(data.mainloop); + + if (pw_thread_loop_start(data.mainloop) != 0) { + deinit(); + die("pw: pw_thread_loop_start() failed: %m"); + } + + data.core = pw_context_connect(data.context, props, 0); + if (!data.core) { + deinit(); + die("pw: pw_context_connect() failed: %m"); + } + + pw_core_add_listener(data.core, &data.core_listener, &core_events, &data); + + data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, 0); + if (!data.registry) { + deinit(); + die("pw: pw_core_get_registry() failed: %m"); + } + + pw_registry_add_listener(data.registry, &data.registry_listener, ®istry_events, &data); + + data.sync = pw_core_sync(data.core, 0, data.sync); + + pw_thread_loop_unlock(data.mainloop); + + return 0; +} + +static enum spa_audio_format sps_format_to_spa_format(sps_format_t sps_format) { + switch (sps_format) { + case SPS_FORMAT_S8: + return SPA_AUDIO_FORMAT_S8; + case SPS_FORMAT_U8: + return SPA_AUDIO_FORMAT_U8; + case SPS_FORMAT_S16: + return SPA_AUDIO_FORMAT_S16; + case SPS_FORMAT_S16_LE: + return SPA_AUDIO_FORMAT_S16_LE; + case SPS_FORMAT_S16_BE: + return SPA_AUDIO_FORMAT_S16_BE; + case SPS_FORMAT_S24: + return SPA_AUDIO_FORMAT_S24_32; + case SPS_FORMAT_S24_LE: + return SPA_AUDIO_FORMAT_S24_32_LE; + case SPS_FORMAT_S24_BE: + return SPA_AUDIO_FORMAT_S24_32_BE; + case SPS_FORMAT_S24_3LE: + return SPA_AUDIO_FORMAT_S24_LE; + case SPS_FORMAT_S24_3BE: + return SPA_AUDIO_FORMAT_S24_BE; + case SPS_FORMAT_S32: + return SPA_AUDIO_FORMAT_S32; + case SPS_FORMAT_S32_LE: + return SPA_AUDIO_FORMAT_S32_LE; + case SPS_FORMAT_S32_BE: + return SPA_AUDIO_FORMAT_S32_BE; + + case SPS_FORMAT_UNKNOWN: + case SPS_FORMAT_AUTO: + case SPS_FORMAT_INVALID: + default: + return SPA_AUDIO_FORMAT_S16; + } +} + +static int spa_format_samplesize(enum spa_audio_format audio_format) +{ + switch(audio_format) { + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_U8: + return 1; + case SPA_AUDIO_FORMAT_S16: + return 2; + case SPA_AUDIO_FORMAT_S24: + return 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S32: + return 4; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return -1; + } +} + +static const char* spa_format_to_str(enum spa_audio_format audio_format) +{ + switch(audio_format) { + case SPA_AUDIO_FORMAT_U8: + return "u8"; + case SPA_AUDIO_FORMAT_S8: + return "s8"; + case SPA_AUDIO_FORMAT_S16: + return "s16"; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_32: + return "s24"; + case SPA_AUDIO_FORMAT_S32: + return "s32"; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return "(invalid)"; + } +} + +static void start(int sample_rate, int sample_format) { + + pw_thread_loop_lock(data.mainloop); + + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + struct spa_audio_info_raw info; + uint32_t nom; + int ret; + + data.format = sps_format_to_spa_format(sample_format); + data.rate = sample_rate; + data.channels = 2; + data.stride = spa_format_samplesize(data.format) * data.channels; + data.latency = 20000; + + nom = nearbyint((data.latency * data.rate) / 1000000.0); + + pw_properties_setf(data.props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data.rate); + + debug(1, "pw: rate: %d", data.rate); + debug(1, "pw: channgels: %d", data.channels); + debug(1, "pw: format: %s", spa_format_to_str(data.format)); + debug(1, "pw: samplesize: %d", spa_format_samplesize(data.format)); + debug(1, "pw: stride: %d", data.stride); + debug(1, "pw: latency: %d samples (%.3fs)", nom, (double)nom/data.rate); + + info = SPA_AUDIO_INFO_RAW_INIT( + .flags = SPA_AUDIO_FLAG_NONE, + .format = data.format, + .rate = data.rate, + .channels = data.channels); + + params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &info); + + data.stream = pw_stream_new(data.core, "shairport-sync", data.props); + + if (!data.stream) { + deinit(); + die("pw: pw_stream_new() failed: %m"); + } + + debug(1, "pw: connecting stream: target_id=%"PRIu32"", PW_ID_ANY); + + pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data); + + ret = pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + + if (ret < 0) { + deinit(); + die("pw: pw_stream_connect() failed: %s", spa_strerror(ret)); + } + + const struct pw_properties *props; + void *pstate; + const char *key, *val; + + if ((props = pw_stream_get_properties(data.stream)) != NULL) { + debug(1, "pw: stream properties:"); + pstate = NULL; + while ((key = pw_properties_iterate(props, &pstate)) != NULL && + (val = pw_properties_get(props, key)) != NULL) { + debug(1, "pw: \t%s = \"%s\"", key, val); + } + } + + while(1) { + enum pw_stream_state stream_state = pw_stream_get_state(data.stream, NULL); + if (stream_state == PW_STREAM_STATE_PAUSED) + break; + + pw_thread_loop_wait(data.mainloop); + } + + pw_thread_loop_unlock(data.mainloop); +} + +static void stop() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, true); + + pw_thread_loop_unlock(data.mainloop); +} + +static void flush() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, false); + + pw_thread_loop_unlock(data.mainloop); +} + +static int play(void *buf, int samples) { + struct pw_buffer *pw_buffer; + struct spa_buffer *spa_buffer; + struct spa_data *spa_data; + + pw_thread_loop_lock(data.mainloop); + + if (pw_stream_get_state(data.stream, NULL) == PW_STREAM_STATE_PAUSED) + pw_stream_set_active(data.stream, true); + + while (pw_buffer == NULL) { + pw_buffer = pw_stream_dequeue_buffer(data.stream); + if (pw_buffer) + break; + + pw_thread_loop_wait(data.mainloop); + } + + spa_buffer = pw_buffer->buffer; + spa_data = &spa_buffer->datas[0]; + + size_t bytes_to_copy = samples * data.stride; + + debug(3, "pw: bytes_to_copy: %d", bytes_to_copy); + + if (spa_data->maxsize < bytes_to_copy) + bytes_to_copy = spa_data->maxsize; + + debug(3, "pw: spa_data->maxsize: %d", spa_data->maxsize); + + memcpy(spa_data->data, buf, bytes_to_copy); + + spa_data->chunk->offset = 0; + spa_data->chunk->stride = data.stride; + spa_data->chunk->size = bytes_to_copy; + + pw_stream_queue_buffer(data.stream, pw_buffer); + + pw_thread_loop_unlock(data.mainloop); + + return 0; +} + +audio_output audio_pw = { + .name = "pw", + .help = NULL, + .init = &init, + .deinit = &deinit, + .prepare = NULL, + .start = &start, + .stop = &stop, + .is_running = NULL, + .flush = &flush, + .delay = NULL, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL +}; diff --git a/configure.ac b/configure.ac index 21ba3cec..34701f84 100644 --- a/configure.ac +++ b/configure.ac @@ -299,6 +299,21 @@ if test "x$with_pa" = "xyes" ; then fi AM_CONDITIONAL([USE_PA], [test "x$with_pa" = "xyes"]) +# Look for pipewire flag +AC_ARG_WITH(pw, [AS_HELP_STRING([--with-pw],[choose Pipewire support.])]) +if test "x$with_pw" = "xyes" ; then + AC_DEFINE([CONFIG_PW], 1, [Include Pipewire support.]) + if test "x${with_pkg_config}" = xyes ; then + PKG_CHECK_MODULES( + [PIPEWIRE], [libpipewire-0.3 >= 0.3.24], + [CFLAGS="${PIPEWIRE_CFLAGS} ${CFLAGS} -Wno-missing-field-initializers" LIBS="${PIPEWIRE_LIBS} ${LIBS}"], + [AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library!)]) + else + AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library.)) + fi +fi +AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"]) + # Look for Convolution flag AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP convolution support])]) if test "x$with_convolution" = "xyes" ; then From 561a129617762281ef948881ae10a9209ae2e376 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Apr 2021 13:56:13 +0100 Subject: [PATCH 013/878] Fix a race condition where the alsa_handle could be checked for null but then set to null before use. The code between the check and the use is now protected. --- audio_alsa.c | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 1360619d..b464279c 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1606,25 +1606,26 @@ int delay(long *the_delay) { // sps_extra_code_output_state_cannot_make_ready codes int ret = 0; *the_delay = 0; + + int oldState; + + snd_pcm_state_t state; + snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); + if (alsa_handle == NULL) ret = ENODEV; - else { - int oldState; - - snd_pcm_state_t state; - snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning - - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); - + else ret = delay_and_status(&state, &my_delay, NULL); - debug_mutex_unlock(&alsa_mutex, 0); - pthread_cleanup_pop(0); - pthread_setcancelstate(oldState, NULL); + debug_mutex_unlock(&alsa_mutex, 0); + pthread_cleanup_pop(0); + pthread_setcancelstate(oldState, NULL); + + *the_delay = my_delay; // note: snd_pcm_sframes_t is a long - *the_delay = my_delay; // note: snd_pcm_sframes_t is a long - } return ret; } From 669c8d6ff930569a52562c012eb2e391f258bf15 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Apr 2021 13:59:44 +0100 Subject: [PATCH 014/878] Update configure.ac --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 21ba3cec..830c686a 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3.8d6], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [3.3.8d7], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From 92e3cd43b190db4f56f6143b81f93cf9f0ed5ae6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Apr 2021 14:03:51 +0100 Subject: [PATCH 015/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 62563c11..e490d184 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 3.3.8d7 +==== +**Bug Fix** +* Fix a bug in the `alsa` back end. In the interval between checking to see if the alsa device handle was NULL and actually using it, the handle could be set to NULL. The interval between check and usage is now protected. + Version 3.3.8d6 ==== **Bug Fix** From 8110851d5f5342c0c1865c9ac1796507775dae57 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 27 Apr 2021 19:26:08 +0100 Subject: [PATCH 016/878] Remove tinyhttp files prior to adding it as a submodule --- Makefile.am | 48 +++++++-- configure.ac | 17 +++- tinyhttp/LICENSE | 23 ----- tinyhttp/README.md | 28 ------ tinyhttp/chunk.c | 79 --------------- tinyhttp/chunk.h | 48 --------- tinyhttp/example.cpp | 190 ------------------------------------ tinyhttp/header.c | 72 -------------- tinyhttp/header.h | 61 ------------ tinyhttp/http.c | 227 ------------------------------------------- tinyhttp/http.h | 93 ------------------ 11 files changed, 54 insertions(+), 832 deletions(-) delete mode 100644 tinyhttp/LICENSE delete mode 100644 tinyhttp/README.md delete mode 100644 tinyhttp/chunk.c delete mode 100644 tinyhttp/chunk.h delete mode 100644 tinyhttp/example.cpp delete mode 100644 tinyhttp/header.c delete mode 100644 tinyhttp/header.h delete mode 100644 tinyhttp/http.c delete mode 100644 tinyhttp/http.h diff --git a/Makefile.am b/Makefile.am index c9ccb75e..f2d20713 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,11 +1,21 @@ SUBDIRS = man ARFLAGS = cr + +lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_OPENSSL -pthread +lib_csrp_a_CFLAGS = -pthread +lib_tinyhttp_a_CFLAGS = -pthread lib_dbus_interface_a_CFLAGS = -pthread lib_mpris_interface_a_CFLAGS = -pthread bin_PROGRAMS = shairport-sync +BUILT_SOURCES = +noinst_HEADERS = +CLEANFILES = +shairport_sync_LDADD = +noinst_LIBRARIES = + # See below for the flags for the test client program shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c alac.c audio.c loudness.c activity_monitor.c @@ -90,29 +100,47 @@ shairport_sync_SOURCES += mdns_dns_sd.c endif if USE_METADATA_HUB -shairport_sync_SOURCES += metadata_hub.c +shairport_sync_SOURCES += metadata_hub.c endif if USE_MQTT -shairport_sync_SOURCES += mqtt.c +shairport_sync_SOURCES += mqtt.c endif if USE_DACP_CLIENT -shairport_sync_SOURCES += dacp.c tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c +shairport_sync_SOURCES += dacp.c +shairport_sync_LDADD += lib_tinyhttp.a +noinst_LIBRARIES += lib_tinyhttp.a +lib_tinyhttp_a_SOURCES = tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c endif -BUILT_SOURCES = -noinst_HEADERS = -CLEANFILES = -shairport_sync_LDADD = -noinst_LIBRARIES = +if USE_AIRPLAY_2 +shairport_sync_SOURCES += ptp-utilities.c plist_xml_strings.c +shairport_sync_LDADD += lib_csrp.a lib_pair_ap.a +lib_pair_ap_a_SOURCES = pair_ap/pair.c pair_ap/pair_fruit.c pair_ap/pair_homekit.c pair_ap/pair-tlv.c +lib_csrp_a_SOURCES = csrp/srp.c +noinst_LIBRARIES += lib_pair_ap.a lib_csrp.a +plist_xml_strings.h: plists/get_info_response.xml + echo "// Do not edit!" > plist_xml_strings.h + echo "// This file is automatically generated from files in the plists folder." >> plist_xml_strings.h + echo "" >> plist_xml_strings.h + xxd -i plists/get_info_response.xml >> plist_xml_strings.h + echo "" >> plist_xml_strings.h +plist_xml_strings.c: plist_xml_strings.h + touch plist_xml_strings.c + +BUILT_SOURCES += plist_xml_strings.c plist_xml_strings.h +noinst_HEADERS += $(BUILT_SOURCES) +# Correctly clean the generated headers, but keep the xml description +CLEANFILES += $(BUILT_SOURCES) +endif if USE_DBUS shairport_sync_LDADD += lib_dbus_interface.a noinst_LIBRARIES += lib_dbus_interface.a lib_dbus_interface_a_SOURCES = dbus-interface.c shairport_sync_SOURCES += dbus-service.c -BUILT_SOURCES += dbus-interface.h dbus-interface.c +BUILT_SOURCES += dbus-interface.h dbus-interface.c # We don't want to install this header noinst_HEADERS += $(BUILT_SOURCES) # Correctly clean the generated headers, but keep the xml description @@ -203,7 +231,7 @@ if INSTALL_FREEBSD_SERVICE [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync chown shairport-sync:shairport-sync /var/run/shairport-sync [ -f /usr/local/etc/rc.d/shairport_sync ] || cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync - chmod 555 /usr/local/etc/rc.d/shairport_sync + chmod 555 /usr/local/etc/rc.d/shairport_sync endif if INSTALL_CYGWIN_SERVICE cp scripts/shairport-sync-config /usr/local/bin diff --git a/configure.ac b/configure.ac index 830c686a..79a08b4b 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3.8d7], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0d0], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -371,6 +371,21 @@ if test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1" || test "x$with_metadata" fi AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"]) +# Look for AirPlay 2 flag +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_MSG_RESULT(>>Include libraries required for AirPlay 2) + AC_CHECK_LIB([plist], [plist_from_memory], , AC_MSG_ERROR(AirPlay 2 support requires libplist -- libplist-dev suggested!)) + AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) + AC_CHECK_LIB([avutil],[av_malloc], [], [AirPlay([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) + AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [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])]) +fi +AM_CONDITIONAL([USE_AIRPLAY_2], [test "x$with_airplay_2" = "xyes"]) + if test "x${with_systemd}" = xyes ; then # Find systemd unit dir AC_ARG_WITH([systemdsystemunitdir], diff --git a/tinyhttp/LICENSE b/tinyhttp/LICENSE deleted file mode 100644 index ba420590..00000000 --- a/tinyhttp/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright 2012 Matthew Endsley -All rights reserved - -Redistribution and use in source and binary forms, with or without -modification, are permitted providing that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/tinyhttp/README.md b/tinyhttp/README.md deleted file mode 100644 index 367138f7..00000000 --- a/tinyhttp/README.md +++ /dev/null @@ -1,28 +0,0 @@ -tinyhttp -======== -Tiny (as in minimal) implementation of an HTTP response parser. The parser -itself is only dependent on: -* ` - tolower` -* ` - memcpy` - -For more information please see my blog post at: - -[![Build Status](https://api.travis-ci.org/mendsley/tinyhttp.png)](http://travis-ci.org/mendsley/tinyhttp) - -Contact -------- -[@MatthewEndsley](https://twitter.com/#!/MatthewEndsley) - - -License -------- -Copyright 2012 Matthew Endsley - -This project is governed by the BSD 2-clause license. For details see the file -titled LICENSE in the project root folder. - -Compiling ---------- -`gcc -c *.c && g++ -std=c++0x example.cpp *.o -o example` - -`./example` will fetch the root of diff --git a/tinyhttp/chunk.c b/tinyhttp/chunk.c deleted file mode 100644 index 3f7c889e..00000000 --- a/tinyhttp/chunk.c +++ /dev/null @@ -1,79 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -static const unsigned char http_chunk_state[] = { -/* * LF CR HEX */ - 0xC1, 0xC1, 0xC1, 1, /* s0: initial hex char */ - 0xC1, 0xC1, 2, 0x81, /* s1: additional hex chars, followed by CR */ - 0xC1, 0x83, 0xC1, 0xC1, /* s2: trailing LF */ - 0xC1, 0xC1, 4, 0xC1, /* s3: CR after chunk block */ - 0xC1, 0xC0, 0xC1, 0xC1, /* s4: LF after chunk block */ -}; - -int http_parse_chunked(int* state, int *size, char ch) -{ - int newstate, code = 0; - switch (ch) { - case '\n': code = 1; break; - case '\r': code = 2; break; - case '0': case '1': case '2': case '3': - case '4': case '5': case '6': case '7': - case '8': case '9': case 'a': case 'b': - case 'c': case 'd': case 'e': case 'f': - case 'A': case 'B': case 'C': case 'D': - case 'E': case 'F': code = 3; break; - } - - newstate = http_chunk_state[*state * 4 + code]; - *state = (newstate & 0xF); - - switch (newstate) { - case 0xC0: - return *size != 0; - - case 0xC1: /* error */ - *size = -1; - return 0; - - case 0x01: /* initial char */ - *size = 0; - /* fallthrough */ - case 0x81: /* size char */ - if (ch >= 'a') - *size = *size * 16 + (ch - 'a' + 10); - else if (ch >= 'A') - *size = *size * 16 + (ch - 'A' + 10); - else - *size = *size * 16 + (ch - '0'); - break; - - case 0x83: - return *size == 0; - } - - return 1; -} - diff --git a/tinyhttp/chunk.h b/tinyhttp/chunk.h deleted file mode 100644 index e0df0600..00000000 --- a/tinyhttp/chunk.h +++ /dev/null @@ -1,48 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef HTTP_CHUNK_H -#define HTTP_CHUNK_H - -#if defined(__cplusplus) -extern "C" { -#endif - -/** - * Parses the size out of a chunk-encoded HTTP response. Returns non-zero if it - * needs more data. Returns zero success or error. When error: size == -1 On - * success, size = size of following chunk data excluding trailing \r\n. User is - * expected to process or otherwise seek past chunk data up to the trailing - * \r\n. The state parameter is used for internal state and should be - * initialized to zero the first call. - */ -int http_parse_chunked(int* state, int *size, char ch); - -#if defined(__cplusplus) -} -#endif - -#endif diff --git a/tinyhttp/example.cpp b/tinyhttp/example.cpp deleted file mode 100644 index 4054f9a7..00000000 --- a/tinyhttp/example.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - - -/* -Compiling example: -$ g++ -o example example.cpp -*/ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "http.h" - -// directly embed the source here -extern "C" { - #include "http.c" - #include "header.c" - #include "chunk.c" -} - -// return a socket connected to a hostname, or -1 -int connectsocket(const char* host, int port) -{ - - addrinfo* result = NULL; - sockaddr_in addr = {0}; - int s; - - if (getaddrinfo(host, NULL, NULL, &result)) - goto error; - - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - addr.sin_addr.s_addr = INADDR_ANY; - for (addrinfo* ai = result; ai != NULL; ai = ai->ai_next) { - if (ai->ai_family != AF_INET) - continue; - - const sockaddr_in *ai_in = (const sockaddr_in*)ai->ai_addr; - addr.sin_addr = ai_in->sin_addr; - break; - } - - freeaddrinfo(result); - - if (addr.sin_addr.s_addr == INADDR_ANY) - goto error; - - s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (s == -1) - goto error; - - if (connect(s, (const sockaddr*)&addr, sizeof(addr))) - goto error; - - return s; - -error: - if (s != -1) - close(s); - if (result) - freeaddrinfo(result); - return -1; -} - -// Response data/funcs -struct HttpResponse { - std::vector body; - int code; -}; - -static void* response_realloc(void* opaque, void* ptr, int size) -{ - return realloc(ptr, size); -} - -static void response_body(void* opaque, const char* data, int size) -{ - HttpResponse* response = (HttpResponse*)opaque; - response->body.insert(response->body.end(), data, data + size); -} - -static void response_header(void* opaque, const char* ckey, int nkey, const char* cvalue, int nvalue) -{ /* example doesn't care about headers */ } - -static void response_code(void* opaque, int code) -{ - HttpResponse* response = (HttpResponse*)opaque; - response->code = code; -} - -static const http_funcs responseFuncs = { - response_realloc, - response_body, - response_header, - response_code, -}; - -int main() -{ - - int conn = connectsocket("nothings.org", 80); - if (conn < 0) { - fprintf(stderr, "Failed to connect socket\n"); - return -1; - } - - const char request[] = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n"; - int len = send(conn, request, sizeof(request) - 1, 0); - if (len != sizeof(request) - 1) { - fprintf(stderr, "Failed to send request\n"); - close(conn); - return -1; - } - - HttpResponse response; - response.code = 0; - - http_roundtripper rt; - http_init(&rt, responseFuncs, &response); - - bool needmore = true; - char buffer[1024]; - while (needmore) { - const char* data = buffer; - int ndata = recv(conn, buffer, sizeof(buffer), 0); - if (ndata <= 0) { - fprintf(stderr, "Error receiving data\n"); - http_free(&rt); - close(conn); - return -1; - } - - while (needmore && ndata) { - int read; - needmore = http_data(&rt, data, ndata, &read); - ndata -= read; - data += read; - } - } - - if (http_iserror(&rt)) { - fprintf(stderr, "Error parsing data\n"); - http_free(&rt); - close(conn); - return -1; - } - - http_free(&rt); - close(conn); - - printf("Response: %d\n", response.code); - if (!response.body.empty()) { - printf("%s\n", &response.body[0]); - } - - return 0; -} diff --git a/tinyhttp/header.c b/tinyhttp/header.c deleted file mode 100644 index cda9eb54..00000000 --- a/tinyhttp/header.c +++ /dev/null @@ -1,72 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include "header.h" - -static unsigned char http_header_state[] = { -/* * \t \n \r ' ' , : PAD */ - 0x80, 1, 0xC1, 0xC1, 1, 0x80, 0x80, 0xC1, /* state 0: HTTP version */ - 0x81, 2, 0xC1, 0xC1, 2, 1, 1, 0xC1, /* state 1: Response code */ - 0x82, 0x82, 4, 3, 0x82, 0x82, 0x82, 0xC1, /* state 2: Response reason */ - 0xC1, 0xC1, 4, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 3: HTTP version newline */ - 0x84, 0xC1, 0xC0, 5, 0xC1, 0xC1, 6, 0xC1, /* state 4: Start of header field */ - 0xC1, 0xC1, 0xC0, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 5: Last CR before end of header */ - 0x87, 6, 0xC1, 0xC1, 6, 0x87, 0x87, 0xC1, /* state 6: leading whitespace before header value */ - 0x87, 0x87, 0xC4, 10, 0x87, 0x88, 0x87, 0xC1, /* state 7: header field value */ - 0x87, 0x88, 6, 9, 0x88, 0x88, 0x87, 0xC1, /* state 8: Split value field value */ - 0xC1, 0xC1, 6, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 9: CR after split value field */ - 0xC1, 0xC1, 0xC4, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 10:CR after header value */ -}; - -int http_parse_header_char(int* state, char ch) -{ - int newstate, code = 0; - switch (ch) { - case '\t': code = 1; break; - case '\n': code = 2; break; - case '\r': code = 3; break; - case ' ': code = 4; break; - case ',': code = 5; break; - case ':': code = 6; break; - } - - newstate = http_header_state[*state * 8 + code]; - *state = (newstate & 0xF); - - switch (newstate) { - case 0xC0: return http_header_status_done; - case 0xC1: return http_header_status_done; - case 0xC4: return http_header_status_store_keyvalue; - case 0x80: return http_header_status_version_character; - case 0x81: return http_header_status_code_character; - case 0x82: return http_header_status_status_character; - case 0x84: return http_header_status_key_character; - case 0x87: return http_header_status_value_character; - case 0x88: return http_header_status_value_character; - } - - return http_header_status_continue; -} diff --git a/tinyhttp/header.h b/tinyhttp/header.h deleted file mode 100644 index 7f8ee6ab..00000000 --- a/tinyhttp/header.h +++ /dev/null @@ -1,61 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef HTTP_HEADER_H -#define HTTP_HEADER_H - -#if defined(__cplusplus) -extern "C" { -#endif - -enum http_header_status -{ - http_header_status_done, - http_header_status_continue, - http_header_status_version_character, - http_header_status_code_character, - http_header_status_status_character, - http_header_status_key_character, - http_header_status_value_character, - http_header_status_store_keyvalue -}; - -/** - * Parses a single character of an HTTP header stream. The state parameter is - * used as internal state and should be initialized to zero for the first call. - * Return value is a value from the http_header_status enuemeration specifying - * the semantics of the character. If an error is encountered, - * http_header_status_done will be returned with a non-zero state parameter. On - * success http_header_status_done is returned with the state parameter set to - * zero. - */ -int http_parse_header_char(int* state, char ch); - -#if defined(__cplusplus) -} -#endif - -#endif diff --git a/tinyhttp/http.c b/tinyhttp/http.c deleted file mode 100644 index 5ef0ae3f..00000000 --- a/tinyhttp/http.c +++ /dev/null @@ -1,227 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include "http.h" - -#include -#include - -#include "header.h" -#include "chunk.h" - -static void append_body(struct http_roundtripper* rt, const char* data, int ndata) -{ - rt->funcs.body(rt->opaque, data, ndata); -} - -static void grow_scratch(struct http_roundtripper* rt, int size) -{ - if (rt->nscratch >= size) - return; - - if (size < 64) - size = 64; - int nsize = (rt->nscratch * 3) / 2; - if (nsize < size) - nsize = size; - - rt->scratch = (char*)rt->funcs.realloc_scratch(rt->opaque, rt->scratch, nsize); - rt->nscratch = nsize; -} - -static int min(int a, int b) -{ - return a > b ? b : a; -} - -enum http_roundtripper_state { - http_roundtripper_header, - http_roundtripper_chunk_header, - http_roundtripper_chunk_data, - http_roundtripper_raw_data, - http_roundtripper_unknown_data, - http_roundtripper_close, - http_roundtripper_error, -}; - -void http_init(struct http_roundtripper* rt, struct http_funcs funcs, void* opaque) -{ - rt->funcs = funcs; - rt->scratch = 0; - rt->opaque = opaque; - rt->code = 0; - rt->parsestate = 0; - rt->contentlength = -1; - rt->state = http_roundtripper_header; - rt->nscratch = 0; - rt->nkey = 0; - rt->nvalue = 0; - rt->chunked = 0; -} - -void http_free(struct http_roundtripper* rt) -{ - if (rt->scratch) { - rt->funcs.realloc_scratch(rt->opaque, rt->scratch, 0); - rt->scratch = 0; - } -} - -int http_data(struct http_roundtripper* rt, const char* data, int size, int* read) -{ - const int initial_size = size; - while (size) { - switch (rt->state) { - case http_roundtripper_header: - switch (http_parse_header_char(&rt->parsestate, *data)) { - case http_header_status_done: - rt->funcs.code(rt->opaque, rt->code); - if (rt->parsestate != 0) - rt->state = http_roundtripper_error; - else if (rt->chunked) { - rt->contentlength = 0; - rt->state = http_roundtripper_chunk_header; - } else if (rt->contentlength == 0) - rt->state = http_roundtripper_close; - else if (rt->contentlength > 0) - rt->state = http_roundtripper_raw_data; - else if (rt->contentlength == -1) - rt->state = http_roundtripper_unknown_data; - else - rt->state = http_roundtripper_error; - break; - - case http_header_status_code_character: - rt->code = rt->code * 10 + *data - '0'; - break; - - case http_header_status_key_character: - grow_scratch(rt, rt->nkey + 1); - rt->scratch[rt->nkey] = tolower(*data); - ++rt->nkey; - break; - - case http_header_status_value_character: - grow_scratch(rt, rt->nkey + rt->nvalue + 1); - rt->scratch[rt->nkey+rt->nvalue] = *data; - ++rt->nvalue; - break; - - case http_header_status_store_keyvalue: - if (rt->nkey == 17 && 0 == strncmp(rt->scratch, "transfer-encoding", rt->nkey)) - rt->chunked = (rt->nvalue == 7 && 0 == strncmp(rt->scratch + rt->nkey, "chunked", rt->nvalue)); - else if (rt->nkey == 14 && 0 == strncmp(rt->scratch, "content-length", rt->nkey)) { - int ii, end; - rt->contentlength = 0; - for (ii = rt->nkey, end = rt->nkey + rt->nvalue; ii != end; ++ii) - rt->contentlength = rt->contentlength * 10 + rt->scratch[ii] - '0'; - } - - rt->funcs.header(rt->opaque, rt->scratch, rt->nkey, rt->scratch + rt->nkey, rt->nvalue); - - rt->nkey = 0; - rt->nvalue = 0; - break; - } - - --size; - ++data; - break; - - case http_roundtripper_chunk_header: - if (!http_parse_chunked(&rt->parsestate, &rt->contentlength, *data)) { - if (rt->contentlength == -1) - rt->state = http_roundtripper_error; - else if (rt->contentlength == 0) - rt->state = http_roundtripper_close; - else - rt->state = http_roundtripper_chunk_data; - } - - --size; - ++data; - break; - - case http_roundtripper_chunk_data: { - const int chunksize = min(size, rt->contentlength); - append_body(rt, data, chunksize); - rt->contentlength -= chunksize; - size -= chunksize; - data += chunksize; - - if (rt->contentlength == 0) { - rt->contentlength = 1; - rt->state = http_roundtripper_chunk_header; - } - } - break; - - case http_roundtripper_raw_data: { - const int chunksize = min(size, rt->contentlength); - append_body(rt, data, chunksize); - rt->contentlength -= chunksize; - size -= chunksize; - data += chunksize; - - if (rt->contentlength == 0) - rt->state = http_roundtripper_close; - } - break; - - case http_roundtripper_unknown_data: { - if (size == 0) - rt->state = http_roundtripper_close; - else { - append_body(rt, data, size); - size -= size; - data += size; - } - } - break; - - case http_roundtripper_close: - case http_roundtripper_error: - break; - } - - if (rt->state == http_roundtripper_error || rt->state == http_roundtripper_close) { - if (rt->scratch) { - rt->funcs.realloc_scratch(rt->opaque, rt->scratch, 0); - rt->scratch = 0; - } - *read = initial_size - size; - return 0; - } - } - - *read = initial_size - size; - return 1; -} - -int http_iserror(struct http_roundtripper* rt) -{ - return rt->state == http_roundtripper_error; -} diff --git a/tinyhttp/http.h b/tinyhttp/http.h deleted file mode 100644 index ba7e4cfb..00000000 --- a/tinyhttp/http.h +++ /dev/null @@ -1,93 +0,0 @@ -/*- - * Copyright 2012 Matthew Endsley - * All rights reserved - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted providing that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef HTTP_HTTP_H -#define HTTP_HTTP_H - -#if defined(__cplusplus) -extern "C" { -#endif - -/** - * Callbacks for handling response data. - * realloc_scratch - reallocate memory, cannot fail. There will only - * be one scratch buffer. Implemnentation may take - * advantage of this fact. - * body - handle HTTP response body data - * header - handle an HTTP header key/value pair - * code - handle the HTTP status code for the response - */ -struct http_funcs { - void* (*realloc_scratch)(void* opaque, void* ptr, int size); - void (*body)(void* opaque, const char* data, int size); - void (*header)(void* opaque, const char* key, int nkey, const char* value, int nvalue); - void (*code)(void* opqaue, int code); -}; - -struct http_roundtripper { - struct http_funcs funcs; - void *opaque; - char *scratch; - int code; - int parsestate; - int contentlength; - int state; - int nscratch; - int nkey; - int nvalue; - int chunked; -}; - -/** - * Initializes a rountripper with the specified response functions. This must - * be called before the rt object is used. - */ -void http_init(struct http_roundtripper* rt, struct http_funcs, void* opaque); - -/** - * Frees any scratch memory allocated during parsing. - */ -void http_free(struct http_roundtripper* rt); - -/** - * Parses a block of HTTP response data. Returns zero if the parser reached the - * end of the response, or an error was encountered. Use http_iserror to check - * for the presence of an error. Returns non-zero if more data is required for - * the response. - */ -int http_data(struct http_roundtripper* rt, const char* data, int size, int* read); - -/** - * Returns non-zero if a completed parser encounted an error. If http_data did - * not return non-zero, the results of this function are undefined. - */ -int http_iserror(struct http_roundtripper* rt); - -#if defined(__cplusplus) -} -#endif - -#endif From 796f1075a9f39803da6a0f40d9b3f1242446594b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 27 Apr 2021 19:27:43 +0100 Subject: [PATCH 017/878] tinyhttp added as a submodule --- .gitmodules | 3 +++ tinyhttp | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 tinyhttp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..8bd541ac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tinyhttp"] + path = tinyhttp + url = https://github.com/mendsley/tinyhttp.git diff --git a/tinyhttp b/tinyhttp new file mode 160000 index 00000000..a524562a --- /dev/null +++ b/tinyhttp @@ -0,0 +1 @@ +Subproject commit a524562a5318457d63ec21488c4ccc54202c0436 From c862dc5c5bd5e13d42d3b85091ed39ebe8c9a18d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 27 Apr 2021 19:28:55 +0100 Subject: [PATCH 018/878] pair_ap added as a submodule --- .gitmodules | 3 +++ pair_ap | 1 + 2 files changed, 4 insertions(+) create mode 160000 pair_ap diff --git a/.gitmodules b/.gitmodules index 8bd541ac..9d79c516 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "tinyhttp"] path = tinyhttp url = https://github.com/mendsley/tinyhttp.git +[submodule "pair_ap"] + path = pair_ap + url = https://github.com/ejurgensen/pair_ap diff --git a/pair_ap b/pair_ap new file mode 160000 index 00000000..ad2bc75e --- /dev/null +++ b/pair_ap @@ -0,0 +1 @@ +Subproject commit ad2bc75edaf09816a484c39dfebf6de016247db4 From 0af7f4fbdeb8af154300a75fc1c1486a013a8e89 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 27 Apr 2021 20:07:50 +0100 Subject: [PATCH 019/878] Bring in files and settings from sps2-ex1 --- .gitignore | 2 +- .gitmodules | 1 + configure.ac | 2 +- csrp/LICENSE | 21 + csrp/README.md | 155 ++++++ csrp/srp.c | 955 +++++++++++++++++++++++++++++++++++ csrp/srp.h | 193 +++++++ plists/get_info_response.xml | 66 +++ ptp-utilities.c | 167 ++++++ ptp-utilities.h | 39 ++ 10 files changed, 1599 insertions(+), 2 deletions(-) create mode 100644 csrp/LICENSE create mode 100644 csrp/README.md create mode 100644 csrp/srp.c create mode 100644 csrp/srp.h create mode 100644 plists/get_info_response.xml create mode 100644 ptp-utilities.c create mode 100644 ptp-utilities.h diff --git a/.gitignore b/.gitignore index 9d13084c..5187a2b8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ /shairport-sync-mpris-test-client *.o -*.log /*~ *.xml~ /config.mk @@ -33,6 +32,7 @@ shairport-sync.core /org.gnome.ShairportSync.service /dbus-interface.* /mpris-interface.* +/plist_xml_strings.* /lib_*.* # Some eclipse project files diff --git a/.gitmodules b/.gitmodules index 9d79c516..a2828361 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "tinyhttp"] path = tinyhttp url = https://github.com/mendsley/tinyhttp.git + ignore = dirty [submodule "pair_ap"] path = pair_ap url = https://github.com/ejurgensen/pair_ap diff --git a/configure.ac b/configure.ac index 79a08b4b..36d8bec3 100644 --- a/configure.ac +++ b/configure.ac @@ -378,7 +378,7 @@ if test "x$with_airplay_2" = "xyes" ; then AC_MSG_RESULT(>>Include libraries required for AirPlay 2) AC_CHECK_LIB([plist], [plist_from_memory], , AC_MSG_ERROR(AirPlay 2 support requires libplist -- libplist-dev suggested!)) AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) - AC_CHECK_LIB([avutil],[av_malloc], [], [AirPlay([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) + AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [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])]) diff --git a/csrp/LICENSE b/csrp/LICENSE new file mode 100644 index 00000000..030645de --- /dev/null +++ b/csrp/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Tom Cocagne + +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. diff --git a/csrp/README.md b/csrp/README.md new file mode 100644 index 00000000..4e655bc5 --- /dev/null +++ b/csrp/README.md @@ -0,0 +1,155 @@ +csrp +==== +Tom Cocagne <tom.cocagne@gmail.com> + +csrp is a minimal C implementation of the [Secure Remote Password +protocol](http://srp.stanford.edu/). The project consists of a single +C file and is intended for direct inclusion into utilizing programs. +It's only dependency is OpenSSL. + +*NOTE* This SRP implementation was created before the hashing algoritim specified +in RFC 5054 became the de-facto standard for interoperable SRP implementations. +The rfc5054_compat branch of this repository uses the RFC 5054 hashing algorithms +and is known to be compatible with other SRP implementations. If this version works +for you, please consider submitting a patch to this library that implements both the +original default and the RFC 5054 implementation to allow a single mainline version +of this library going forward. + + +SRP Overview +------------ + +SRP is a cryptographically strong authentication +protocol for password-based, mutual authentication over an insecure +network connection. + +Unlike other common challenge-response autentication protocols, such +as Kerberos and SSL, SRP does not rely on an external infrastructure +of trusted key servers or certificate management. Instead, SRP server +applications use verification keys derived from each user's password +to determine the authenticity of a network connection. + +SRP provides mutual-authentication in that successful authentication +requires both sides of the connection to have knowledge of the +user's password. If the client side lacks the user's password or the +server side lacks the proper verification key, the authentication will +fail. + +Unlike SSL, SRP does not directly encrypt all data flowing through +the authenticated connection. However, successful authentication does +result in a cryptographically strong shared key that can be used +for symmetric-key encryption. + +This library serves as the basis for a compatible Python module called +[pysrp](https://github.com/cocagne/pysrp). The +[pysrp](https://github.com/cocagne/pysrp) project contains complete, +user-friendly API documentation as well as a comprehensive overview of the SRP +protocol. As the APIs are virtually identical, the [pysrp +documentation](http://pythonhosted.org/srp/) is an excellent reference for +understanding this library. + + +Usage Example +------------- + +```c +#include +#include +#include + +#include "srp.h" + + +int main( int argc, char * argv[] ) +{ + int auth_failed = 1; + + struct SRPVerifier * ver; + struct SRPUser * usr; + + const unsigned char * bytes_s = 0; + const unsigned char * bytes_v = 0; + const unsigned char * bytes_A = 0; + const unsigned char * bytes_B = 0; + + const unsigned char * bytes_M = 0; + const unsigned char * bytes_HAMK = 0; + + int len_s = 0; + int len_v = 0; + int len_A = 0; + int len_B = 0; + int len_M = 0; + + const char * username = "testuser"; + const char * password = "password"; + + const char * auth_username = 0; + + SRP_HashAlgorithm alg = SRP_SHA1; + SRP_NGType ng_type = SRP_NG_2048; + + /* Create a salt+verification key for the user's password. The salt and + * key need to be computed at the time the user's password is set and + * must be stored by the server-side application for use during the + * authentication process. + */ + srp_create_salted_verification_key( alg, ng_type, username, + (const unsigned char *)password, + strlen(password), + &bytes_s, &len_s, + &bytes_v, &len_v, + NULL, NULL ); + + /* Begin authentication process */ + usr = srp_user_new( alg, ng_type, username, + (const unsigned char *)password, + strlen(password), NULL, NULL ); + + srp_user_start_authentication( usr, &auth_username, &bytes_A, &len_A ); + + /* User -> Host: (username, bytes_A) */ + ver = srp_verifier_new( alg, ng_type, username, bytes_s, len_s, bytes_v, len_v, + bytes_A, len_A, & bytes_B, &len_B, NULL, NULL ); + + if ( !bytes_B ) { + printf("Verifier SRP-6a safety check violated!\n"); + goto auth_failed; + } + + /* Host -> User: (bytes_s, bytes_B) */ + srp_user_process_challenge( usr, bytes_s, len_s, bytes_B, len_B, &bytes_M, &len_M ); + + if ( !bytes_M ) { + printf("User SRP-6a safety check violation!\n"); + goto auth_failed; + } + + /* User -> Host: (bytes_M) */ + srp_verifier_verify_session( ver, bytes_M, &bytes_HAMK ); + + if ( !bytes_HAMK ) { + printf("User authentication failed!\n"); + goto auth_failed; + } + + /* Host -> User: (HAMK) */ + srp_user_verify_session( usr, bytes_HAMK ); + + if ( !srp_user_is_authenticated(usr) ) { + printf("Server authentication failed!\n"); + goto auth_failed; + } + + auth_failed = 0; /* auth success! */ + +auth_failed: + srp_verifier_delete( ver ); + srp_user_delete( usr ); + + free( (char *)bytes_s ); + free( (char *)bytes_v ); + + return auth_failed; +} +``` diff --git a/csrp/srp.c b/csrp/srp.c new file mode 100644 index 00000000..1bfd908b --- /dev/null +++ b/csrp/srp.c @@ -0,0 +1,955 @@ +/* + * Secure Remote Password 6a implementation + * Copyright (c) 2010 Tom Cocagne. All rights reserved. + * https://github.com/cocagne/csrp + * + * The MIT License (MIT) + * + * Copyright (c) 2013 Tom Cocagne + * + * 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. + * + */ + +#ifdef WIN32 + #include +#else + #include +#endif + +#include +#include +#include + +#include +#include +#include +#include + + +#include "srp.h" + +static int g_initialized = 0; + +typedef struct +{ + BIGNUM * N; + BIGNUM * g; +} NGConstant; + +struct NGHex +{ + const char * n_hex; + const char * g_hex; +}; + +/* All constants here were pulled from Appendix A of RFC 5054 */ +static struct NGHex global_Ng_constants[] = { + { /* 1024 */ + "EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496" + "EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E" + "F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA" + "9AFD5138FE8376435B9FC61D2FC0EB06E3", + "2" + }, + { /* 2048 */ + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + { /* 4096 */ + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" + "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" + "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" + "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" + "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" + "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" + "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" + "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" + "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" + "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" + "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" + "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" + "FFFFFFFFFFFFFFFF", + "5" + }, + { /* 8192 */ + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" + "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" + "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" + "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" + "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" + "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" + "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" + "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" + "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" + "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" + "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" + "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406" + "AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918" + "DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151" + "2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03" + "F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F" + "BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B" + "B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632" + "387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E" + "6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA" + "3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C" + "5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" + "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886" + "2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6" + "6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5" + "0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268" + "359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6" + "FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" + "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", + "13" + }, + {0,0} /* null sentinel */ +}; + + +static NGConstant * new_ng( SRP_NGType ng_type, const char * n_hex, const char * g_hex ) +{ + NGConstant * ng = (NGConstant *) malloc( sizeof(NGConstant) ); + ng->N = BN_new(); + ng->g = BN_new(); + + if( !ng || !ng->N || !ng->g ) + return 0; + + if ( ng_type != SRP_NG_CUSTOM ) + { + n_hex = global_Ng_constants[ ng_type ].n_hex; + g_hex = global_Ng_constants[ ng_type ].g_hex; + } + + BN_hex2bn( &ng->N, n_hex ); + BN_hex2bn( &ng->g, g_hex ); + + return ng; +} + +static void delete_ng( NGConstant * ng ) +{ + if (ng) + { + BN_free( ng->N ); + BN_free( ng->g ); + ng->N = 0; + ng->g = 0; + free(ng); + } +} + + + +typedef union +{ + SHA_CTX sha; + SHA256_CTX sha256; + SHA512_CTX sha512; +} HashCTX; + + +struct SRPVerifier +{ + SRP_HashAlgorithm hash_alg; + NGConstant *ng; + + const char * username; + const unsigned char * bytes_B; + int authenticated; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; +}; + + +struct SRPUser +{ + SRP_HashAlgorithm hash_alg; + NGConstant *ng; + + BIGNUM *a; + BIGNUM *A; + BIGNUM *S; + + const unsigned char * bytes_A; + int authenticated; + + const char * username; + const unsigned char * password; + int password_len; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; +}; + + +static int hash_init( SRP_HashAlgorithm alg, HashCTX *c ) +{ + switch (alg) + { + case SRP_SHA1 : return SHA1_Init( &c->sha ); + case SRP_SHA224: return SHA224_Init( &c->sha256 ); + case SRP_SHA256: return SHA256_Init( &c->sha256 ); + case SRP_SHA384: return SHA384_Init( &c->sha512 ); + case SRP_SHA512: return SHA512_Init( &c->sha512 ); + default: + return -1; + }; +} +static int hash_update( SRP_HashAlgorithm alg, HashCTX *c, const void *data, size_t len ) +{ + switch (alg) + { + case SRP_SHA1 : return SHA1_Update( &c->sha, data, len ); + case SRP_SHA224: return SHA224_Update( &c->sha256, data, len ); + case SRP_SHA256: return SHA256_Update( &c->sha256, data, len ); + case SRP_SHA384: return SHA384_Update( &c->sha512, data, len ); + case SRP_SHA512: return SHA512_Update( &c->sha512, data, len ); + default: + return -1; + }; +} +static int hash_final( SRP_HashAlgorithm alg, HashCTX *c, unsigned char *md ) +{ + switch (alg) + { + case SRP_SHA1 : return SHA1_Final( md, &c->sha ); + case SRP_SHA224: return SHA224_Final( md, &c->sha256 ); + case SRP_SHA256: return SHA256_Final( md, &c->sha256 ); + case SRP_SHA384: return SHA384_Final( md, &c->sha512 ); + case SRP_SHA512: return SHA512_Final( md, &c->sha512 ); + default: + return -1; + }; +} +static unsigned char * hash( SRP_HashAlgorithm alg, const unsigned char *d, size_t n, unsigned char *md ) +{ + switch (alg) + { + case SRP_SHA1 : return SHA1( d, n, md ); + case SRP_SHA224: return SHA224( d, n, md ); + case SRP_SHA256: return SHA256( d, n, md ); + case SRP_SHA384: return SHA384( d, n, md ); + case SRP_SHA512: return SHA512( d, n, md ); + default: + return 0; + }; +} +static int hash_length( SRP_HashAlgorithm alg ) +{ + switch (alg) + { + case SRP_SHA1 : return SHA_DIGEST_LENGTH; + case SRP_SHA224: return SHA224_DIGEST_LENGTH; + case SRP_SHA256: return SHA256_DIGEST_LENGTH; + case SRP_SHA384: return SHA384_DIGEST_LENGTH; + case SRP_SHA512: return SHA512_DIGEST_LENGTH; + default: + return -1; + }; +} + + +static BIGNUM * H_nn( SRP_HashAlgorithm alg, const BIGNUM * n1, const BIGNUM * n2 ) +{ + unsigned char buff[ SHA512_DIGEST_LENGTH ]; + int len_n1 = BN_num_bytes(n1); + int len_n2 = BN_num_bytes(n2); + int nbytes = len_n1 + len_n2; + unsigned char * bin = (unsigned char *) malloc( nbytes ); + if (!bin) + return 0; + BN_bn2bin(n1, bin); + BN_bn2bin(n2, bin + len_n1); + hash( alg, bin, nbytes, buff ); + free(bin); + return BN_bin2bn(buff, hash_length(alg), NULL); +} + +static BIGNUM * H_nn_pad( SRP_HashAlgorithm alg, const BIGNUM * n1, const BIGNUM * n2 ) +{ + unsigned char * bin; + unsigned char buff[ SHA512_DIGEST_LENGTH ]; + int len_n1 = BN_num_bytes(n1); + int len_n2 = BN_num_bytes(n2); + int nbytes = 2 * len_n1; + + if ((len_n2 < 1) || (len_n2 > len_n1)) + return 0; + bin = (unsigned char *) calloc( 1, nbytes ); + if (!bin) + return 0; + BN_bn2bin(n1, bin); + BN_bn2bin(n2, bin + nbytes - len_n2); + hash( alg, bin, nbytes, buff ); + free(bin); + return BN_bin2bn(buff, hash_length(alg), NULL); +} + +static BIGNUM * H_ns( SRP_HashAlgorithm alg, const BIGNUM * n, const unsigned char * bytes, int len_bytes ) +{ + unsigned char buff[ SHA512_DIGEST_LENGTH ]; + int len_n = BN_num_bytes(n); + int nbytes = len_n + len_bytes; + unsigned char * bin = (unsigned char *) malloc( nbytes ); + if (!bin) + return 0; + BN_bn2bin(n, bin); + memcpy( bin + len_n, bytes, len_bytes ); + hash( alg, bin, nbytes, buff ); + free(bin); + return BN_bin2bn(buff, hash_length(alg), NULL); +} + +static BIGNUM * calculate_x( SRP_HashAlgorithm alg, const BIGNUM * salt, const char * username, const unsigned char * password, int password_len ) +{ + unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; + HashCTX ctx; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, username, strlen(username) ); + hash_update( alg, &ctx, ":", 1 ); + hash_update( alg, &ctx, password, password_len ); + + hash_final( alg, &ctx, ucp_hash ); + + return H_ns( alg, salt, ucp_hash, hash_length(alg) ); +} + +static void update_hash_n( SRP_HashAlgorithm alg, HashCTX *ctx, const BIGNUM * n ) +{ + unsigned long len = BN_num_bytes(n); + unsigned char * n_bytes = (unsigned char *) malloc( len ); + if (!n_bytes) + return; + BN_bn2bin(n, n_bytes); + hash_update(alg, ctx, n_bytes, len); + free(n_bytes); +} + +static void hash_num( SRP_HashAlgorithm alg, const BIGNUM * n, unsigned char * dest ) +{ + int nbytes = BN_num_bytes(n); + unsigned char * bin = (unsigned char *) malloc( nbytes ); + if(!bin) + return; + BN_bn2bin(n, bin); + hash( alg, bin, nbytes, dest ); + free(bin); +} + +static void calculate_M( SRP_HashAlgorithm alg, NGConstant *ng, unsigned char * dest, const char * I, const BIGNUM * s, + const BIGNUM * A, const BIGNUM * B, const unsigned char * K ) +{ + unsigned char H_N[ SHA512_DIGEST_LENGTH ]; + unsigned char H_g[ SHA512_DIGEST_LENGTH ]; + unsigned char H_I[ SHA512_DIGEST_LENGTH ]; + unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; + HashCTX ctx; + int i = 0; + int hash_len = hash_length(alg); + + hash_num( alg, ng->N, H_N ); + hash_num( alg, ng->g, H_g ); + + hash(alg, (const unsigned char *)I, strlen(I), H_I); + + + for (i=0; i < hash_len; i++ ) + H_xor[i] = H_N[i] ^ H_g[i]; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, H_xor, hash_len ); + hash_update( alg, &ctx, H_I, hash_len ); + update_hash_n( alg, &ctx, s ); + update_hash_n( alg, &ctx, A ); + update_hash_n( alg, &ctx, B ); + hash_update( alg, &ctx, K, hash_len ); + + hash_final( alg, &ctx, dest ); +} + +static void calculate_H_AMK( SRP_HashAlgorithm alg, unsigned char *dest, const BIGNUM * A, const unsigned char * M, const unsigned char * K ) +{ + HashCTX ctx; + + hash_init( alg, &ctx ); + + update_hash_n( alg, &ctx, A ); + hash_update( alg, &ctx, M, hash_length(alg) ); + hash_update( alg, &ctx, K, hash_length(alg) ); + + hash_final( alg, &ctx, dest ); +} + + +static void init_random() +{ +#ifdef WIN32 + HCRYPTPROV wctx; +#else + FILE *fp = 0; +#endif + + unsigned char buff[64]; + + if (g_initialized) + return; + +#ifdef WIN32 + + CryptAcquireContext(&wctx, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT); + + CryptGenRandom(wctx, sizeof(buff), (BYTE*) buff); + + CryptReleaseContext(wctx, 0); + + g_initialized = 1; + +#else + fp = fopen("/dev/urandom", "r"); + + if (fp) + { + size_t read = fread(buff, sizeof(buff), 1, fp); + g_initialized = read == 1; + fclose(fp); + } +#endif + + if (g_initialized) + RAND_seed( buff, sizeof(buff) ); +} + + +/*********************************************************************************************************** + * + * Exported Functions + * + ***********************************************************************************************************/ + +void srp_random_seed( const unsigned char * random_data, int data_length ) +{ + g_initialized = 1; + + if (random_data) + RAND_seed( random_data, data_length ); +} + + +void srp_create_salted_verification_key( SRP_HashAlgorithm alg, + SRP_NGType ng_type, const char * username, + const unsigned char * password, int len_password, + const unsigned char ** bytes_s, int * len_s, + const unsigned char ** bytes_v, int * len_v, + const char * n_hex, const char * g_hex ) +{ + BIGNUM * s = BN_new(); + BIGNUM * v = BN_new(); + BIGNUM * x = 0; + BN_CTX * ctx = BN_CTX_new(); + NGConstant * ng = new_ng( ng_type, n_hex, g_hex ); + + if( !s || !v || !ctx || !ng ) + goto cleanup_and_exit; + + init_random(); /* Only happens once */ + + BN_rand(s, 4*32, -1, 0); // MODIFIED!!! + + x = calculate_x( alg, s, username, password, len_password ); + + if( !x ) + goto cleanup_and_exit; + + BN_mod_exp(v, ng->g, x, ng->N, ctx); + + *len_s = BN_num_bytes(s); + *len_v = BN_num_bytes(v); + + *bytes_s = (const unsigned char *) malloc( *len_s ); + *bytes_v = (const unsigned char *) malloc( *len_v ); + + if (!bytes_s || !bytes_v) + goto cleanup_and_exit; + + BN_bn2bin(s, (unsigned char *) *bytes_s); + BN_bn2bin(v, (unsigned char *) *bytes_v); + + cleanup_and_exit: + delete_ng( ng ); + BN_free(s); + BN_free(v); + BN_free(x); + BN_CTX_free(ctx); +} + +void +srp_verifier_get_B(SRP_HashAlgorithm alg, SRP_NGType ng_type, + const unsigned char * bytes_v, int len_v, + const unsigned char ** bytes_b, int * len_b, + const unsigned char ** bytes_B, int * len_B, + const char * n_hex, const char * g_hex) +{ + BIGNUM *v = BN_bin2bn(bytes_v, len_v, NULL); + BIGNUM *k = 0; + BIGNUM *b = BN_new(); + BIGNUM *B = BN_new(); + BIGNUM *tmp1 = BN_new(); + BIGNUM *tmp2 = BN_new(); + BN_CTX *ctx = BN_CTX_new(); + NGConstant *ng = new_ng( ng_type, n_hex, g_hex ); + + *len_b = 0; + *bytes_b = 0; + *len_B = 0; + *bytes_B = 0; + + init_random(); /* Only happens once */ + + BN_rand(b, 256, -1, 0); + + k = H_nn_pad(alg, ng->N, ng->g); //MODIFIED + + /* B = kv + g^b */ + BN_mul(tmp1, k, v, ctx); + BN_mod_exp(tmp2, ng->g, b, ng->N, ctx); + BN_mod_add(B, tmp1, tmp2, ng->N, ctx); + + *len_B = BN_num_bytes(B); + *bytes_B = (const unsigned char *) malloc( *len_B ); + BN_bn2bin( B, (unsigned char *) *bytes_B ); + + *len_b = BN_num_bytes(b); + *bytes_b = (const unsigned char *) malloc( *len_b ); + BN_bn2bin( b, (unsigned char *) *bytes_b ); + + BN_free(b); // Added -- not sure about these, but they look necessary + BN_free(B); // Added -- not sure about these, but they look necessary + BN_free(v); + BN_free(k); + BN_free(tmp1); + BN_free(tmp2); + BN_CTX_free(ctx); + delete_ng(ng); +} + +/* Out: bytes_B, len_B. + * + * On failure, bytes_B will be set to NULL and len_B will be set to 0 + */ + +struct SRPVerifier * srp_verifier_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, + const unsigned char * bytes_s, int len_s, + const unsigned char * bytes_v, int len_v, + const unsigned char * bytes_A, int len_A, + const unsigned char * bytes_b, int len_b, + const unsigned char * bytes_B, int len_B, + const char * n_hex, const char * g_hex ) +{ + BIGNUM *s = BN_bin2bn(bytes_s, len_s, NULL); + BIGNUM *v = BN_bin2bn(bytes_v, len_v, NULL); + BIGNUM *A = BN_bin2bn(bytes_A, len_A, NULL); + BIGNUM *u = 0; + BIGNUM *b = BN_bin2bn(bytes_b, len_b, NULL);; + BIGNUM *B = BN_bin2bn(bytes_B, len_B, NULL);; + BIGNUM *S = BN_new(); + BIGNUM *k = 0; + BIGNUM *tmp1 = BN_new(); + BIGNUM *tmp2 = BN_new(); + BN_CTX *ctx = BN_CTX_new(); + int ulen = strlen(username) + 1; + NGConstant *ng = new_ng( ng_type, n_hex, g_hex ); + struct SRPVerifier *ver = 0; + + if( !s || !v || !A || !B || !S || !b || !tmp1 || !tmp2 || !ctx || !ng ) + goto cleanup_and_exit; + + ver = (struct SRPVerifier *) malloc( sizeof(struct SRPVerifier) ); + + if (!ver) + goto cleanup_and_exit; + + init_random(); /* Only happens once */ + + ver->username = (char *) malloc( ulen ); + ver->hash_alg = alg; + ver->ng = ng; + + if (!ver->username) + { + free(ver); + ver = 0; + goto cleanup_and_exit; + } + + memcpy( (char*)ver->username, username, ulen ); + + ver->authenticated = 0; + + /* SRP-6a safety check */ + BN_mod(tmp1, A, ng->N, ctx); + if ( !BN_is_zero(tmp1) ) + { + k = H_nn_pad(alg, ng->N, ng->g); //MODIFIED + + u = H_nn(alg, A, B); + + /* S = (A *(v^u)) ^ b */ + BN_mod_exp(tmp1, v, u, ng->N, ctx); + BN_mul(tmp2, A, tmp1, ctx); + BN_mod_exp(S, tmp2, b, ng->N, ctx); + + hash_num(alg, S, ver->session_key); + + calculate_M( alg, ng, ver->M, username, s, A, B, ver->session_key ); + calculate_H_AMK( alg, ver->H_AMK, A, ver->M, ver->session_key ); + + ver->bytes_B = bytes_B; + } + + cleanup_and_exit: + BN_free(s); + BN_free(v); + BN_free(A); + if (u) BN_free(u); + if (k) BN_free(k); + BN_free(B); + BN_free(S); + BN_free(b); + BN_free(tmp1); + BN_free(tmp2); + BN_CTX_free(ctx); + + return ver; +} + + + + +void srp_verifier_delete( struct SRPVerifier * ver ) +{ + if (ver) + { + delete_ng( ver->ng ); + free( (char *) ver->username ); + free( (unsigned char *) ver->bytes_B ); + memset(ver, 0, sizeof(*ver)); + free( ver ); + } +} + + + +int srp_verifier_is_authenticated( struct SRPVerifier * ver ) +{ + return ver->authenticated; +} + + +const char * srp_verifier_get_username( struct SRPVerifier * ver ) +{ + return ver->username; +} + + +const unsigned char * srp_verifier_get_session_key( struct SRPVerifier * ver, int * key_length ) +{ + if (key_length) + *key_length = hash_length( ver->hash_alg ); + return ver->session_key; +} + + +int srp_verifier_get_session_key_length( struct SRPVerifier * ver ) +{ + return hash_length( ver->hash_alg ); +} + + +/* user_M must be exactly SHA512_DIGEST_LENGTH bytes in size */ +void srp_verifier_verify_session( struct SRPVerifier * ver, const unsigned char * user_M, const unsigned char ** bytes_HAMK ) +{ + if ( memcmp( ver->M, user_M, hash_length(ver->hash_alg) ) == 0 ) + { + ver->authenticated = 1; + *bytes_HAMK = ver->H_AMK; + } + else + *bytes_HAMK = NULL; +} + +/*******************************************************************************/ + +struct SRPUser * srp_user_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, + const unsigned char * bytes_password, int len_password, + const char * n_hex, const char * g_hex ) +{ + struct SRPUser *usr = (struct SRPUser *) malloc( sizeof(struct SRPUser) ); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + init_random(); /* Only happens once */ + + usr->hash_alg = alg; + usr->ng = new_ng( ng_type, n_hex, g_hex ); + + usr->a = BN_new(); + usr->A = BN_new(); + usr->S = BN_new(); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = (const char *) malloc(ulen); + usr->password = (const unsigned char *) malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy((char *)usr->username, username, ulen); + memcpy((char *)usr->password, bytes_password, len_password); + + usr->authenticated = 0; + + usr->bytes_A = 0; + + return usr; + + err_exit: + if (usr) + { + BN_free(usr->a); + BN_free(usr->A); + BN_free(usr->S); + if (usr->username) + free((void*)usr->username); + if (usr->password) + { + memset((void*)usr->password, 0, usr->password_len); + free((void*)usr->password); + } + free(usr); + } + + return 0; +} + + + +void srp_user_delete( struct SRPUser * usr ) +{ + if( usr ) + { + BN_free( usr->a ); + BN_free( usr->A ); + BN_free( usr->S ); + + delete_ng( usr->ng ); + + memset((void*)usr->password, 0, usr->password_len); + + free((char *)usr->username); + free((char *)usr->password); + + if (usr->bytes_A) + free( (char *)usr->bytes_A ); + + memset(usr, 0, sizeof(*usr)); + free( usr ); + } +} + + + +int srp_user_is_authenticated( struct SRPUser * usr) +{ + return usr->authenticated; +} + + +const char * srp_user_get_username( struct SRPUser * usr ) +{ + return usr->username; +} + + + +const unsigned char * srp_user_get_session_key( struct SRPUser * usr, int * key_length ) +{ + if (key_length) + *key_length = hash_length( usr->hash_alg ); + return usr->session_key; +} + + +int srp_user_get_session_key_length( struct SRPUser * usr ) +{ + return hash_length( usr->hash_alg ); +} + + + +/* Output: username, bytes_A, len_A */ +void srp_user_start_authentication( struct SRPUser * usr, const char ** username, + const unsigned char ** bytes_A, int * len_A ) +{ + BN_CTX *ctx = BN_CTX_new(); + + BN_rand(usr->a, 256, -1, 0); + + BN_mod_exp(usr->A, usr->ng->g, usr->a, usr->ng->N, ctx); + + BN_CTX_free(ctx); + + *len_A = BN_num_bytes(usr->A); + *bytes_A = (const unsigned char *) malloc( *len_A ); + + if (!*bytes_A) + { + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + BN_bn2bin( usr->A, (unsigned char *) *bytes_A ); + + usr->bytes_A = *bytes_A; + *username = usr->username; +} + + +/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ +void srp_user_process_challenge( struct SRPUser * usr, + const unsigned char * bytes_s, int len_s, + const unsigned char * bytes_B, int len_B, + const unsigned char ** bytes_M, int * len_M ) +{ + BIGNUM *s = BN_bin2bn(bytes_s, len_s, NULL); + BIGNUM *B = BN_bin2bn(bytes_B, len_B, NULL); + BIGNUM *u = 0; + BIGNUM *x = 0; + BIGNUM *k = 0; + BIGNUM *v = BN_new(); + BIGNUM *tmp1 = BN_new(); + BIGNUM *tmp2 = BN_new(); + BIGNUM *tmp3 = BN_new(); + BN_CTX *ctx = BN_CTX_new(); + + *len_M = 0; + *bytes_M = 0; + + if( !s || !B || !v || !tmp1 || !tmp2 || !tmp3 || !ctx ) + goto cleanup_and_exit; + + u = H_nn_pad(usr->hash_alg, usr->A, B); //MODIFIED + + if (!u) + goto cleanup_and_exit; + + x = calculate_x( usr->hash_alg, s, usr->username, usr->password, usr->password_len ); + + if (!x) + goto cleanup_and_exit; + + k = H_nn_pad(usr->hash_alg, usr->ng->N, usr->ng->g); //MODIFIED + + if (!k) + goto cleanup_and_exit; + + /* SRP-6a safety check */ + if ( !BN_is_zero(B) && !BN_is_zero(u) ) + { + BN_mod_exp(v, usr->ng->g, x, usr->ng->N, ctx); + + /* S = (B - k*(g^x)) ^ (a + ux) */ + BN_mul(tmp1, u, x, ctx); + BN_add(tmp2, usr->a, tmp1); /* tmp2 = (a + ux) */ + BN_mod_exp(tmp1, usr->ng->g, x, usr->ng->N, ctx); + BN_mul(tmp3, k, tmp1, ctx); /* tmp3 = k*(g^x) */ + BN_sub(tmp1, B, tmp3); /* tmp1 = (B - K*(g^x)) */ + BN_mod_exp(usr->S, tmp1, tmp2, usr->ng->N, ctx); + + hash_num(usr->hash_alg, usr->S, usr->session_key); + + calculate_M( usr->hash_alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key ); + calculate_H_AMK( usr->hash_alg, usr->H_AMK, usr->A, usr->M, usr->session_key ); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length( usr->hash_alg ); + } + else + { + *bytes_M = NULL; + if (len_M) + *len_M = 0; + } + + cleanup_and_exit: + + BN_free(s); + BN_free(B); + BN_free(u); + BN_free(x); + BN_free(k); + BN_free(v); + BN_free(tmp1); + BN_free(tmp2); + BN_free(tmp3); + BN_CTX_free(ctx); +} + + +void srp_user_verify_session( struct SRPUser * usr, const unsigned char * bytes_HAMK ) +{ + if ( memcmp( usr->H_AMK, bytes_HAMK, hash_length(usr->hash_alg) ) == 0 ) + usr->authenticated = 1; +} diff --git a/csrp/srp.h b/csrp/srp.h new file mode 100644 index 00000000..42885691 --- /dev/null +++ b/csrp/srp.h @@ -0,0 +1,193 @@ +/* + * Secure Remote Password 6a implementation + * Copyright (c) 2010 Tom Cocagne. All rights reserved. + * https://github.com/cocagne/csrp + * + * The MIT License (MIT) + * + * Copyright (c) 2013 Tom Cocagne + * + * 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. + * + */ + +/* + * + * Purpose: This is a direct implementation of the Secure Remote Password + * Protocol version 6a as described by + * http://srp.stanford.edu/design.html + * + * Author: tom.cocagne@gmail.com (Tom Cocagne) + * + * Dependencies: OpenSSL (and Advapi32.lib on Windows) + * + * Usage: Refer to test_srp.c for a demonstration + * + * Notes: + * This library allows multiple combinations of hashing algorithms and + * prime number constants. For authentication to succeed, the hash and + * prime number constants must match between + * srp_create_salted_verification_key(), srp_user_new(), + * and srp_verifier_new(). A recommended approach is to determine the + * desired level of security for an application and globally define the + * hash and prime number constants to the predetermined values. + * + * As one might suspect, more bits means more security. As one might also + * suspect, more bits also means more processing time. The test_srp.c + * program can be easily modified to profile various combinations of + * hash & prime number pairings. + */ + +#ifndef SRP_H +#define SRP_H + + +struct SRPVerifier; +struct SRPUser; + +typedef enum +{ + SRP_NG_1024, + SRP_NG_2048, + SRP_NG_4096, + SRP_NG_8192, + SRP_NG_CUSTOM +} SRP_NGType; + +typedef enum +{ + SRP_SHA1, + SRP_SHA224, + SRP_SHA256, + SRP_SHA384, + SRP_SHA512 +} SRP_HashAlgorithm; + + +/* This library will automatically seed the OpenSSL random number generator + * using cryptographically sound random data on Windows & Linux. If this is + * undesirable behavior or the host OS does not provide a /dev/urandom file, + * this function may be called to seed the random number generator with + * alternate data. + * + * The random data should include at least as many bits of entropy as the + * largest hash function used by the application. So, for example, if a + * 512-bit hash function is used, the random data requies at least 512 + * bits of entropy. + * + * Passing a null pointer to this function will cause this library to skip + * seeding the random number generator. This is only legitimate if it is + * absolutely known that the OpenSSL random number generator has already + * been sufficiently seeded within the running application. + * + * Notes: + * * This function is optional on Windows & Linux and mandatory on all + * other platforms. + */ +void srp_random_seed( const unsigned char * random_data, int data_length ); + + +/* Out: bytes_s, len_s, bytes_v, len_v + * + * The caller is responsible for freeing the memory allocated for bytes_s and bytes_v + * + * The n_hex and g_hex parameters should be 0 unless SRP_NG_CUSTOM is used for ng_type. + * If provided, they must contain ASCII text of the hexidecimal notation. + */ +void srp_create_salted_verification_key( SRP_HashAlgorithm alg, + SRP_NGType ng_type, const char * username, + const unsigned char * password, int len_password, + const unsigned char ** bytes_s, int * len_s, + const unsigned char ** bytes_v, int * len_v, + const char * n_hex, const char * g_hex ); + + +/* Out: bytes_B, len_B. + */ + +void +srp_verifier_get_B(SRP_HashAlgorithm alg, SRP_NGType ng_type, + const unsigned char * bytes_v, int len_v, + const unsigned char ** bytes_b, int * len_b, + const unsigned char ** bytes_B, int * len_B, + const char * n_hex, const char * g_hex); + +struct SRPVerifier * srp_verifier_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, + const unsigned char * bytes_s, int len_s, + const unsigned char * bytes_v, int len_v, + const unsigned char * bytes_A, int len_A, + const unsigned char * bytes_b, int len_b, + const unsigned char * bytes_B, int len_B, + const char * n_hex, const char * g_hex ); + + +void srp_verifier_delete( struct SRPVerifier * ver ); + + +int srp_verifier_is_authenticated( struct SRPVerifier * ver ); + + +const char * srp_verifier_get_username( struct SRPVerifier * ver ); + +/* key_length may be null */ +const unsigned char * srp_verifier_get_session_key( struct SRPVerifier * ver, int * key_length ); + + +int srp_verifier_get_session_key_length( struct SRPVerifier * ver ); + + +/* user_M must be exactly srp_verifier_get_session_key_length() bytes in size */ +void srp_verifier_verify_session( struct SRPVerifier * ver, + const unsigned char * user_M, + const unsigned char ** bytes_HAMK ); + +/*******************************************************************************/ + +/* The n_hex and g_hex parameters should be 0 unless SRP_NG_CUSTOM is used for ng_type */ +struct SRPUser * srp_user_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, + const unsigned char * bytes_password, int len_password, + const char * n_hex, const char * g_hex ); + +void srp_user_delete( struct SRPUser * usr ); + +int srp_user_is_authenticated( struct SRPUser * usr); + + +const char * srp_user_get_username( struct SRPUser * usr ); + +/* key_length may be null */ +const unsigned char * srp_user_get_session_key( struct SRPUser * usr, int * key_length ); + +int srp_user_get_session_key_length( struct SRPUser * usr ); + +/* Output: username, bytes_A, len_A */ +void srp_user_start_authentication( struct SRPUser * usr, const char ** username, + const unsigned char ** bytes_A, int * len_A ); + +/* Output: bytes_M, len_M (len_M may be null and will always be + * srp_user_get_session_key_length() bytes in size) */ +void srp_user_process_challenge( struct SRPUser * usr, + const unsigned char * bytes_s, int len_s, + const unsigned char * bytes_B, int len_B, + const unsigned char ** bytes_M, int * len_M ); + +/* bytes_HAMK must be exactly srp_user_get_session_key_length() bytes in size */ +void srp_user_verify_session( struct SRPUser * usr, const unsigned char * bytes_HAMK ); + +#endif /* Include Guard */ diff --git a/plists/get_info_response.xml b/plists/get_info_response.xml new file mode 100644 index 00000000..7a3b7a05 --- /dev/null +++ b/plists/get_info_response.xml @@ -0,0 +1,66 @@ + + + + + + audioLatencies + + + inputLatencyMicros + 0 + outputLatencyMicros + 400000 + type + 100 + + + audioType + default + inputLatencyMicros + 0 + outputLatencyMicros + 400000 + type + 100 + + + audioType + media + inputLatencyMicros + 0 + outputLatencyMicros + 400000 + type + 100 + + + audioType + media + inputLatencyMicros + 0 + outputLatencyMicros + 400000 + type + 102 + + + keepAliveLowPower + + keepAliveSendStatsAsBody + + manufacturer + Shairport Sync + nameIsFactoryDefault + + protocolVersion + 1.1 + sdk + AirPlay;2.0.2 + sourceVersion + 366.0 + statusFlags + 4 + txtAirPlay + + + diff --git a/ptp-utilities.c b/ptp-utilities.c new file mode 100644 index 00000000..53dbd4d0 --- /dev/null +++ b/ptp-utilities.c @@ -0,0 +1,167 @@ +/* + * This file is part of Shairport Sync. + * Copyright (c) Mike Brady 2020 -- 2021 + * 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 +#include +#include +#include +#define __STDC_FORMAT_MACROS +#include "common.h" +#include +#include + +#include "../nqptp/nqptp-shm-structures.h" +#include "ptp-utilities.h" + +int shm_fd; +void *mapped_addr = NULL; + +int failure_message_sent = 0; + +static pthread_mutex_t ptp_access_mutex = PTHREAD_MUTEX_INITIALIZER; + +// returns a copy of the shared memory data from the nqptp +// shared memory interface, so long as it's open. +int get_nqptp_data(struct shm_structure *nqptp_data) { + int response = -1; // presume the worst. Fix it on success + // the first part of the shared memory is a mutex lock, so use it to get + // exclusive access while copying + + if ((mapped_addr != MAP_FAILED) && (mapped_addr != NULL)) { + pthread_cleanup_debug_mutex_lock((pthread_mutex_t *)mapped_addr, 100000, 1); + memcpy(nqptp_data, (char *)mapped_addr, sizeof(struct shm_structure)); + pthread_cleanup_pop(1); // release the mutex + response = 0; + } + return response; +} + +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { + int response = -1; + pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 10000, 1); + // 0 -> valid and working; -1 -> can't connect to nqptp + if (actual_clock_id != NULL) + *actual_clock_id = 0; + if (raw_offset != NULL) + *raw_offset = 0; + + if (ptp_shm_interface_open() == 0) { + struct shm_structure nqptp_data; + if (get_nqptp_data(&nqptp_data) == 0) { + if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { + if (actual_clock_id != NULL) + *actual_clock_id = nqptp_data.master_clock_id; + if (raw_offset != NULL) + *raw_offset = nqptp_data.local_to_master_time_offset; + response = 0; + } else { + if (failure_message_sent == 0) { + warn("This version of Shairport Sync requires an NQPTP with a Shared Memory Interface Version %u, but the installed version is %u. Please install the correct version of NQPTP.", NQPTP_SHM_STRUCTURES_VERSION, nqptp_data.version); + failure_message_sent = 1; + } + } + } + if (response != -1) + response = ptp_shm_interface_close(); + } else { + if (failure_message_sent == 0) { + warn("Can't open the interface to nqptp. Is the service running?"); + failure_message_sent = 1; + } + } + pthread_cleanup_pop(1); // release the mutex + if (response == 0) + failure_message_sent = 0; + return response; +} + +int ptp_shm_interface_open() { + mapped_addr = NULL; + int shared_memory_file_descriptor = shm_open("/nqptp", O_RDWR, 0); + int response = -1; + if (shared_memory_file_descriptor >= 0) { + mapped_addr = + // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the + // mutex to lock and unlock + mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, + shared_memory_file_descriptor, 0); + if (mapped_addr == MAP_FAILED) { + if (failure_message_sent == 0) { + debug(1, "unable to open the shared memory interface with nqptp. Is the service running?"); + failure_message_sent = 1; + } + } + if (close(shared_memory_file_descriptor) == -1) { + if (failure_message_sent == 0) { + debug(1, "error closing \"/nqptp\" after mapping it."); + failure_message_sent = 1; + } + } else { + response = 0; + } + } + return response; +} + +int ptp_shm_interface_close() { + int response = -1; + if ((mapped_addr != MAP_FAILED) && (mapped_addr != NULL)) { + response = munmap(mapped_addr, sizeof(struct shm_structure)); + if (response != 0) + debug(1, "error unmapping shared memory."); + } + mapped_addr = NULL; + return response; +} + +void ptp_send_control_message_string(const char *msg) { + debug(2, "Send control message to NQPTP: \"%s\"", msg); + int s; + unsigned short port = htons(NQPTP_CONTROL_PORT); + struct sockaddr_in server; + + /* Create a datagram socket in the internet domain and use the + * default protocol (UDP). + */ + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + die("Can't open a socket to NQPTP"); + } + + /* Set up the server name */ + server.sin_family = AF_INET; /* Internet Domain */ + server.sin_port = port; /* Server Port */ + server.sin_addr.s_addr = 0; /* Server's Address */ + + /* Send the message in buf to the server */ + if (sendto(s, msg, (strlen(msg) + 1), 0, (struct sockaddr *)&server, sizeof(server)) < 0) { + die("error sending timing_peer_list to NQPTP"); + } + /* Deallocate the socket */ + close(s); +} diff --git a/ptp-utilities.h b/ptp-utilities.h new file mode 100644 index 00000000..11a52c8f --- /dev/null +++ b/ptp-utilities.h @@ -0,0 +1,39 @@ +/* + * This file is part of Shairport Sync. + * Copyright (c) Mike Brady 2020 -- 2021 + * 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. + */ +#ifndef __PTP_UTILITIES_H +#define __PTP_UTILITIES_H + +#include "config.h" +#include + +int ptp_get_clock_info(uint64_t *clock_id, uint64_t *offset); + +void ptp_send_control_message_string(const char *msg); + +int ptp_shm_interface_open(); +int ptp_shm_interface_close(); + +#endif /* __PTP_UTILITIES_H */ From a8caa0f913211fa7b32cbfc883b74f3ce343992a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 28 Apr 2021 13:03:27 +0100 Subject: [PATCH 020/878] Move latency calculations to rtp.c. Remove checks for infeasible latencies from player.c. --- player.c | 197 +++++++------------------------------------------------ rtp.c | 34 ++++++---- 2 files changed, 46 insertions(+), 185 deletions(-) diff --git a/player.c b/player.c index 78912e3b..7c0fd007 100644 --- a/player.c +++ b/player.c @@ -4,7 +4,7 @@ * All rights reserved. * * Modifications for audio synchronisation - * and related work, copyright (c) Mike Brady 2014 -- 2020 + * and related work, copyright (c) Mike Brady 2014 -- 2021 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -641,31 +641,6 @@ int32_t rand_in_range(int32_t exclusive_range_limit) { return sp >> 32; } -int get_and_check_effective_latency(rtsp_conn_info *conn, uint32_t *effective_latency, - double offset_time) { - // check that the overall effective latency remains positive and is not greater than the capacity - // of the buffers return 0 if okay, -1 if the latency would be negative, +1 if it would be too - // large - - int result = 0; - if (offset_time >= 0.0) { - uint32_t latency_addition = (uint32_t)(offset_time * conn->input_rate); - // keep about one second of buffers back - if ((*effective_latency + latency_addition) <= - (conn->max_frames_per_packet * (BUFFER_FRAMES - config.minimum_free_buffer_headroom))) - *effective_latency += latency_addition; - else - result = 1; - } else { - uint32_t latency_reduction = (uint32_t)((-offset_time) * conn->input_rate); - if (latency_reduction <= *effective_latency) - *effective_latency -= latency_reduction; - else - result = -1; - } - return result; -} - static inline void process_sample(int32_t sample, char **outp, sps_format_t format, int volume, int dither, rtsp_conn_info *conn) { /* @@ -1081,33 +1056,21 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // To calculate when the first packet will be played, we figure out the exact time the // packet should be played according to its timestamp and the reference time. - // We then need to add the desired latency, typically 88200 frames. + // The desired latency, typically 88200 frames, will be calculated for in rtp.c, + // and any desired backend latency offset included in it there. - // Then we need to offset this by the backend latency offset. For example, if we knew - // that the audio back end has a latency of 100 ms, we would - // ask for the first packet to be emitted 100 ms earlier than it should, i.e. -4410 - // frames, so that when it got through the audio back end, - - // if would be in sync. To do this, we would give it a latency offset of -0.1 sec, - // i.e. -4410 frames. uint64_t should_be_time; - uint32_t effective_latency = conn->latency; - get_and_check_effective_latency(conn, &effective_latency, - config.audio_backend_latency_offset); - // we are ignoring the returned status because it will be captured on subsequent - // frames, below. - - // what we are asking for here is "what is the local time at which time the calculated - // frame should be played" - - frame_to_local_time(conn->first_packet_timestamp + - effective_latency, // this will go modulo 2^32 + frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 &should_be_time, conn); conn->first_packet_time_to_play = should_be_time; - debug(2, "first_packet_time set for frame %u.", conn->first_packet_timestamp); + + int64_t lt = conn->first_packet_time_to_play - local_time_now; + + debug(1, "Connection %d: Lead time: %f seconds.", conn->connection_number, + lt * 0.000000001); if (local_time_now > conn->first_packet_time_to_play) { uint64_t lateness = local_time_now - conn->first_packet_time_to_play; @@ -1130,39 +1093,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // here, we figure out whether and what silence to send. uint64_t should_be_time; - uint32_t effective_latency = conn->latency; - - switch (get_and_check_effective_latency(conn, &effective_latency, - config.audio_backend_latency_offset)) { - case -1: - if (conn->unachievable_audio_backend_latency_offset_notified == 0) { - warn( - "Negative latency! A latency of %d frames requested by the player, when " - "combined with an audio_backend_latency_offset of %f seconds, would make the " - "overall latency negative. The audio_backend_latency_offset setting is " - "ignored.", - conn->latency, config.audio_backend_latency_offset); - config.audio_backend_latency_offset = 0; // set it to zero - conn->unachievable_audio_backend_latency_offset_notified = 1; - }; - break; - case 1: - if (conn->unachievable_audio_backend_latency_offset_notified == 0) { - warn("An audio_backend_latency_offset of %f seconds may exceed the frame " - "buffering " - "capacity -- the setting is ignored.", - config.audio_backend_latency_offset); - config.audio_backend_latency_offset = 0; // set it to zero; - conn->unachievable_audio_backend_latency_offset_notified = 1; - }; - break; - default: - break; - } // readjust first packet time to play - frame_to_local_time(conn->first_packet_timestamp + - effective_latency, // this will go modulo 2^32 + frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 &should_be_time, conn); int64_t change_in_should_be_time = @@ -1315,50 +1248,13 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (have_timestamp_timing_information(conn)) { // if we have a reference time uint64_t time_to_play; - uint32_t effective_latency = conn->latency; - switch (get_and_check_effective_latency(conn, &effective_latency, - config.audio_backend_latency_offset - - config.audio_backend_buffer_desired_length)) { - case -1: - // this means that the latency is negative, i.e. the packet must be played before its - // time. This is clearly a mistake, and can arise if the combination of a large negative - // latency offset and the desired buffer length are greater than the latency chosen by the - // player. For example if the latency is 88200 frames (2 seconds), the - // audio_backend_latency_offset is -1.9 and the audio_backend_buffer_desired_length is 0.2 - // the effective latency is -0.1, so the frame must be played 0.1 seconds before it is - // time-tagged for, and in reality before it arrives at the player. + // we must enable packets to be released early enough for the + // audio buffer to be filled to the desired length - // to deal with the problem, rather than block the packets, we'll just let 'em go... - - // - if (conn->unachievable_audio_backend_latency_offset_notified == 0) { - warn("Negative latency! A latency of %d frames requested by the player, when combined " - "with an audio_backend_latency_offset of %f seconds an " - "audio_backend_buffer_desired_length of %f seconds, would make the overall " - "latency negative. No latency is used. Synchronisation may fail.", - conn->latency, config.audio_backend_latency_offset, - config.audio_backend_buffer_desired_length); - conn->unachievable_audio_backend_latency_offset_notified = 1; - }; - time_to_play = local_time_now; // pretend the frame should be played now... - break; - case 1: - if (conn->unachievable_audio_backend_latency_offset_notified == 0) { - warn("Latency too long! An audio_backend_latency_offset of %f seconds combined with an " - "audio_backend_buffer_desired_length of %f seconds may exceed the frame " - "buffering capacity.", - config.audio_backend_latency_offset, config.audio_backend_buffer_desired_length); - conn->unachievable_audio_backend_latency_offset_notified = 1; - }; - time_to_play = local_time_now; // pretend the frame should be played now... - break; - default: - frame_to_local_time(curframe->given_timestamp + - effective_latency, // this will go modulo 2^32 - &time_to_play, conn); - break; - } + uint32_t buffer_latency_offset = (uint32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); + frame_to_local_time(curframe->given_timestamp - buffer_latency_offset, // this will go modulo 2^32 + &time_to_play, conn); if (local_time_now >= time_to_play) { do_wait = 0; @@ -2134,7 +2030,7 @@ void *player_thread_func(void *arg) { die("Shairport Sync only supports 16 bit input"); } - at_least_one_frame_seen = 1; + inbuflength *= conn->output_sample_ratio; // We have a frame of data. We need to see if we want to add or remove a frame from it to // keep in sync. @@ -2146,22 +2042,7 @@ void *player_thread_func(void *arg) { // now, go back as far as the total latency less, say, 100 ms, and check the presence of // frames from then onwards - inbuflength *= conn->output_sample_ratio; - /* - uint32_t reference_timestamp; - uint64_t reference_timestamp_time, remote_reference_timestamp_time; - get_reference_timestamp_stuff(&reference_timestamp, &reference_timestamp_time, - &remote_reference_timestamp_time, conn); // types okay - */ - - // nt is the rtp timestamp of the first frame of the current packet of - // frames from the source. - // multiply it by the output frame ratio to get, effectively, the rtp timestamp - // of the first frame of the corresponding packet of output frames. - - uint64_t nt; - nt = inframe->given_timestamp; // uint32_t to int64_t - nt = nt * conn->output_sample_ratio; + at_least_one_frame_seen = 1; uint64_t local_time_now = get_absolute_time_in_ns(); // types okay @@ -2244,23 +2125,13 @@ void *player_thread_func(void *arg) { uint32_t should_be_frame_32; local_time_to_frame(local_time_now, &should_be_frame_32, conn); - // int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; + int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; int64_t delay = - int64_mod_difference(should_be_frame_32 * conn->output_sample_ratio, - nt - current_delay, UINT32_MAX * conn->output_sample_ratio); + should_be_frame - (inframe->given_timestamp * conn->output_sample_ratio - + current_delay); // all int64_t - // int64_t delay = should_be_frame - (nt - current_delay); // all int64_t - - // the original frame numbers are unsigned 32-bit integers that roll over modulo 2^32 - // hence these delay figures will be unsigned numbers that roll over modulo 2^32 * - // conn->output_sample_ratio; therefore, calculating the delay must be done in the light - // of possible rollover - - sync_error = - delay - ((int64_t)conn->latency * conn->output_sample_ratio + - (int64_t)(config.audio_backend_latency_offset * - config.output_rate)); // int64_t from int64_t - int32_t, so okay + sync_error = delay; // int64_t from int64_t - int32_t, so okay if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; @@ -2361,7 +2232,8 @@ void *player_thread_func(void *arg) { debug(2, "Large negative sync error: %" PRId64 " with should_be_frame_32 of %" PRIu32 ", nt of %" PRId64 " and current_delay of %" PRId64 ".", - sync_error, should_be_frame_32, nt, current_delay); + sync_error, should_be_frame_32, + inframe->given_timestamp * conn->output_sample_ratio, current_delay); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) silence_length = filler_length * 5; @@ -2588,29 +2460,6 @@ void *player_thread_func(void *arg) { } } else { - // if there is no delay procedure, then we should be sending the packet - // to the output at the time determined by - // the packet's time to play + requested latency + requested offset. - /* - // This is just for checking during - development - - uint32_t should_be_frame_32; - local_time_to_frame(local_time_now, &should_be_frame_32, conn); - // int64_t should_be_frame = ((int64_t)should_be_frame_32) * - conn->output_sample_ratio; - - int32_t ilatency = (int32_t)((config.audio_backend_latency_offset - - config.audio_backend_buffer_desired_length) * conn->input_rate) + conn->latency; if - (ilatency < 0) debug(1,"incorrect latency %d.", ilatency); - - int32_t idelay = (int32_t)(should_be_frame_32 - inframe->given_timestamp); - - idelay = idelay - ilatency; - - debug(2,"delay is %d input frames.", idelay); - */ - // 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) { diff --git a/rtp.c b/rtp.c index c558645e..87fd25dd 100644 --- a/rtp.c +++ b/rtp.c @@ -263,8 +263,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -275,19 +275,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -364,18 +364,30 @@ void *rtp_control_receiver(void *arg) { la, max_frames, conn->latency); } else { + // here we have the latency but it does not yet account for the audio_backend_latency_offset + int32_t latency_offset = (uint32_t)(config.audio_backend_latency_offset * conn->input_rate); + int32_t adjusted_latency = latency_offset + (int32_t)la; + if ((adjusted_latency < 0) || (adjusted_latency > (int32_t)(conn->max_frames_per_packet * (BUFFER_FRAMES - config.minimum_free_buffer_headroom)))) + warn("audio_backend_latency_offset out of range -- ignored."); + else + la = adjusted_latency; + if (la != conn->latency) { conn->latency = la; - debug(3, - "New latency detected: %" PRIu32 ", sync latency: %" PRIu32 + debug(1, + "New latency: %" PRIu32 ", sync latency: %" PRIu32 ", minimum latency: %" PRIu32 ", maximum " - "latency: %" PRIu32 ", fixed offset: %" PRIu32 ".", + "latency: %" PRIu32 ", fixed offset: %" PRIu32 ", audio_backend_latency_offset: %f.", la, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, - conn->maximum_latency, config.fixedLatencyOffset); + conn->maximum_latency, config.fixedLatencyOffset, config.audio_backend_latency_offset); } } } + // here, we apply the latency to the sync_rtp_timestamp + + sync_rtp_timestamp = sync_rtp_timestamp - conn->latency; + debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); if (conn->initial_reference_time == 0) { From 8a73d5972c09a2f863dcfceca01ac0efc72876fd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Apr 2021 15:04:50 +0100 Subject: [PATCH 021/878] rtp meged with lots of AP2 stuff still commented out. But it still works. --- common.c | 145 ++++++ common.h | 6 + player.c | 9 +- player.h | 77 ++- rtp.c | 1443 ++++++++++++++++++++++++++++++++++++++++++++++++------ rtp.h | 8 +- 6 files changed, 1509 insertions(+), 179 deletions(-) diff --git a/common.c b/common.c index 6e0f12ab..0fe331da 100644 --- a/common.c +++ b/common.c @@ -43,6 +43,9 @@ #include #include +#include +#include + #ifdef COMPILE_FOR_OSX #include #include @@ -266,6 +269,148 @@ uint16_t nextFreeUDPPort() { return UDPPortIndex; } +// if port is zero, pick any port +// otherwise, try the given port only +int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, uint32_t scope_id, + uint16_t *port, int *sock) { + int ret = 0; // no error + int local_socket = socket(ip_family, type, 0); + if (local_socket == -1) + ret = errno; + if (ret == 0) { + SOCKADDR myaddr; + memset(&myaddr, 0, sizeof(myaddr)); + if (ip_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr; + sa->sin_family = AF_INET; + sa->sin_port = ntohs(*port); + inet_pton(AF_INET, self_ip_address, &(sa->sin_addr)); + ret = bind(local_socket, (struct sockaddr *)sa, sizeof(struct sockaddr_in)); + } +#ifdef AF_INET6 + if (ip_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&myaddr; + sa6->sin6_family = AF_INET6; + sa6->sin6_port = ntohs(*port); + inet_pton(AF_INET6, self_ip_address, &(sa6->sin6_addr)); + sa6->sin6_scope_id = scope_id; + ret = bind(local_socket, (struct sockaddr *)sa6, sizeof(struct sockaddr_in6)); + } +#endif + if (ret < 0) { + ret = errno; + close(local_socket); + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + warn("error %d: \"%s\". Could not bind a port!", errno, errorstring); + } else { + uint16_t sport; + SOCKADDR local; + socklen_t local_len = sizeof(local); + ret = getsockname(local_socket, (struct sockaddr *)&local, &local_len); + if (ret < 0) { + ret = errno; + close(local_socket); + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + warn("error %d: \"%s\". Could not retrieve socket's port!", errno, errorstring); + } else { +#ifdef AF_INET6 + if (local.SAFAMILY == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&local; + sport = ntohs(sa6->sin6_port); + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in *)&local; + sport = ntohs(sa->sin_port); + } + *sock = local_socket; + *port = sport; + } + } + } + return ret; +} + +uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scope_id, int *sock) { + // look for a port in the range, if any was specified. + int ret = 0; + + int local_socket = socket(ip_family, SOCK_DGRAM, IPPROTO_UDP); + if (local_socket == -1) + die("Could not allocate a socket."); + + /* + int val = 1; + ret = setsockopt(local_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + if (ret < 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "Error %d: \"%s\". Couldn't set SO_REUSEADDR"); + } + */ + + SOCKADDR myaddr; + int tryCount = 0; + uint16_t desired_port; + do { + tryCount++; + desired_port = nextFreeUDPPort(); + memset(&myaddr, 0, sizeof(myaddr)); + if (ip_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr; + sa->sin_family = AF_INET; + sa->sin_port = ntohs(desired_port); + inet_pton(AF_INET, self_ip_address, &(sa->sin_addr)); + ret = bind(local_socket, (struct sockaddr *)sa, sizeof(struct sockaddr_in)); + } +#ifdef AF_INET6 + if (ip_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&myaddr; + sa6->sin6_family = AF_INET6; + sa6->sin6_port = ntohs(desired_port); + inet_pton(AF_INET6, self_ip_address, &(sa6->sin6_addr)); + sa6->sin6_scope_id = scope_id; + ret = bind(local_socket, (struct sockaddr *)sa6, sizeof(struct sockaddr_in6)); + } +#endif + + } while ((ret < 0) && (errno == EADDRINUSE) && (desired_port != 0) && + (tryCount < config.udp_port_range)); + + // debug(1,"UDP port chosen: %d.",desired_port); + + if (ret < 0) { + close(local_socket); + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- " + "it must be " + "at least 3, and 10 or more is suggested -- or " + "check for restrictive firewall settings or a bad router! UDP base is %u, range is %u and " + "current suggestion is %u.", + errno, errorstring, config.udp_port_base, config.udp_port_range, desired_port); + } + + uint16_t sport; + SOCKADDR local; + socklen_t local_len = sizeof(local); + getsockname(local_socket, (struct sockaddr *)&local, &local_len); +#ifdef AF_INET6 + if (local.SAFAMILY == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&local; + sport = ntohs(sa6->sin6_port); + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in *)&local; + sport = ntohs(sa->sin_port); + } + *sock = local_socket; + return sport; +} + int get_requested_connection_state_to_output() { return requested_connection_state_to_output; } void set_requested_connection_state_to_output(int v) { requested_connection_state_to_output = v; } diff --git a/common.h b/common.h index 68666ac9..6602985a 100644 --- a/common.h +++ b/common.h @@ -447,4 +447,10 @@ int string_update_with_size(char **str, int *flag, char *s, size_t len); // from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks void *memdup(const void *mem, size_t size); +int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, uint32_t scope_id, + uint16_t *port, int *sock); + +uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scope_id, int *sock); + + #endif // _COMMON_H diff --git a/player.c b/player.c index 7c0fd007..ff889408 100644 --- a/player.c +++ b/player.c @@ -117,13 +117,6 @@ uint32_t modulo_32_offset(uint32_t from, uint32_t to) { return UINT32_MAX - from + to + 1; } -uint64_t modulo_64_offset(uint64_t from, uint64_t to) { - if (from <= to) - return to - from; - else - return UINT64_MAX - from + to + 1; -} - void do_flush(uint32_t timestamp, rtsp_conn_info *conn); static void ab_resync(rtsp_conn_info *conn) { @@ -1589,7 +1582,7 @@ void player_thread_cleanup_handler(void *arg) { if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); - clear_reference_timestamp(conn); + reset_anchor_info(conn); conn->rtp_running = 0; pthread_setcancelstate(oldState, NULL); } diff --git a/player.h b/player.h index 105ce869..e8942710 100644 --- a/player.h +++ b/player.h @@ -21,6 +21,11 @@ #include #endif +#ifdef CONFIG_AIRPLAY_2 +#include "pair_ap/pair-internal.h" +#include "plist/plist.h" +#endif + #include "alac.h" #include "audio.h" @@ -76,6 +81,11 @@ typedef enum { ast_apple_lossless, } audio_stream_type; +typedef enum { + ts_ntp, + ts_ptp +} timing_source_type; + typedef struct { int encrypted; uint8_t aesiv[16], aeskey[16]; @@ -83,8 +93,20 @@ typedef struct { audio_stream_type type; } stream_cfg; +#ifdef CONFIG_AIRPLAY_2 +typedef struct file_cipher_context { + struct pair_cipher_context *cipher_context; + int active; // can be created during a pair setup but not activated until next read + int fd; + void *input_plaintext_buffer; + void *input_plaintext_buffer_toq; + size_t input_plaintext_buffer_bytes_occupied; +} file_cipher_context; +#endif + typedef struct { int connection_number; // for debug ID purposes, nothing else... + timing_source_type type_of_timing; // are we using NTP or PTP? int resend_interval; // this is really just for debugging char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency @@ -94,7 +116,7 @@ typedef struct { uint32_t maximum_latency; // set if an a=max-latency: line appears in the ANNOUNCE message; zero // otherwise int software_mute_enabled; // if we don't have a real mute that we can use - int unachievable_audio_backend_latency_offset_notified; // set when a latency warning is issued + int fd; int authorized; // set if a password is required and has been supplied @@ -214,8 +236,56 @@ typedef struct { // this is what connects an rtp timestamp to the remote time - uint32_t reference_timestamp; - uint64_t remote_reference_timestamp_time; + int anchor_remote_info_is_valid; + uint64_t anchor_clock; + uint64_t anchor_time; // this is the time according to the clock + uint32_t anchor_rtptime; + + +#ifdef CONFIG_AIRPLAY_2 + pthread_t rtp_event_thread; + pthread_t rtp_ap2_control_thread; + pthread_t rtp_realtime_audio_thread; + pthread_t rtp_buffered_audio_thread; + + int pairing_mode; + file_cipher_context control_cipher_context; + struct verifier_setup_context *server_setup_ctx; + + int last_anchor_info_is_valid; + uint64_t last_anchor_clock_offset; + uint64_t last_anchor_time_of_update; + uint64_t last_anchor_clock; + + + ssize_t ap2_audio_buffer_size; + int ap2_flush_requested; + uint32_t ap2_flush_rtp_timestamp; + uint32_t ap2_flush_sequence_number; + int ap2_rate; // protect with flush mutex, 0 means don't play, 1 means play + int ap2_play_enabled; // protect with flush mutex + + int event_socket; + SOCKADDR ap2_remote_control_socket_addr; // a socket pointing to the control port of the client + socklen_t ap2_remote_control_socket_addr_length; + int ap2_control_socket; + int realtime_audio_socket; + int buffered_audio_socket; + + uint16_t local_event_port; + uint16_t local_ap2_control_port; + uint16_t local_realtime_audio_port; + uint16_t local_buffered_audio_port; + + plist_t client_setup_plist; + uint64_t audio_format; + uint64_t compression; + unsigned char *session_key; // needs to be free'd at the end + uint64_t frames_packet; + uint64_t type; + uint64_t networkTimeTimelineID; // the clock ID used by the player + +#endif // used as the initials values for calculating the rate at which the source thinks it's sending // frames @@ -272,7 +342,6 @@ typedef struct { } rtsp_conn_info; uint32_t modulo_32_offset(uint32_t from, uint32_t to); -uint64_t modulo_64_offset(uint64_t from, uint64_t to); int player_play(rtsp_conn_info *conn); int player_stop(rtsp_conn_info *conn); diff --git a/rtp.c b/rtp.c index 87fd25dd..b96a344d 100644 --- a/rtp.c +++ b/rtp.c @@ -44,6 +44,15 @@ #include #include #include +#ifdef CONFIG_AIRPLAY_2 +#include "ptp-utilities.h" +#include +#include +#include +#include +#include +#endif + struct Nvll { char *name; @@ -56,6 +65,20 @@ typedef struct Nvll nvll; uint64_t local_to_remote_time_jitter; uint64_t local_to_remote_time_jitter_count; +typedef struct { + int closed; + int error_code; + int sock_fd; + char *buffer; + char *toq; + char *eoq; + size_t buffer_max_size; + size_t buffer_occupancy; + pthread_mutex_t mutex; + pthread_cond_t not_empty_cv; + pthread_cond_t not_full_cv; +} buffered_tcp_desc; + void rtp_initialise(rtsp_conn_info *conn) { conn->rtp_time_of_last_resend_request_error_ns = 0; conn->rtp_running = 0; @@ -66,7 +89,7 @@ void rtp_initialise(rtsp_conn_info *conn) { } void rtp_terminate(rtsp_conn_info *conn) { - conn->reference_timestamp = 0; + conn->anchor_rtptime = 0; // destroy the timer mutex int rc = pthread_mutex_destroy(&conn->reference_time_mutex); if (rc) @@ -199,7 +222,8 @@ void *rtp_audio_receiver(void *arg) { if (plen >= 16) { if ((config.diagnostic_drop_packet_fraction == 0.0) || (drand48() > config.diagnostic_drop_packet_fraction)) - player_put_packet(seqno, actual_timestamp, pktp, plen, conn); + player_put_packet(seqno, actual_timestamp, pktp, plen, + conn); // the '1' means is original format else debug(3, "Dropping audio packet %u to simulate a bad connection.", seqno); continue; @@ -213,7 +237,9 @@ void *rtp_audio_receiver(void *arg) { } warn("Audio receiver -- Unknown RTP packet of type 0x%02X length %d.", type, nread); } else { - debug(1, "Error receiving an audio packet."); + char em[1024]; + strerror_r(errno, em, sizeof(em)); + debug(1, "Error %d receiving an audio packet: \"%s\".", errno, em); } } @@ -229,14 +255,14 @@ void *rtp_audio_receiver(void *arg) { } void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) { - debug(3, "Control Receiver Cleanup Done."); + debug(1, "Control Receiver Cleanup Done."); } void *rtp_control_receiver(void *arg) { pthread_cleanup_push(rtp_control_handler_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; - conn->reference_timestamp = 0; // nothing valid received yet + conn->anchor_rtptime = 0; // nothing valid received yet uint8_t packet[2048], *pktp; // struct timespec tn; uint64_t remote_time_of_sync; @@ -397,11 +423,11 @@ void *rtp_control_receiver(void *arg) { } } else { uint64_t remote_frame_time_interval = - conn->remote_reference_timestamp_time - + conn->anchor_time - conn->initial_reference_time; // here, this should never be zero if (remote_frame_time_interval) { conn->remote_frame_rate = - (1.0E9 * (conn->reference_timestamp - conn->initial_reference_timestamp)) / + (1.0E9 * (conn->anchor_rtptime - conn->initial_reference_timestamp)) / remote_frame_time_interval; } else { conn->remote_frame_rate = 0.0; // use as a flag. @@ -409,14 +435,14 @@ void *rtp_control_receiver(void *arg) { } // this is for debugging - uint64_t old_remote_reference_time = conn->remote_reference_timestamp_time; - uint32_t old_reference_timestamp = conn->reference_timestamp; + uint64_t old_remote_reference_time = conn->anchor_time; + uint32_t old_reference_timestamp = conn->anchor_rtptime; // int64_t old_latency_delayed_timestamp = conn->latency_delayed_timestamp; - conn->remote_reference_timestamp_time = remote_time_of_sync; + conn->anchor_time = remote_time_of_sync; // conn->reference_timestamp_time = // remote_time_of_sync - local_to_remote_time_difference_now(conn); - conn->reference_timestamp = sync_rtp_timestamp; + conn->anchor_rtptime = sync_rtp_timestamp; conn->latency_delayed_timestamp = rtp_timestamp_less_latency; debug_mutex_unlock(&conn->reference_time_mutex, 0); @@ -443,7 +469,8 @@ void *rtp_control_receiver(void *arg) { // check if packet contains enough content to be reasonable if (plen >= 16) { - player_put_packet(seqno, actual_timestamp, pktp, plen, conn); + player_put_packet(seqno, actual_timestamp, pktp, plen, + conn); // the '1' means is original format continue; } else { debug(3, "Too-short retransmitted audio packet received in control port, ignored."); @@ -455,7 +482,10 @@ void *rtp_control_receiver(void *arg) { debug(3, "Control Receiver -- dropping a packet to simulate a bad network."); } } else { - debug(1, "Control Receiver -- error receiving a packet."); + + char em[1024]; + strerror_r(errno, em, sizeof(em)); + debug(1, "Control Receiver -- error %d receiving a packet: \"%s\".", errno, em); } } debug(1, "Control RTP thread \"normal\" exit -- this can't happen. Hah!"); @@ -849,85 +879,6 @@ void *rtp_timing_receiver(void *arg) { pthread_exit(NULL); } -static uint16_t bind_port(int ip_family, const char *self_ip_address, uint32_t scope_id, - int *sock) { - // look for a port in the range, if any was specified. - int ret = 0; - - int local_socket = socket(ip_family, SOCK_DGRAM, IPPROTO_UDP); - if (local_socket == -1) - die("Could not allocate a socket."); - - /* - int val = 1; - ret = setsockopt(local_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); - if (ret < 0) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "Error %d: \"%s\". Couldn't set SO_REUSEADDR"); - } - */ - - SOCKADDR myaddr; - int tryCount = 0; - uint16_t desired_port; - do { - tryCount++; - desired_port = nextFreeUDPPort(); - memset(&myaddr, 0, sizeof(myaddr)); - if (ip_family == AF_INET) { - struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr; - sa->sin_family = AF_INET; - sa->sin_port = ntohs(desired_port); - inet_pton(AF_INET, self_ip_address, &(sa->sin_addr)); - ret = bind(local_socket, (struct sockaddr *)sa, sizeof(struct sockaddr_in)); - } -#ifdef AF_INET6 - if (ip_family == AF_INET6) { - struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&myaddr; - sa6->sin6_family = AF_INET6; - sa6->sin6_port = ntohs(desired_port); - inet_pton(AF_INET6, self_ip_address, &(sa6->sin6_addr)); - sa6->sin6_scope_id = scope_id; - ret = bind(local_socket, (struct sockaddr *)sa6, sizeof(struct sockaddr_in6)); - } -#endif - - } while ((ret < 0) && (errno == EADDRINUSE) && (desired_port != 0) && - (tryCount < config.udp_port_range)); - - // debug(1,"UDP port chosen: %d.",desired_port); - - if (ret < 0) { - close(local_socket); - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- " - "it must be " - "at least 3, and 10 or more is suggested -- or " - "check for restrictive firewall settings or a bad router! UDP base is %u, range is %u and " - "current suggestion is %u.", - errno, errorstring, config.udp_port_base, config.udp_port_range, desired_port); - } - - uint16_t sport; - SOCKADDR local; - socklen_t local_len = sizeof(local); - getsockname(local_socket, (struct sockaddr *)&local, &local_len); -#ifdef AF_INET6 - if (local.SAFAMILY == AF_INET6) { - struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&local; - sport = ntohs(sa6->sin6_port); - } else -#endif - { - struct sockaddr_in *sa = (struct sockaddr_in *)&local; - sport = ntohs(sa->sin_port); - } - *sock = local_socket; - return sport; -} - void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport, rtsp_conn_info *conn) { @@ -1027,17 +978,17 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport conn->remote_control_port = cport; conn->remote_timing_port = tport; - conn->local_control_port = bind_port(conn->connection_ip_family, conn->self_ip_string, + conn->local_control_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, conn->self_scope_id, &conn->control_socket); - conn->local_timing_port = bind_port(conn->connection_ip_family, conn->self_ip_string, + conn->local_timing_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, conn->self_scope_id, &conn->timing_socket); - conn->local_audio_port = bind_port(conn->connection_ip_family, conn->self_ip_string, + conn->local_audio_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, conn->self_scope_id, &conn->audio_socket); debug(3, "listening for audio, control and timing on ports %d, %d, %d.", conn->local_audio_port, conn->local_control_port, conn->local_timing_port); - conn->reference_timestamp = 0; + conn->anchor_rtptime = 0; conn->request_sent = 0; conn->rtp_running = 1; @@ -1049,31 +1000,28 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport } } -void get_reference_timestamp_stuff(uint32_t *timestamp, uint64_t *timestamp_time, - uint64_t *remote_timestamp_time, rtsp_conn_info *conn) { - // types okay - debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); - *timestamp = conn->reference_timestamp; - *remote_timestamp_time = conn->remote_reference_timestamp_time; - *timestamp_time = - conn->remote_reference_timestamp_time - local_to_remote_time_difference_now(conn); - debug_mutex_unlock(&conn->reference_time_mutex, 0); -} - -void clear_reference_timestamp(rtsp_conn_info *conn) { +void reset_ntp_anchor_info(rtsp_conn_info *conn) { debug_mutex_lock(&conn->reference_time_mutex, 1000, 1); - conn->reference_timestamp = 0; - conn->remote_reference_timestamp_time = 0; + conn->anchor_rtptime = 0; + conn->anchor_time = 0; debug_mutex_unlock(&conn->reference_time_mutex, 3); } -int have_timestamp_timing_information(rtsp_conn_info *conn) { - if (conn->reference_timestamp == 0) +void reset_anchor_info(rtsp_conn_info *conn) { + reset_ntp_anchor_info(conn); +} + +int have_ntp_timestamp_timing_information(rtsp_conn_info *conn) { + if (conn->anchor_rtptime == 0) return 0; else return 1; } +int have_timestamp_timing_information(rtsp_conn_info *conn) { + return have_ntp_timestamp_timing_information(conn); +} + // set this to zero to use the rates supplied by the sources, which might not always be completely // right... const int use_nominal_rate = 0; // specify whether to use the nominal input rate, usually 44100 fps @@ -1086,8 +1034,8 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con if ((conn->initial_reference_time) && (conn->initial_reference_timestamp)) { // uint32_t local_frames = conn->reference_timestamp - conn->initial_reference_timestamp; uint32_t local_frames = - modulo_32_offset(conn->initial_reference_timestamp, conn->reference_timestamp); - uint64_t local_time = conn->remote_reference_timestamp_time - conn->initial_reference_time; + modulo_32_offset(conn->initial_reference_timestamp, conn->anchor_rtptime); + uint64_t local_time = conn->anchor_time - conn->initial_reference_time; if ((local_frames == 0) || (local_time == 0) || (use_nominal_rate)) { result = 1; } else { @@ -1110,87 +1058,198 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con return result; } +void set_ntp_anchor_info(rtsp_conn_info *conn, uint32_t rtptime, + uint64_t networktime) { + conn->anchor_remote_info_is_valid = 1; + conn->anchor_rtptime = rtptime; + conn->anchor_time = networktime; +} + // the timestamp is a timestamp calculated at the input rate // the reference timestamps are denominated in terms of the input rate -int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { +int frame_to_ntp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); int result = 0; uint64_t time_difference; uint32_t frame_difference; result = sanitised_source_rate_information(&frame_difference, &time_difference, conn); - uint64_t timestamp_interval_time; uint64_t remote_time_of_timestamp; - uint32_t timestamp_interval = modulo_32_offset(conn->reference_timestamp, timestamp); - if (timestamp_interval <= - conn->input_rate * 3600) { // i.e. timestamp was really after the reference timestamp - timestamp_interval_time = (timestamp_interval * time_difference) / + int32_t timestamp_interval = timestamp - conn->anchor_rtptime; + int64_t timestamp_interval_time = timestamp_interval; + timestamp_interval_time = timestamp_interval_time * time_difference; + timestamp_interval_time = timestamp_interval_time / frame_difference; // this is the nominal time, based on the // fps specified between current and // previous sync frame. - remote_time_of_timestamp = conn->remote_reference_timestamp_time + + remote_time_of_timestamp = conn->anchor_time + timestamp_interval_time; // based on the reference timestamp time // plus the time interval calculated based // on the specified fps. - } else { // i.e. timestamp was actually before the reference timestamp - timestamp_interval = - modulo_32_offset(timestamp, conn->reference_timestamp); // fix the calculation - timestamp_interval_time = (timestamp_interval * time_difference) / - frame_difference; // this is the nominal time, based on the - // fps specified between current and - // previous sync frame. - remote_time_of_timestamp = conn->remote_reference_timestamp_time - - timestamp_interval_time; // based on the reference timestamp time - // plus the time interval calculated based - // on the specified fps. - } *time = remote_time_of_timestamp - local_to_remote_time_difference_now(conn); debug_mutex_unlock(&conn->reference_time_mutex, 0); return result; } -int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { +int local_ntp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); int result = 0; - uint64_t time_difference; uint32_t frame_difference; result = sanitised_source_rate_information(&frame_difference, &time_difference, conn); - // first, get from [local] time to remote time. uint64_t remote_time = time + local_to_remote_time_difference_now(conn); // next, get the remote time interval from the remote_time to the reference time - uint64_t time_interval; - // here, we calculate the time interval, in terms of remote time - uint64_t offset = modulo_64_offset(conn->remote_reference_timestamp_time, remote_time); - int reference_time_was_earlier = (offset <= (uint64_t)3600000000000); - if (reference_time_was_earlier) // if we haven't had a reference within the last hour, it'll be - // taken as afterwards - time_interval = remote_time - conn->remote_reference_timestamp_time; - else - time_interval = conn->remote_reference_timestamp_time - remote_time; - + int64_t offset = remote_time - conn->anchor_time; // now, convert the remote time interval into frames using the frame rate we have observed or // which has been nominated - uint32_t frame_interval = 0; - if (time_difference) - frame_interval = (time_interval * frame_difference) / time_difference; - else - debug(1, "local_time_to_frame: time_difference is zero"); - if (reference_time_was_earlier) { - // debug(1,"Frame interval is %" PRId64 " frames.",frame_interval); - *frame = (conn->reference_timestamp + frame_interval); - } else { - // debug(1,"Frame interval is %" PRId64 " frames.",-frame_interval); - *frame = (conn->reference_timestamp - frame_interval); - } + int64_t frame_interval = 0; + frame_interval = (offset * frame_difference) / time_difference; + int32_t frame_interval_32 = frame_interval; + uint32_t new_frame = conn->anchor_rtptime + frame_interval_32; + *frame = new_frame; debug_mutex_unlock(&conn->reference_time_mutex, 0); return result; } +void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, + uint64_t networktime) { + if (conn->anchor_clock != clock_id) + debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); + conn->anchor_remote_info_is_valid = 1; + conn->anchor_rtptime = rtptime; + conn->anchor_time = networktime; + conn->anchor_clock = clock_id; +} + +void reset_ptp_anchor_info(rtsp_conn_info *conn) { + debug(2, "Connection %d: Clear anchor information.", conn->connection_number); + conn->last_anchor_info_is_valid = 0; + conn->anchor_remote_info_is_valid = 0; +} + +int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, + uint64_t *anchorLocalTime) { + int response = 0; + uint64_t actual_clock_id, actual_offset; + + int ptp_status = ptp_get_clock_info(&actual_clock_id, &actual_offset); + + if (ptp_status == 0) { + if (conn->anchor_remote_info_is_valid != 0) { + if (actual_clock_id == conn->anchor_clock) { + conn->last_anchor_clock_offset = actual_offset; + conn->last_anchor_time_of_update = get_absolute_time_in_ns(); + conn->last_anchor_info_is_valid = 1; + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + } else { + if (conn->last_anchor_info_is_valid != 0) { + int64_t time_since_last_update = + get_absolute_time_in_ns() - conn->last_anchor_time_of_update; + if (time_since_last_update > 5000000000) { + debug(1, "change master clock to %" PRIx64 ".", actual_clock_id); + uint64_t new_anchor_time = conn->anchor_time; + new_anchor_time = new_anchor_time - conn->last_anchor_clock_offset; // to local + new_anchor_time = new_anchor_time + actual_offset; // to the next clock + conn->anchor_time = new_anchor_time; + conn->anchor_clock = actual_clock_id; + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - actual_offset; + } else { + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + } + } else { + debug(2, "no clock and no old anchor times"); + response = -1; + } + } + } else { + debug(2, "don't have anchor_remote_time_info"); + response = -1; + } + } else if (ptp_status == -1) { + debug(3, "don't have the ptp clock interface"); + response = -1; + } else { + debug(3, "ptp clock not valid"); + response = -1; + } + return response; +} + +int get_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, + uint64_t *anchorLocalTime) { + return get_ptp_anchor_local_time_info(conn, anchorRTP,anchorLocalTime); +} + +int have_ptp_timing_information(rtsp_conn_info *conn) { + if (get_anchor_local_time_info(conn, NULL, NULL) == 0) + return 1; + else + return 0; +} + +int have_timing_information(rtsp_conn_info *conn) { + return have_ptp_timing_information(conn); +} + +int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + int result = -1; + uint32_t anchor_rtptime; + uint64_t anchor_local_time; + if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + int32_t frame_difference = timestamp - anchor_rtptime; + int64_t time_difference = frame_difference; + time_difference = time_difference * 1000000000; + time_difference = time_difference / conn->input_rate; + uint64_t ltime = anchor_local_time + time_difference; + *time = ltime; + result = 0; + } else { + debug(3, "frame_to_local_time can't get anchor local time information"); + } + return result; +} + +int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + return frame_to_ntp_local_time(timestamp, time, conn); +} + +int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + int result = -1; + uint32_t anchor_rtptime; + uint64_t anchor_local_time; + if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + int64_t time_difference = time - anchor_local_time; + int64_t frame_difference = time_difference; + frame_difference = frame_difference * conn->input_rate; // but this is by 10^9 + frame_difference = frame_difference / 1000000000; + int32_t fd32 = frame_difference; + uint32_t lframe = anchor_rtptime + fd32; + *frame = lframe; + result = 0; + } else { + debug(3, "local_time_to_frame can't get anchor local time information"); + } + return result; +} + +int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + return local_ntp_time_to_frame(time, frame, conn); +} + void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { + // debug(1, "rtp_request_resend of %u packets from sequence number %u.", count, first); if (conn->rtp_running) { // if (!request_sent) { // debug(2, "requesting resend of %d packets starting at %u.", count, first); @@ -1199,15 +1258,25 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { char req[8]; // *not* a standard RTCP NACK req[0] = 0x80; - req[1] = (char)0x55 | (char)0x80; // Apple 'resend' +#ifdef CONFIG_AIRPLAY_2 + if (conn->ap2_remote_control_socket_addr_length == 0) { + debug(2, "No remote socket -- skipping the resend"); + return; // hack + } + req[1] = 0xD5; // Airplay 2 'resend' +#else + req[1] = (char)0x55 | (char)0x80; // Apple 'resend' +#endif *(unsigned short *)(req + 2) = htons(1); // our sequence number *(unsigned short *)(req + 4) = htons(first); // missed seqnum *(unsigned short *)(req + 6) = htons(count); // count +#ifndef CONFIG_AIRPLAY_2 socklen_t msgsize = sizeof(struct sockaddr_in); #ifdef AF_INET6 if (conn->rtp_client_control_socket.SAFAMILY == AF_INET6) { msgsize = sizeof(struct sockaddr_in6); } +#endif #endif uint64_t time_of_sending_ns = get_absolute_time_in_ns(); uint64_t resend_error_backoff_time = 300000000; // 0.3 seconds @@ -1221,13 +1290,21 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 100000; - +#ifdef CONFIG_AIRPLAY_2 + if (setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, + sizeof(timeout)) < 0) + debug(1, "Can't set timeout on resend request socket."); + if (sendto(conn->ap2_control_socket, req, sizeof(req), 0, + (struct sockaddr *)&conn->ap2_remote_control_socket_addr, + conn->ap2_remote_control_socket_addr_length) == -1) { +#else if (setsockopt(conn->control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) debug(1, "Can't set timeout on resend request socket."); if (sendto(conn->control_socket, req, sizeof(req), 0, (struct sockaddr *)&conn->rtp_client_control_socket, msgsize) == -1) { +#endif char em[1024]; strerror_r(errno, em, sizeof(em)); debug(2, "Error %d using sendto to request a resend: \"%s\".", errno, em); @@ -1252,3 +1329,1049 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { //} } } + +#if 0 +#ifdef CONFIG_AIRPLAY_2 + +void rtp_event_receiver_cleanup_handler(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(2, "Connection %d: Event Receiver Cleanup.", conn->connection_number); +} + +void *rtp_event_receiver(void *arg) { + debug(2, "Event Receiver started"); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + pthread_cleanup_push(rtp_event_receiver_cleanup_handler, arg); + + listen(conn->event_socket, 5); + + uint8_t packet[4096]; + ssize_t nread; + SOCKADDR remote_addr; + memset(&remote_addr, 0, sizeof(remote_addr)); + socklen_t addr_size = sizeof(remote_addr); + + int fd = accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size); + pthread_cleanup_push(socket_cleanup, (void *)fd); + int finished = 0; + do { + nread = recv(fd, packet, sizeof(packet), 0); + + if (nread < 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "error in rtp_event_receiver %d: \"%s\". Could not recv a packet.", errno, + errorstring); + // if ((config.diagnostic_drop_packet_fraction == 0.0) || + // (drand48() > config.diagnostic_drop_packet_fraction)) { + } else if (nread > 0) { + + // ssize_t plen = nread; + debug(1, "Packet Received on Event Port."); + if (packet[1] == 0xD7) { + debug(1, "Event Receiver -- Time Announce RTP packet of type 0x%02X length %d received.", + packet[1], nread); + } else { + debug(1, "Event Receiver -- Unknown RTP packet of type 0x%02X length %d received.", + packet[1], nread); + } + // } else { + // debug(3, "Event Receiver Thread -- dropping incoming packet to simulate a bad network."); + // } + } else { + finished = 1; + } + } while (finished == 0); + debug(1, "Event Receiver RTP thread \"normal\" exit."); + pthread_cleanup_pop(1); // close the socket + + pthread_cleanup_pop(1); // do the cleanup + debug(1, "Connection %d: Event Receiver RTP thread exit.", conn->connection_number); + pthread_exit(NULL); +} + +void rtp_ap2_control_handler_cleanup_handler(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); +} + +int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread, + rtsp_conn_info *conn) { + + // this deciphers the packet -- it doesn't decode it from ALAC + uint16_t sequence_number = 0; + + // if the packet is too small, don't go ahead. + // it must contain an uint16_t sequence number and eight bytes of AAD followed by the + // ciphertext and then followed by an eight-byte nonce. Thus it must be greater than 18 + if (nread > 18) { + + memcpy(&sequence_number, ciphered_audio_alt, sizeof(uint16_t)); + sequence_number = ntohs(sequence_number); + + uint32_t timestamp; + memcpy(×tamp, ciphered_audio_alt + sizeof(uint16_t), sizeof(uint32_t)); + timestamp = ntohl(timestamp); + + /* + uint32_t ssrc; + memcpy(&ssrc, packet+8, sizeof(uint32_t)); + ssrc = ntohl(ssrc); + */ + + // debug(1, "Realtime Audio Receiver Packet received. Version: %u, Padding: %u, Extension: + // %u, Csrc Count: %u, Marker: %u, Payload Type: %u, Sequence Number: %u, Timestamp: %u, + // SSRC: %u.", version, padding, extension, csrc_count, marker, payload_type, + // sequence_number, timestamp, ssrc); + + unsigned char nonce[12]; + memset(nonce, 0, sizeof(nonce)); + memcpy(nonce + 4, ciphered_audio_alt + nread - 8, + 8); // front-pad the 8-byte nonce received to get the 12-byte nonce expected + + // 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. + + unsigned char m[4096]; + unsigned long long new_payload_length = 0; + int response = crypto_aead_chacha20poly1305_ietf_decrypt( + m, // m + &new_payload_length, // mlen_p + NULL, // nsec, + ciphered_audio_alt + + 10, // the ciphertext starts 10 bytes in and is followed by the MAC tag, + nread - (8 + 10), // clen -- the last 8 bytes are the nonce + ciphered_audio_alt + 2, // authenticated additional data + 8, // authenticated additional data length + nonce, + conn->session_key); // *k + if (response != 0) { + debug(1, "Error decrypting an audio packet."); + } + // now pass it in to the regular processing chain + + unsigned long long max_int = INT_MAX; // put in the right format + if (new_payload_length > max_int) + debug(1, "Madly long payload length!"); + int plen = new_payload_length; // + player_put_packet(sequence_number, timestamp, m, plen, + conn); // the '1' means is original format + return sequence_number; + } else { + debug(1, "packet was too small -- ignored"); + return -1; + } +} + +void *rtp_ap2_control_receiver(void *arg) { + pthread_cleanup_push(rtp_ap2_control_handler_cleanup_handler, arg); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + uint8_t packet[4096]; + ssize_t nread; + while (1) { + SOCKADDR from_sock_addr; + socklen_t from_sock_addr_length = sizeof(SOCKADDR); + memset(&from_sock_addr, 0, sizeof(SOCKADDR)); + + nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, + (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); + + if (nread >= 0) { + // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); + + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + // store the from_sock_addr if we haven't already done so + // v remember to zero this when you're finished! + if (conn->ap2_remote_control_socket_addr_length == 0) { + memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length); + conn->ap2_remote_control_socket_addr_length = from_sock_addr_length; + } + switch (packet[1]) { + case 215: // code 215, effectively an anchoring announcement + { + // struct timespec tnr; + // clock_gettime(CLOCK_REALTIME, &tnr); + // uint64_t local_realtime_now = timespec_to_ns(&tnr); + + /* + char obf[4096]; + char *obfp = obf; + int obfc; + for (obfc=0;obfcinput_rate); + if (added_latency < (-(notified_latency + 11035))) + debug(1, "the audio_backend_latency_offset is causing a negative latency!"); + + /* + debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); + conn->remote_reference_timestamp_time = remote_packet_time_ns; + conn->reference_timestamp = + frame_1 - 11035 - added_latency; // add the latency in to the anchortime + debug_mutex_unlock(&conn->reference_time_mutex, 0); + */ + + set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, remote_packet_time_ns); + + } break; + case 0xd6: + // six bytes in is the sequence number at the start of the encrypted audio packet + // returns the sequence number but we're not really interested + decipher_player_put_packet(packet + 6, nread - 6, conn); + break; + default: { + char *packet_in_hex_cstring = + debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards + debug(1, + "AP2 Control Receiver Packet of first byte 0x%02X, type 0x%02X length %d received: " + "\"%s\".", + packet[0], packet[1], nread, packet_in_hex_cstring); + free(packet_in_hex_cstring); + } break; + } + } else { + debug(1, "AP2 Control Receiver -- dropping a packet."); + } + } else { + debug(1, "AP2 Control Receiver -- error %d receiving a packet.", errno); + } + } + debug(1, "AP2 Control RTP thread \"normal\" exit -- this can't happen. Hah!"); + pthread_cleanup_pop(0); // don't execute anything here. + debug(1, "AP2 Control RTP thread exit."); + pthread_exit(NULL); +} + +void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { + debug(1, "Realtime Audio Receiver Cleanup Start."); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + close(conn->realtime_audio_socket); + conn->realtime_audio_socket = 0; + debug(1, "Realtime Audio Receiver Cleanup Done."); +} + +void *rtp_realtime_audio_receiver(void *arg) { + pthread_cleanup_push(rtp_realtime_audio_cleanup_handler, arg); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + uint8_t packet[4096]; + int32_t last_seqno = -1; + ssize_t nread; + while (1) { + nread = recv(conn->realtime_audio_socket, packet, sizeof(packet), 0); + + if (nread > 36) { // 36 is the 12-byte header and and 24-byte footer + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + + /* + char *packet_in_hex_cstring = + debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards + debug(1, "Audio Receiver Packet of type 0x%02X length %d received: \"%s\".", + packet[1], nread, packet_in_hex_cstring); + free(packet_in_hex_cstring); + */ + + /* + // debug(1, "Realtime Audio Receiver Packet of type 0x%02X length %d received.", packet[1], + nread); + // now get hold of its various bits and pieces + uint8_t version = (packet[0] & 0b11000000) >> 6; + uint8_t padding = (packet[0] & 0b00100000) >> 5; + uint8_t extension = (packet[0] & 0b00010000) >> 4; + uint8_t csrc_count = packet[0] & 0b00001111; + uint8_t marker = (packet[1] & 0b1000000) >> 7; + uint8_t payload_type = packet[1] & 0b01111111; + */ + + int32_t seqno = decipher_player_put_packet(packet + 2, nread - 2, conn); + if (seqno >= 0) { + if (last_seqno == -1) { + last_seqno = seqno; + } else { + last_seqno = (last_seqno + 1) & 0xffff; + // if (seqno != last_seqno) + // debug(3, "RTP: Packets out of sequence: expected: %d, got %d.", last_seqno, seqno); + last_seqno = seqno; // reset warning... + } + } else { + debug(1, "Realtime Audio Receiver -- bad packet dropped."); + } + } else { + debug(3, "Realtime Audio Receiver -- dropping a packet."); + } + } else { + debug(1, "Realtime Audio Receiver -- error receiving a packet."); + } + } + pthread_cleanup_pop(0); // don't execute anything here. + pthread_exit(NULL); +} + +void rtp_buffered_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { + debug(1, "Buffered Audio Receiver Cleanup Start."); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + close(conn->buffered_audio_socket); + conn->buffered_audio_socket = 0; + debug(1, "Buffered Audio Receiver Cleanup Done."); +} + +ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { + ssize_t response; + // usleep(1500000); + if (pthread_mutex_lock(&descriptor->mutex) != 0) + debug(1, "problem with mutex"); + pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); + if ((descriptor->closed == 0)) { + while ((descriptor->buffer_occupancy == 0) && (descriptor->error_code == 0)) { + if (pthread_cond_wait(&descriptor->not_empty_cv, &descriptor->mutex)) + debug(1, "Error waiting for buffered read"); + } + } + if (descriptor->buffer_occupancy != 0) { + ssize_t bytes_to_move = count; + + if (descriptor->buffer_occupancy < count) + bytes_to_move = descriptor->buffer_occupancy; + + ssize_t top_gap = descriptor->buffer + descriptor->buffer_max_size - descriptor->toq; + if (top_gap < bytes_to_move) + bytes_to_move = top_gap; + + memcpy(buf, descriptor->toq, bytes_to_move); + descriptor->toq += bytes_to_move; + if (descriptor->toq == descriptor->buffer + descriptor->buffer_max_size) + descriptor->toq = descriptor->buffer; + descriptor->buffer_occupancy -= bytes_to_move; + response = bytes_to_move; + if (pthread_cond_signal(&descriptor->not_full_cv)) + debug(1, "Error signalling"); + } else if (descriptor->error_code) { + errno = descriptor->error_code; + response = -1; + } else if (descriptor->closed != 0) { + response = 0; + } + + pthread_cleanup_pop(1); // release the mutex + return response; +} + +#define STANDARD_PACKET_SIZE 65536 + +void buffered_tcp_reader_cleanup_handler(__attribute__((unused)) void *arg) { + debug(2, "Buffered TCP Reader Thread Exit via Cleanup."); +} + +void *buffered_tcp_reader(void *arg) { + pthread_cleanup_push(buffered_tcp_reader_cleanup_handler, NULL); + buffered_tcp_desc *descriptor = (buffered_tcp_desc *)arg; + + listen(descriptor->sock_fd, 5); + ssize_t nread; + SOCKADDR remote_addr; + memset(&remote_addr, 0, sizeof(remote_addr)); + socklen_t addr_size = sizeof(remote_addr); + int finished = 0; + int fd = accept(descriptor->sock_fd, (struct sockaddr *)&remote_addr, &addr_size); + pthread_cleanup_push(socket_cleanup, (void *)fd); + + do { + if (pthread_mutex_lock(&descriptor->mutex) != 0) + debug(1, "problem with mutex"); + pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); + while ((descriptor->buffer_occupancy == descriptor->buffer_max_size) || + (descriptor->error_code != 0) || (descriptor->closed != 0)) { + if (pthread_cond_wait(&descriptor->not_full_cv, &descriptor->mutex)) + debug(1, "Error waiting for buffered read"); + } + pthread_cleanup_pop(1); // release the mutex + + // now we know it is not full, so go ahead and try to read some more into it + + // wrap + if ((size_t)(descriptor->eoq - descriptor->buffer) == descriptor->buffer_max_size) + descriptor->eoq = descriptor->buffer; + + // figure out how much to ask for + size_t bytes_to_request = STANDARD_PACKET_SIZE; + size_t free_space = descriptor->buffer_max_size - descriptor->buffer_occupancy; + if (bytes_to_request > free_space) + bytes_to_request = free_space; // don't ask for more than will fit + + size_t gap_to_end_of_buffer = + descriptor->buffer + descriptor->buffer_max_size - descriptor->eoq; + if (gap_to_end_of_buffer < bytes_to_request) + bytes_to_request = + gap_to_end_of_buffer; // only ask for what will fill to the top of the buffer + + // do the read + // debug(1, "Request buffered read of up to %d bytes.", bytes_to_request); + nread = recv(fd, descriptor->eoq, bytes_to_request, 0); + // debug(1, "Received %d bytes for a buffer size of %d bytes.",nread, + // descriptor->buffer_occupancy + nread); + if (pthread_mutex_lock(&descriptor->mutex) != 0) + debug(1, "problem with not empty mutex"); + pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); + if (nread < 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "error in buffered_read %d: \"%s\". Could not recv a packet.", errno, errorstring); + descriptor->error_code = errno; + } else if (nread == 0) { + descriptor->closed = 1; + } else if (nread > 0) { + descriptor->eoq += nread; + descriptor->buffer_occupancy += nread; + } else { + debug(1, "buffered audio port closed!"); + } + // signal if we got data or an error or the file closed + if (pthread_cond_signal(&descriptor->not_empty_cv)) + debug(1, "Error signalling"); + pthread_cleanup_pop(1); // release the mutex + } while (finished == 0); + + debug(1, "Buffered TCP Reader Thread Exit \"Normal\" Exit Begin."); + pthread_cleanup_pop(1); // close the socket + pthread_cleanup_pop(1); // cleanup + debug(1, "Buffered TCP Reader Thread Exit \"Normal\" Exit -- Shouldn't happen!."); + pthread_exit(NULL); +} + +void avcodec_alloc_context3_cleanup_handler(void *arg) { + debug(3, "avcodec_alloc_context3_cleanup_handler"); + AVCodecContext *codec_context = arg; + av_free(codec_context); +} + +void avcodec_open2_cleanup_handler(void *arg) { + debug(3, "avcodec_open2_cleanup_handler"); + AVCodecContext *codec_context = arg; + avcodec_close(codec_context); +} + +void av_parser_init_cleanup_handler(void *arg) { + debug(3, "av_parser_init_cleanup_handler"); + AVCodecParserContext *codec_parser_context = arg; + av_parser_close(codec_parser_context); +} + +void swr_alloc_cleanup_handler(void *arg) { + debug(3, "swr_alloc_cleanup_handler"); + SwrContext **swr = arg; + swr_free(swr); +} + +void av_packet_alloc_cleanup_handler(void *arg) { + debug(3, "av_packet_alloc_cleanup_handler"); + AVPacket **pkt = arg; + av_packet_free(pkt); +} + +// this will read a block of the size specified to the buffer +// and will return either with the block or on error +ssize_t lread_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count) { + ssize_t response, nread; + size_t inbuf = 0; // bytes already in the buffer + int keep_trying = 1; + + do { + nread = buffered_read(descriptor, buf + inbuf, count - inbuf); + if (nread == 0) { + // a blocking read that returns zero means eof -- implies connection closed + debug(3, "read_sized_block connection closed."); + keep_trying = 0; + } else if (nread < 0) { + if (errno == EAGAIN) { + debug(1, "read_sized_block getting Error 11 -- EAGAIN from a blocking read!"); + } + if ((errno != ECONNRESET) && (errno != EAGAIN) && (errno != EINTR)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "read_sized_block read error %d: \"%s\".", errno, (char *)errorstring); + keep_trying = 0; + } + } else { + inbuf += (size_t)nread; + } + } while ((keep_trying != 0) && (inbuf < count)); + if (nread <= 0) + response = nread; + else + response = inbuf; + return response; +} + +// From +// https://stackoverflow.com/questions/18862715/how-to-generate-the-aac-adts-elementary-stream-with-android-mediacodec +// with thanks! +/** + * Add ADTS header at the beginning of each and every AAC packet. + * This is needed as MediaCodec encoder generates a packet of raw + * AAC data. + * + * Note the packetLen must count in the ADTS header itself. + **/ +void addADTStoPacket(uint8_t *packet, int packetLen) { + int profile = 2; // AAC LC + // 39=MediaCodecInfo.CodecProfileLevel.AACObjectELD; + int freqIdx = 4; // 44.1KHz + int chanCfg = 2; // CPE + + // fill in ADTS data + packet[0] = 0xFF; + packet[1] = 0xF9; + packet[2] = ((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2); + packet[3] = ((chanCfg & 3) << 6) + (packetLen >> 11); + packet[4] = (packetLen & 0x7FF) >> 3; + packet[5] = ((packetLen & 7) << 5) + 0x1F; + packet[6] = 0xFC; +} + +void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { + debug(3, "Buffered Audio Receiver Cleanup Start."); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + close(conn->buffered_audio_socket); + conn->buffered_audio_socket = 0; + debug(2, "Buffered Audio Receiver Cleanup Done."); +} + +void *rtp_buffered_audio_processor(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + pthread_cleanup_push(rtp_buffered_audio_cleanup_handler, arg); + + pthread_t *buffered_reader_thread = malloc(sizeof(pthread_t)); + if (buffered_reader_thread == NULL) + debug(1, "cannot allocate a buffered_reader_thread!"); + memset(buffered_reader_thread, 0, sizeof(pthread_t)); + pthread_cleanup_push(malloc_cleanup, buffered_reader_thread); + + buffered_tcp_desc *buffered_audio = malloc(sizeof(buffered_tcp_desc)); + if (buffered_audio == NULL) + debug(1, "cannot allocate a buffered_tcp_desc!"); + // initialise the descriptor + memset(buffered_audio, 0, sizeof(buffered_tcp_desc)); + pthread_cleanup_push(malloc_cleanup, buffered_audio); + + if (pthread_mutex_init(&buffered_audio->mutex, NULL)) + debug(1, "Connection %d: error %d initialising buffered_audio mutex.", conn->connection_number, + errno); + pthread_cleanup_push(mutex_cleanup, &buffered_audio->mutex); + + if (pthread_cond_init(&buffered_audio->not_empty_cv, NULL)) + die("Connection %d: error %d initialising not_empty cv.", conn->connection_number, errno); + pthread_cleanup_push(cv_cleanup, &buffered_audio->not_empty_cv); + + if (pthread_cond_init(&buffered_audio->not_full_cv, NULL)) + die("Connection %d: error %d initialising not_full cv.", conn->connection_number, errno); + pthread_cleanup_push(cv_cleanup, &buffered_audio->not_full_cv); + + // initialise the buffer data structure + buffered_audio->buffer_max_size = conn->ap2_audio_buffer_size; + buffered_audio->buffer = malloc(conn->ap2_audio_buffer_size); + if (buffered_audio->buffer == NULL) + debug(1, "cannot allocate an audio buffer of %u bytes!", buffered_audio->buffer_max_size); + pthread_cleanup_push(malloc_cleanup, buffered_audio->buffer); + + // pthread_mutex_lock(&conn->buffered_audio_mutex); + buffered_audio->toq = buffered_audio->buffer; + buffered_audio->eoq = buffered_audio->buffer; + + buffered_audio->sock_fd = conn->buffered_audio_socket; + + pthread_create(buffered_reader_thread, NULL, &buffered_tcp_reader, buffered_audio); + pthread_cleanup_push(thread_cleanup, buffered_reader_thread); + + // ideas and some code from https://rodic.fr/blog/libavcodec-tutorial-decode-audio-file/ + // with thanks + + // initialize all muxers, demuxers and protocols for libavformat + // (does nothing if called twice during the course of one program execution) + // not needed in ffmpeg 4.0 and later... + // av_register_all(); + + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); + if (codec == NULL) { + debug(1, "Can't find an AAC decoder!"); + } + + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + if (codec_context == NULL) { + debug(1, "Could not allocate audio codec context!"); + } + // push a deallocator -- av_free(codec_context) + pthread_cleanup_push(avcodec_alloc_context3_cleanup_handler, codec_context); + + if (avcodec_open2(codec_context, codec, NULL) < 0) { + debug(1, "Could not open a codec into the audio codec context"); + } + // push a closer -- avcodec_close(codec_context); + pthread_cleanup_push(avcodec_open2_cleanup_handler, codec_context); + + AVCodecParserContext *codec_parser_context = av_parser_init(codec->id); + if (codec_parser_context == NULL) { + debug(1, "Can't initialise a parser context!"); + } + // push a closer -- av_parser_close(codec_parser_context); + pthread_cleanup_push(av_parser_init_cleanup_handler, codec_parser_context); + + AVPacket *pkt = av_packet_alloc(); + if (pkt == NULL) { + debug(1, "Can't allocate an AV packet"); + } + // push a deallocator -- av_packet_free(pkt); + pthread_cleanup_push(av_packet_alloc_cleanup_handler, &pkt); + + AVFrame *decoded_frame = NULL; + int dst_linesize; + int dst_bufsize; + + // Prepare software resampler to convert floating point (?) to S16 + SwrContext *swr = swr_alloc(); + if (swr == NULL) { + debug(1, "can not allocate a swr context"); + } + // push a deallocator -- av_packet_free(pkt); + pthread_cleanup_push(swr_alloc_cleanup_handler, &swr); + + av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_int(swr, "in_sample_rate", 44100, 0); + av_opt_set_int(swr, "out_sample_rate", 44100, 0); + av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); + av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); + swr_init(swr); + + uint8_t packet[16 * 1024]; + unsigned char m[16 * 1024]; // leave the first 7 bytes blank to make room for the ADTS + uint8_t *pcm_audio = NULL; // the S16 output + unsigned char *data_to_process; + ssize_t data_remaining; + uint32_t seq_no, last_block_flushed_seq_no; // audio packet number + uint32_t previous_seq_no = 0; + int new_buffer_needed; + + uint32_t last_block_flushed_rtp_time; + + ssize_t nread; + + int finished = 0; + int pcm_buffer_size = + (1024 + 352) * 8; // This seems to be right. 8 is for 2 * 32-bit samples per frame + uint8_t pcm_buffer[pcm_buffer_size]; + + int pcm_buffer_occupancy = 0; + int pcm_buffer_read_point = 0; // offset to where the next buffer should come from + uint32_t pcm_buffer_read_point_rtptime = 0; + + uint64_t blocks_read = 0; + int flush_requested = 0; + int streaming_has_started = 0; + do { + int flush_newly_requested = 0; + // are we in in flush mode, or just about to leave it? + debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! + uint32_t flushUntilSeq = conn->ap2_flush_sequence_number; + uint32_t flushUntilTS = conn->ap2_flush_rtp_timestamp; + // if we are in flush mode + if (conn->ap2_flush_requested) { + if (flush_requested == 0) { + // here, a flush has been newly requested + debug(1, + "flush requested. flushUntilSeq = %u, flushUntilTS = %u, seq_no: %u, " + "pcm_buffer_read_point_rtptime = " + "%u, buffer_size: %u.", + flushUntilSeq, flushUntilTS, seq_no, pcm_buffer_read_point_rtptime, + buffered_audio->buffer_occupancy); + last_block_flushed_rtp_time = pcm_buffer_read_point_rtptime; + last_block_flushed_seq_no = seq_no; + flush_newly_requested = 1; + } + // if we have arrived at the nominated flush block + // (it must be the case that the block is in the pcm buffer) + if ((blocks_read != 0) && (seq_no == flushUntilSeq)) { + // we may need to flush the pcm_buffer up to flushUntilTS + int32_t flush_difference = flushUntilTS - pcm_buffer_read_point_rtptime; + if (flush_difference < 0) { + debug(1, "flushUntilTS is before the flushUntilSeq's start TS"); + } else if (flush_difference > pcm_buffer_occupancy) { + debug(1, + "flushUntilTS is after the flushUntilSeq's last TS. flushUntilTS = %u, " + "pcm_buffer_read_point_rtptime = %u, pcm_buffer_occupancy = %u.", + flushUntilTS, pcm_buffer_read_point_rtptime, pcm_buffer_occupancy); + } else { + // move the buffer contents from flush_difference to pcm_buffer_occupancy + // to the front of the pcm_buffer + memcpy(pcm_buffer, pcm_buffer + flush_difference * 4, + pcm_buffer_occupancy - flush_difference * 4); + pcm_buffer_occupancy = pcm_buffer_occupancy - flush_difference * 4; + pcm_buffer_read_point = 0; + debug(1, "set pcm_buffer_read_point_rtptime from %u to %u.", + pcm_buffer_read_point_rtptime, flushUntilTS); + pcm_buffer_read_point_rtptime = flushUntilTS; + } + conn->ap2_flush_requested = 0; + } else { + if (seq_no > flushUntilSeq) { + debug(1, + "we have overshot the flush ending block. flushUntilSeq is %u, this block is: %u.", + flushUntilSeq, seq_no); + conn->ap2_flush_requested = 0; // but conn->ap2_play_enabled will remain at zero + } + } + } + flush_requested = conn->ap2_flush_requested; + + // this isn't always the case, i.e. that play is re-enabled after a flush + // if (flush_requested) + // conn->ap2_play_enabled = 0; + // play is disabled until an anchor time is sent and play is restarted + + int play_enabled = conn->ap2_play_enabled; + debug_mutex_unlock(&conn->flush_mutex, 3); + + // do this outside the flush mutex + if (flush_newly_requested) { + player_full_flush(conn); + streaming_has_started = 0; + pcm_buffer_occupancy = 0; + pcm_buffer_read_point = 0; + } + + // now, if a flush is not requested, we can do the normal stuff + if (flush_requested == 0) { + // is there space in the player thread's buffer system? + unsigned int player_buffer_size, player_buffer_occupancy; + get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); + // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, + // player_buffer_occupancy); + if (player_buffer_occupancy > + ((0.5 + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. + // if there is enough stuff in the player's buffer, sleep for a while and try again + // debug(1,"sleep for 20 ms"); + usleep(20000); // wait for a while + } else { + if ((pcm_buffer_occupancy - pcm_buffer_read_point) >= (352 * 4)) { + new_buffer_needed = 0; + // send a frame to the player if allowed + // it it's way too late, it means that a new anchor time is needed + + /* + uint32_t at_rtp = conn->reference_timestamp; + at_rtp = + at_rtp - (44100 * 10); // allow it to start a few seconds late, but not + madly late int rtp_diff = pcm_buffer_read_point_rtptime - at_rtp; + */ + + if (play_enabled) { + uint64_t buffer_should_be_time; + if (frame_to_local_time(pcm_buffer_read_point_rtptime, &buffer_should_be_time, conn) == + 0) { + int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); + // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); + // ask for 0.5 sec of leadtime + if ((lead_time >= (int64_t)(0.5 * 1000000000)) || (streaming_has_started == 1)) { + if (streaming_has_started == 0) + debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); + streaming_has_started = 1; +// player_put_packet(0, 0, pcm_buffer_read_point_rtptime, + player_put_packet(0, pcm_buffer_read_point_rtptime, + pcm_buffer + pcm_buffer_read_point, 352, conn); + } + + pcm_buffer_read_point_rtptime += 352; + pcm_buffer_read_point += 352 * 4; + } + // usleep(2000); // let other stuff happens + } else { + usleep(20000); // wait before asking if play is enabled again + } + } else { + new_buffer_needed = 1; + if (pcm_buffer_read_point != 0) { + // debug(1,"pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy (frames): %u", + // pcm_buffer_read_point/4, pcm_buffer_occupancy/4); + // if there is anything to move down to the front of the buffer, do it now; + if ((pcm_buffer_occupancy - pcm_buffer_read_point) > 0) { + // move the remaining frames down to the start of the buffer + // debug(1,"move the remaining frames down to the start of the pcm_buffer"); + memcpy(pcm_buffer, pcm_buffer + pcm_buffer_read_point, + pcm_buffer_occupancy - pcm_buffer_read_point); + pcm_buffer_occupancy = pcm_buffer_occupancy - pcm_buffer_read_point; + } else { + // debug(1,"nothing to move to the front of the buffer"); + pcm_buffer_occupancy = 0; + } + pcm_buffer_read_point = 0; + } + } + } + } + if ((flush_requested) || (new_buffer_needed)) { + + // debug(1,"pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy (frames): %u", + // pcm_buffer_read_point/4, pcm_buffer_occupancy/4); + // ok, so here we know we need material from the sender + // do we will get in a packet of audio + uint16_t data_len; + // here we read from the buffer that our thread has been reading + nread = lread_sized_block(buffered_audio, &data_len, sizeof(data_len)); + if (nread < 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "error in rtp_buffered_audio_processor %d: \"%s\". Could not recv a data_len .", + errno, errorstring); + // if ((config.diagnostic_drop_packet_fraction == 0.0) || + // (drand48() > config.diagnostic_drop_packet_fraction)) { + } + data_len = ntohs(data_len); + // debug(1,"buffered audio packet of size %u detected.", data_len - 2); + nread = lread_sized_block(buffered_audio, packet, data_len - 2); + // debug(1, "buffered audio packet of size %u received.", nread); + if (nread < 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "error in rtp_buffered_audio_processor %d: \"%s\". Could not recv a data packet.", + errno, errorstring); + // if ((config.diagnostic_drop_packet_fraction == 0.0) || + // (drand48() > config.diagnostic_drop_packet_fraction)) { + } else if (nread > 0) { + blocks_read++; // note, this doesn't mean they are valid audio blocks + // debug(1, "Realtime Audio Receiver Packet of length %d received.", nread); + // now get hold of its various bits and pieces + /* + uint8_t version = (packet[0] & 0b11000000) >> 6; + uint8_t padding = (packet[0] & 0b00100000) >> 5; + uint8_t extension = (packet[0] & 0b00010000) >> 4; + uint8_t csrc_count = packet[0] & 0b00001111; + */ + seq_no = packet[1] * (1 << 16) + packet[2] * (1 << 8) + packet[3]; + uint32_t timestamp = nctohl(&packet[4]); + // uint32_t ssrc = nctohl(&packet[8]); + // uint8_t marker = 0; + // uint8_t payload_type = 0; + + if ((blocks_read != 0) && (seq_no != previous_seq_no + 1)) { + if (previous_seq_no != 0) + debug(1, "block discontinuity: from sequence number %u to sequence number %u.", + previous_seq_no, seq_no); + if (pcm_buffer_occupancy != 0) { + debug(1, + "rtptime discontinuity! Existing pcm buffer contents with timestamp " + "%u, seq_no %u to new block with timestamp %u, seq_no %u", + pcm_buffer_read_point_rtptime, previous_seq_no, timestamp, seq_no); + pcm_buffer_occupancy = 0; + } + // if still playing, this is a problem -- need to reset the player + if (play_enabled) + player_full_flush(conn); + } + + previous_seq_no = seq_no; + + // at this point, we can check if we can to flush this packet -- we won't have + // to decipher it first + // debug(1,"seq_no %u, timestamp %u", seq_no, timestamp); + + if ((flush_requested) && (seq_no < flushUntilSeq)) { + last_block_flushed_seq_no = seq_no; + last_block_flushed_rtp_time = timestamp; + // debug(1, "flushing block %u, rtptime %u up to block %u, rtptime %u.", seq_no, + // timestamp, flushUntilSeq, flushUntilTS); + } + + if (((flush_requested) && (seq_no == flushUntilSeq)) || (new_buffer_needed)) { + + if ((flush_requested) && (seq_no == flushUntilSeq)) { + int32_t timestamp_difference = timestamp - flushUntilTS; + debug(1, + "end of flush reached. flushUntilSeq = %u, flushUntilTS = %u, last block flushed " + "seq_no = %u, last block flushed rtptime = %u, next block seq_no = %u, next " + "block timestamp = " + "%u, timestamp difference: %d, buffer_size: %u.", + flushUntilSeq, flushUntilTS, last_block_flushed_seq_no, + last_block_flushed_rtp_time, seq_no, timestamp, timestamp_difference, + buffered_audio->buffer_occupancy); + debug(2, + "pcm_buffer_read_point_rtptime = %u, pcm_buffer_occupancy = %u, " + "pcm_buffer_read_point = %u", + pcm_buffer_read_point_rtptime, pcm_buffer_occupancy, pcm_buffer_read_point); + } + + // if we are here because of a flush request, it must be the case that + // flushing the pcm buffer wasn't enough, as the request would have been turned off by now + // so we better indicate that the pcm buffer is empty and its contents invalid + if (flush_requested) { + pcm_buffer_occupancy = 0; + } + + // decode the block and add it to or put it in the pcm buffer + + if (pcm_buffer_occupancy == 0) { + // they should match and the read point should be zero + if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { + debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", + pcm_buffer_read_point_rtptime, timestamp); + pcm_buffer_read_point_rtptime = timestamp; + pcm_buffer_read_point = 0; + } + } + + unsigned char nonce[12]; + memset(nonce, 0, sizeof(nonce)); + memcpy(nonce + 4, packet + nread - 8, + 8); // front-pad the 8-byte nonce received to get the 12-byte nonce expected + + // 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. + unsigned long long new_payload_length = 0; + int 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, + nread - (8 + 12), // clen -- the last 8 bytes are the nonce + packet + 4, // authenticated additional data + 8, // authenticated additional data length + nonce, + conn->session_key); // *k + if (response != 0) { + debug(1, "Error decrypting audio packet %u -- packet length %d.", seq_no, nread); + } else { + // now pass it in to the regular processing chain + + unsigned long long max_int = INT_MAX; // put in the right format + if (new_payload_length > max_int) + debug(1, "Madly long payload length!"); + int payload_length = new_payload_length; // change from long long to int + int aac_packet_length = payload_length + 7; + + // now, fill in the 7-byte ADTS information, which seems to be needed by the decoder + // we made room for it in the front of the buffer + + addADTStoPacket(m, aac_packet_length); + + // now we are ready to send this to the decoder + + data_to_process = m; + data_remaining = aac_packet_length; + int ret = 0; + // there can be more than one av packet (? terminology) in a block + while (data_remaining > 0) { + if (decoded_frame == NULL) { + decoded_frame = av_frame_alloc(); + if (decoded_frame == NULL) + debug(1, "could not allocate av_frame"); + } else { + ret = av_parser_parse2(codec_parser_context, codec_context, &pkt->data, &pkt->size, + data_to_process, data_remaining, AV_NOPTS_VALUE, + AV_NOPTS_VALUE, 0); + if (ret < 0) { + debug(1, "error while parsing deciphered audio packet."); + } else { + data_to_process += ret; + data_remaining -= ret; + // debug(1, "frame found"); + // now pass each packet to be decoded + if (pkt->size) { + ret = avcodec_send_packet(codec_context, pkt); + if (ret < 0) { + debug(1, "error sending packet to decoder"); + } else { + while (ret >= 0) { + ret = avcodec_receive_frame(codec_context, decoded_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + else if (ret < 0) { + debug(1, "error %d during decoding", ret); + } else { + av_samples_alloc(&pcm_audio, &dst_linesize, codec_context->channels, + decoded_frame->nb_samples, AV_SAMPLE_FMT_S16, 1); + // remember to free pcm_audio + ret = swr_convert(swr, &pcm_audio, decoded_frame->nb_samples, + (const uint8_t **)decoded_frame->extended_data, + decoded_frame->nb_samples); + dst_bufsize = av_samples_get_buffer_size( + &dst_linesize, codec_context->channels, ret, AV_SAMPLE_FMT_S16, 1); + // debug(1,"generated %d bytes of PCM", dst_bufsize); + // copy the PCM audio into the PCM buffer. + // make sure it's big enough first + if ((pcm_buffer_size - pcm_buffer_occupancy) < dst_bufsize) { + debug(1, + "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " + "(frames): %u", + pcm_buffer_read_point / 4, pcm_buffer_occupancy / 4); + pcm_buffer_size = dst_bufsize + pcm_buffer_occupancy; + debug(1, "fatal error! pcm buffer too small at %d bytes.", + pcm_buffer_size); + } else { + memcpy(pcm_buffer + pcm_buffer_occupancy, pcm_audio, dst_bufsize); + pcm_buffer_occupancy += dst_bufsize; + } + // debug(1,"decoded %d samples", decoded_frame->nb_samples); + // memcpy(sampleBuffer,outputBuffer16,dst_bufsize); + av_freep(&pcm_audio); + } + } + } + } + } + if (decoded_frame == NULL) + debug(1, "decoded_frame is NULL"); + if (decoded_frame != NULL) + av_frame_free(&decoded_frame); + } + } + + // revert the state of cancellability + } + } + } else { + // nread is 0 -- the port has been closed + // finished = 1; + debug(1, "buffered audio port closed!"); + finished = 1; + } + } + + } while (finished == 0); + debug(1, "Buffered Audio Receiver RTP thread \"normal\" exit."); + pthread_cleanup_pop(1); // deallocate the swr + pthread_cleanup_pop(1); // deallocate the av_packet + pthread_cleanup_pop(1); // av_parser_init_cleanup_handler + pthread_cleanup_pop(1); // avcodec_open2_cleanup_handler + pthread_cleanup_pop(1); // avcodec_alloc_context3_cleanup_handler + pthread_cleanup_pop(1); // thread creation + pthread_cleanup_pop(1); // buffer malloc + pthread_cleanup_pop(1); // not_full_cv + pthread_cleanup_pop(1); // not_empty_cv + pthread_cleanup_pop(1); // mutex + pthread_cleanup_pop(1); // descriptor malloc + pthread_cleanup_pop(1); // pthread_t malloc + pthread_cleanup_pop(1); // do the cleanup. + pthread_exit(NULL); +} + +#endif +#endif + diff --git a/rtp.h b/rtp.h index 171bd651..4d5f778f 100644 --- a/rtp.h +++ b/rtp.h @@ -17,17 +17,11 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t controlport, uint16_t void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn); void rtp_request_client_pause(rtsp_conn_info *conn); // ask the client to pause -void get_reference_timestamp_stuff(uint32_t *timestamp, uint64_t *timestamp_time, - uint64_t *remote_timestamp_time, rtsp_conn_info *conn); -void clear_reference_timestamp(rtsp_conn_info *conn); +void reset_anchor_info(rtsp_conn_info *conn); int have_timestamp_timing_information(rtsp_conn_info *conn); -int get_frame_play_time(int64_t timestamp, int sample_ratio, uint64_t *time_to_play); - int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn); int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn); -int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_conn_info *conn); - #endif // _RTP_H From bdfb1c6ed6baebb65fa356967f30f86ec9439806 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Apr 2021 16:51:29 +0100 Subject: [PATCH 022/878] player.c compiles and everything builds and runs with the airplay 2 flag off. With airplay_2 flag on, rtp.c won't build, but we know that. --- player.c | 398 +++++++++++++++++++++++++++++++++---------------------- player.h | 4 +- rtp.c | 272 ++++++++++++++++++------------------- 3 files changed, 375 insertions(+), 299 deletions(-) diff --git a/player.c b/player.c index ff889408..66637292 100644 --- a/player.c +++ b/player.c @@ -3,7 +3,7 @@ * Copyright (c) James Laird 2011, 2013 * All rights reserved. * - * Modifications for audio synchronisation + * Modifications for audio synchronisation, AirPlay 2 * and related work, copyright (c) Mike Brady 2014 -- 2021 * All rights reserved. * @@ -110,12 +110,7 @@ int64_t first_frame_early_bias = 8; // static abuf_t audio_buffer[BUFFER_FRAMES]; #define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) -uint32_t modulo_32_offset(uint32_t from, uint32_t to) { - if (from <= to) - return to - from; - else - return UINT32_MAX - from + to + 1; -} +int32_t modulo_32_offset(uint32_t from, uint32_t to) { return to - from; } void do_flush(uint32_t timestamp, rtsp_conn_info *conn); @@ -141,79 +136,34 @@ static void ab_resync(rtsp_conn_info *conn) { // returns true if x is actually within the buffer int position_in_modulo_uint16_t_buffer(uint16_t x, uint16_t start, uint16_t end, uint16_t *pos) { - int response = 0; // not in the buffer - if (start <= end) { - if (x < start) { // this means that it's up around the wrap - if (pos) - *pos = UINT16_MAX - start + 1 + x; - } else { - if (pos) - *pos = x - start; - if (x < end) - response = 1; - } - } else if ((x >= start)) { // && (x <= UINT16_MAX)) { // always true + int response = 0; + + int16_t x_disp = x - start; + int16_t b_size = + end - start; // this will be positive as long as (a) start and end are within 2^15 of each + // other and (b) end is at or after start on the modulo wheel + if ((x_disp >= 0) && (x_disp < b_size)) response = 1; - if (pos) - *pos = x - start; - } else { - if (pos) - *pos = UINT16_MAX - start + 1 + x; - if (x < end) { - response = 1; - } - } return response; } +// wrapped number between two seq_t. +// as long as the two seq_t numbers are supposed to be within 2^15 of one another, +// that is, you go the shorter way around the 2^16 modulo wheel, +// this will return positive if b is equal to or after a and negative otherwise +int32_t seq_diff(seq_t a, seq_t b, seq_t base) { + int16_t d = b - a; + return d; +} -static inline seq_t SUCCESSOR(seq_t x) { return x + 1; } - -// a minus b -int16_t seq_diff(seq_t a, seq_t b) { - int16_t response; - seq_t diff = a - b; - seq_t invdiff = b - a; - if (diff < invdiff) - response = diff; - else - response = -invdiff; - return response; +// the sequence numbers will wrap pretty often. +// this returns true if the second arg is strictly after the first +static inline int seq_order(seq_t a, seq_t b, seq_t base) { + int16_t d = b - a; + return d > 0; } static inline seq_t seq_sum(seq_t a, seq_t b) { return a + b; } -// This orders u and v by picking the smaller of the two modulo differences -// in unsigned modulo arithmetic and setting the sign of the result accordingly. - -// if u - v gives the smaller modulo difference, then that modulo difference is returned -// otherwise the negative of the v - u modulo difference is returned. - -// (Think of a modulo ring starting at 0 and circling around clockwise finally to -// modulo-2, modulo-1 to 0 again. To determine the ordering of the two numbers, -// Find the shorter path between them around the ring. -// If you go from v to u in the positive (0, 1, 2 ... or clockwise) direction, -// then u is greater than (or "after") v.) - -// The result will always fit in an int64_t as it must be less or equal to half way -// around the ring. -// Due to the asymmetry of 2's complement representation, however, -// if the difference is equal to modulo / 2, it is ambiguous as to whether -// u is "after" or "before" (i.e. greater or less than) v. -// and will be returned as -modulo / 2, that is, "before". -// If that ever happens and there is a real ambiguity in the application, -// the modulo chosen is too small. - -int64_t int64_mod_difference(const uint64_t u, const uint64_t v, const uint64_t modulo) { - int64_t response; - uint64_t diff = (u - v) % modulo; - uint64_t invdiff = (v - u) % modulo; - if (diff < invdiff) - response = diff; - else - response = -invdiff; - return response; -} - void reset_input_flow_metrics(rtsp_conn_info *conn) { conn->play_number_after_flush = 0; conn->packet_count_since_flush = 0; @@ -444,8 +394,40 @@ static void free_audio_buffers(rtsp_conn_info *conn) { int first_possibly_missing_frame = -1; -void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, - rtsp_conn_info *conn) { +void reset_buffer(rtsp_conn_info *conn) { + debug_mutex_lock(&conn->ab_mutex, 30000, 0); + ab_resync(conn); + debug_mutex_unlock(&conn->ab_mutex, 0); +} + +void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy, + rtsp_conn_info *conn) { + debug_mutex_lock(&conn->ab_mutex, 30000, 0); + *size = BUFFER_FRAMES; + if (conn->ab_synced) { + int16_t occ = + conn->ab_write - conn->ab_read; // will be zero or positive if read and write are within + // 2^15 of each other and write is at or after read + *occupancy = occ; + } else { + *occupancy = 0; + } + debug_mutex_unlock(&conn->ab_mutex, 0); +} + +void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, + int len, rtsp_conn_info *conn) { + + // if it's original format, it has a valid seqno and must be decoded + // otherwise, it can take the next seqno and doesn't need decoding. + + // ignore a request to flush that has been made before the first packet... + if (conn->packet_count == 0) { + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + conn->flush_requested = 0; + conn->flush_rtp_timestamp = 0; + debug_mutex_unlock(&conn->flush_mutex, 3); + } debug_mutex_lock(&conn->ab_mutex, 30000, 0); uint64_t time_now = get_absolute_time_in_ns(); @@ -453,77 +435,93 @@ void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, in conn->packet_count_since_flush++; conn->time_of_last_audio_packet = time_now; if (conn->connection_state_to_output) { // if we are supposed to be processing these packets - abuf_t *abuf = 0; - if (!conn->ab_synced) { - // if this is the first packet... - debug(3, "syncing to seqno %u.", seqno); - conn->ab_write = seqno; - conn->ab_read = seqno; - conn->ab_synced = 1; - } - int16_t write_point_gap = seq_diff(seqno, conn->ab_write); // this is the difference between - // the incoming packet number and the packet number that was expected. - if (write_point_gap == - 0) { // if this is the expected packet (which could be the first packet...) - if (conn->input_frame_rate_starting_point_is_valid == 0) { - if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { - conn->frames_inward_measurement_start_time = time_now; - conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; - conn->input_frame_rate_starting_point_is_valid = 1; // valid now + abuf_t *abuf = 0; + if (!conn->ab_synced) { + // if this is the first packet... + debug(3, "syncing to seqno %u.", seqno); + conn->ab_write = seqno; + conn->ab_read = seqno; + conn->ab_synced = 1; + } else if (original_format == 0) { + // if the packet is coming in original format, the sequence number is important + // otherwise, ignore is by setting it equal to the expected sequence number in ab_write + seqno = conn->ab_write; + } + if (conn->ab_write == + seqno) { // if this is the expected packet (which could be the first packet...) + if (conn->input_frame_rate_starting_point_is_valid == 0) { + if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { + conn->frames_inward_measurement_start_time = time_now; + conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; + conn->input_frame_rate_starting_point_is_valid = 1; // valid now + } + } + conn->frames_inward_measurement_time = time_now; + conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; + abuf = conn->audio_buffer + BUFIDX(seqno); + conn->ab_write = seqno + 1; // move the write pointer to the next free space + } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected + int32_t gap = seq_diff(conn->ab_write, seqno, conn->ab_read); + if (gap <= 0) + debug(1, "Unexpected gap size: %d.", gap); + int i; + for (i = 0; i < gap; i++) { + abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); + abuf->ready = 0; // to be sure, to be sure + abuf->resend_request_number = 0; + abuf->initialisation_time = + time_now; // this represents when the packet was noticed to be missing + abuf->status = 1 << 0; // signifying missing + abuf->resend_time = 0; + abuf->given_timestamp = 0; + abuf->sequence_number = 0; + } + // debug(1,"N %d s %u.",seq_diff(ab_write,PREDECESSOR(seqno))+1,ab_write); + abuf = conn->audio_buffer + BUFIDX(seqno); + // rtp_request_resend(ab_write, gap); + // resend_requests++; + conn->ab_write = seqno + 1; + } else if (seq_order(conn->ab_read, seqno, + conn->ab_read)) { // older than expected but not too late + conn->late_packets++; + abuf = conn->audio_buffer + BUFIDX(seqno); + } else { // too late. + conn->too_late_packets++; + } + + if (abuf) { + int datalen = conn->max_frames_per_packet; + abuf->initialisation_time = time_now; + abuf->resend_time = 0; + if ((original_format != 0) && + (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0)) { + abuf->ready = 1; + abuf->status = 0; // signifying that it was received + abuf->length = datalen; + abuf->given_timestamp = actual_timestamp; + abuf->sequence_number = seqno; + } else if (original_format == 0) { + memcpy(abuf->data, data, len * conn->input_bytes_per_frame); + abuf->ready = 1; + abuf->status = 0; // signifying that it was received + abuf->length = len; + abuf->given_timestamp = actual_timestamp; + abuf->sequence_number = seqno; + } else { + debug(1, "Bad audio packet detected and discarded."); + abuf->ready = 0; + abuf->status = 1 << 1; // bad packet, discarded + abuf->resend_request_number = 0; + abuf->given_timestamp = 0; + abuf->sequence_number = 0; } } - conn->frames_inward_measurement_time = time_now; - conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; - abuf = conn->audio_buffer + BUFIDX(seqno); - conn->ab_write = SUCCESSOR(seqno); // move the write pointer to the next free space - } else if (write_point_gap > 0) { // newer than expected - // initialise the frames in between - int i; - for (i = 0; i < write_point_gap; i++) { - abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); - abuf->ready = 0; // to be sure, to be sure - abuf->resend_request_number = 0; - abuf->initialisation_time = - time_now; // this represents when the packet was noticed to be missing - abuf->status = 1 << 0; // signifying missing - abuf->resend_time = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } - abuf = conn->audio_buffer + BUFIDX(seqno); - conn->ab_write = SUCCESSOR(seqno); - } else if (seq_diff(seqno, conn->ab_read) > 0) { // older than expected but still not too late - conn->late_packets++; - abuf = conn->audio_buffer + BUFIDX(seqno); - } else { // too late. - conn->too_late_packets++; - } - if (abuf) { - int datalen = conn->max_frames_per_packet; - abuf->initialisation_time = time_now; - abuf->resend_time = 0; - if (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0) { - abuf->ready = 1; - abuf->status = 0; // signifying that it was received - abuf->length = datalen; - abuf->given_timestamp = actual_timestamp; - abuf->sequence_number = seqno; - } else { - debug(1, "Bad audio packet detected and discarded."); - abuf->ready = 0; - abuf->status = 1 << 1; // bad packet, discarded - abuf->resend_request_number = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } - } + int rc = pthread_cond_signal(&conn->flowcontrol); + if (rc) + debug(1, "Error signalling flowcontrol."); - int rc = pthread_cond_signal(&conn->flowcontrol); - if (rc) - debug(1, "Error signalling flowcontrol."); - - // resend checks + // resend checks { uint64_t minimum_wait_time = (uint64_t)(config.resend_control_first_check_time * (uint64_t)1000000000); @@ -1252,6 +1250,29 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (local_time_now >= time_to_play) { do_wait = 0; } + // here, do a sanity check. if the time_to_play is not within a few seconds of the + // time now, the frame is probably not meant to be there, so let it go. + if (do_wait != 0) { + // this is a hack. + // we subtract two 2^n unsigned numbers and get a signed 2^n result. + // If we think of the calculation as occurring in modulo 2^n arithmetic + // then the signed result's magnitude represents the shorter distance around + // the modulo wheel of values from one number to the other. + // The sign indicates the direction: positive means clockwise (upwards) from the + // second number to the first (i.e. the first number comes "after" the second). + + int64_t time_difference = local_time_now - time_to_play; + if ((time_difference > 10000000000) || (time_difference < -10000000000)) { + debug(2, + "crazy time interval of %f seconds between time now: 0x%" PRIx64 + " and time of packet: %" PRIx64 ".", + 0.000000001 * time_difference, local_time_now, time_to_play); + debug(2, "packet rtptime: %u, reference_timestamp: %u", curframe->given_timestamp, + conn->anchor_rtptime); + + do_wait = 0; // let it go + } + } } } if (do_wait == 0) @@ -1307,7 +1328,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } curframe->ready = 0; } - conn->ab_read = SUCCESSOR(conn->ab_read); + conn->ab_read++; pthread_cleanup_pop(1); return curframe; } @@ -1544,6 +1565,40 @@ void player_thread_cleanup_handler(void *arg) { mdns_dacp_monitor_set_id(NULL); // say we're not interested in following that DACP id any more #endif +#ifdef CONFIG_AIRPORT_2 + if (conn->rtp_ap2_control_thread) { + debug(3, "Cancelling rtp_ap2_control_thread"); + pthread_cancel(conn->rtp_ap2_control_thread); + pthread_join(conn->rtp_ap2_control_thread, NULL); + } + if (conn->ap2_control_socket) { + close(conn->ap2_control_socket); + conn->ap2_control_socket = 0; + } + + // realtime stuff + if (conn->rtp_realtime_audio_thread) { + debug(3, "Cancelling rtp_realtime_audio_thread"); + pthread_cancel(conn->rtp_realtime_audio_thread); + pthread_join(conn->rtp_realtime_audio_thread, NULL); + } + if (conn->realtime_audio_socket) { + close(conn->realtime_audio_socket); + conn->realtime_audio_socket = 0; + } + + // buffered stuff + if (conn->rtp_buffered_audio_thread) { + debug(3, "Cancelling rtp_buffered_audio_thread"); + pthread_cancel(conn->rtp_buffered_audio_thread); + pthread_join(conn->rtp_buffered_audio_thread, NULL); + } + if (conn->buffered_audio_socket) { + close(conn->buffered_audio_socket); + conn->buffered_audio_socket = 0; + } +#endif + debug(3, "Cancelling timing, control and audio threads..."); debug(3, "Cancel timing thread."); pthread_cancel(conn->rtp_timing_thread); @@ -1604,11 +1659,7 @@ void *player_thread_func(void *arg) { // be used as a null operand, so we'll use it like that too conn->fix_volume = 0x10000; - if (conn->latency == 0) { - debug(3, "No latency has (yet) been specified. Setting 88,200 (2 seconds) frames " - "as a default."); - conn->latency = 88200; - } + reset_anchor_info(conn); if (conn->stream.type == ast_apple_lossless) init_alac_decoder((int32_t *)&conn->stream.fmtp, @@ -1870,10 +1921,12 @@ void *player_thread_func(void *arg) { } } +#ifndef CONFIG_AIRPLAY_2 // create and start the timing, control and audio receiver threads pthread_create(&conn->rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn); pthread_create(&conn->rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn); pthread_create(&conn->rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn); +#endif pthread_cleanup_push(player_thread_cleanup_handler, arg); // undo what's been done so far @@ -1910,10 +1963,10 @@ void *player_thread_func(void *arg) { debug(2, "Player has supplied a silent frame, (possibly frame %u) for play number %d, " "status 0x%X after %u resend requests.", - SUCCESSOR(conn->last_seqno_read), play_number, inframe->status, + conn->last_seqno_read + 1, play_number, inframe->status, inframe->resend_request_number); conn->last_seqno_read = - SUCCESSOR(conn->last_seqno_read); // manage the packet out of sequence minder + ((conn->last_seqno_read + 1) & 0xffff); // manage the packet out of sequence minder void *silence = malloc(conn->output_bytes_per_frame * conn->max_frames_per_packet * conn->output_sample_ratio); @@ -2050,7 +2103,7 @@ void *player_thread_func(void *arg) { inframe->sequence_number; // int32_t from seq_t, i.e. uint16_t, so okay. else { conn->last_seqno_read = - SUCCESSOR(conn->last_seqno_read); // int32_t from seq_t, i.e. uint16_t, so okay. + (conn->last_seqno_read + 1) & 0xffff; // int32_t from seq_t, i.e. uint16_t, so okay. if (inframe->sequence_number != conn->last_seqno_read) { // seq_t, ei.e. uint16_t and int32_t, so okay debug(2, @@ -2061,7 +2114,8 @@ void *player_thread_func(void *arg) { } } - conn->buffer_occupancy = seq_diff(conn->ab_write, conn->ab_read); // int32_t from int16_t + conn->buffer_occupancy = + seq_diff(conn->ab_read, conn->ab_write, conn->ab_read); // int32_t from int32 if (conn->buffer_occupancy < minimum_buffer_occupancy) minimum_buffer_occupancy = conn->buffer_occupancy; @@ -2075,6 +2129,7 @@ void *player_thread_func(void *arg) { // If any of these are false, we don't do any synchronisation stuff int resp = -1; // use this as a flag -- if negative, we can't rely on a real known delay + current_delay = -1; // use this as a failure flag if (config.output->delay) { long l_delay; @@ -2929,13 +2984,46 @@ void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { debug(3, "player_flush"); do_flush(timestamp, conn); #ifdef CONFIG_METADATA - debug(2, "pfls"); - char numbuf[32]; - snprintf(numbuf, sizeof(numbuf), "%u", timestamp); - send_ssnc_metadata('pfls', numbuf, strlen(numbuf), 1); // contains cancellation points +// only send a flush metadata message if the first packet has been seen -- it's a bogus message + // otherwise + if (conn->first_packet_timestamp) { + debug(2, "pfls"); + char numbuf[32]; + snprintf(numbuf, sizeof(numbuf), "%u", timestamp); + send_ssnc_metadata('pfls', numbuf, strlen(numbuf), 1); // contains cancellation points + } #endif } +void player_full_flush(rtsp_conn_info *conn) { + // this basically flushes everything from the player + // here, find the rtptime of the last from in the buffer and add 1 to it + // so as to ask to flush everything + int flush_needed = 0; + uint32_t rtpTimestamp; + debug_mutex_lock(&conn->ab_mutex, 30000, 0); + if ((conn->ab_synced != 0) && (conn->ab_write != conn->ab_read)) { + abuf_t *abuf = NULL; + seq_t last_seqno_written; + do { + last_seqno_written = conn->ab_write - 1; + abuf = conn->audio_buffer + BUFIDX(last_seqno_written); + } while ((abuf->ready == 0) && (last_seqno_written != conn->ab_read)); + if ((abuf != NULL) && (abuf->ready != 0)) { + debug(2, "full flush needed"); + rtpTimestamp = abuf->given_timestamp + abuf->length + 1; + flush_needed = 1; + } else { + debug(2, "full flush not needed"); + } + } else { + debug(2, "full flush not needed -- buffers empty or not synced"); + } + debug_mutex_unlock(&conn->ab_mutex, 0); + if (flush_needed) + player_flush(rtpTimestamp, conn); +} + int player_play(rtsp_conn_info *conn) { // need to use conn in place of stream below. Need to put the stream as a parameter to he if (conn->player_thread != NULL) diff --git a/player.h b/player.h index e8942710..22bbccf3 100644 --- a/player.h +++ b/player.h @@ -341,7 +341,7 @@ typedef struct { uint64_t dac_buffer_queue_minimum_length; } rtsp_conn_info; -uint32_t modulo_32_offset(uint32_t from, uint32_t to); +int32_t modulo_32_offset(uint32_t from, uint32_t to); int player_play(rtsp_conn_info *conn); int player_stop(rtsp_conn_info *conn); @@ -349,7 +349,7 @@ int player_stop(rtsp_conn_info *conn); void player_volume(double f, rtsp_conn_info *conn); void player_volume_without_notification(double f, rtsp_conn_info *conn); void player_flush(uint32_t timestamp, rtsp_conn_info *conn); -void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, +void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn); int64_t monotonic_timestamp(uint32_t timestamp, rtsp_conn_info *conn); // add an epoch to the timestamp. The monotonic diff --git a/rtp.c b/rtp.c index b96a344d..34115c9b 100644 --- a/rtp.c +++ b/rtp.c @@ -222,7 +222,7 @@ void *rtp_audio_receiver(void *arg) { if (plen >= 16) { if ((config.diagnostic_drop_packet_fraction == 0.0) || (drand48() > config.diagnostic_drop_packet_fraction)) - player_put_packet(seqno, actual_timestamp, pktp, plen, + player_put_packet(1, seqno, actual_timestamp, pktp, plen, conn); // the '1' means is original format else debug(3, "Dropping audio packet %u to simulate a bad connection.", seqno); @@ -469,7 +469,7 @@ void *rtp_control_receiver(void *arg) { // check if packet contains enough content to be reasonable if (plen >= 16) { - player_put_packet(seqno, actual_timestamp, pktp, plen, + player_put_packet(1, seqno, actual_timestamp, pktp, plen, conn); // the '1' means is original format continue; } else { @@ -1114,140 +1114,6 @@ int local_ntp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn return result; } -void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, - uint64_t networktime) { - if (conn->anchor_clock != clock_id) - debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); - conn->anchor_remote_info_is_valid = 1; - conn->anchor_rtptime = rtptime; - conn->anchor_time = networktime; - conn->anchor_clock = clock_id; -} - -void reset_ptp_anchor_info(rtsp_conn_info *conn) { - debug(2, "Connection %d: Clear anchor information.", conn->connection_number); - conn->last_anchor_info_is_valid = 0; - conn->anchor_remote_info_is_valid = 0; -} - -int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, - uint64_t *anchorLocalTime) { - int response = 0; - uint64_t actual_clock_id, actual_offset; - - int ptp_status = ptp_get_clock_info(&actual_clock_id, &actual_offset); - - if (ptp_status == 0) { - if (conn->anchor_remote_info_is_valid != 0) { - if (actual_clock_id == conn->anchor_clock) { - conn->last_anchor_clock_offset = actual_offset; - conn->last_anchor_time_of_update = get_absolute_time_in_ns(); - conn->last_anchor_info_is_valid = 1; - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; - } else { - if (conn->last_anchor_info_is_valid != 0) { - int64_t time_since_last_update = - get_absolute_time_in_ns() - conn->last_anchor_time_of_update; - if (time_since_last_update > 5000000000) { - debug(1, "change master clock to %" PRIx64 ".", actual_clock_id); - uint64_t new_anchor_time = conn->anchor_time; - new_anchor_time = new_anchor_time - conn->last_anchor_clock_offset; // to local - new_anchor_time = new_anchor_time + actual_offset; // to the next clock - conn->anchor_time = new_anchor_time; - conn->anchor_clock = actual_clock_id; - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - actual_offset; - } else { - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; - } - } else { - debug(2, "no clock and no old anchor times"); - response = -1; - } - } - } else { - debug(2, "don't have anchor_remote_time_info"); - response = -1; - } - } else if (ptp_status == -1) { - debug(3, "don't have the ptp clock interface"); - response = -1; - } else { - debug(3, "ptp clock not valid"); - response = -1; - } - return response; -} - -int get_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, - uint64_t *anchorLocalTime) { - return get_ptp_anchor_local_time_info(conn, anchorRTP,anchorLocalTime); -} - -int have_ptp_timing_information(rtsp_conn_info *conn) { - if (get_anchor_local_time_info(conn, NULL, NULL) == 0) - return 1; - else - return 0; -} - -int have_timing_information(rtsp_conn_info *conn) { - return have_ptp_timing_information(conn); -} - -int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { - int result = -1; - uint32_t anchor_rtptime; - uint64_t anchor_local_time; - if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { - int32_t frame_difference = timestamp - anchor_rtptime; - int64_t time_difference = frame_difference; - time_difference = time_difference * 1000000000; - time_difference = time_difference / conn->input_rate; - uint64_t ltime = anchor_local_time + time_difference; - *time = ltime; - result = 0; - } else { - debug(3, "frame_to_local_time can't get anchor local time information"); - } - return result; -} - -int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { - return frame_to_ntp_local_time(timestamp, time, conn); -} - -int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { - int result = -1; - uint32_t anchor_rtptime; - uint64_t anchor_local_time; - if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { - int64_t time_difference = time - anchor_local_time; - int64_t frame_difference = time_difference; - frame_difference = frame_difference * conn->input_rate; // but this is by 10^9 - frame_difference = frame_difference / 1000000000; - int32_t fd32 = frame_difference; - uint32_t lframe = anchor_rtptime + fd32; - *frame = lframe; - result = 0; - } else { - debug(3, "local_time_to_frame can't get anchor local time information"); - } - return result; -} - -int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { - return local_ntp_time_to_frame(time, frame, conn); -} - void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { // debug(1, "rtp_request_resend of %u packets from sequence number %u.", count, first); if (conn->rtp_running) { @@ -1330,9 +1196,134 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { } } -#if 0 +int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + return frame_to_ntp_local_time(timestamp, time, conn); +} + +int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + return local_ntp_time_to_frame(time, frame, conn); +} + + #ifdef CONFIG_AIRPLAY_2 +void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, + uint64_t networktime) { + if (conn->anchor_clock != clock_id) + debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); + conn->anchor_remote_info_is_valid = 1; + conn->anchor_rtptime = rtptime; + conn->anchor_time = networktime; + conn->anchor_clock = clock_id; +} + +void reset_ptp_anchor_info(rtsp_conn_info *conn) { + debug(2, "Connection %d: Clear anchor information.", conn->connection_number); + conn->last_anchor_info_is_valid = 0; + conn->anchor_remote_info_is_valid = 0; +} + +int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, + uint64_t *anchorLocalTime) { + int response = 0; + uint64_t actual_clock_id, actual_offset; + + int ptp_status = ptp_get_clock_info(&actual_clock_id, &actual_offset); + + if (ptp_status == 0) { + if (conn->anchor_remote_info_is_valid != 0) { + if (actual_clock_id == conn->anchor_clock) { + conn->last_anchor_clock_offset = actual_offset; + conn->last_anchor_time_of_update = get_absolute_time_in_ns(); + conn->last_anchor_info_is_valid = 1; + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + } else { + if (conn->last_anchor_info_is_valid != 0) { + int64_t time_since_last_update = + get_absolute_time_in_ns() - conn->last_anchor_time_of_update; + if (time_since_last_update > 5000000000) { + debug(1, "change master clock to %" PRIx64 ".", actual_clock_id); + uint64_t new_anchor_time = conn->anchor_time; + new_anchor_time = new_anchor_time - conn->last_anchor_clock_offset; // to local + new_anchor_time = new_anchor_time + actual_offset; // to the next clock + conn->anchor_time = new_anchor_time; + conn->anchor_clock = actual_clock_id; + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - actual_offset; + } else { + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + } + } else { + debug(2, "no clock and no old anchor times"); + response = -1; + } + } + } else { + debug(2, "don't have anchor_remote_time_info"); + response = -1; + } + } else if (ptp_status == -1) { + debug(3, "don't have the ptp clock interface"); + response = -1; + } else { + debug(3, "ptp clock not valid"); + response = -1; + } + return response; +} + +int have_ptp_timing_information(rtsp_conn_info *conn) { + if (get_anchor_local_time_info(conn, NULL, NULL) == 0) + return 1; + else + return 0; +} + +int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + int result = -1; + uint32_t anchor_rtptime; + uint64_t anchor_local_time; + if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + int32_t frame_difference = timestamp - anchor_rtptime; + int64_t time_difference = frame_difference; + time_difference = time_difference * 1000000000; + time_difference = time_difference / conn->input_rate; + uint64_t ltime = anchor_local_time + time_difference; + *time = ltime; + result = 0; + } else { + debug(3, "frame_to_local_time can't get anchor local time information"); + } + return result; +} + +int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + int result = -1; + uint32_t anchor_rtptime; + uint64_t anchor_local_time; + if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + int64_t time_difference = time - anchor_local_time; + int64_t frame_difference = time_difference; + frame_difference = frame_difference * conn->input_rate; // but this is by 10^9 + frame_difference = frame_difference / 1000000000; + int32_t fd32 = frame_difference; + uint32_t lframe = anchor_rtptime + fd32; + *frame = lframe; + result = 0; + } else { + debug(3, "local_time_to_frame can't get anchor local time information"); + } + return result; +} + void rtp_event_receiver_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: Event Receiver Cleanup.", conn->connection_number); @@ -1454,7 +1445,7 @@ int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread, if (new_payload_length > max_int) debug(1, "Madly long payload length!"); int plen = new_payload_length; // - player_put_packet(sequence_number, timestamp, m, plen, + player_put_packet(1, sequence_number, timestamp, m, plen, conn); // the '1' means is original format return sequence_number; } else { @@ -2094,8 +2085,7 @@ void *rtp_buffered_audio_processor(void *arg) { if (streaming_has_started == 0) debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); streaming_has_started = 1; -// player_put_packet(0, 0, pcm_buffer_read_point_rtptime, - player_put_packet(0, pcm_buffer_read_point_rtptime, + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); } @@ -2373,5 +2363,3 @@ void *rtp_buffered_audio_processor(void *arg) { } #endif -#endif - From 3a02b79a622e96ee6cc0054480e460075640229d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 09:29:30 +0100 Subject: [PATCH 023/878] Update the mdns stuff to work with a string, update audio_alsa rtp.c, rtsp.c and shairport.c to build and work in AirPlay 1 but with AirPlay 2 code in place. Some of it is #defined out. --- audio_alsa.c | 2 +- mdns.c | 9 +- mdns.h | 4 +- mdns_avahi.c | 56 +++++----- mdns_dns_sd.c | 3 +- mdns_external.c | 6 +- mdns_tinysvcmdns.c | 3 +- rtp.c | 2 +- rtsp.c | 247 +++++++++++++++++++++++++++++++++------------ rtsp.h | 3 +- shairport.c | 138 ++++++++++++++++++++++--- 11 files changed, 356 insertions(+), 117 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index b464279c..19127d11 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1771,7 +1771,7 @@ int do_close() { // debug(1,"alsa: do_close() -- closing the output device"); if ((derr = snd_pcm_drop(alsa_handle))) debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); - usleep(5000); + usleep(5000); // this make the function pthread cancellable if ((derr = snd_pcm_hw_free(alsa_handle))) debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr)); diff --git a/mdns.c b/mdns.c index 0e305c00..7eb1f433 100644 --- a/mdns.c +++ b/mdns.c @@ -1,6 +1,7 @@ /* * mDNS registration handler. This file is part of Shairport. * Copyright (c) James Laird 2013 + * Modifications, updates and additions (c) Mike Brady 2014 -- 2020 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -63,15 +64,17 @@ static mdns_backend *mdns_backends[] = { #endif NULL}; -void mdns_register(void) { +void mdns_register(char **txt_records) { char *mdns_service_name = alloca(strlen(config.service_name) + 14); char *p = mdns_service_name; +#ifndef CONFIG_AIRPLAY_2 int i; for (i = 0; i < 6; i++) { snprintf(p, 3, "%02X", config.hw_addr[i]); p += 2; } *p++ = '@'; +#endif strcpy(p, config.service_name); mdns_backend **b = NULL; @@ -80,7 +83,7 @@ void mdns_register(void) { for (b = mdns_backends; *b; b++) { if (strcmp((*b)->name, config.mdns_name) != 0) // Not the one we are looking for continue; - int error = (*b)->mdns_register(mdns_service_name, config.port); + int error = (*b)->mdns_register(mdns_service_name, config.port, txt_records); if (error >= 0) { config.mdns = *b; } @@ -91,7 +94,7 @@ void mdns_register(void) { warn("%s mDNS backend not found"); } else { for (b = mdns_backends; *b; b++) { - int error = (*b)->mdns_register(mdns_service_name, config.port); + int error = (*b)->mdns_register(mdns_service_name, config.port, txt_records); if (error >= 0) { config.mdns = *b; break; diff --git a/mdns.h b/mdns.h index b69f4c85..73c15e8e 100644 --- a/mdns.h +++ b/mdns.h @@ -8,7 +8,7 @@ extern int mdns_pid; void mdns_unregister(void); -void mdns_register(void); +void mdns_register(char **txt_records); void mdns_dacp_monitor_start(); void mdns_dacp_monitor_stop(void); void mdns_dacp_monitor_set_id(const char *dacp_id); @@ -17,7 +17,7 @@ void mdns_ls_backends(void); typedef struct { char *name; - int (*mdns_register)(char *apname, int port); + int (*mdns_register)(char *apname, int port, char **txt_records); void (*mdns_unregister)(void); void (*mdns_dacp_monitor_start)(); void (*mdns_dacp_monitor_set_id)(const char *); diff --git a/mdns_avahi.c b/mdns_avahi.c index 47ff8a1d..4ab991c7 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -61,6 +61,8 @@ typedef struct { dacp_browser_struct private_dbs; +AvahiStringList *text_record_string_list = NULL; + // static AvahiServiceBrowser *sb = NULL; static AvahiClient *client = NULL; // static AvahiClient *service_client = NULL; @@ -229,36 +231,29 @@ static void register_service(AvahiClient *c) { if (!avahi_entry_group_is_empty(group)) return; - int ret; + int ret = 0; + AvahiIfIndex selected_interface; if (config.interface != NULL) selected_interface = config.interface_index; else selected_interface = AVAHI_IF_UNSPEC; -#ifdef CONFIG_METADATA - if (config.metadata_enabled) { - ret = avahi_entry_group_add_service(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, - service_name, config.regtype, NULL, NULL, port, - MDNS_RECORD_WITH_METADATA, NULL); - if (ret == 0) - debug(2, "avahi: request to add \"%s\" service with metadata", config.regtype); + if (text_record_string_list) { + ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + service_name, config.regtype, NULL, NULL, port, + text_record_string_list); + if (ret == 0) { + ret = avahi_entry_group_commit(group); + debug(2, "avahi: avahi_entry_group_commit %d", ret); + if (ret < 0) + debug(1, "avahi: avahi_entry_group_commit failed"); + } else if (ret < 0) { + debug(1, "avahi: avahi_entry_group_add_service failed"); + } else { + debug(1, "avahi: unexpected positive return"); + } } else { -#endif - ret = avahi_entry_group_add_service(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, - service_name, config.regtype, NULL, NULL, port, - MDNS_RECORD_WITHOUT_METADATA, NULL); - if (ret == 0) - debug(2, "avahi: request to add \"%s\" service without metadata", config.regtype); -#ifdef CONFIG_METADATA - } -#endif - - if (ret < 0) - debug(1, "avahi: avahi_entry_group_add_service failed"); - else { - ret = avahi_entry_group_commit(group); - if (ret < 0) - debug(1, "avahi: avahi_entry_group_commit failed"); + debug(1, "Can't find a valid text_record_string_list"); } } } @@ -317,9 +312,12 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } -static int avahi_register(char *srvname, int srvport) { +static int avahi_register(char *srvname, int srvport, char **txt_records) { // debug(1, "avahi_register."); service_name = strdup(srvname); + + text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1); + port = srvport; int err; @@ -342,7 +340,7 @@ static int avahi_register(char *srvname, int srvport) { } static void avahi_unregister(void) { - // debug(1, "avahi_unregister."); + debug(2, "avahi_unregister."); if (tpoll) { debug(2, "avahi: stop the threaded poll."); avahi_threaded_poll_stop(tpoll); @@ -366,6 +364,12 @@ static void avahi_unregister(void) { free(service_name); } else debug(1, "avahi attempt to free NULL service name"); + + if (text_record_string_list) { + debug(2, "avahi free text_record_string_list"); + avahi_string_list_free(text_record_string_list); + } + service_name = NULL; } diff --git a/mdns_dns_sd.c b/mdns_dns_sd.c index cf178f1d..4952a9f0 100644 --- a/mdns_dns_sd.c +++ b/mdns_dns_sd.c @@ -33,7 +33,8 @@ static DNSServiceRef service; -static int mdns_dns_sd_register(char *apname, int port) { +static int mdns_dns_sd_register(char *apname, int port, + __attribute__((unused)) char **txt_records) { char *recordwithoutmetadata[] = {MDNS_RECORD_WITHOUT_METADATA, NULL}; #ifdef CONFIG_METADATA char *recordwithmetadata[] = {MDNS_RECORD_WITH_METADATA, NULL}; diff --git a/mdns_external.c b/mdns_external.c index 974aa85b..17884f94 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -80,7 +80,8 @@ static int fork_execvp(const char *file, char *const argv[]) { return response; } -static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port) { +static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); @@ -119,7 +120,8 @@ static int mdns_external_avahi_register(char *apname, __attribute__((unused)) in return -1; } -static int mdns_external_dns_sd_register(char *apname, __attribute__((unused)) int port) { +static int mdns_external_dns_sd_register(char *apname, __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); diff --git a/mdns_tinysvcmdns.c b/mdns_tinysvcmdns.c index ad6be65a..619f9d31 100644 --- a/mdns_tinysvcmdns.c +++ b/mdns_tinysvcmdns.c @@ -38,7 +38,8 @@ static struct mdnsd *svr = NULL; -static int mdns_tinysvcmdns_register(char *apname, int port) { +static int mdns_tinysvcmdns_register(char *apname, int port, + __attribute__((unused)) char **txt_records) { struct ifaddrs *ifalist; struct ifaddrs *ifa; diff --git a/rtp.c b/rtp.c index 34115c9b..69c04ef2 100644 --- a/rtp.c +++ b/rtp.c @@ -1205,7 +1205,7 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { } -#ifdef CONFIG_AIRPLAY_2 +#if 0 void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { diff --git a/rtsp.c b/rtsp.c index 2661ccec..8be05db0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -299,7 +299,7 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { #endif -int have_player(rtsp_conn_info *conn) { +int have_play_lock(rtsp_conn_info *conn) { int response = 0; debug_mutex_lock(&playing_conn_lock, 1000000, 3); if (playing_conn == conn) // this connection definitely has the play lock @@ -308,6 +308,102 @@ int have_player(rtsp_conn_info *conn) { return response; } +int try_to_hold_play_lock(rtsp_conn_info *conn) { + int response = -1; + if (pthread_mutex_trylock(&playing_conn_lock) == 0) { + if (playing_conn == conn) { + response = 0; + } else { + pthread_mutex_unlock(&playing_conn_lock); + } + } + return response; +} + +void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { + pthread_mutex_unlock(&playing_conn_lock); +} + +void release_play_lock(rtsp_conn_info *conn) { + debug_mutex_lock(&playing_conn_lock, 1000000, 3); + if (playing_conn == conn) { // if we have the player + playing_conn = NULL; // let it go + debug(2, "Connection %d: release play lock.", conn->connection_number); + } + debug_mutex_unlock(&playing_conn_lock, 3); +} + +int get_play_lock(rtsp_conn_info *conn) { + // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but + // interrupted an existing session + int response = 0; + + int have_the_player = 0; + int should_wait = 0; // this will be true if you're trying to break in to the current session + int interrupting_current_session = 0; + + // try to become the current playing_conn + + debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it + + if (playing_conn == NULL) { + playing_conn = conn; + have_the_player = 1; + } else if (playing_conn == conn) { + have_the_player = 1; + warn("Duplicate attempt to acquire the player by the same connection, by the look of it!"); + } else if (playing_conn->stop) { + debug(1, "Connection %d: Waiting for Connection %d to stop playing.", conn->connection_number, + playing_conn->connection_number); + should_wait = 1; +#ifdef CONFIG_AIRPLAY_2 + } else { // ignore the allow_session_interruption in AirPlay 2 -- it is always permissible, it + // seems +#else + } else if (config.allow_session_interruption == 1) { +#endif + debug(1, "Connection %d: Asking Connection %d to stop playing.", conn->connection_number, + playing_conn->connection_number); + playing_conn->stop = 1; + interrupting_current_session = 1; + should_wait = 1; + pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit + } + debug_mutex_unlock(&playing_conn_lock, 3); + + if (should_wait) { + int time_remaining = 3000000; // must be signed, as it could go negative... + + while ((time_remaining > 0) && (have_the_player == 0)) { + debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it + if (playing_conn == NULL) { + playing_conn = conn; + have_the_player = 1; + } + debug_mutex_unlock(&playing_conn_lock, 3); + + if (have_the_player == 0) { + usleep(100000); + time_remaining -= 100000; + } + } + + if ((have_the_player == 1) && (interrupting_current_session == 1)) { + debug(1, "Connection %d: got player lock", conn->connection_number); + response = 1; + } else { + debug(1, "Connection %d: failed to get player lock", conn->connection_number); + response = -1; + } + } + + if ((have_the_player) && (interrupting_current_session == 0)) { + debug(2, "Connection %d: got player lock.", conn->connection_number); + response = 0; + } + return response; +} + void player_watchdog_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(3, "Connection %d: Watchdog Exit.", conn->connection_number); @@ -853,7 +949,7 @@ int msg_write_response(int fd, rtsp_message *resp) { void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(2, "Connection %d: RECORD", conn->connection_number); - if (have_player(conn)) { + if (have_play_lock(conn)) { if (conn->player_thread) warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); else @@ -909,7 +1005,7 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { debug(2, "Connection %d: TEARDOWN", conn->connection_number); - if (have_player(conn)) { + if (have_play_lock(conn)) { resp->respcode = 200; msg_add_header(resp, "Connection", "close"); debug( @@ -928,7 +1024,7 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: FLUSH", conn->connection_number); - if (have_player(conn)) { + if (have_play_lock(conn)) { char *p = NULL; uint32_t rtptime = 0; char *hdr = msg_get_header(req, "RTP-Info"); @@ -968,7 +1064,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: SETUP", conn->connection_number); resp->respcode = 451; // invalid arguments -- expect them - if (have_player(conn)) { + if (have_play_lock(conn)) { uint16_t cport, tport; char *ar = msg_get_header(req, "Active-Remote"); if (ar) { @@ -1962,66 +2058,14 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: ANNOUNCE", conn->connection_number); - - int have_the_player = 0; - int should_wait = 0; // this will be true if you're trying to break in to the current session - int interrupting_current_session = 0; - - // try to become the current playing_conn - - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - - if (playing_conn == NULL) { - playing_conn = conn; - have_the_player = 1; - } else if (playing_conn == conn) { - have_the_player = 1; - warn("Duplicate ANNOUNCE, by the look of it!"); - } else if (playing_conn->stop) { - debug(1, "Connection %d ANNOUNCE is waiting for connection %d to shut down.", - conn->connection_number, playing_conn->connection_number); - should_wait = 1; - } else if (config.allow_session_interruption == 1) { - debug(2, "Connection %d: ANNOUNCE: asking playing connection %d to shut down.", - conn->connection_number, playing_conn->connection_number); - playing_conn->stop = 1; - interrupting_current_session = 1; - should_wait = 1; - pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit - } - debug_mutex_unlock(&playing_conn_lock, 3); - - if (should_wait) { - int time_remaining = 3000000; // must be signed, as it could go negative... - - while ((time_remaining > 0) && (have_the_player == 0)) { - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - if (playing_conn == NULL) { - playing_conn = conn; - have_the_player = 1; - } - debug_mutex_unlock(&playing_conn_lock, 3); - - if (have_the_player == 0) { - usleep(100000); - time_remaining -= 100000; - } - } - - if ((have_the_player == 1) && (interrupting_current_session == 1)) { - debug(2, "Connection %d: ANNOUNCE got the player", conn->connection_number); - } else { - debug(2, "Connection %d: ANNOUNCE failed to get the player", conn->connection_number); - } - } - - if (have_the_player) { + int get_play_status = get_play_lock(conn); + if (get_play_status != -1) { debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); // now, if this new session did not break in, then it's okay to reset the next UDP ports // to the start of the range - if (interrupting_current_session == 0) { // will be zero if it wasn't waiting to break in + if (get_play_status == 1) { // will be zero if it wasn't waiting to break in resetFreeUDPPort(); } @@ -2036,7 +2080,7 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag cp_left -= strlen(cp) + 1; } } -*/ + */ conn->stream.type = ast_unknown; resp->respcode = 456; // 456 - Header Field Not Valid for Resource @@ -2815,7 +2859,7 @@ void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { pthread_setcancelstate(oldState, NULL); } -void rtsp_listen_loop(void) { +void *rtsp_listen_loop(__attribute((unused)) void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); struct addrinfo hints, *info, *p; @@ -2916,7 +2960,82 @@ void rtsp_listen_loop(void) { maxfd = sockfd[i]; } - mdns_register(); + // make up a txt record + char *txt_records[64]; + char **p = txt_records; + +#ifdef CONFIG_AIRPLAY_2 + *p++ = "srcvers=366.0"; + char deviceIdString[64]; + snprintf(deviceIdString, sizeof(deviceIdString) - 1, "deviceid=%s", config.airplay_device_id); + *p++ = deviceIdString; + // features is a 64 bit number, least significant 32 bits + + char featuresString[64]; + uint64_t features_hi = config.airplay_features; + features_hi = (features_hi >> 32) & 0xffffffff; + uint64_t features_lo = config.airplay_features; + features_lo = features_lo & 0xffffffff; + snprintf(featuresString, sizeof(featuresString) - 1, "features=0x%" PRIx64 ",0x%" PRIx64 "", + features_lo, features_hi); + + *p++ = featuresString; + *p++ = "flags=0x4"; + *p++ = "protovers=1.1"; + *p++ = "acl=0"; + *p++ = "rsf=0x0"; + *p++ = "fv=p20.78000.12"; + *p++ = "model=SPS"; + char piString[64]; + snprintf(piString, sizeof(piString) - 1, "pi=%s", config.airplay_pi); + *p++ = piString; + char gidString[64]; + snprintf(gidString, sizeof(gidString) - 1, "gid=%s", config.airplay_gid); + *p++ = gidString; + *p++ = "gcgl=0"; + char pkString[128]; + snprintf(pkString, sizeof(pkString) - 1, "pk=%s", config.airplay_pk); + *p++ = pkString; + *p++ = NULL; +#else + // here, just replicate what happens in mdns.h when using those #defines + *p++ = "sf=0x4"; + *p++ = "fv=76400.10"; + *p++ = "am=ShairportSync"; + *p++ = "vs=105.1"; + *p++ = "tp=TCP,UDP"; + *p++ = "vn=65537"; +#ifdef CONFIG_METADATA + if (config.get_coverart == 0) + *p++ = "md=0,2"; + else + *p++ = "md=0,1,2"; +#endif + *p++ = "ss=16"; + *p++ = "sr=44100"; + *p++ = "da=true"; + *p++ = "sv=false"; + *p++ = "et=0,1"; + *p++ = "ek=1"; + *p++ = "cn=0,1"; + *p++ = "ch=2"; + *p++ = "am=ShairportSync"; + *p++ = "txtvers=1"; + if (config.password == 0) + *p++ = "pw=false"; + else + *p++ = "pw=true"; + *p++ = NULL; +#endif + + /* + debug(1,"txt_record:"); + p = txt_records; + while (*p) + debug(1," %s", *p++); + */ + + mdns_register(txt_records); pthread_setcancelstate(oldState, NULL); int acceptfd; @@ -3030,4 +3149,6 @@ void rtsp_listen_loop(void) { config.port); } // debug(1, "Oops -- fell out of the RTSP select loop"); + debug(1, "Oops -- fell out of the RTSP select loop"); + pthread_exit(NULL); } diff --git a/rtsp.h b/rtsp.h index 703c1006..89f5e151 100644 --- a/rtsp.h +++ b/rtsp.h @@ -6,7 +6,8 @@ extern rtsp_conn_info *playing_conn; extern rtsp_conn_info **conns; -void rtsp_listen_loop(void); +void *rtsp_listen_loop(__attribute((unused)) void *arg); + // void rtsp_shutdown_stream(void); void rtsp_request_shutdown_stream(void); diff --git a/shairport.c b/shairport.c index a32216d3..6168b59e 100644 --- a/shairport.c +++ b/shairport.c @@ -2,7 +2,7 @@ * Shairport, an Apple Airplay receiver * Copyright (c) James Laird 2013 * All rights reserved. - * Modifications and additions (c) Mike Brady 2014--2019 + * Modifications and additions (c) Mike Brady 2014--2021 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -43,6 +43,11 @@ #include "config.h" +#ifdef CONFIG_AIRPLAY_2 +#include +#include +#endif + #ifdef CONFIG_MBEDTLS #include #include @@ -110,6 +115,8 @@ pid_t pid; int this_is_the_daemon_process = 0; #endif +pthread_t rtsp_listener_thread; + int killOption = 0; int daemonisewith = 0; int daemonisewithout = 0; @@ -494,8 +501,13 @@ int parse_options(int argc, char **argv) { /* Get the port setting. */ if (config_lookup_int(config.cfg, "general.port", &value)) { if ((value < 0) || (value > 65535)) +#ifdef CONFIG_AIRPLAY_2 + die("Invalid port number \"%sd\". It should be between 0 and 65535, default is 7000", + value); +#else die("Invalid port number \"%sd\". It should be between 0 and 65535, default is 5000", value); +#endif else config.port = value; } @@ -1234,7 +1246,11 @@ int parse_options(int argc, char **argv) { /* if the regtype hasn't been set, do it now */ if (config.regtype == NULL) +#ifdef CONFIG_AIRPLAY_2 + config.regtype = strdup("_airplay._tcp"); +#else config.regtype = strdup("_raop._tcp"); +#endif if (tdebuglev != 0) debuglev = tdebuglev; @@ -1313,6 +1329,11 @@ const char *pid_file_proc(void) { } #endif +void exit_rtsp_listener() { + pthread_cancel(rtsp_listener_thread); + pthread_join(rtsp_listener_thread, NULL); // not sure you need this +} + void exit_function() { if (emergency_exit == 0) { @@ -1342,13 +1363,14 @@ void exit_function() { #endif */ #ifdef CONFIG_DBUS_INTERFACE - stop_dbus_service(); + debug(2, "Stopping D-Bus service"); + stop_dbus_service(); #endif - if (g_main_loop) { - debug(2, "Stopping DBUS Loop Thread"); - g_main_loop_quit(g_main_loop); - pthread_join(dbus_thread, NULL); - } + if (g_main_loop) { + debug(2, "Stopping D-Bus Loop Thread"); + g_main_loop_quit(g_main_loop); + pthread_join(dbus_thread, NULL); + } #endif #ifdef CONFIG_DACP_CLIENT @@ -1362,8 +1384,11 @@ void exit_function() { #endif #ifdef CONFIG_METADATA - metadata_stop(); // close down the metadata pipe + debug(2, "Stopping metadata"); + metadata_stop(); // close down the metadata pipe #endif + debug(2, "Deinitialising the audio backend."); + activity_monitor_stop(0); activity_monitor_stop(0); @@ -1373,8 +1398,9 @@ void exit_function() { } #ifdef CONFIG_SOXR - // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down - pthread_join(soxr_time_check_thread, NULL); + // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down + debug(2, "Waiting for SoXr timecheck to terminate..."); + pthread_join(soxr_time_check_thread, NULL); #endif if (conns) @@ -1436,8 +1462,14 @@ void handle_sigchld(__attribute__((unused)) int sig) { errno = saved_errno; } -void main_thread_cleanup_handler(__attribute__((unused)) void *arg) { - debug(2, "main thread cleanup handler called"); +// for clean exits +void intHandler(__attribute__((unused)) int k) { + debug(2, "exit on SIGINT"); + exit(EXIT_SUCCESS); +} + +void termHandler(__attribute__((unused)) int k) { + debug(2, "exit on SIGTERM"); exit(EXIT_SUCCESS); } @@ -1535,7 +1567,54 @@ int main(int argc, char **argv) { config.tolerance = 0.002; // this number of seconds of timing error before attempting to correct it. config.buffer_start_fill = 220; + +#if 0 +// #ifdef CONFIG_AIRPLAY_2 + config.timeout = 0; // disable watchdog + config.port = 7000; + // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 + // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as + // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: + // 496155702020608 this setting here is the source of both the plist features response and the + // mDNS string. + config.airplay_features = 0x1C340405F4A00; + // get a device id -- the first non-local MAC address, not necessarily the one in use + config.airplay_device_id = get_device_id(); + if (config.airplay_device_id) { + debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); + } else + debug(1, "Started in Airplay 2 mode!"); + + // now make up a 32-byte "pk" string from random numbers + const ssize_t pk_size = 32; + char pk_bytes[pk_size]; + char pk_string[pk_size * 2 + 1]; + randombytes_buf(pk_bytes, pk_size); // using libsodium + char *obfp = pk_string; + int obfc; + for (obfc = 0; obfc < pk_size; obfc++) { + snprintf(obfp, 3, "%02X", pk_bytes[obfc]); + obfp += 2; + }; + *obfp = 0; + debug(1, "pk string: \"%s\"", pk_string); + config.airplay_pk = strdup(pk_string); + + // now generate a UUID + // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c + // with thanks + uuid_t binuuid; + uuid_generate_random(binuuid); + char *uuid = malloc(UUID_STR_LEN); + /* Produces a UUID string at uuid consisting of lower-case letters. */ + uuid_unparse_lower(binuuid, uuid); + config.airplay_pi = strdup(uuid); + config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi + + // now we need to create the sequence of items for a Bonjour TXT record. +#else config.port = 5000; +#endif #ifdef CONFIG_SOXR config.packet_stuffing = ST_auto; // use soxr interpolation by default if support has been @@ -1728,6 +1807,24 @@ int main(int argc, char **argv) { #endif debug(1, "Started!"); +#ifdef CONFIG_AIRPLAY_2 + debug(1, "Started in Airplay 2 mode!"); +#else + debug(1, "Started in Airplay 1 mode!"); +#endif + + // control-c (SIGINT) cleanly + struct sigaction act; + memset(&act, 0, sizeof(struct sigaction)); + act.sa_handler = intHandler; + sigaction(SIGINT, &act, NULL); + + // terminate (SIGTERM) + struct sigaction act2; + memset(&act2, 0, sizeof(struct sigaction)); + act2.sa_handler = termHandler; + sigaction(SIGTERM, &act2, NULL); + // stop a pipe signal from killing the program signal(SIGPIPE, SIG_IGN); @@ -1768,7 +1865,7 @@ int main(int argc, char **argv) { } config.output->init(argc - audio_arg, argv + audio_arg); - pthread_cleanup_push(main_thread_cleanup_handler, NULL); + // pthread_cleanup_push(main_cleanup_handler, NULL); // daemon_log(LOG_NOTICE, "startup"); @@ -1784,6 +1881,14 @@ int main(int argc, char **argv) { break; } +#ifdef CONFIG_AIRPLAY_2 + if (sodium_init() < 0) { + debug(1, "Can't initialise libsodium!"); + } else { + debug(1, "libsodium initialised."); + } +#endif + /* Mess around with the latency options */ // Basically, we expect the source to set the latency and add a fixed offset of 11025 frames to // it, which sounds right @@ -1983,8 +2088,9 @@ int main(int argc, char **argv) { } #endif - activity_monitor_start(); - rtsp_listen_loop(); - pthread_cleanup_pop(1); + activity_monitor_start(); // not yet for AP2 + pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL); + atexit(exit_rtsp_listener); + pthread_join(rtsp_listener_thread, NULL); return 0; } From b3cec5c690bdfbc54f9486a5c4f13023eb914f09 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 10:12:59 +0100 Subject: [PATCH 024/878] move over to clock_realtime from clock_monotonic. Seems to work. --- activity_monitor.c | 11 ----------- common.c | 3 ++- dacp.c | 11 +---------- rtsp.c | 9 --------- 4 files changed, 3 insertions(+), 31 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 735fd03a..5d178e5d 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -153,18 +153,7 @@ void *activity_monitor_thread_code(void *arg) { if (rc) die("activity_monitor: error %d initialising activity_monitor_mutex.", rc); -// set the flowcontrol condition variable to wait on a monotonic clock -#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - pthread_condattr_t attr; - pthread_condattr_init(&attr); - pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it. - rc = pthread_cond_init(&activity_monitor_cv, &attr); - pthread_condattr_destroy(&attr); - -#endif -#ifdef COMPILE_FOR_OSX rc = pthread_cond_init(&activity_monitor_cv, NULL); -#endif if (rc) die("activity_monitor: error %d initialising activity_monitor_cv."); pthread_cleanup_push(activity_thread_cleanup_handler, arg); diff --git a/common.c b/common.c index 0fe331da..9c9e6ccf 100644 --- a/common.c +++ b/common.c @@ -1250,7 +1250,8 @@ uint64_t get_absolute_time_in_ns() { #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD struct timespec tn; // can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt - clock_gettime(CLOCK_MONOTONIC, &tn); + // CLOCK_REALTIME because PTP uses it. + clock_gettime(CLOCK_REALTIME, &tn); uint64_t tnnsec = tn.tv_sec; tnnsec = tnnsec * 1000000000; uint64_t tnjnsec = tn.tv_nsec; diff --git a/dacp.c b/dacp.c index 0230ac1c..54d1e4c0 100644 --- a/dacp.c +++ b/dacp.c @@ -936,18 +936,9 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { void dacp_monitor_start() { int rc; -#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - pthread_condattr_t attr; - pthread_condattr_init(&attr); - pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it. - rc = pthread_cond_init(&dacp_server_information_cv, &attr); + rc = pthread_cond_init(&dacp_server_information_cv, NULL); if (rc) debug(1, "Error initialising the DACP Server Information Condition Variable"); - pthread_condattr_destroy(&attr); -#endif -#ifdef COMPILE_FOR_OSX - rc = pthread_cond_init(&dacp_server_information_cv, NULL); -#endif pthread_mutexattr_t mta; diff --git a/rtsp.c b/rtsp.c index 8be05db0..dbe1f795 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2662,16 +2662,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { rc = pthread_mutex_init(&conn->ab_mutex, NULL); if (rc) die("Connection %d: error %d initialising ab_mutex.", conn->connection_number, rc); -// set the flowcontrol condition variable to wait on a monotonic clock -#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - pthread_condattr_t attr; - pthread_condattr_init(&attr); - pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it. - rc = pthread_cond_init(&conn->flowcontrol, &attr); -#endif -#ifdef COMPILE_FOR_OSX rc = pthread_cond_init(&conn->flowcontrol, NULL); -#endif if (rc) die("Connection %d: error %d initialising flow control condition variable.", conn->connection_number, rc); From 796b7da07cb517505ae4f9e009f7ec2263619675 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 10:44:43 +0100 Subject: [PATCH 025/878] Bringing across the extra AP2 components of common.h and common.c --- common.c | 148 ++++++++++++++++++++++++++++++++++++++++++------------- common.h | 42 +++++++++++++++- 2 files changed, 154 insertions(+), 36 deletions(-) diff --git a/common.c b/common.c index 9c9e6ccf..2cfed813 100644 --- a/common.c +++ b/common.c @@ -1412,6 +1412,16 @@ uint16_t nctohs(const uint8_t *p) { // read 2 characters from *p and do ntohs on return ntohs(holder); } +uint64_t nctoh64(const uint8_t *p) { + uint32_t landing = nctohl(p); // get the high order 32 bits + uint64_t vl = landing; + vl = vl << 32; // shift them into the correct location + landing = nctohl(p + sizeof(uint32_t)); // and the low order 32 bits + uint64_t ul = landing; + vl = vl + ul; + return vl; +} + pthread_mutex_t barrier_mutex = PTHREAD_MUTEX_INITIALIZER; void memory_barrier() { @@ -1441,35 +1451,16 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - struct timespec tn; - clock_gettime(CLOCK_REALTIME, &tn); - uint64_t tnfpsec = tn.tv_sec; - if (tnfpsec > 0x100000000) - warn("clock_gettime seconds overflow!"); - uint64_t tnfpnsec = tn.tv_nsec; - if (tnfpnsec > 0x100000000) - warn("clock_gettime nanoseconds seconds overflow!"); - tnfpsec = tnfpsec << 32; - tnfpnsec = tnfpnsec << 32; - tnfpnsec = tnfpnsec / 1000000000; - - uint64_t time_now_in_fp = tnfpsec + tnfpnsec; // types okay - - uint64_t dally_time_in_fp = dally_time; // microseconds - dally_time_in_fp = (dally_time_in_fp << 32) / 1000000; // convert to fp format - uint64_t time_then = time_now_in_fp + dally_time_in_fp; - - uint64_t time_then_nsec = time_then & 0xffffffff; // remove integral part - time_then_nsec = time_then_nsec * 1000000000; // multiply fractional part to nanoseconds struct timespec timeoutTime; + uint64_t wait_until_time = dally_time * 1000; // to nanoseconds + uint64_t start_time = get_absolute_time_in_ns(); // this is from CLOCK_REALTIME + wait_until_time = wait_until_time + start_time; + uint64_t wait_until_sec = wait_until_time / 1000000000; + uint64_t wait_until_nsec = wait_until_time % 1000000000; + timeoutTime.tv_sec = wait_until_sec; + timeoutTime.tv_nsec = wait_until_nsec; - time_then = time_then >> 32; // get the seconds - time_then_nsec = time_then_nsec >> 32; // and the nanoseconds - - timeoutTime.tv_sec = time_then; - timeoutTime.tv_nsec = time_then_nsec; - uint64_t start_time = get_absolute_time_in_ns(); int r = pthread_mutex_timedlock(mutex, &timeoutTime); uint64_t et = get_absolute_time_in_ns() - start_time; @@ -1477,9 +1468,9 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, char errstr[1000]; if (r == ETIMEDOUT) debug(debuglevel, - "timed out waiting for a mutex, having waited %f microseconds, with a maximum " - "waiting time of %d microseconds. \"%s\".", - (1.0E6 * et) / 1000000000, dally_time, debugmessage); + "Timed out waiting for a mutex, having waited %f seconds with a maximum " + "waiting time of %f seconds. \"%s\".", + (1.0 * et) / 1000000000, dally_time * 0.000001, debugmessage); else debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r, strerror_r(r, errstr, sizeof(errstr)), debugmessage); @@ -1508,9 +1499,8 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) { char errstr[1000]; if (r == EBUSY) { - debug(debuglevel, - "waiting for a mutex, maximum expected time of %d microseconds exceeded \"%s\".", - dally_time, debugmessage); + debug(debuglevel, "waiting for a mutex, maximum expected time of %f seconds exceeded \"%s\".", + dally_time * 0.000001, debugmessage); r = ETIMEDOUT; // for compatibility } else { debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r, @@ -1539,8 +1529,8 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char result = pthread_mutex_lock(mutex); uint64_t time_delay = get_absolute_time_in_ns() - time_at_start; debug(debuglevel, - "mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f microseconds.", - mutexname, dstring, (1.0 * dally_time), 0.001 * time_delay); + "Mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f sec.", + mutexname, dstring, (1.0 * dally_time) / 1000000, 0.000000001 * time_delay); } pthread_setcancelstate(oldState, NULL); return result; @@ -1571,6 +1561,36 @@ void malloc_cleanup(void *arg) { arg = NULL; } +void socket_cleanup(void *arg) { + // debug(1, "socket_cleanup called."); + intptr_t fdp = (intptr_t)arg; + close(fdp); +} + +void cv_cleanup(void *arg) { + // debug(1, "cv_cleanup called."); + pthread_cond_t *cv = (pthread_cond_t *)arg; + pthread_cond_destroy(cv); +} + +void mutex_cleanup(void *arg) { + // debug(1, "mutex_cleanup called."); + pthread_mutex_t *mutex = (pthread_mutex_t *)arg; + pthread_mutex_destroy(mutex); +} + +void mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); } + +void thread_cleanup(void *arg) { + // debug(1, "mutex_cleanup called."); + pthread_t *thread = (pthread_t *)arg; + pthread_cancel(*thread); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + pthread_join(*thread, NULL); + pthread_setcancelstate(oldState, NULL); +} + void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); } char *get_version_string() { @@ -1869,3 +1889,63 @@ void *memdup(const void *mem, size_t size) { return out; } + +// This will allocate memory and place the NUL-terminated hex character equivalent of +// the bytearray passed in whose length is given. +char *debug_malloc_hex_cstring(void *packet, size_t nread) { + char *response = malloc(nread * 3 + 1); + unsigned char *q = packet; + char *obfp = response; + size_t obfc; + for (obfc = 0; obfc < nread; obfc++) { + snprintf(obfp, 4, "%02x ", *q); + obfp += 3; // two digit characters and a space + q++; + }; + obfp--; // overwrite the last space with a NUL + *obfp = 0; + return response; +} + +// the difference between two unsigned 32-bit modulo values as a signed 32-bit result +// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another, +// we can use their as a signed 2^n bit number which will be positive +// if the first number is the same or "after" the second, and +// negative otherwise + +int32_t mod32Difference(uint32_t a, uint32_t b) { + int32_t result = a - b; + return result; +} + +char *get_device_id() { + char *response = NULL; + struct ifaddrs *ifaddr = NULL; + struct ifaddrs *ifa = NULL; + int i = 0; + + if (getifaddrs(&ifaddr) == -1) { + debug(1, "getifaddrs"); + } else { + int found = 0; + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { + char obf[256] = {0}; + char *obfp = obf; + struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; + if ((strcmp(ifa->ifa_name, "lo") != 0) && (found == 0)) { + for (i = 0; i < s->sll_halen; i++) { + snprintf(obfp, 4, "%02x:", s->sll_addr[i]); + obfp += 3; + } + obfp -= 1; + *obfp = 0; + response = strdup(obf); + found = 1; + } + } + } + freeifaddrs(ifaddr); + } + return response; +} \ No newline at end of file diff --git a/common.h b/common.h index 6602985a..3c4fba65 100644 --- a/common.h +++ b/common.h @@ -1,3 +1,7 @@ +#ifdef __cplusplus +extern "C" { +#endif + #ifndef _COMMON_H #define _COMMON_H @@ -289,14 +293,23 @@ typedef struct { // can't use IP numbers as they might be given to different devices // can't get hold of MAC addresses. // can't define the nvll linked list struct here + +#ifdef CONFIG_AIRPLAY_2 + uint64_t airplay_features; + char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList + char *airplay_pk; // "pk" string in the Bonjour advertisement + char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist + char *airplay_gid; // UUID in the Bonjour advertisement -- initially the same as the pi +#endif } shairport_cfg; // accessors to config for multi-thread access double get_config_airplay_volume(); void set_config_airplay_volume(double v); -uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them -uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them +uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them +uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them +uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t void memory_barrier(); @@ -452,5 +465,30 @@ int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, u uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scope_id, int *sock); +void socket_cleanup(void *arg); +void mutex_unlock(void *arg); +void mutex_cleanup(void *arg); +void cv_cleanup(void *arg); +void thread_cleanup(void *arg); + +char *debug_malloc_hex_cstring(void *packet, size_t nread); + +// from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks +// allocates memory and copies the content to it +// analogous to strndup; +void *memdup(const void *mem, size_t size); + +// the difference between two unsigned 32-bit modulo values as a signed 32-bit result +// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another, +// we can use their as a signed 2^n bit number which will be positive +// if the first number is the same or "after" the second, and +// negative otherwise + +int32_t mod32Difference(uint32_t a, uint32_t b); +char *get_device_id(); #endif // _COMMON_H + +#ifdef __cplusplus +} +#endif From 6760bab493ad5c1672be6a722ca4b448531f5753 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 12:30:12 +0100 Subject: [PATCH 026/878] Compile with AP2 flag set --- player.h | 8 +++++++- rtp.c | 9 ++++----- shairport.c | 3 +-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/player.h b/player.h index 22bbccf3..6c88a1c8 100644 --- a/player.h +++ b/player.h @@ -215,8 +215,10 @@ typedef struct { uint64_t rtp_time_of_last_resend_request_error_ns; char client_ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client + uint16_t client_rtsp_port; char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it - // could be one of many, so we need to know it + uint16_t self_rtsp_port; // could be one of many, so we need to know it + uint32_t self_scope_id; // if it's an ipv6 connection, this will be its scope short connection_ip_family; // AF_INET / AF_INET6 @@ -341,6 +343,9 @@ typedef struct { uint64_t dac_buffer_queue_minimum_length; } rtsp_conn_info; +void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy, + rtsp_conn_info *conn); + int32_t modulo_32_offset(uint32_t from, uint32_t to); int player_play(rtsp_conn_info *conn); @@ -349,6 +354,7 @@ int player_stop(rtsp_conn_info *conn); void player_volume(double f, rtsp_conn_info *conn); void player_volume_without_notification(double f, rtsp_conn_info *conn); void player_flush(uint32_t timestamp, rtsp_conn_info *conn); +void player_full_flush(rtsp_conn_info *conn); void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn); int64_t monotonic_timestamp(uint32_t timestamp, diff --git a/rtp.c b/rtp.c index 69c04ef2..6d4a0a5c 100644 --- a/rtp.c +++ b/rtp.c @@ -1204,8 +1204,7 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { return local_ntp_time_to_frame(time, frame, conn); } - -#if 0 +#ifdef CONFIG_AIRPLAY_2 void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { @@ -1281,7 +1280,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } int have_ptp_timing_information(rtsp_conn_info *conn) { - if (get_anchor_local_time_info(conn, NULL, NULL) == 0) + if (get_ptp_anchor_local_time_info(conn, NULL, NULL) == 0) return 1; else return 0; @@ -1291,7 +1290,7 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * int result = -1; uint32_t anchor_rtptime; uint64_t anchor_local_time; - if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { int32_t frame_difference = timestamp - anchor_rtptime; int64_t time_difference = frame_difference; time_difference = time_difference * 1000000000; @@ -1309,7 +1308,7 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn int result = -1; uint32_t anchor_rtptime; uint64_t anchor_local_time; - if (get_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { int64_t time_difference = time - anchor_local_time; int64_t frame_difference = time_difference; frame_difference = frame_difference * conn->input_rate; // but this is by 10^9 diff --git a/shairport.c b/shairport.c index 6168b59e..bdb837d8 100644 --- a/shairport.c +++ b/shairport.c @@ -1568,8 +1568,7 @@ int main(int argc, char **argv) { 0.002; // this number of seconds of timing error before attempting to correct it. config.buffer_start_fill = 220; -#if 0 -// #ifdef CONFIG_AIRPLAY_2 +#ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog config.port = 7000; // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 From 3d025af4928406f36a31d4dd0671887b067c2316 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 12:30:43 +0100 Subject: [PATCH 027/878] Compiles and runs in AirPlay 1 mode --- rtsp.c | 1904 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 1724 insertions(+), 180 deletions(-) diff --git a/rtsp.c b/rtsp.c index dbe1f795..327f1770 100644 --- a/rtsp.c +++ b/rtsp.c @@ -30,7 +30,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -44,6 +48,8 @@ #include #include +#include + #include "config.h" #ifdef CONFIG_OPENSSL @@ -78,6 +84,17 @@ #define INETx_ADDRSTRLEN INET_ADDRSTRLEN #endif +#ifdef CONFIG_AIRPLAY_2 +#include "csrp/srp.h" +#include "pair_ap/pair-tlv.h" +#include "pair_ap/pair.h" +#include "plist/plist.h" +#include +// #include "proxy_ap/server.h" +#include "plist_xml_strings.h" +#include "ptp-utilities.h" +#endif + #define METADATA_SNDBUF (4 * 1024 * 1024) enum rtsp_read_request_response { @@ -138,6 +155,7 @@ typedef struct { // for requests char method[16]; + char path[256]; // for responses int respcode; @@ -451,13 +469,6 @@ void *player_watchdog_thread_code(void *arg) { pthread_exit(NULL); } -void ask_other_rtsp_conversation_threads_to_stop(pthread_t except_this_thread); - -void rtsp_request_shutdown_stream(void) { - debug(1, "Request to shut down all rtsp conversation threads"); - ask_other_rtsp_conversation_threads_to_stop(0); // i.e. ask all playing threads to stop -} - // keep track of the threads we have spawned so we can join() them static int nconns = 0; static void track_thread(rtsp_conn_info *conn) { @@ -503,24 +514,6 @@ void cleanup_threads(void) { } } -// ask all rtsp_conversation threads to stop -- there should be at most one, but -// ya never know. - -void ask_other_rtsp_conversation_threads_to_stop(pthread_t except_this_thread) { - int i; - debug(1, "asking playing threads to stop"); - for (i = 0; i < nconns; i++) { - if (((except_this_thread == 0) || (pthread_equal(conns[i]->thread, except_this_thread) == 0)) && - (conns[i]->running != 0)) { - pthread_cancel(conns[i]->thread); - pthread_join(conns[i]->thread, NULL); - debug(1, "Connection %d: asked to stop.", conns[i]->connection_number); - // conns[i]->stop = 1; - // pthread_kill(conns[i]->thread, SIGUSR1); - } - } -} - // park a null at the line ending, and return the next line pointer // accept \r, \n, or \r\n static char *nextline(char *in, int inbuf) { @@ -683,6 +676,7 @@ int msg_handle_line(rtsp_message **pmsg, char *line) { p = strtok_r(NULL, " ", &sp); if (!p) goto fail; + strncpy(msg->path, p, sizeof(msg->path) - 1); p = strtok_r(NULL, " ", &sp); if (!p) @@ -720,6 +714,502 @@ fail: return 0; } +#ifdef CONFIG_AIRPLAY_2 + +int rtsp_message_contains_plist(rtsp_message *message) { + int reply = 0; // assume there is no plist in the message + if ((message->contentlength >= strlen("bplist00")) && + (strstr(message->content, "bplist00") == message->content)) + reply = 1; + return reply; +} + +plist_t plist_from_rtsp_content(rtsp_message *message) { + plist_t the_plist = NULL; + if (rtsp_message_contains_plist(message)) { + plist_from_memory(message->content, message->contentlength, &the_plist); + } + return the_plist; +} + +char *plist_content(plist_t the_plist) { + // caller must free the returned character buffer + // convert it to xml format + uint32_t size; + char *plist_out = NULL; + plist_to_xml(the_plist, &plist_out, &size); + + // put it into a NUL-terminated string + char *reply = malloc(size + 1); + if (reply) { + memcpy(reply, plist_out, size); + reply[size] = '\0'; + } + if (the_plist) + plist_free(the_plist); + if (plist_out) + free(plist_out); + return reply; +} + +// caller must free the returned character buffer +char *rtsp_plist_content(rtsp_message *message) { + char *reply = NULL; + // first, check if it has binary plist content + if (rtsp_message_contains_plist(message)) { + // get the plist from the content + + plist_t the_plist = plist_from_rtsp_content(message); + + // convert it to xml format + uint32_t size; + char *plist_out = NULL; + plist_to_xml(the_plist, &plist_out, &size); + + // put it into a NUL-terminated string + reply = malloc(size + 1); + if (reply) { + memcpy(reply, plist_out, size); + reply[size] = '\0'; + } + if (the_plist) + plist_free(the_plist); + if (plist_out) + free(plist_out); + } + return reply; +} + +#endif + +void debug_log_rtsp_message(int level, char *prompt, rtsp_message *message) { + if (level > debuglev) + return; + if ((prompt) && (*prompt != '\0')) // okay to pass NULL or an empty list... + debug(level, prompt); + // debug_print_msg_headers(level, message); +#ifdef CONFIG_AIRPLAY_2 + char *plist_content = rtsp_plist_content(message); + if (plist_content) { + debug(level, " Content Plist (as XML):\n--\n%s--", plist_content); + free(plist_content); + } else +#endif + { + debug(level, " No Content Plist. Content length: %d.", message->contentlength); + } +} + +// this will read a block of the size specified to the buffer +// and will return either with the block or on error +ssize_t read_sized_block(int fd, void *buf, size_t count) { + ssize_t response, nread; + size_t inbuf = 0; // bytes already in the buffer + int keep_trying = 1; + + do { + nread = read(fd, buf + inbuf, count - inbuf); + if (nread == 0) { + // a blocking read that returns zero means eof -- implies connection closed + debug(3, "read_sized_block connection closed."); + keep_trying = 0; + } else if (nread < 0) { + if (errno == EAGAIN) { + debug(1, "read_sized_block getting Error 11 -- EAGAIN from a blocking read!"); + } + if ((errno != ECONNRESET) && (errno != EAGAIN) && (errno != EINTR)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "read_sized_block read error %d: \"%s\".", errno, (char *)errorstring); + keep_trying = 0; + } + } else { + inbuf += (size_t)nread; + } + } while ((keep_trying != 0) && (inbuf < count)); + if (nread <= 0) + response = nread; + else + response = inbuf; + return response; +} + +#ifdef CONFIG_AIRPLAY_2 + +/* ----------------------------- DEFINES ETC ------------------------------- */ + +#define USERNAME "Pair-Setup" +#define AUTHTAG_LENGTH 16 +#define NONCE_LENGTH 12 // 96 bits according to chacha poly1305 +#define RESPONSE_BUFSIZE 8192 +#define ENCRYPTED_LEN_MAX 0x400 +#define PASSWORD "3939" + +#define RTSP_VERSION "RTSP/1.0" +#define POST_PAIR_SETUP "POST /pair-setup" +#define OPTIONS "OPTIONS *" +#define GET_INFO "GET /info" +#define POST_AUTH_SETUP "POST /auth-setup" +#define POST_FP_SETUP "POST /fp-setup" + +/* Fairplay magic */ +/* +static uint8_t server_fp_reply1[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x00\x0f\x9f\x3f\x9e\x0a" + "\x25\x21\xdb\xdf\x31\x2a\xb2\xbf\xb2\x9e\x8d\x23\x2b\x63\x76\xa8\xc8\x18\x70" + "\x1d\x22\xae\x93\xd8\x27\x37\xfe\xaf\x9d\xb4\xfd\xf4\x1c\x2d\xba\x9d\x1f\x49" + "\xca\xaa\xbf\x65\x91\xac\x1f\x7b\xc6\xf7\xe0\x66\x3d\x21\xaf\xe0\x15\x65\x95" + "\x3e\xab\x81\xf4\x18\xce\xed\x09\x5a\xdb\x7c\x3d\x0e\x25\x49\x09\xa7\x98\x31" + "\xd4\x9c\x39\x82\x97\x34\x34\xfa\xcb\x42\xc6\x3a\x1c\xd9\x11\xa6\xfe\x94\x1a" + "\x8a\x6d\x4a\x74\x3b\x46\xc3\xa7\x64\x9e\x44\xc7\x89\x55\xe4\x9d\x81\x55\x00" + "\x95\x49\xc4\xe2\xf7\xa3\xf6\xd5\xba"; +static uint8_t server_fp_reply2[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x01\xcf\x32\xa2\x57\x14" + "\xb2\x52\x4f\x8a\xa0\xad\x7a\xf1\x64\xe3\x7b\xcf\x44\x24\xe2\x00\x04\x7e\xfc" + "\x0a\xd6\x7a\xfc\xd9\x5d\xed\x1c\x27\x30\xbb\x59\x1b\x96\x2e\xd6\x3a\x9c\x4d" + "\xed\x88\xba\x8f\xc7\x8d\xe6\x4d\x91\xcc\xfd\x5c\x7b\x56\xda\x88\xe3\x1f\x5c" + "\xce\xaf\xc7\x43\x19\x95\xa0\x16\x65\xa5\x4e\x19\x39\xd2\x5b\x94\xdb\x64\xb9" + "\xe4\x5d\x8d\x06\x3e\x1e\x6a\xf0\x7e\x96\x56\x16\x2b\x0e\xfa\x40\x42\x75\xea" + "\x5a\x44\xd9\x59\x1c\x72\x56\xb9\xfb\xe6\x51\x38\x98\xb8\x02\x27\x72\x19\x88" + "\x57\x16\x50\x94\x2a\xd9\x46\x68\x8a"; +static uint8_t server_fp_reply3[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x02\xc1\x69\xa3\x52\xee" + "\xed\x35\xb1\x8c\xdd\x9c\x58\xd6\x4f\x16\xc1\x51\x9a\x89\xeb\x53\x17\xbd\x0d" + "\x43\x36\xcd\x68\xf6\x38\xff\x9d\x01\x6a\x5b\x52\xb7\xfa\x92\x16\xb2\xb6\x54" + "\x82\xc7\x84\x44\x11\x81\x21\xa2\xc7\xfe\xd8\x3d\xb7\x11\x9e\x91\x82\xaa\xd7" + "\xd1\x8c\x70\x63\xe2\xa4\x57\x55\x59\x10\xaf\x9e\x0e\xfc\x76\x34\x7d\x16\x40" + "\x43\x80\x7f\x58\x1e\xe4\xfb\xe4\x2c\xa9\xde\xdc\x1b\x5e\xb2\xa3\xaa\x3d\x2e" + "\xcd\x59\xe7\xee\xe7\x0b\x36\x29\xf2\x2a\xfd\x16\x1d\x87\x73\x53\xdd\xb9\x9a" + "\xdc\x8e\x07\x00\x6e\x56\xf8\x50\xce"; +static uint8_t server_fp_reply4[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x03\x90\x01\xe1\x72\x7e" + "\x0f\x57\xf9\xf5\x88\x0d\xb1\x04\xa6\x25\x7a\x23\xf5\xcf\xff\x1a\xbb\xe1\xe9" + "\x30\x45\x25\x1a\xfb\x97\xeb\x9f\xc0\x01\x1e\xbe\x0f\x3a\x81\xdf\x5b\x69\x1d" + "\x76\xac\xb2\xf7\xa5\xc7\x08\xe3\xd3\x28\xf5\x6b\xb3\x9d\xbd\xe5\xf2\x9c\x8a" + "\x17\xf4\x81\x48\x7e\x3a\xe8\x63\xc6\x78\x32\x54\x22\xe6\xf7\x8e\x16\x6d\x18" + "\xaa\x7f\xd6\x36\x25\x8b\xce\x28\x72\x6f\x66\x1f\x73\x88\x93\xce\x44\x31\x1e" + "\x4b\xe6\xc0\x53\x51\x93\xe5\xef\x72\xe8\x68\x62\x33\x72\x9c\x22\x7d\x82\x0c" + "\x99\x94\x45\xd8\x92\x46\xc8\xc3\x59"; + +static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; +*/ + +/* 3072 n and g for SRP*/ + +const char *nl_hex = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" + "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" + "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" + "E649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23" + "DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32" + "905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558" + "17183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521" + "ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D7" + "1E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B1817" + "7B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82" + "D120A93AD2CAFFFFFFFFFFFFFFFF"; +const char *gl_hex = "5"; + +enum pair_keys { + PAIR_SETUP_MSG01 = 0, + PAIR_SETUP_MSG02, + PAIR_SETUP_MSG03, + PAIR_SETUP_MSG04, + PAIR_SETUP_MSG05, + PAIR_SETUP_MSG06, + PAIR_SETUP_SIGN, + PAIR_VERIFY_MSG01, + PAIR_VERIFY_MSG02, + PAIR_VERIFY_MSG03, + PAIR_VERIFY_MSG04, + PAIR_CONTROL_WRITE, + PAIR_CONTROL_READ, + PAIR_EVENTS_WRITE, + PAIR_EVENTS_READ, +}; + +struct pair_keys_map { + uint8_t state; + const char *salt; + const char *info; + const char nonce[8]; +}; + +static struct pair_keys_map pair_keys_map[] = { + // Used for /pair-setup + {0x01, NULL, NULL, ""}, + {0x02, NULL, NULL, ""}, + {0x03, NULL, NULL, ""}, + {0x04, NULL, NULL, ""}, + {0x05, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg05"}, + {0x06, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg06"}, + {0, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", ""}, + + // Used for /pair-verify + {0x01, NULL, NULL, ""}, + {0x02, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg02"}, + {0x03, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg03"}, + {0x04, NULL, NULL, ""}, + + // Encryption/decryption of control channel + {0, "Control-Salt", "Control-Write-Encryption-Key", ""}, + {0, "Control-Salt", "Control-Read-Encryption-Key", ""}, + + // Encryption/decryption of event channel + {0, "Events-Salt", "Events-Write-Encryption-Key", ""}, + {0, "Events-Salt", "Events-Read-Encryption-Key", ""}, +}; + +enum pair_method { + PairingMethodPairSetup = 0x00, + PairingMethodPairSetupWithAuth = 0x01, + PairingMethodPairVerify = 0x02, + PairingMethodAddPairing = 0x03, + PairingMethodRemovePairing = 0x04, + PairingMethodListPairings = 0x05 +}; + +enum pair_flags { + PairingFlagsTransient = 0x10, +}; + +struct verifier_setup_context { + struct pair_definition *type; + + struct SRPVerifier *verifier; + + char pin[4]; + char device_id[17]; // Incl. zero term + + uint8_t *pkA; + uint64_t pkA_len; + + const uint8_t *pkB; + int pkB_len; + + const uint8_t *b; + int b_len; + + uint8_t *M1; + uint64_t M1_len; + + const uint8_t *M2; + int M2_len; + + const uint8_t *v; + int v_len; + + const uint8_t *salt; + int salt_len; + uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + // Hex-formatet concatenation of public + private, 0-terminated + char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1]; + + // We don't actually use the server's epk and authtag for anything + uint8_t *epk; + uint64_t epk_len; + uint8_t *authtag; + uint64_t authtag_len; + + int setup_is_completed; + const char *errmsg; +}; + +// static struct verifier_setup_context server_setup_ctx; + +ssize_t write_encrypted(file_cipher_context *context, const void *buf, size_t count) { + // need to make this cancellable! + // encrypt the contents of the buffer + ssize_t response; + uint8_t nonce[NONCE_LENGTH] = {0}; + // uint8_t tag[AUTHTAG_LENGTH]; + uint8_t *plain_block; + uint8_t *cipher_block; + uint16_t block_len; + int nblocks; + int ret; + int i; + + if ((count == 0) || (buf == NULL)) { + debug(1, "encrypting a null character sequence"); + response = 0; + } else { + // Encryption is done in blocks, where each block consists of a short, the + // encrypted data and an auth tag. The short is the size of the encrypted + // data. The encrypted data in the block cannot exceed ENCRYPTED_LEN_MAX. + nblocks = 1 + ((count - 1) / ENCRYPTED_LEN_MAX); // Ceiling of division + + ssize_t ctl = nblocks * (sizeof(block_len) + AUTHTAG_LENGTH) + count; + // size_t hdh = 0; + void *ciphertext = malloc(ctl); + if (ciphertext != NULL) { + + context->cipher_context->encryption_counter_prev = + context->cipher_context->encryption_counter; + + for (i = 0, plain_block = (uint8_t *)buf, cipher_block = ciphertext; i < nblocks; i++) { + // If it is the last block we will encrypt only the remaining data + if (i == nblocks - 1) { + uint8_t *b = (uint8_t *)buf; + block_len = b + count - plain_block; + } else + block_len = ENCRYPTED_LEN_MAX; + + memcpy(nonce + 4, &(context->cipher_context->encryption_counter), + sizeof(context->cipher_context->encryption_counter)); // TODO BE or LE? + + // Write the ciphered block + memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE? + + unsigned long long cipher_length = 0; + ret = crypto_aead_chacha20poly1305_ietf_encrypt( + cipher_block + sizeof(block_len), // ciphertext + &cipher_length, + plain_block, // message + block_len, // message length + (unsigned char *)&block_len, // additional data + sizeof(block_len), // additional data length + NULL, + nonce, // nonce + context->cipher_context->encryption_key // key + ); + + if (ret < 0) { + debug(1, "Encryption with chacha poly1305 failed"); + context->cipher_context->encryption_counter = + context->cipher_context->encryption_counter_prev; + free(ciphertext); + } else { + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; + context->cipher_context->encryption_counter++; + } + } + // maybe this really should be write_sized_block, similar to read_sized_block + ssize_t trywrite = write(context->fd, ciphertext, ctl); + if (trywrite != ctl) + debug(1, "write_encrypted write failure"); + free(ciphertext); + response = count; // pretend only the plaintext number of bytes were written + } else { + // can't allocate memory for the cyphertext + errno = ENOMEM; + response = -1; + } + } + return response; +} + +ssize_t read_encrypted(file_cipher_context *context, void *buf, size_t count) { + // need to make this cancellable! + // if the plaintext buffer is empty, this will + // read an encrypted block and decipher it to plaintext + // this will then return bytes in the plaintext buffer + // up to the limit of the count + ssize_t response = 0; + if (context->input_plaintext_buffer == NULL) { + response = -1; + uint16_t block_len; + if (read_sized_block(context->fd, &block_len, sizeof(block_len)) == sizeof(block_len)) { + void *cipher_block = malloc(block_len + AUTHTAG_LENGTH); + if (cipher_block != NULL) { + if (read_sized_block(context->fd, cipher_block, (block_len + AUTHTAG_LENGTH)) == + block_len + AUTHTAG_LENGTH) { + // here we have the encrypted block + uint8_t nonce[NONCE_LENGTH] = {0}; + uint8_t tag[AUTHTAG_LENGTH]; + context->input_plaintext_buffer = malloc(block_len); // this should be more than enough + if (context->input_plaintext_buffer != NULL) { + memcpy(tag, cipher_block + block_len, sizeof(tag)); + memcpy(nonce + 4, &(context->cipher_context->decryption_counter), + sizeof(context->cipher_context->decryption_counter)); // TODO BE or LE? + unsigned long long new_payload_length = 0; + int ret = crypto_aead_chacha20poly1305_ietf_decrypt( + context->input_plaintext_buffer, // m + &new_payload_length, // mlen_p + NULL, // nsec, + cipher_block, // cipher text + block_len + AUTHTAG_LENGTH, // length of the ciphertext + (unsigned char *)&block_len, // authenticated additional data + sizeof(block_len), // authenticated additional data length + nonce, // nonce + context->cipher_context->decryption_key // key + ); + free(cipher_block); + cipher_block = NULL; + if (ret == 0) { + response = 0; // decryption was successful + context->cipher_context->decryption_counter++; + context->input_plaintext_buffer_toq = context->input_plaintext_buffer; + context->input_plaintext_buffer_bytes_occupied = block_len; + } else { + // decryption failed + debug(1, "decryption failed."); + free(context->input_plaintext_buffer); + context->input_plaintext_buffer = NULL; + errno = EILSEQ; // illegal byte sequence + } + } else { + // failed to allocate a plaintext buffer + debug(1, "could not allocate a plaintext buffer"); + free(cipher_block); + cipher_block = NULL; + errno = ENOMEM; + } + } else { + // failed to read a block properly -- errno will be set + // debug(1, "could not read a block"); + free(cipher_block); + cipher_block = NULL; + } + } else { + // failed to allocate a ciphertext buffer + debug(1, "could not allocate a ciphertext buffer"); + errno = ENOMEM; + } + } else { + debug(2, "could not read the block_len"); + // failed to read the encrypted block size -- errno will be set + } + } + if (response == 0) { + // now, transfer bytes to the output buffer up to the limit of the count requested + if (context->input_plaintext_buffer_bytes_occupied) { + size_t bytes_to_transfer = context->input_plaintext_buffer_bytes_occupied; + if (bytes_to_transfer > count) + bytes_to_transfer = count; + if (bytes_to_transfer == 0) + debug(1, "Shome problem -- zero bytes to transfer!"); + memcpy(buf, context->input_plaintext_buffer_toq, bytes_to_transfer); + context->input_plaintext_buffer_bytes_occupied = + context->input_plaintext_buffer_bytes_occupied - bytes_to_transfer; + if (context->input_plaintext_buffer_bytes_occupied == 0) { + free(context->input_plaintext_buffer); + context->input_plaintext_buffer = NULL; + } else { + context->input_plaintext_buffer_toq = + context->input_plaintext_buffer_toq + bytes_to_transfer; + } + response = bytes_to_transfer; + } + } + return response; +} +#endif + +ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { +#ifdef CONFIG_AIRPLAY_2 + if ((conn->control_cipher_context.cipher_context != NULL) && + (conn->control_cipher_context.active != 0)) { + return read_encrypted(&conn->control_cipher_context, buf, count); + } else { + return read(conn->fd, buf, count); + } +#else + return read(conn->fd, buf, count); +#endif +} + enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) { *the_packet = NULL; // need this for error handling @@ -744,7 +1234,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes goto shutdown; } - nread = read(conn->fd, buf + inbuf, buflen - inbuf); + nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); if (nread == 0) { // a blocking read that returns zero means eof -- implies connection closed @@ -764,8 +1254,9 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes if (errno != ECONNRESET) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", - conn->connection_number, errno, (char *)errorstring); + if (errno != 0) + debug(2, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", + conn->connection_number, errno, (char *)errorstring); } reply = rtsp_read_request_response_read_error; goto shutdown; @@ -843,8 +1334,8 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes size_t read_chunk = msg_size - inbuf; if (read_chunk > max_read_chunk) read_chunk = max_read_chunk; - usleep(80000); // wait about 80 milliseconds between reads of up to about 64 kB - nread = read(conn->fd, buf + inbuf, read_chunk); + usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk + nread = read_from_rtsp_connection(conn, buf + inbuf, read_chunk); if (!nread) { reply = rtsp_read_request_response_error; goto shutdown; @@ -883,8 +1374,8 @@ shutdown: return reply; } -int msg_write_response(int fd, rtsp_message *resp) { - char pkt[2048]; +int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { + char pkt[4096]; int pktfree = sizeof(pkt); char *p = pkt; int n; @@ -910,7 +1401,7 @@ int msg_write_response(int fd, rtsp_message *resp) { // Here, if there's content, write the Content-Length header ... if (resp->contentlength) { - debug(1, "Responding with content of length %d", resp->contentlength); + debug(2, "Responding with content of length %d", resp->contentlength); n = snprintf(p, pktfree, "Content-Length: %d\r\n", resp->contentlength); pktfree -= n; p += n; @@ -932,7 +1423,21 @@ int msg_write_response(int fd, rtsp_message *resp) { debug(1, "Attempted to write overlong RTSP packet 3"); return -3; } - ssize_t reply = write(fd, pkt, p - pkt); + + // here, if the link is encrypted, better do it + +#ifdef CONFIG_AIRPLAY_2 + ssize_t reply; + if ((conn->control_cipher_context.cipher_context != NULL) && + (conn->control_cipher_context.active != 0)) { + reply = write_encrypted(&conn->control_cipher_context, pkt, p - pkt); + } else { + reply = write(conn->fd, pkt, p - pkt); + } +#else + ssize_t reply = write(conn->fd, pkt, p - pkt); +#endif + if (reply == -1) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); @@ -947,6 +1452,12 @@ int msg_write_response(int fd, rtsp_message *resp) { return 0; } +void handle_record_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(2, "Connection %d: RECORD (AirPlay 2)", conn->connection_number); + debug_log_rtsp_message(2, "RECORD incoming message", req); + resp->respcode = 200; +} + void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(2, "Connection %d: RECORD", conn->connection_number); if (have_play_lock(conn)) { @@ -992,8 +1503,572 @@ 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) { + if (rtsp_message_contains_plist(req)) { // it's stage one + + resp->close_on_response = 0; + resp->suppress_crlf = 0; + + plist_t info_plist = NULL; + plist_from_memory(req->content, req->contentlength, &info_plist); + + plist_t qualifier = plist_dict_get_item(info_plist, "qualifier"); + if (qualifier == NULL) { + debug(1, "GET /info Stage 1: plist->qualifier was NULL"); + goto user_fail; + } + if (plist_array_get_size(qualifier) < 1) { + debug(1, "GET /info Stage 1: plist->qualifier array length < 1"); + goto user_fail; + } + plist_t qualifier_array_value = plist_array_get_item(qualifier, 0); + char *qualifier_array_val_cstr; + plist_get_string_val(qualifier_array_value, &qualifier_array_val_cstr); + if (qualifier_array_val_cstr == NULL) { + debug(1, "GET /info Stage 1: first item in qualifier array not a string"); + goto user_fail; + } + debug(2, "GET /info Stage 1: qualifier: %s", qualifier_array_val_cstr); + plist_free(info_plist); + free(qualifier_array_val_cstr); + + // uint8_t bt_addr[6] = {0xB8, 0x27, 0xEB, 0xB7, 0xD4, 0x0E}; + plist_t response_plist = NULL; + plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, + &response_plist); + if (response_plist == NULL) { + debug(1, "GET /info Stage 1: response plist not created from XML!"); + } else { + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); + plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); + char *vs = get_version_string(); + plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + if (resp->contentlength == 0) + debug(1, "GET /info Stage 1: response bplist not created!"); + plist_free(response_plist); + free(vs); + } + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "GET /info Stage 1 Response:", resp); + resp->respcode = 200; + return; + + user_fail: + resp->respcode = 400; + return; + } else { // stage two + plist_t response_plist = NULL; + plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, + &response_plist); + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); + plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); + char *vs = get_version_string(); + plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + free(vs); + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "GET /info Stage 2 Response", resp); + resp->respcode = 200; + return; + } +} + +void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: FLUSHBUFFERED %s :: Content-Length %d", conn->connection_number, + req->path, req->contentlength); + debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); + + uint64_t flushUntilSeq = 0; + uint64_t flushUntilTS = 0; + plist_t messagePlist = plist_from_rtsp_content(req); + + plist_t item = plist_dict_get_item(messagePlist, "flushUntilSeq"); + if (item == NULL) { + debug(1, "Can't find the flushUntilSeq"); + } else { + plist_get_uint_val(item, &flushUntilSeq); + debug(2, "flushUntilSeq is %" PRId64 ".", flushUntilSeq); + } + + item = plist_dict_get_item(messagePlist, "flushUntilTS"); + if (item == NULL) { + debug(1, "Can't find the flushUntilTS"); + } else { + plist_get_uint_val(item, &flushUntilTS); + debug(2, "flushUntilTS is %" PRId64 ".", flushUntilTS); + } + + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + conn->ap2_flush_sequence_number = flushUntilSeq; + conn->ap2_flush_rtp_timestamp = flushUntilTS; + conn->ap2_flush_requested = 1; + debug_mutex_unlock(&conn->flush_mutex, 3); + + // player_full_flush(conn); + + resp->respcode = 200; +} + +void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SETRATEANCHORI %s :: Content-Length %d", conn->connection_number, + req->path, req->contentlength); + debug_log_rtsp_message(2, "SETRATEANCHORI request", req); + + plist_t messagePlist = plist_from_rtsp_content(req); + + if (messagePlist != NULL) { + plist_t item = plist_dict_get_item(messagePlist, "networkTimeSecs"); + if (item != NULL) { + plist_t item_2 = plist_dict_get_item(messagePlist, "networkTimeTimelineID"); + if (item_2 == NULL) { + debug(1, "Can't identify the Clock ID of the player."); + } else { + uint64_t nid; + plist_get_uint_val(item_2, &nid); + debug(2, "networkTimeTimelineID \"%" PRIx64 "\".", nid); + conn->networkTimeTimelineID = nid; + } + uint64_t networkTimeSecs; + plist_get_uint_val(item, &networkTimeSecs); + debug(2, "anchor networkTimeSecs is 0x%" PRIx64 ".", networkTimeSecs); + + item = plist_dict_get_item(messagePlist, "networkTimeFrac"); + uint64_t networkTimeFrac; + plist_get_uint_val(item, &networkTimeFrac); + debug(2, "anchor networkTimeFrac is 0x%" PRIx64 ".", networkTimeFrac); + // it looks like the networkTimeFrac is a fraction where the msb is work 1/2, the + // next 1/4 and so on + // now, convert the network time and fraction into nanoseconds + networkTimeFrac = networkTimeFrac >> 32; // reduce precision to about 1/4 nanosecond + networkTimeFrac = networkTimeFrac * 1000000000; + networkTimeFrac = networkTimeFrac >> 32; // we should now be left with the ns + + networkTimeSecs = networkTimeSecs * 1000000000; // turn the whole seconds into ns + uint64_t anchorTimeNanoseconds = networkTimeSecs + networkTimeFrac; + + debug(2, "anchorTimeNanoseconds looks like 0x%" PRIx64 ".", anchorTimeNanoseconds); + + item = plist_dict_get_item(messagePlist, "rtpTime"); + uint64_t rtpTime; + + plist_get_uint_val(item, &rtpTime); + // debug(1, "anchor rtpTime is %" PRId64 ".", rtpTime); + uint32_t anchorRTPTime = rtpTime; + + int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, + anchorTimeNanoseconds); + } + + item = plist_dict_get_item(messagePlist, "rate"); + if (item != NULL) { + uint64_t rate; + plist_get_uint_val(item, &rate); + debug(2, "anchor rate 0x%016" PRIx64 ".", rate); + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + conn->ap2_rate = rate; + if ((rate & 1) != 0) { + + get_play_lock(conn); + + if (conn->ap2_timing_peer_list_message) + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + else + debug(1, "Connection %d: No timing peer list!", conn->connection_number); + + debug(2, "Connection %d: Start playing.", conn->connection_number); + conn->ap2_play_enabled = 1; + } else { + debug(1, "Connection %d: Stop playing.", conn->connection_number); + conn->ap2_play_enabled = 0; + // not sure this is needed yet + reset_anchor_info(conn); // needed if the player resumes + release_play_lock(conn); + } + debug_mutex_unlock(&conn->flush_mutex, 3); + if ((rate & 1) == 0) + player_full_flush(conn); + } + plist_free(messagePlist); + } else { + debug(1, "missing plist!"); + } + resp->respcode = 200; +} + +void handle_get(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(2, "Connection %d: GET %s :: Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, "GET request", req); + if (strcmp(req->path, "/info") == 0) { + handle_get_info(conn, req, resp); + } else { + debug(1, "Unhandled GET, path \"%s\".", req->path); + resp->respcode = 404; // makes sense, right? + } +} + +#else +void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { + debug(1, "Connection %d: GET %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + resp->respcode = 500; +} +#endif + +#ifdef CONFIG_AIRPLAY_2 +void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + int ret; + resp->respcode = 200; // assume everything works out okay + debug(2, "Connection %d: pair-setup Content-Length %d", conn->connection_number, + req->contentlength); + pair_tlv_values_t *values = pair_tlv_new(); + pair_tlv_t *state; + pair_tlv_t *method; + pair_tlv_t *flags; + ret = pair_tlv_parse((const unsigned char *)req->content, req->contentlength, values); + if (ret < 0) + debug(1, "Could not parse TLV"); + state = pair_tlv_get_value(values, TLVType_State); + if (!state || state->size != 1) { + debug(1, "Missing/unexpected pairing state in TLV."); + } + + if (state->value[0] == pair_keys_map[PAIR_SETUP_MSG01].state) { + debug(2, "pair-setup part 1"); + method = pair_tlv_get_value(values, TLVType_Method); + if (!method || method->size != 1 || method->value[0] != 0) { + debug(1, "Missing/unexpected pairing method in TLV."); + } + if (method->value[0] != PairingMethodPairSetup) + debug(1, "Unexpected method value %u.", method->value[0]); + flags = pair_tlv_get_value(values, TLVType_Flags); + if (!flags || flags->size != 1) { + debug(1, "Missing/unexpected pairing flags in TLV."); + } + if (flags->value[0] == PairingFlagsTransient) { + debug(2, "Transient pairing selected."); + conn->pairing_mode = + PairingFlagsTransient; // when pairing step 2 is finished, turn on encryption + } + + // Note this is modified to return a 16 byte salt + srp_create_salted_verification_key( + SRP_SHA512, SRP_NG_CUSTOM, USERNAME, (const unsigned char *)PASSWORD, strlen(PASSWORD), + &conn->server_setup_ctx->salt, &conn->server_setup_ctx->salt_len, + &conn->server_setup_ctx->v, &conn->server_setup_ctx->v_len, nl_hex, gl_hex); + + srp_verifier_get_B(SRP_SHA512, SRP_NG_CUSTOM, conn->server_setup_ctx->v, + conn->server_setup_ctx->v_len, &conn->server_setup_ctx->b, + &conn->server_setup_ctx->b_len, &conn->server_setup_ctx->pkB, + &conn->server_setup_ctx->pkB_len, nl_hex, gl_hex); + + pair_tlv_values_t *payload = pair_tlv_new(); + + pair_tlv_add_value(payload, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG02].state, + sizeof(pair_keys_map[PAIR_SETUP_MSG02].state)); + pair_tlv_add_value(payload, TLVType_Salt, conn->server_setup_ctx->salt, + conn->server_setup_ctx->salt_len); // 16 + pair_tlv_add_value(payload, TLVType_PublicKey, conn->server_setup_ctx->pkB, + conn->server_setup_ctx->pkB_len); // 384 + + // turn the tlv to binary form + + char *body = malloc(RESPONSE_BUFSIZE); + size_t body_len; + body_len = RESPONSE_BUFSIZE; + + ret = pair_tlv_format(payload, (uint8_t *)body, &body_len); + if (ret < 0) + debug(1, "Can't convert response to a binary stream"); + pair_tlv_free(payload); + resp->content = body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-setup part 1 response", resp); + resp->respcode = 200; // it all worked out okay + + } else if (state->value[0] == pair_keys_map[PAIR_SETUP_MSG03].state) { + debug(2, "pair setup part 2"); + + // uint8_t *out; + // size_t out_len; + const uint8_t *key; + int key_len; + pair_tlv_t *pk; + pair_tlv_t *proof; + pair_tlv_values_t *payload; + + pk = pair_tlv_get_value(values, TLVType_PublicKey); + proof = pair_tlv_get_value(values, TLVType_Proof); + + if ((pk != NULL) && (proof != NULL)) { + conn->server_setup_ctx->pkA_len = pk->size; // 384 + conn->server_setup_ctx->pkA = malloc(conn->server_setup_ctx->pkA_len); + memcpy(conn->server_setup_ctx->pkA, pk->value, conn->server_setup_ctx->pkA_len); + + conn->server_setup_ctx->M1_len = proof->size; // 64 + conn->server_setup_ctx->M1 = malloc(conn->server_setup_ctx->M1_len); + memcpy(conn->server_setup_ctx->M1, proof->value, conn->server_setup_ctx->M1_len); + + conn->server_setup_ctx->verifier = srp_verifier_new( + SRP_SHA512, SRP_NG_CUSTOM, USERNAME, conn->server_setup_ctx->salt, + conn->server_setup_ctx->salt_len, conn->server_setup_ctx->v, + conn->server_setup_ctx->v_len, conn->server_setup_ctx->pkA, + conn->server_setup_ctx->pkA_len, conn->server_setup_ctx->b, conn->server_setup_ctx->b_len, + conn->server_setup_ctx->pkB, conn->server_setup_ctx->pkB_len, nl_hex, gl_hex); + if (conn->server_setup_ctx->verifier != NULL) { + if (!conn->server_setup_ctx->verifier) { + die("Error verifier"); + } + + conn->server_setup_ctx->M2_len = 64; // 512 bit hash + srp_verifier_verify_session(conn->server_setup_ctx->verifier, conn->server_setup_ctx->M1, + &conn->server_setup_ctx->M2); + if (conn->server_setup_ctx->M2 != NULL) { + + key = srp_verifier_get_session_key(conn->server_setup_ctx->verifier, &key_len); + + conn->control_cipher_context.cipher_context = + pair_cipher_new(PAIR_SERVER_HOMEKIT_TRANSIENT, 2, key, key_len); + conn->control_cipher_context.fd = conn->fd; + conn->control_cipher_context.input_plaintext_buffer = NULL; + conn->control_cipher_context.input_plaintext_buffer_toq = NULL; + conn->control_cipher_context.input_plaintext_buffer_bytes_occupied = 0; + conn->control_cipher_context.active = 0; + payload = pair_tlv_new(); + + pair_tlv_add_value(payload, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG04].state, + sizeof(pair_keys_map[PAIR_SETUP_MSG04].state)); + pair_tlv_add_value(payload, TLVType_Proof, conn->server_setup_ctx->M2, + conn->server_setup_ctx->M2_len); // 384 + + char *body = malloc(RESPONSE_BUFSIZE); + size_t body_len; + body_len = RESPONSE_BUFSIZE; + + ret = pair_tlv_format(payload, (uint8_t *)body, &body_len); + if (ret < 0) + debug(1, "Can't convert response to a binary stream"); + pair_tlv_free(payload); + + // free the verifier created by srp_verifier_new() + if (conn->server_setup_ctx->verifier != NULL) { + srp_verifier_delete(conn->server_setup_ctx->verifier); + } + + // free mallocs made here for pkA and M1 + if (conn->server_setup_ctx->pkA != NULL) + free((void *)conn->server_setup_ctx->pkA); + if (conn->server_setup_ctx->M1 != NULL) + free((void *)conn->server_setup_ctx->M1); + + // free mallocs made by srp_create_salted_verification_key() + if (conn->server_setup_ctx->salt != NULL) + free((void *)conn->server_setup_ctx->salt); + if (conn->server_setup_ctx->v != NULL) + free((void *)conn->server_setup_ctx->v); + + // free mallocs made by srp_verifier_get_B() + if (conn->server_setup_ctx->b != NULL) + free((void *)conn->server_setup_ctx->b); + // if (conn->server_setup_ctx->pkB != NULL) + // free((void *)conn->server_setup_ctx->pkB); + + resp->content = body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-setup part 2 response", resp); + resp->respcode = 200; // it all worked out okay + } else { + debug(1, "Error M2"); + resp->respcode = 451; // 451 is "Parameter not understood" + } + } else { + debug(1, "Error verifier"); + resp->respcode = 451; + } + } else { + debug(1, "Error pkA ver"); + resp->respcode = 451; + } + } else { + debug(1, "don't recognise pair setup message"); + } + pair_tlv_free(values); +} + +void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { + + /* Fairplay magic */ + static uint8_t server_fp_reply1[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x00\x0f\x9f\x3f\x9e\x0a" + "\x25\x21\xdb\xdf\x31\x2a\xb2\xbf\xb2\x9e\x8d\x23\x2b\x63\x76\xa8\xc8\x18\x70" + "\x1d\x22\xae\x93\xd8\x27\x37\xfe\xaf\x9d\xb4\xfd\xf4\x1c\x2d\xba\x9d\x1f\x49" + "\xca\xaa\xbf\x65\x91\xac\x1f\x7b\xc6\xf7\xe0\x66\x3d\x21\xaf\xe0\x15\x65\x95" + "\x3e\xab\x81\xf4\x18\xce\xed\x09\x5a\xdb\x7c\x3d\x0e\x25\x49\x09\xa7\x98\x31" + "\xd4\x9c\x39\x82\x97\x34\x34\xfa\xcb\x42\xc6\x3a\x1c\xd9\x11\xa6\xfe\x94\x1a" + "\x8a\x6d\x4a\x74\x3b\x46\xc3\xa7\x64\x9e\x44\xc7\x89\x55\xe4\x9d\x81\x55\x00" + "\x95\x49\xc4\xe2\xf7\xa3\xf6\xd5\xba"; + static uint8_t server_fp_reply2[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x01\xcf\x32\xa2\x57\x14" + "\xb2\x52\x4f\x8a\xa0\xad\x7a\xf1\x64\xe3\x7b\xcf\x44\x24\xe2\x00\x04\x7e\xfc" + "\x0a\xd6\x7a\xfc\xd9\x5d\xed\x1c\x27\x30\xbb\x59\x1b\x96\x2e\xd6\x3a\x9c\x4d" + "\xed\x88\xba\x8f\xc7\x8d\xe6\x4d\x91\xcc\xfd\x5c\x7b\x56\xda\x88\xe3\x1f\x5c" + "\xce\xaf\xc7\x43\x19\x95\xa0\x16\x65\xa5\x4e\x19\x39\xd2\x5b\x94\xdb\x64\xb9" + "\xe4\x5d\x8d\x06\x3e\x1e\x6a\xf0\x7e\x96\x56\x16\x2b\x0e\xfa\x40\x42\x75\xea" + "\x5a\x44\xd9\x59\x1c\x72\x56\xb9\xfb\xe6\x51\x38\x98\xb8\x02\x27\x72\x19\x88" + "\x57\x16\x50\x94\x2a\xd9\x46\x68\x8a"; + static uint8_t server_fp_reply3[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x02\xc1\x69\xa3\x52\xee" + "\xed\x35\xb1\x8c\xdd\x9c\x58\xd6\x4f\x16\xc1\x51\x9a\x89\xeb\x53\x17\xbd\x0d" + "\x43\x36\xcd\x68\xf6\x38\xff\x9d\x01\x6a\x5b\x52\xb7\xfa\x92\x16\xb2\xb6\x54" + "\x82\xc7\x84\x44\x11\x81\x21\xa2\xc7\xfe\xd8\x3d\xb7\x11\x9e\x91\x82\xaa\xd7" + "\xd1\x8c\x70\x63\xe2\xa4\x57\x55\x59\x10\xaf\x9e\x0e\xfc\x76\x34\x7d\x16\x40" + "\x43\x80\x7f\x58\x1e\xe4\xfb\xe4\x2c\xa9\xde\xdc\x1b\x5e\xb2\xa3\xaa\x3d\x2e" + "\xcd\x59\xe7\xee\xe7\x0b\x36\x29\xf2\x2a\xfd\x16\x1d\x87\x73\x53\xdd\xb9\x9a" + "\xdc\x8e\x07\x00\x6e\x56\xf8\x50\xce"; + static uint8_t server_fp_reply4[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x03\x90\x01\xe1\x72\x7e" + "\x0f\x57\xf9\xf5\x88\x0d\xb1\x04\xa6\x25\x7a\x23\xf5\xcf\xff\x1a\xbb\xe1\xe9" + "\x30\x45\x25\x1a\xfb\x97\xeb\x9f\xc0\x01\x1e\xbe\x0f\x3a\x81\xdf\x5b\x69\x1d" + "\x76\xac\xb2\xf7\xa5\xc7\x08\xe3\xd3\x28\xf5\x6b\xb3\x9d\xbd\xe5\xf2\x9c\x8a" + "\x17\xf4\x81\x48\x7e\x3a\xe8\x63\xc6\x78\x32\x54\x22\xe6\xf7\x8e\x16\x6d\x18" + "\xaa\x7f\xd6\x36\x25\x8b\xce\x28\x72\x6f\x66\x1f\x73\x88\x93\xce\x44\x31\x1e" + "\x4b\xe6\xc0\x53\x51\x93\xe5\xef\x72\xe8\x68\x62\x33\x72\x9c\x22\x7d\x82\x0c" + "\x99\x94\x45\xd8\x92\x46\xc8\xc3\x59"; + + static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; + + resp->respcode = 200; // assume it's not handled + + // uint8_t *out; + // size_t out_len; + int version_pos = 4; + int mode_pos = 14; + int type_pos = 5; + int seq_pos = 6; + int setup_message_type = 1; + int setup1_message_seq = 1; + int setup2_message_seq = 3; + int setup2_suffix_len = 20; + // int ret; + + // response and len are dummy values and can be ignored + + // debug(1, "Version: %02x, mode: %02x, type: %02x, seq: %02x", req->content[version_pos], + // req->content[mode_pos], req->content[type_pos], req->content[seq_pos]); + + if (req->content[version_pos] != 3 || req->content[type_pos] != setup_message_type) { + debug(1, "Unsupported FP version."); + } + + char *response = NULL; + size_t len = 0; + + if (req->content[seq_pos] == setup1_message_seq) { + // All replies are the same length. -1 to account for the NUL byte at the end. + len = sizeof(server_fp_reply1) - 1; + + if (req->content[mode_pos] == 0) + response = memdup(server_fp_reply1, len); + if (req->content[mode_pos] == 1) + response = memdup(server_fp_reply2, len); + if (req->content[mode_pos] == 2) + response = memdup(server_fp_reply3, len); + if (req->content[mode_pos] == 3) + response = memdup(server_fp_reply4, len); + + } else if (req->content[seq_pos] == setup2_message_seq) { + // -1 to account for the NUL byte at the end. + len = sizeof(server_fp_header) - 1 + setup2_suffix_len; + response = malloc(len); + if (response) { + memcpy(response, server_fp_header, sizeof(server_fp_header) - 1); + memcpy(response + sizeof(server_fp_header) - 1, + req->content + req->contentlength - setup2_suffix_len, setup2_suffix_len); + } + } + + if (response == NULL) { + debug(1, "Cannot create a response."); + } + + resp->content = response; // these will be freed when the data is sent + resp->contentlength = len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); +} + +void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + resp->respcode = 200; + if (strcmp(req->path, "/pair-setup") == 0) { + handle_pair_setup(conn, req, resp); + } else if (strcmp(req->path, "/fp-setup") == 0) { + handle_fp_setup(conn, req, resp); + } else { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, "POST request", req); + debug(2, "Unhandled POST, path \"%s\".", req->path); + } +} + +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; + + 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); + } + + if (conn->ap2_timing_peer_list_message != NULL) + free(conn->ap2_timing_peer_list_message); + conn->ap2_timing_peer_list_message = strdup(timing_list_message); + // if this rtsp thread is playing... + if (try_to_hold_play_lock(conn) == 0) { + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + release_hold_on_play_lock(conn); + } + } + plist_free(addresses_array); + resp->respcode = 200; +} +#endif + +#ifndef CONFIG_AIRPLAY_2 void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { + debug_log_rtsp_message(2, "OPTIONS request", req); debug(3, "Connection %d: OPTIONS", conn->connection_number); resp->respcode = 200; msg_add_header(resp, "Public", @@ -1001,10 +2076,135 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * "PAUSE, FLUSH, TEARDOWN, " "OPTIONS, GET_PARAMETER, SET_PARAMETER"); } +#endif + +#ifdef CONFIG_AIRPLAY_2 + +void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "OPTIONS request", req); + debug(3, "Connection %d: OPTIONS", conn->connection_number); + resp->respcode = 200; + msg_add_header(resp, "Public", + "ANNOUNCE, SETUP, RECORD, " + "PAUSE, FLUSH, TEARDOWN, " + "OPTIONS, GET_PARAMETER, SET_PARAMETER, GET"); +} void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { debug(2, "Connection %d: TEARDOWN", conn->connection_number); + + // debug_log_rtsp_message(1, "TEARDOWN: ", req); + // if (have_player(conn)) { + resp->respcode = 200; + msg_add_header(resp, "Connection", "close"); + + plist_t messagePlist = plist_from_rtsp_content(req); + if (messagePlist != NULL) { + // now see if the incoming plist contains a "streams" array + plist_t streams = plist_dict_get_item(messagePlist, "streams"); + + if (streams) { + // we are being asked to close a stream + // at present, we just close this one + debug(2, + "Connection %d: A \"streams\" array has been found -- stop the player, stop and delete " + "the control " + "and audio threads and close the ports", + conn->connection_number); + + // get stream[0] + plist_t stream0 = plist_array_get_item(streams, 0); + + plist_t item = plist_dict_get_item(stream0, "type"); + uint64_t item_value; + plist_get_uint_val(item, &item_value); + + switch (item_value) { + case 96: { + release_play_lock(conn); + debug(2, "Connection %d: TEARDOWN Delete Realtime Audio Stream Thread", + conn->connection_number); + pthread_cancel(conn->rtp_realtime_audio_thread); + pthread_join(conn->rtp_realtime_audio_thread, NULL); + if (conn->realtime_audio_socket) + close(conn->realtime_audio_socket); + } break; + case 103: { + release_play_lock(conn); // shoulda been released when stopped... + debug(2, "Connection %d: TEARDOWN Delete Buffered Audio Stream Thread", + conn->connection_number); + pthread_cancel(conn->rtp_buffered_audio_thread); + pthread_join(conn->rtp_buffered_audio_thread, NULL); + if (conn->buffered_audio_socket) + close(conn->buffered_audio_socket); + } break; + default: {} + } + + debug(2, "Connection %d: TEARDOWN Close Control Thread", conn->connection_number); + pthread_cancel(conn->rtp_ap2_control_thread); + pthread_join(conn->rtp_ap2_control_thread, NULL); + + debug(2, "Connection %d: TEARDOWN Close Control Socket.", conn->connection_number); + if (conn->ap2_control_socket) { + close(conn->ap2_control_socket); + conn->ap2_remote_control_socket_addr_length = + 0; // indicates to the control receiver thread that the socket address need to be + // recreated. + } + + debug(2, "Connection %d: TEARDOWN Stop the Player.", conn->connection_number); + player_stop(conn); + + if (conn->session_key) { + free(conn->session_key); + conn->session_key = NULL; + } + plist_free(streams); + + release_play_lock(conn); + + // if (ptp_shm_interface_close() != 0) + // warn("Error closing the nqptp interface."); + + debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); + } else { + // we are being asked to disconnect + debug(2, "Connection %d: TEARDOWN No \"streams\" array has been found.", + conn->connection_number); + debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); + pthread_cancel(conn->rtp_event_thread); + pthread_join(conn->rtp_event_thread, NULL); + debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); + if (conn->event_socket) + close(conn->event_socket); + + if (conn->client_setup_plist) { + plist_free(conn->client_setup_plist); + conn->client_setup_plist = NULL; + } + debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); + } + //} else { + // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", + // conn->connection_number); + // resp->respcode = 451; + + plist_free(messagePlist); + resp->respcode = 200; + } else { + debug(1, "Connection %d: missing plist!", conn->connection_number); + resp->respcode = 451; // don't know what to do here + } +} + +#else +void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "TEARDOWN request", req); + debug(2, "Connection %d: TEARDOWN", conn->connection_number); if (have_play_lock(conn)) { resp->respcode = 200; msg_add_header(resp, "Connection", "close"); @@ -1021,46 +2221,342 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message resp->respcode = 451; } } +#endif void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + // TODO -- don't know what this is for in AP2 + debug_log_rtsp_message(2, "FLUSH request", req); debug(3, "Connection %d: FLUSH", conn->connection_number); - if (have_play_lock(conn)) { - char *p = NULL; - uint32_t rtptime = 0; - char *hdr = msg_get_header(req, "RTP-Info"); + char *p = NULL; + uint32_t rtptime = 0; + char *hdr = msg_get_header(req, "RTP-Info"); - if (hdr) { - // debug(1,"FLUSH message received: \"%s\".",hdr); - // get the rtp timestamp - p = strstr(hdr, "rtptime="); - if (p) { - p = strchr(p, '='); - if (p) - rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 - } + if (hdr) { + // debug(1,"FLUSH message received: \"%s\".",hdr); + // get the rtp timestamp + p = strstr(hdr, "rtptime="); + if (p) { + p = strchr(p, '='); + if (p) + rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 } - // debug(1,"RTSP Flush Requested: %u.",rtptime); - - // the following is now done better by the player_flush routine as a 'pfls' - /* - #ifdef CONFIG_METADATA - if (p) - send_metadata('ssnc', 'flsr', p + 1, strlen(p + 1), req, 1); - else - send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); - #endif - */ - + } + // debug(1,"RTSP Flush Requested: %u.",rtptime); + if (have_play_lock(conn)) { +#ifdef CONFIG_METADATA + if (p) + send_metadata('ssnc', 'flsr', p + 1, strlen(p + 1), req, 1); + else + send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); +#endif player_flush(rtptime, conn); // will not crash even it there is no player thread. resp->respcode = 200; } else { - warn("Connection %d FLUSH received without having the player (no ANNOUNCE?)", - conn->connection_number); + warn("Connection %d FLUSH %u received without having the player (no ANNOUNCE?)", + conn->connection_number, rtptime); resp->respcode = 451; + // #ifdef CONFIG_AIRPLAY_2 + // debug(1, "Connection %d FLUSH %u received without having the player but do it anyway...", + // conn->connection_number, rtptime); + // player_flush(rtptime, conn); // will not crash even it there is no player thread. + // resp->respcode = 200; + // #endif } } +#ifdef CONFIG_AIRPLAY_2 +void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + int err; + resp->close_on_response = 0; + resp->suppress_crlf = 0; + debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); + + // we need to get the timing peer interfaces. + // I'm guessing they are all this device's adresses that are on the same subnets as + // the timing peer info in the accompanying plist + + plist_t messagePlist = plist_from_rtsp_content(req); + plist_t setupResponsePlist = plist_new_dict(); + + // now see if the incoming plist contains a "streams" array + plist_t streams = plist_dict_get_item(messagePlist, "streams"); + + if (streams) { + debug(2, + "SETUP on Connection %d: A \"streams\" array has been found -- create control and audio " + "threads and ports", + conn->connection_number); + // open a player) + + // if (ptp_shm_interface_open() != 0) + // warn("Error opening the nqptp clock daemon!"); + + // get stream[0] + plist_t stream0 = plist_array_get_item(streams, 0); + + plist_t streams_array = plist_new_array(); // to hold the ports and stuff + plist_t stream0dict = plist_new_dict(); + // more stuff + // set up a UDP control stream and thread and a UDP or TCP audio stream and thread + + // bind a new UDP port and get a socket + conn->local_ap2_control_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_ap2_control_port, + &conn->ap2_control_socket); + if (err) { + die("Error %d: could not find a UDP port to use as an ap2_control port", err); + } + + pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); + + // get the session key + + plist_t item = plist_dict_get_item(stream0, "shk"); // session key + uint64_t item_value = 0; + plist_get_data_val(item, (char **)&conn->session_key, &item_value); + + // now, get the type of the stream. + item = plist_dict_get_item(stream0, "type"); + item_value = 0; + plist_get_uint_val(item, &item_value); + + switch (item_value) { + case 96: { + debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); + get_play_lock(conn); + + // bind a new UDP port and get a socket + conn->local_realtime_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_realtime_audio_port, + &conn->realtime_audio_socket); + if (err) { + die("Error %d: could not find a UDP port to use as a realtime_audio port", err); + } + + pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); + plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_realtime_audio_port)); + + conn->stream.type = ast_apple_lossless; + debug(3, "An ALAC stream has been detected."); + + // Set reasonable connection defaults + conn->stream.fmtp[0] = 96; + conn->stream.fmtp[1] = 352; + conn->stream.fmtp[2] = 0; + conn->stream.fmtp[3] = 16; + conn->stream.fmtp[4] = 40; + conn->stream.fmtp[5] = 10; + conn->stream.fmtp[6] = 14; + conn->stream.fmtp[7] = 2; + conn->stream.fmtp[8] = 255; + conn->stream.fmtp[9] = 0; + conn->stream.fmtp[10] = 0; + conn->stream.fmtp[11] = 44100; + + // set the parameters of the player (as distinct from the parameters of the decoder -- + // that's done later). + conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. + conn->input_rate = conn->stream.fmtp[11]; + conn->input_num_channels = conn->stream.fmtp[7]; + conn->input_bit_depth = conn->stream.fmtp[3]; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + debug(1, "Realtime Stream Play"); + if (conn->ap2_timing_peer_list_message) + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + else + debug(1, "No timing peer list!"); + + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + case 103: { + debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); + debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); + // get needed stuff + + // bind a new TCP port and get a socket + conn->local_buffered_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_buffered_audio_port, + &conn->buffered_audio_socket); + if (err) { + die("Error %d: could not find a TCP port to use as a buffered_audio port", err); + } + + pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); + plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_buffered_audio_port)); + plist_dict_set_item(stream0dict, "audioBufferSize", + plist_new_uint(conn->ap2_audio_buffer_size)); + + // hack. + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + default: + debug(1, "Unhandled stream type %" PRIu64 ".", item_value); + } + + plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); + + plist_array_append_item(streams_array, stream0dict); + plist_dict_set_item(setupResponsePlist, "streams", streams_array); + + } else { + debug(2, + "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " + "and open a TCP port.", + conn->connection_number); + // first, get and retain the incoming plist. + if (conn->client_setup_plist) { + plist_free(conn->client_setup_plist); + } + conn->client_setup_plist = messagePlist; + plist_t addresses = plist_new_array(); // to hold the device's peer interfaces + + // now determine which of the present device's IP numbers are in the + // same subnet as any IP numbers in the timing peer info + // provided by the client + + plist_t timing_peer_info = plist_dict_get_item(conn->client_setup_plist, "timingPeerInfo"); + if (timing_peer_info) { + plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); + if (addresses_array) { + // iterate through the array of items + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + + uint32_t item; + struct ifaddrs *addrs, *iap; + getifaddrs(&addrs); + 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, "Timing peer: %s", ip_address); + // find its family and convert it into an IPv4/6 address + + struct sockaddr_storage peer_address; + struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; + struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; + + if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == 1) { // is an IPv6 address... + peer_address.ss_family = AF_INET6; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET6)) { + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); + struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); + unsigned int i; + int different = 0; + for (i = 0; ((i < 16) && (different == 0)); i++) { + unsigned char host_byte = + (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + unsigned char peer_byte = + (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + if (host_byte != peer_byte) + different = 1; + } + + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); + if (!different) { + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { + peer_address.ss_family = AF_INET; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET)) { + struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); + struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); + if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == + (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else { + debug(1, "Don't recognise this as an IP address."); + } + free(ip_address); + } + freeifaddrs(addrs); + } else { + debug(1, "No timingPeerInfo addresses in the array."); + } + } else { + debug(1, "Can't find timingPeerInfo addresses"); + } + } else { + debug(1, "Can't find timingPeerInfo"); + } + + // make up the timing peer info list part of the response... + // debug(1,"Create timingPeerInfoPlist"); + plist_t timingPeerInfoPlist = plist_new_dict(); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); + plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); + plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); + // get a port to use as an event port + + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + + int err = + bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_event_port, &conn->event_socket); + if (err) { + die("Error %d: could not find a TCP port to use as an event port", err); + } + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + } + + plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); + plist_free(setupResponsePlist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, " SETUP response", resp); + resp->respcode = 200; // it all worked out okay + // } else { + // resp->respcode = 453; + // debug(1, "Connection %d: SETUP failed because another connection is already playing.", + // conn->connection_number); + // } +} +#endif + void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: SETUP", conn->connection_number); resp->respcode = 451; // invalid arguments -- expect them @@ -1172,10 +2668,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } if (resp->respcode != 200) { debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number); - debug_mutex_lock(&playing_conn_lock, 1000000, 3); - if (playing_conn == conn) // if we have the player - playing_conn = NULL; // let it go - debug_mutex_unlock(&playing_conn_lock, 3); + release_play_lock(conn); } } else { @@ -2294,10 +3787,7 @@ out: if (resp->respcode != 200 && resp->respcode != 453) { debug(1, "Connection %d: Error in handling ANNOUNCE. Unlocking the play lock.", conn->connection_number); - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - if (playing_conn == conn) // if we managed to acquire it - playing_conn = NULL; // let it go - debug_mutex_unlock(&playing_conn_lock, 3); + release_play_lock(conn); } } @@ -2308,10 +3798,26 @@ static struct method_handler { {"ANNOUNCE", handle_announce}, {"FLUSH", handle_flush}, {"TEARDOWN", handle_teardown}, +#ifdef CONFIG_AIRPLAY_2 + {"SETUP", handle_setup_2}, +#else {"SETUP", handle_setup}, +#endif {"GET_PARAMETER", handle_get_parameter}, {"SET_PARAMETER", handle_set_parameter}, +#ifdef CONFIG_AIRPLAY_2 + {"RECORD", handle_record_2}, +#else {"RECORD", handle_record}, +#endif +#ifdef CONFIG_AIRPLAY_2 + + {"GET", handle_get}, + {"POST", handle_post}, + {"SETPEERS", handle_setpeers}, + {"SETRATEANCHORTI", handle_setrateanchori}, + {"FLUSHBUFFERED", handle_flushbuffered}, +#endif {NULL, NULL}}; static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { @@ -2578,7 +4084,8 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { if (conn->player_thread) player_stop(conn); - debug(3, "Closing timing, control and audio sockets..."); + debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", + conn->connection_number); if (conn->control_socket) close(conn->control_socket); if (conn->timing_socket) @@ -2587,14 +4094,27 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { close(conn->audio_socket); if (conn->fd > 0) { - debug(3, "Connection %d: closing fd %d.", conn->connection_number, conn->fd); + debug(3, "Connection %d terminating: closing fd %d.", conn->connection_number, conn->fd); close(conn->fd); - debug(3, "Connection %d: closed fd %d.", conn->connection_number, conn->fd); + debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); } if (conn->auth_nonce) { free(conn->auth_nonce); conn->auth_nonce = NULL; } + +#ifdef CONFIG_AIRPLAY_2 + if (conn->client_setup_plist) { + plist_free(conn->client_setup_plist); + conn->client_setup_plist = NULL; + } + + // free the cipher context, if allocated + if (conn->control_cipher_context.cipher_context) + free(conn->control_cipher_context.cipher_context); + +#endif + rtp_terminate(conn); if (conn->dacp_id) { @@ -2631,14 +4151,14 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { pthread_mutex_destroy(&conn->watchdog_mutex); debug(3, "Connection %d: Checking play lock.", conn->connection_number); - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - if (playing_conn == conn) { // if it's ours - debug(3, "Connection %d: Unlocking play lock.", conn->connection_number); - playing_conn = NULL; // let it go - } - debug_mutex_unlock(&playing_conn_lock, 3); + release_play_lock(conn); - debug(2, "Connection %d: terminated.", conn->connection_number); + #ifdef CONFIG_AIRPLAY_2 + if (conn->server_setup_ctx) + free(conn->server_setup_ctx); + #endif + + debug(1, "Connection %d: terminated.", conn->connection_number); conn->running = 0; pthread_setcancelstate(oldState, NULL); } @@ -2681,8 +4201,22 @@ static void *rtsp_conversation_thread_func(void *pconn) { int rtsp_read_request_attempt_count = 1; // 1 means exit immediately rtsp_message *req, *resp; +#ifdef CONFIG_AIRPLAY_2 + conn->ap2_audio_buffer_size = 1024 * 1024 * 8; + conn->server_setup_ctx = + (struct verifier_setup_context *)malloc(sizeof(struct verifier_setup_context)); + if (conn->server_setup_ctx != NULL) + memset(conn->server_setup_ctx, 0, sizeof(struct verifier_setup_context)); +#endif + while (conn->stop == 0) { int debug_level = 3; // for printing the request and response +#ifdef CONFIG_AIRPLAY_2 + // transactions should be begin to be encrypted after a read/response cycle has + // been completed + conn->control_cipher_context.active = 1; + +#endif reply = rtsp_read_request(conn, &req); if (reply == rtsp_read_request_response_ok) { pthread_cleanup_push(msg_cleanup_function, (void *)&req); @@ -2702,8 +4236,12 @@ static void *rtsp_conversation_thread_func(void *pconn) { hdr = msg_get_header(req, "CSeq"); if (hdr) msg_add_header(resp, "CSeq", hdr); - // msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog"); + // msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog"); +#ifdef CONFIG_AIRPLAY_2 + msg_add_header(resp, "Server", "AirTunes/366.0"); +#else msg_add_header(resp, "Server", "AirTunes/105.1"); +#endif if ((conn->authorized == 1) || (rtsp_auth(&conn->auth_nonce, req, resp)) == 0) { conn->authorized = 1; // it must have been authorized or didn't need a password @@ -2717,7 +4255,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { } } if (method_selected == 0) { - debug(3, "Connection %d: Unrecognised and unhandled rtsp request \"%s\".", + debug(1, "Connection %d: Unrecognised and unhandled rtsp request \"%s\".", conn->connection_number, req->method); int y = req->contentlength; @@ -2734,7 +4272,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { obfp += 2; }; *obfp = 0; - debug(3, "Content: \"%s\".", obf); + debug(1, "Content: \"%s\".", obf); } } } @@ -2742,7 +4280,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { debug_print_msg_headers(debug_level, resp); if (conn->stop == 0) { - int err = msg_write_response(conn->fd, resp); + int err = msg_write_response(conn, resp); if (err) { debug(1, "Connection %d: Unable to write an RTSP message response. Terminating the " @@ -2817,6 +4355,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { } pthread_cleanup_pop(1); pthread_exit(NULL); + debug(1, "Connection %d: RTSP thread exit.", conn->connection_number); } /* @@ -2845,8 +4384,14 @@ void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { cancel_all_RTSP_threads(); int *sockfd = (int *)arg; mdns_unregister(); - if (sockfd) + if (sockfd) { + int i; + for (i = 1; i <= sockfd[0]; i++) { + debug(2, "closing socket %d.", sockfd[i]); + close(sockfd[i]); + } free(sockfd); + } pthread_setcancelstate(oldState, NULL); } @@ -2881,63 +4426,67 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { int yes = 1; // Handle socket open failures if protocol unavailable (or IPV6 not handled) - if (fd == -1) { - // debug(1, "Failed to get socket: fam=%d, %s\n", p->ai_family, - // strerror(errno)); - continue; - } - // Set the RTSP socket to close on exec() of child processes - // otherwise background run_this_before_play_begins or run_this_after_play_ends commands - // that are sleeping prevent the daemon from being restarted because - // the listening RTSP port is still in use. - // See: https://github.com/mikebrady/shairport-sync/issues/329 - fcntl(fd, F_SETFD, FD_CLOEXEC); - ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + if (fd != -1) { + // Set the RTSP socket to close on exec() of child processes + // otherwise background run_this_before_play_begins or run_this_after_play_ends commands + // that are sleeping prevent the daemon from being restarted because + // the listening RTSP port is still in use. + // See: https://github.com/mikebrady/shairport-sync/issues/329 + fcntl(fd, F_SETFD, FD_CLOEXEC); + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - struct timeval tv; - tv.tv_sec = 3; // three seconds write timeout - tv.tv_usec = 0; - if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) - debug(1, "Error %d setting send timeout for rtsp writeback.", errno); - - if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { - tv.tv_sec = config.timeout; // 120 seconds read timeout by default. + struct timeval tv; + tv.tv_sec = 3; // three seconds write timeout tv.tv_usec = 0; - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) - debug(1, "Error %d setting read timeout for rtsp connection.", errno); - } + if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) + debug(1, "Error %d setting send timeout for rtsp writeback.", errno); + + if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { + tv.tv_sec = config.timeout; // 120 seconds read timeout by default. + tv.tv_usec = 0; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) + debug(1, "Error %d setting read timeout for rtsp connection.", errno); + } #ifdef IPV6_V6ONLY - // some systems don't support v4 access on v6 sockets, but some do. - // since we need to account for two sockets we might as well - // always. - if (p->ai_family == AF_INET6) { - ret |= setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); - } -#endif - - if (!ret) - ret = bind(fd, p->ai_addr, p->ai_addrlen); - - // one of the address families will fail on some systems that - // report its availability. do not complain. - - if (ret) { - char *family; -#ifdef AF_INET6 + // some systems don't support v4 access on v6 sockets, but some do. + // since we need to account for two sockets we might as well + // always. if (p->ai_family == AF_INET6) { - family = "IPv6"; - } else + ret |= setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); + } #endif - family = "IPv4"; - debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, - strerror(errno)); - continue; + + if (!ret) + ret = bind(fd, p->ai_addr, p->ai_addrlen); + + // one of the address families will fail on some systems that + // report its availability. do not complain. + + if (ret) { + char *family; +#ifdef AF_INET6 + if (p->ai_family == AF_INET6) { + family = "IPv6"; + } else +#endif + family = "IPv4"; + debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, + strerror(errno)); + } else { + listen(fd, 5); + nsock++; + sockfd = realloc(sockfd, (nsock + 1) * sizeof(int)); + sockfd[nsock] = fd; + sockfd[0] = nsock; // the first entry is the number of sockets in the array + } } - listen(fd, 5); - nsock++; - sockfd = realloc(sockfd, nsock * sizeof(int)); - sockfd[nsock - 1] = fd; + /* + listen(fd, 5); + nsock++; + sockfd = realloc(sockfd, nsock * sizeof(int)); + sockfd[nsock - 1] = fd; + */ } freeaddrinfo(info); @@ -2946,7 +4495,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { int maxfd = -1; fd_set fds; FD_ZERO(&fds); - for (i = 0; i < nsock; i++) { + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) { if (sockfd[i] > maxfd) maxfd = sockfd[i]; } @@ -3037,7 +4587,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { tv.tv_sec = 60; tv.tv_usec = 0; - for (i = 0; i < nsock; i++) + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) FD_SET(sockfd[i], &fds); ret = select(maxfd + 1, &fds, 0, 0, &tv); @@ -3050,7 +4601,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { cleanup_threads(); acceptfd = -1; - for (i = 0; i < nsock; i++) { + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) { if (FD_ISSET(sockfd[i], &fds)) { acceptfd = sockfd[i]; break; @@ -3064,61 +4616,54 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { die("Couldn't allocate memory for an rtsp_conn_info record."); memset(conn, 0, sizeof(rtsp_conn_info)); conn->connection_number = RTSP_connection_index++; - socklen_t slen = sizeof(conn->remote); - conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &slen); + socklen_t size_of_reply = sizeof(SOCKADDR); + conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &size_of_reply); if (conn->fd < 0) { debug(1, "Connection %d: New connection on port %d not accepted:", conn->connection_number, config.port); perror("failed to accept connection"); free(conn); } else { - SOCKADDR *local_info = (SOCKADDR *)&conn->local; - socklen_t size_of_reply = sizeof(*local_info); - memset(local_info, 0, sizeof(SOCKADDR)); - if (getsockname(conn->fd, (struct sockaddr *)local_info, &size_of_reply) == 0) { + size_of_reply = sizeof(SOCKADDR); + if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { + // initialise the connection info + void *client_addr = NULL, *self_addr = NULL; + conn->connection_ip_family = conn->local.SAFAMILY; - // IPv4: - if (local_info->SAFAMILY == AF_INET) { - char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string - char remote_ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string - struct sockaddr_in *sa = (struct sockaddr_in *)local_info; - inet_ntop(AF_INET, &(sa->sin_addr), ip4, INET_ADDRSTRLEN); - unsigned short int tport = ntohs(sa->sin_port); - sa = (struct sockaddr_in *)&conn->remote; - inet_ntop(AF_INET, &(sa->sin_addr), remote_ip4, INET_ADDRSTRLEN); - unsigned short int rport = ntohs(sa->sin_port); - debug(2, "Connection %d: new connection from %s:%u to self at %s:%u.", - conn->connection_number, remote_ip4, rport, ip4, tport); - } #ifdef AF_INET6 - if (local_info->SAFAMILY == AF_INET6) { - // IPv6: + if (conn->connection_ip_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&conn->remote; + client_addr = &(sa6->sin6_addr); + conn->client_rtsp_port = ntohs(sa6->sin6_port); - char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string - char remote_ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string - struct sockaddr_in6 *sa6 = - (struct sockaddr_in6 *)local_info; // pretend this is loaded with something - inet_ntop(AF_INET6, &(sa6->sin6_addr), ip6, INET6_ADDRSTRLEN); - u_int16_t tport = ntohs(sa6->sin6_port); - - sa6 = (struct sockaddr_in6 *)&conn->remote; // pretend this is loaded with something - inet_ntop(AF_INET6, &(sa6->sin6_addr), remote_ip6, INET6_ADDRSTRLEN); - u_int16_t rport = ntohs(sa6->sin6_port); - debug(2, "Connection %d: new connection from [%s]:%u to self at [%s]:%u.", - conn->connection_number, remote_ip6, rport, ip6, tport); + sa6 = (struct sockaddr_in6 *)&conn->local; + self_addr = &(sa6->sin6_addr); + conn->self_rtsp_port = ntohs(sa6->sin6_port); + conn->self_scope_id = sa6->sin6_scope_id; } #endif + if (conn->connection_ip_family == AF_INET) { + struct sockaddr_in *sa4 = (struct sockaddr_in *)&conn->remote; + client_addr = &(sa4->sin_addr); + conn->client_rtsp_port = ntohs(sa4->sin_port); + sa4 = (struct sockaddr_in *)&conn->local; + self_addr = &(sa4->sin_addr); + conn->self_rtsp_port = ntohs(sa4->sin_port); + } + + inet_ntop(conn->connection_ip_family, client_addr, conn->client_ip_string, + sizeof(conn->client_ip_string)); + inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, + sizeof(conn->self_ip_string)); + + debug(1, "Connection %d: new 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); } else { debug(1, "Error figuring out Shairport Sync's own IP number."); } - // usleep(500000); - // pthread_t rtsp_conversation_thread; - // conn->thread = rtsp_conversation_thread; - // conn->stop = 0; // record's memory has been zeroed - // conn->authorized = 0; // record's memory has been zeroed - // fcntl(conn->fd, F_SETFL, O_NONBLOCK); ret = pthread_create(&conn->thread, NULL, rtsp_conversation_thread_func, conn); // also acts as a memory barrier @@ -3135,11 +4680,10 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { } while (1); pthread_cleanup_pop(1); // should never happen } else { - die("could not establish a service on port %d -- program terminating. Is another instance of " - "Shairport Sync running on this device?", - config.port); + warn("could not establish a service on port %d -- program terminating. Is another instance of " + "Shairport Sync running on this device?", + config.port); } - // debug(1, "Oops -- fell out of the RTSP select loop"); debug(1, "Oops -- fell out of the RTSP select loop"); pthread_exit(NULL); } From 3a9af591f41956ad3a267a97187821ffe8a4eb6b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 12:40:52 +0100 Subject: [PATCH 028/878] Compiles in AirPlay2 and AirPlay1 modes. Runs in AirPlay1 mode. Not tested in AirPlay2 mode. --- player.h | 2 ++ rtp.h | 11 +++++++++++ rtsp.c | 7 +------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/player.h b/player.h index 6c88a1c8..849860fd 100644 --- a/player.h +++ b/player.h @@ -287,6 +287,8 @@ typedef struct { uint64_t type; uint64_t networkTimeTimelineID; // the clock ID used by the player + char *ap2_timing_peer_list_message; + #endif // used as the initials values for calculating the rate at which the source thinks it's sending diff --git a/rtp.h b/rtp.h index 4d5f778f..d0a0ed01 100644 --- a/rtp.h +++ b/rtp.h @@ -24,4 +24,15 @@ int have_timestamp_timing_information(rtsp_conn_info *conn); int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn); int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn); +#ifdef CONFIG_AIRPLAY_2 +void *rtp_event_receiver(void *arg); +void *rtp_ap2_control_receiver(void *arg); +void *rtp_realtime_audio_receiver(void *arg); +void *rtp_buffered_audio_processor(void *arg); +void *rtp_ap2_timing_receiver(void *arg); +void *rtp_ap2_general_message_timing_receiver(void *arg); +void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, + uint64_t networktime); +#endif + #endif // _RTP_H diff --git a/rtsp.c b/rtsp.c index 327f1770..f107420a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -150,7 +150,7 @@ typedef struct { char *name[16]; char *value[16]; - int contentlength; + uint32_t contentlength; char *content; // for requests @@ -1509,9 +1509,6 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r rtsp_message *resp) { if (rtsp_message_contains_plist(req)) { // it's stage one - resp->close_on_response = 0; - resp->suppress_crlf = 0; - plist_t info_plist = NULL; plist_from_memory(req->content, req->contentlength, &info_plist); @@ -2268,8 +2265,6 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; - resp->close_on_response = 0; - resp->suppress_crlf = 0; debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); // we need to get the timing peer interfaces. From 390eeae3df730ae5bb23ee2956ebaa139b078d5f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:23:31 +0100 Subject: [PATCH 029/878] In AirPlay 2 mode it can be selected by players --- rtsp.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index f107420a..13364690 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1409,16 +1409,18 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { debug(1, "Attempted to write overlong RTSP packet 2"); return -2; } - debug(1, "Content is \"%s\"", resp->content); - memcpy(p, resp->content, resp->contentlength); - pktfree -= resp->contentlength; - p += resp->contentlength; } n = snprintf(p, pktfree, "\r\n"); pktfree -= n; p += n; + if (resp->contentlength) { + memcpy(p, resp->content, resp->contentlength); + pktfree -= resp->contentlength; + p += resp->contentlength; + } + if (pktfree <= 1024) { debug(1, "Attempted to write overlong RTSP packet 3"); return -3; From 91b85a020714f74bd2a3363c7309cfbd39f101b7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:55:11 +0100 Subject: [PATCH 030/878] Working for Realtime Audio Streams --- dacp.c | 10 ++++++---- rtp.c | 49 +++++++++++++++++++++++++++++++++---------------- rtsp.c | 4 ++-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/dacp.c b/dacp.c index 54d1e4c0..906519fd 100644 --- a/dacp.c +++ b/dacp.c @@ -415,10 +415,12 @@ void set_dacp_server_information(rtsp_conn_info *conn) { // This is different to other AirPlay clients // which return immediately with a 403 code if there are no changes. dacp_server.always_use_revision_number_1 = 0; - char *p = strstr(conn->UserAgent, "forked-daapd"); - if ((p != 0) && - (p == conn->UserAgent)) { // must exist and be at the start of the UserAgent string - dacp_server.always_use_revision_number_1 = 1; + if (conn->UserAgent != NULL) { + char *p = strstr(conn->UserAgent, "forked-daapd"); + if ((p != 0) && + (p == conn->UserAgent)) { // must exist and be at the start of the UserAgent string + dacp_server.always_use_revision_number_1 = 1; + } } metadata_hub_modify_prolog(); diff --git a/rtp.c b/rtp.c index 6d4a0a5c..f0f90ed8 100644 --- a/rtp.c +++ b/rtp.c @@ -1007,10 +1007,6 @@ void reset_ntp_anchor_info(rtsp_conn_info *conn) { debug_mutex_unlock(&conn->reference_time_mutex, 3); } -void reset_anchor_info(rtsp_conn_info *conn) { - reset_ntp_anchor_info(conn); -} - int have_ntp_timestamp_timing_information(rtsp_conn_info *conn) { if (conn->anchor_rtptime == 0) return 0; @@ -1018,10 +1014,6 @@ int have_ntp_timestamp_timing_information(rtsp_conn_info *conn) { return 1; } -int have_timestamp_timing_information(rtsp_conn_info *conn) { - return have_ntp_timestamp_timing_information(conn); -} - // set this to zero to use the rates supplied by the sources, which might not always be completely // right... const int use_nominal_rate = 0; // specify whether to use the nominal input rate, usually 44100 fps @@ -1196,14 +1188,6 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { } } -int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { - return frame_to_ntp_local_time(timestamp, time, conn); -} - -int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { - return local_ntp_time_to_frame(time, frame, conn); -} - #ifdef CONFIG_AIRPLAY_2 void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, @@ -2360,5 +2344,38 @@ void *rtp_buffered_audio_processor(void *arg) { pthread_cleanup_pop(1); // do the cleanup. pthread_exit(NULL); } +int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + return frame_to_ptp_local_time(timestamp, time, conn); +} +int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + return local_ptp_time_to_frame(time, frame, conn); +} + +void reset_anchor_info(rtsp_conn_info *conn) { + reset_ptp_anchor_info(conn); +} + +int have_timestamp_timing_information(rtsp_conn_info *conn) { + return have_ptp_timing_information(conn); +} + +#else +int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + return frame_to_ntp_local_time(timestamp, time, conn); +} + +int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + return local_ntp_time_to_frame(time, frame, conn); +} + +void reset_anchor_info(rtsp_conn_info *conn) { + reset_ntp_anchor_info(conn); +} + +int have_timestamp_timing_information(rtsp_conn_info *conn) { + return have_ntp_timestamp_timing_information(conn); +} #endif + + diff --git a/rtsp.c b/rtsp.c index 13364690..8236765b 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2016,10 +2016,10 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } else if (strcmp(req->path, "/fp-setup") == 0) { handle_fp_setup(conn, req, resp); } else { - debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "POST request", req); - debug(2, "Unhandled POST, path \"%s\".", req->path); + debug(3, "Unhandled POST, path \"%s\".", req->path); } } From 9a29fac7c71b8dcdf706903927b9900e19df28ea Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Apr 2021 15:17:11 +0100 Subject: [PATCH 031/878] Initial working SPS4. AirPlay 1 build works and AirPlay 2 build works. Yay. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 36d8bec3..fdeaa139 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0d0], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0d1], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From 61b6b5934d4b4aed2a963c36f9dbb5169dab3d21 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 1 May 2021 11:03:45 +0100 Subject: [PATCH 032/878] check that the clock is valid and reduce the Buffered Audio lead time to 0.25 seconds --- ptp-utilities.c | 16 +++++++++++----- rtp.c | 9 ++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 53dbd4d0..88700522 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -75,11 +75,17 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { struct shm_structure nqptp_data; if (get_nqptp_data(&nqptp_data) == 0) { if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { - if (actual_clock_id != NULL) - *actual_clock_id = nqptp_data.master_clock_id; - if (raw_offset != NULL) - *raw_offset = nqptp_data.local_to_master_time_offset; - response = 0; + // assuming a clock id can not be zero + if (nqptp_data.master_clock_id != 0) { + if (actual_clock_id != NULL) + *actual_clock_id = nqptp_data.master_clock_id; + if (raw_offset != NULL) + *raw_offset = nqptp_data.local_to_master_time_offset; + response = 0; + } else { + debug(1,"clock not valid"); + response = -2; // clock info not valid + } } else { if (failure_message_sent == 0) { warn("This version of Shairport Sync requires an NQPTP with a Shared Memory Interface Version %u, but the installed version is %u. Please install the correct version of NQPTP.", NQPTP_SHM_STRUCTURES_VERSION, nqptp_data.version); diff --git a/rtp.c b/rtp.c index f0f90ed8..59cea3fa 100644 --- a/rtp.c +++ b/rtp.c @@ -1256,8 +1256,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } else if (ptp_status == -1) { debug(3, "don't have the ptp clock interface"); response = -1; + } else if (ptp_status == -2) { + debug(1, "ptp clock not valid"); + response = -1; } else { - debug(3, "ptp clock not valid"); + debug(3, "ptp clock error"); response = -1; } return response; @@ -2040,7 +2043,7 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); if (player_buffer_occupancy > - ((0.5 + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. + ((0.25 + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while @@ -2064,7 +2067,7 @@ void *rtp_buffered_audio_processor(void *arg) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); // ask for 0.5 sec of leadtime - if ((lead_time >= (int64_t)(0.5 * 1000000000)) || (streaming_has_started == 1)) { + if ((lead_time >= (int64_t)(0.25 * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); streaming_has_started = 1; From 5f1cbf6d20661197406eb58a9b6042bb2c5d934b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 1 May 2021 11:49:24 +0100 Subject: [PATCH 033/878] store the latency so that the resend requst timing computations can be done correctly in the realtime streaming mode. --- rtp.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index 59cea3fa..88628ebf 100644 --- a/rtp.c +++ b/rtp.c @@ -1497,6 +1497,8 @@ void *rtp_ap2_control_receiver(void *arg) { // add in the audio_backend_latency_offset; int32_t notified_latency = frame_2 - frame_1; int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + // the actual latency is the notified latency plus the fixed latency + the added latency + if (added_latency < (-(notified_latency + 11035))) debug(1, "the audio_backend_latency_offset is causing a negative latency!"); @@ -1507,7 +1509,9 @@ void *rtp_ap2_control_receiver(void *arg) { frame_1 - 11035 - added_latency; // add the latency in to the anchortime debug_mutex_unlock(&conn->reference_time_mutex, 0); */ - + // this is now only used for calculating when to ask for resends + conn->latency = notified_latency + 11035 + added_latency; + // debug(1,"conn->latency is %d.", conn->latency); set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, remote_packet_time_ns); } break; From a68f28ac2b5a2f4fc99961ddacaf959146fed524 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 1 May 2021 14:09:38 +0100 Subject: [PATCH 034/878] clang format --- audio_alsa.c | 48 +++++++------- player.c | 173 ++++++++++++++++++++++++------------------------ player.h | 28 +++----- ptp-utilities.c | 7 +- rtp.c | 74 ++++++++++----------- rtp.h | 2 +- rtsp.c | 6 +- shairport.c | 28 ++++---- 8 files changed, 183 insertions(+), 183 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 19127d11..d5757bef 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1457,30 +1457,29 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); -/* -// must be 1.1 or later to use snd_pcm_status_get_driver_htstamp -#if SND_LIB_MINOR != 0 - snd_htimestamp_t driver_htstamp; - snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp); - uint64_t driver_htstamp_ns = driver_htstamp.tv_sec; - driver_htstamp_ns = driver_htstamp_ns * 1000000000; - driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec; - debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001); -#endif -*/ + /* + // must be 1.1 or later to use snd_pcm_status_get_driver_htstamp + #if SND_LIB_MINOR != 0 + snd_htimestamp_t driver_htstamp; + snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp); + uint64_t driver_htstamp_ns = driver_htstamp.tv_sec; + driver_htstamp_ns = driver_htstamp_ns * 1000000000; + driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec; + debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001); + #endif + */ *state = snd_pcm_status_get_state(alsa_snd_pcm_status); if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) { - // uint64_t update_timestamp_ns = - // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; + // uint64_t update_timestamp_ns = + // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; uint64_t update_timestamp_ns = update_timestamp.tv_sec; update_timestamp_ns = update_timestamp_ns * 1000000000; update_timestamp_ns = update_timestamp_ns + update_timestamp.tv_nsec; - // if the update_timestamp is zero, we take this to mean that the device doesn't report // interrupt timings. (It could be that it's not a real hardware device.) // so we switch to getting the delay the regular way @@ -1531,7 +1530,6 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, time_now_ns = time_now_ns * 1000000000; time_now_ns = time_now_ns + tn.tv_nsec; - // see if it's stalled if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == delay_temp)) { @@ -1561,18 +1559,22 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, if (ret == 0) { uint64_t delta = time_now_ns - update_timestamp_ns; -// uint64_t frames_played_since_last_interrupt = -// ((uint64_t)config.output_rate * delta) / 1000000000; - - uint64_t frames_played_since_last_interrupt = config.output_rate; - frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta; - frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000; + // uint64_t frames_played_since_last_interrupt = + // ((uint64_t)config.output_rate * delta) / 1000000000; + uint64_t frames_played_since_last_interrupt = config.output_rate; + frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta; + frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000; snd_pcm_sframes_t frames_played_since_last_interrupt_sized = frames_played_since_last_interrupt; - if ((frames_played_since_last_interrupt_sized < 0) || ((uint64_t)frames_played_since_last_interrupt_sized != frames_played_since_last_interrupt)) - debug(1,"overflow resizing frames_played_since_last_interrupt % " PRIx64 " to frames_played_since_last_interrupt %lx.", frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); + if ((frames_played_since_last_interrupt_sized < 0) || + ((uint64_t)frames_played_since_last_interrupt_sized != + frames_played_since_last_interrupt)) + debug(1, + "overflow resizing frames_played_since_last_interrupt % " PRIx64 + " to frames_played_since_last_interrupt %lx.", + frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); delay_temp = delay_temp - frames_played_since_last_interrupt_sized; } *delay = delay_temp; diff --git a/player.c b/player.c index 66637292..6b67ee13 100644 --- a/player.c +++ b/player.c @@ -435,93 +435,93 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta conn->packet_count_since_flush++; conn->time_of_last_audio_packet = time_now; if (conn->connection_state_to_output) { // if we are supposed to be processing these packets - abuf_t *abuf = 0; - if (!conn->ab_synced) { - // if this is the first packet... - debug(3, "syncing to seqno %u.", seqno); - conn->ab_write = seqno; - conn->ab_read = seqno; - conn->ab_synced = 1; - } else if (original_format == 0) { - // if the packet is coming in original format, the sequence number is important - // otherwise, ignore is by setting it equal to the expected sequence number in ab_write - seqno = conn->ab_write; - } - if (conn->ab_write == - seqno) { // if this is the expected packet (which could be the first packet...) - if (conn->input_frame_rate_starting_point_is_valid == 0) { - if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { - conn->frames_inward_measurement_start_time = time_now; - conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; - conn->input_frame_rate_starting_point_is_valid = 1; // valid now - } + abuf_t *abuf = 0; + if (!conn->ab_synced) { + // if this is the first packet... + debug(3, "syncing to seqno %u.", seqno); + conn->ab_write = seqno; + conn->ab_read = seqno; + conn->ab_synced = 1; + } else if (original_format == 0) { + // if the packet is coming in original format, the sequence number is important + // otherwise, ignore is by setting it equal to the expected sequence number in ab_write + seqno = conn->ab_write; + } + if (conn->ab_write == + seqno) { // if this is the expected packet (which could be the first packet...) + if (conn->input_frame_rate_starting_point_is_valid == 0) { + if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { + conn->frames_inward_measurement_start_time = time_now; + conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; + conn->input_frame_rate_starting_point_is_valid = 1; // valid now } - conn->frames_inward_measurement_time = time_now; - conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; - abuf = conn->audio_buffer + BUFIDX(seqno); - conn->ab_write = seqno + 1; // move the write pointer to the next free space - } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected - int32_t gap = seq_diff(conn->ab_write, seqno, conn->ab_read); - if (gap <= 0) - debug(1, "Unexpected gap size: %d.", gap); - int i; - for (i = 0; i < gap; i++) { - abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); - abuf->ready = 0; // to be sure, to be sure - abuf->resend_request_number = 0; - abuf->initialisation_time = - time_now; // this represents when the packet was noticed to be missing - abuf->status = 1 << 0; // signifying missing - abuf->resend_time = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } - // debug(1,"N %d s %u.",seq_diff(ab_write,PREDECESSOR(seqno))+1,ab_write); - abuf = conn->audio_buffer + BUFIDX(seqno); - // rtp_request_resend(ab_write, gap); - // resend_requests++; - conn->ab_write = seqno + 1; - } else if (seq_order(conn->ab_read, seqno, - conn->ab_read)) { // older than expected but not too late - conn->late_packets++; - abuf = conn->audio_buffer + BUFIDX(seqno); - } else { // too late. - conn->too_late_packets++; } - - if (abuf) { - int datalen = conn->max_frames_per_packet; - abuf->initialisation_time = time_now; + conn->frames_inward_measurement_time = time_now; + conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; + abuf = conn->audio_buffer + BUFIDX(seqno); + conn->ab_write = seqno + 1; // move the write pointer to the next free space + } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected + int32_t gap = seq_diff(conn->ab_write, seqno, conn->ab_read); + if (gap <= 0) + debug(1, "Unexpected gap size: %d.", gap); + int i; + for (i = 0; i < gap; i++) { + abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); + abuf->ready = 0; // to be sure, to be sure + abuf->resend_request_number = 0; + abuf->initialisation_time = + time_now; // this represents when the packet was noticed to be missing + abuf->status = 1 << 0; // signifying missing abuf->resend_time = 0; - if ((original_format != 0) && - (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0)) { - abuf->ready = 1; - abuf->status = 0; // signifying that it was received - abuf->length = datalen; - abuf->given_timestamp = actual_timestamp; - abuf->sequence_number = seqno; - } else if (original_format == 0) { - memcpy(abuf->data, data, len * conn->input_bytes_per_frame); - abuf->ready = 1; - abuf->status = 0; // signifying that it was received - abuf->length = len; - abuf->given_timestamp = actual_timestamp; - abuf->sequence_number = seqno; - } else { - debug(1, "Bad audio packet detected and discarded."); - abuf->ready = 0; - abuf->status = 1 << 1; // bad packet, discarded - abuf->resend_request_number = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } + abuf->given_timestamp = 0; + abuf->sequence_number = 0; } + // debug(1,"N %d s %u.",seq_diff(ab_write,PREDECESSOR(seqno))+1,ab_write); + abuf = conn->audio_buffer + BUFIDX(seqno); + // rtp_request_resend(ab_write, gap); + // resend_requests++; + conn->ab_write = seqno + 1; + } else if (seq_order(conn->ab_read, seqno, + conn->ab_read)) { // older than expected but not too late + conn->late_packets++; + abuf = conn->audio_buffer + BUFIDX(seqno); + } else { // too late. + conn->too_late_packets++; + } - int rc = pthread_cond_signal(&conn->flowcontrol); - if (rc) - debug(1, "Error signalling flowcontrol."); + if (abuf) { + int datalen = conn->max_frames_per_packet; + abuf->initialisation_time = time_now; + abuf->resend_time = 0; + if ((original_format != 0) && + (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0)) { + abuf->ready = 1; + abuf->status = 0; // signifying that it was received + abuf->length = datalen; + abuf->given_timestamp = actual_timestamp; + abuf->sequence_number = seqno; + } else if (original_format == 0) { + memcpy(abuf->data, data, len * conn->input_bytes_per_frame); + abuf->ready = 1; + abuf->status = 0; // signifying that it was received + abuf->length = len; + abuf->given_timestamp = actual_timestamp; + abuf->sequence_number = seqno; + } else { + debug(1, "Bad audio packet detected and discarded."); + abuf->ready = 0; + abuf->status = 1 << 1; // bad packet, discarded + abuf->resend_request_number = 0; + abuf->given_timestamp = 0; + abuf->sequence_number = 0; + } + } - // resend checks + int rc = pthread_cond_signal(&conn->flowcontrol); + if (rc) + debug(1, "Error signalling flowcontrol."); + + // resend checks { uint64_t minimum_wait_time = (uint64_t)(config.resend_control_first_check_time * (uint64_t)1000000000); @@ -1050,7 +1050,6 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // The desired latency, typically 88200 frames, will be calculated for in rtp.c, // and any desired backend latency offset included in it there. - uint64_t should_be_time; frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 @@ -1243,9 +1242,11 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // we must enable packets to be released early enough for the // audio buffer to be filled to the desired length - uint32_t buffer_latency_offset = (uint32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); - frame_to_local_time(curframe->given_timestamp - buffer_latency_offset, // this will go modulo 2^32 - &time_to_play, conn); + uint32_t buffer_latency_offset = + (uint32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); + frame_to_local_time(curframe->given_timestamp - + buffer_latency_offset, // this will go modulo 2^32 + &time_to_play, conn); if (local_time_now >= time_to_play) { do_wait = 0; @@ -2984,7 +2985,7 @@ void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { debug(3, "player_flush"); do_flush(timestamp, conn); #ifdef CONFIG_METADATA -// only send a flush metadata message if the first packet has been seen -- it's a bogus message + // only send a flush metadata message if the first packet has been seen -- it's a bogus message // otherwise if (conn->first_packet_timestamp) { debug(2, "pfls"); diff --git a/player.h b/player.h index 849860fd..9a92c0c4 100644 --- a/player.h +++ b/player.h @@ -81,10 +81,7 @@ typedef enum { ast_apple_lossless, } audio_stream_type; -typedef enum { - ts_ntp, - ts_ptp -} timing_source_type; +typedef enum { ts_ntp, ts_ptp } timing_source_type; typedef struct { int encrypted; @@ -105,10 +102,10 @@ typedef struct file_cipher_context { #endif typedef struct { - int connection_number; // for debug ID purposes, nothing else... + int connection_number; // for debug ID purposes, nothing else... timing_source_type type_of_timing; // are we using NTP or PTP? - int resend_interval; // this is really just for debugging - char *UserAgent; // free this on teardown + int resend_interval; // this is really just for debugging + char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency uint32_t latency; // the actual latency used for this play session uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero @@ -117,7 +114,6 @@ typedef struct { // otherwise int software_mute_enabled; // if we don't have a real mute that we can use - int fd; int authorized; // set if a password is required and has been supplied char *auth_nonce; // the session nonce, if needed @@ -216,11 +212,11 @@ typedef struct { char client_ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client uint16_t client_rtsp_port; - char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it - uint16_t self_rtsp_port; // could be one of many, so we need to know it + char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it + uint16_t self_rtsp_port; // could be one of many, so we need to know it - uint32_t self_scope_id; // if it's an ipv6 connection, this will be its scope - short connection_ip_family; // AF_INET / AF_INET6 + uint32_t self_scope_id; // if it's an ipv6 connection, this will be its scope + short connection_ip_family; // AF_INET / AF_INET6 SOCKADDR rtp_client_control_socket; // a socket pointing to the control port of the client SOCKADDR rtp_client_timing_socket; // a socket pointing to the timing port of the client @@ -243,7 +239,6 @@ typedef struct { uint64_t anchor_time; // this is the time according to the clock uint32_t anchor_rtptime; - #ifdef CONFIG_AIRPLAY_2 pthread_t rtp_event_thread; pthread_t rtp_ap2_control_thread; @@ -259,7 +254,6 @@ typedef struct { uint64_t last_anchor_time_of_update; uint64_t last_anchor_clock; - ssize_t ap2_audio_buffer_size; int ap2_flush_requested; uint32_t ap2_flush_rtp_timestamp; @@ -285,7 +279,7 @@ typedef struct { unsigned char *session_key; // needs to be free'd at the end uint64_t frames_packet; uint64_t type; - uint64_t networkTimeTimelineID; // the clock ID used by the player + uint64_t networkTimeTimelineID; // the clock ID used by the player char *ap2_timing_peer_list_message; @@ -357,8 +351,8 @@ void player_volume(double f, rtsp_conn_info *conn); void player_volume_without_notification(double f, rtsp_conn_info *conn); void player_flush(uint32_t timestamp, rtsp_conn_info *conn); void player_full_flush(rtsp_conn_info *conn); -void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, - rtsp_conn_info *conn); +void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, + int len, rtsp_conn_info *conn); int64_t monotonic_timestamp(uint32_t timestamp, rtsp_conn_info *conn); // add an epoch to the timestamp. The monotonic // timestamp guaranteed to start between 2^32 2^33 diff --git a/ptp-utilities.c b/ptp-utilities.c index 88700522..01d32c58 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -83,12 +83,15 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { *raw_offset = nqptp_data.local_to_master_time_offset; response = 0; } else { - debug(1,"clock not valid"); + debug(1, "clock not valid"); response = -2; // clock info not valid } } else { if (failure_message_sent == 0) { - warn("This version of Shairport Sync requires an NQPTP with a Shared Memory Interface Version %u, but the installed version is %u. Please install the correct version of NQPTP.", NQPTP_SHM_STRUCTURES_VERSION, nqptp_data.version); + warn("This version of Shairport Sync requires an NQPTP with a Shared Memory Interface " + "Version %u, but the installed version is %u. Please install the correct version of " + "NQPTP.", + NQPTP_SHM_STRUCTURES_VERSION, nqptp_data.version); failure_message_sent = 1; } } diff --git a/rtp.c b/rtp.c index 88628ebf..35e415a9 100644 --- a/rtp.c +++ b/rtp.c @@ -53,7 +53,6 @@ #include #endif - struct Nvll { char *name; double value; @@ -289,8 +288,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -301,19 +300,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -390,10 +389,15 @@ void *rtp_control_receiver(void *arg) { la, max_frames, conn->latency); } else { - // here we have the latency but it does not yet account for the audio_backend_latency_offset - int32_t latency_offset = (uint32_t)(config.audio_backend_latency_offset * conn->input_rate); + // here we have the latency but it does not yet account for the + // audio_backend_latency_offset + int32_t latency_offset = + (uint32_t)(config.audio_backend_latency_offset * conn->input_rate); int32_t adjusted_latency = latency_offset + (int32_t)la; - if ((adjusted_latency < 0) || (adjusted_latency > (int32_t)(conn->max_frames_per_packet * (BUFFER_FRAMES - config.minimum_free_buffer_headroom)))) + if ((adjusted_latency < 0) || + (adjusted_latency > + (int32_t)(conn->max_frames_per_packet * + (BUFFER_FRAMES - config.minimum_free_buffer_headroom)))) warn("audio_backend_latency_offset out of range -- ignored."); else la = adjusted_latency; @@ -403,9 +407,11 @@ void *rtp_control_receiver(void *arg) { debug(1, "New latency: %" PRIu32 ", sync latency: %" PRIu32 ", minimum latency: %" PRIu32 ", maximum " - "latency: %" PRIu32 ", fixed offset: %" PRIu32 ", audio_backend_latency_offset: %f.", + "latency: %" PRIu32 ", fixed offset: %" PRIu32 + ", audio_backend_latency_offset: %f.", la, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, - conn->maximum_latency, config.fixedLatencyOffset, config.audio_backend_latency_offset); + conn->maximum_latency, config.fixedLatencyOffset, + config.audio_backend_latency_offset); } } } @@ -979,11 +985,11 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport conn->remote_timing_port = tport; conn->local_control_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->control_socket); + conn->self_scope_id, &conn->control_socket); conn->local_timing_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->timing_socket); + conn->self_scope_id, &conn->timing_socket); conn->local_audio_port = bind_UDP_port(conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->audio_socket); + conn->self_scope_id, &conn->audio_socket); debug(3, "listening for audio, control and timing on ports %d, %d, %d.", conn->local_audio_port, conn->local_control_port, conn->local_timing_port); @@ -1050,8 +1056,7 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con return result; } -void set_ntp_anchor_info(rtsp_conn_info *conn, uint32_t rtptime, - uint64_t networktime) { +void set_ntp_anchor_info(rtsp_conn_info *conn, uint32_t rtptime, uint64_t networktime) { conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -1071,14 +1076,14 @@ int frame_to_ntp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * int32_t timestamp_interval = timestamp - conn->anchor_rtptime; int64_t timestamp_interval_time = timestamp_interval; timestamp_interval_time = timestamp_interval_time * time_difference; - timestamp_interval_time = timestamp_interval_time / - frame_difference; // this is the nominal time, based on the - // fps specified between current and - // previous sync frame. - remote_time_of_timestamp = conn->anchor_time + - timestamp_interval_time; // based on the reference timestamp time - // plus the time interval calculated based - // on the specified fps. + timestamp_interval_time = + timestamp_interval_time / frame_difference; // this is the nominal time, based on the + // fps specified between current and + // previous sync frame. + remote_time_of_timestamp = + conn->anchor_time + timestamp_interval_time; // based on the reference timestamp time + // plus the time interval calculated based + // on the specified fps. *time = remote_time_of_timestamp - local_to_remote_time_difference_now(conn); debug_mutex_unlock(&conn->reference_time_mutex, 0); return result; @@ -1191,7 +1196,7 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { #ifdef CONFIG_AIRPLAY_2 void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, - uint64_t networktime) { + uint64_t networktime) { if (conn->anchor_clock != clock_id) debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_remote_info_is_valid = 1; @@ -1207,7 +1212,7 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { } int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, - uint64_t *anchorLocalTime) { + uint64_t *anchorLocalTime) { int response = 0; uint64_t actual_clock_id, actual_offset; @@ -1512,7 +1517,8 @@ void *rtp_ap2_control_receiver(void *arg) { // this is now only used for calculating when to ask for resends conn->latency = notified_latency + 11035 + added_latency; // debug(1,"conn->latency is %d.", conn->latency); - set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, remote_packet_time_ns); + set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, + remote_packet_time_ns); } break; case 0xd6: @@ -2359,9 +2365,7 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { return local_ptp_time_to_frame(time, frame, conn); } -void reset_anchor_info(rtsp_conn_info *conn) { - reset_ptp_anchor_info(conn); -} +void reset_anchor_info(rtsp_conn_info *conn) { reset_ptp_anchor_info(conn); } int have_timestamp_timing_information(rtsp_conn_info *conn) { return have_ptp_timing_information(conn); @@ -2376,13 +2380,9 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { return local_ntp_time_to_frame(time, frame, conn); } -void reset_anchor_info(rtsp_conn_info *conn) { - reset_ntp_anchor_info(conn); -} +void reset_anchor_info(rtsp_conn_info *conn) { reset_ntp_anchor_info(conn); } int have_timestamp_timing_information(rtsp_conn_info *conn) { return have_ntp_timestamp_timing_information(conn); } #endif - - diff --git a/rtp.h b/rtp.h index d0a0ed01..8eff06bb 100644 --- a/rtp.h +++ b/rtp.h @@ -32,7 +32,7 @@ void *rtp_buffered_audio_processor(void *arg); void *rtp_ap2_timing_receiver(void *arg); void *rtp_ap2_general_message_timing_receiver(void *arg); void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, - uint64_t networktime); + uint64_t networktime); #endif #endif // _RTP_H diff --git a/rtsp.c b/rtsp.c index 8236765b..41ddff22 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1665,7 +1665,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, - anchorTimeNanoseconds); + anchorTimeNanoseconds); } item = plist_dict_get_item(messagePlist, "rate"); @@ -4150,10 +4150,10 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Connection %d: Checking play lock.", conn->connection_number); release_play_lock(conn); - #ifdef CONFIG_AIRPLAY_2 +#ifdef CONFIG_AIRPLAY_2 if (conn->server_setup_ctx) free(conn->server_setup_ctx); - #endif +#endif debug(1, "Connection %d: terminated.", conn->connection_number); conn->running = 0; diff --git a/shairport.c b/shairport.c index bdb837d8..2e9dff2a 100644 --- a/shairport.c +++ b/shairport.c @@ -1363,14 +1363,14 @@ void exit_function() { #endif */ #ifdef CONFIG_DBUS_INTERFACE - debug(2, "Stopping D-Bus service"); - stop_dbus_service(); + debug(2, "Stopping D-Bus service"); + stop_dbus_service(); #endif - if (g_main_loop) { - debug(2, "Stopping D-Bus Loop Thread"); - g_main_loop_quit(g_main_loop); - pthread_join(dbus_thread, NULL); - } + if (g_main_loop) { + debug(2, "Stopping D-Bus Loop Thread"); + g_main_loop_quit(g_main_loop); + pthread_join(dbus_thread, NULL); + } #endif #ifdef CONFIG_DACP_CLIENT @@ -1384,11 +1384,11 @@ void exit_function() { #endif #ifdef CONFIG_METADATA - debug(2, "Stopping metadata"); - metadata_stop(); // close down the metadata pipe + debug(2, "Stopping metadata"); + metadata_stop(); // close down the metadata pipe #endif - debug(2, "Deinitialising the audio backend."); - activity_monitor_stop(0); + debug(2, "Deinitialising the audio backend."); + activity_monitor_stop(0); activity_monitor_stop(0); @@ -1398,9 +1398,9 @@ void exit_function() { } #ifdef CONFIG_SOXR - // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down - debug(2, "Waiting for SoXr timecheck to terminate..."); - pthread_join(soxr_time_check_thread, NULL); + // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down + debug(2, "Waiting for SoXr timecheck to terminate..."); + pthread_join(soxr_time_check_thread, NULL); #endif if (conns) From c2940eb23bd3f0065cafa9aafe22a973bf5889d5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 12:49:41 +0100 Subject: [PATCH 035/878] works in AP1-compatible AP2 mode, but resend isn't working. --- player.c | 8 +++++++- player.h | 8 ++++++-- rtp.c | 43 ++++++++++++++++++++++++++++----------- rtsp.c | 62 ++++++++++++++++++++++++++++++++++++++------------------ 4 files changed, 86 insertions(+), 35 deletions(-) diff --git a/player.c b/player.c index 6b67ee13..7d6d026e 100644 --- a/player.c +++ b/player.c @@ -1922,11 +1922,17 @@ void *player_thread_func(void *arg) { } } -#ifndef CONFIG_AIRPLAY_2 +#ifdef CONFIG_AIRPLAY_2 + if (conn->timing_type == ts_ntp) { +#endif + // create and start the timing, control and audio receiver threads pthread_create(&conn->rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn); pthread_create(&conn->rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn); pthread_create(&conn->rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn); + +#ifdef CONFIG_AIRPLAY_2 + } #endif pthread_cleanup_push(player_thread_cleanup_handler, arg); // undo what's been done so far diff --git a/player.h b/player.h index 9a92c0c4..5766f6fc 100644 --- a/player.h +++ b/player.h @@ -81,7 +81,6 @@ typedef enum { ast_apple_lossless, } audio_stream_type; -typedef enum { ts_ntp, ts_ptp } timing_source_type; typedef struct { int encrypted; @@ -91,6 +90,9 @@ typedef struct { } stream_cfg; #ifdef CONFIG_AIRPLAY_2 +typedef enum { ts_ntp, ts_ptp } timing_t; +typedef enum { ap_1, ap_2 } airplay_t; + typedef struct file_cipher_context { struct pair_cipher_context *cipher_context; int active; // can be created during a pair setup but not activated until next read @@ -103,7 +105,6 @@ typedef struct file_cipher_context { typedef struct { int connection_number; // for debug ID purposes, nothing else... - timing_source_type type_of_timing; // are we using NTP or PTP? int resend_interval; // this is really just for debugging char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency @@ -240,6 +241,9 @@ typedef struct { uint32_t anchor_rtptime; #ifdef CONFIG_AIRPLAY_2 + airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? + timing_t timing_type; // are we using NTP or PTP on this connection? + pthread_t rtp_event_thread; pthread_t rtp_ap2_control_thread; pthread_t rtp_realtime_audio_thread; diff --git a/rtp.c b/rtp.c index 35e415a9..50166543 100644 --- a/rtp.c +++ b/rtp.c @@ -120,6 +120,7 @@ void rtp_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_audio_receiver(void *arg) { + debug(1,"rtp_audio_receiver start"); pthread_cleanup_push(rtp_audio_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -258,6 +259,7 @@ void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_control_receiver(void *arg) { + debug(1,"rtp_control_receiver start"); pthread_cleanup_push(rtp_control_handler_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -288,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -300,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -506,6 +508,7 @@ void rtp_timing_sender_cleanup_handler(void *arg) { } void *rtp_timing_sender(void *arg) { + debug(1,"rtp_timing_sender start"); pthread_cleanup_push(rtp_timing_sender_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; struct timing_request { @@ -607,6 +610,7 @@ void rtp_timing_receiver_cleanup_handler(void *arg) { } void *rtp_timing_receiver(void *arg) { + debug(1,"rtp_timing_receiver start"); pthread_cleanup_push(rtp_timing_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -672,7 +676,7 @@ void *rtp_timing_receiver(void *arg) { if (packet[1] == 0xd3) { // timing reply return_time = arrival_time - conn->departure_time; - debug(3, "clock synchronisation request: return time is %8.3f milliseconds.", + debug(1, "clock synchronisation request: return time is %8.3f milliseconds.", 0.000001 * return_time); if (return_time < 200000000) { // must be less than 0.2 seconds @@ -2358,20 +2362,35 @@ void *rtp_buffered_audio_processor(void *arg) { pthread_exit(NULL); } int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { - return frame_to_ptp_local_time(timestamp, time, conn); + if (conn->timing_type == ts_ptp) + return frame_to_ptp_local_time(timestamp, time, conn); + else + return frame_to_ntp_local_time(timestamp, time, conn); } int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { - return local_ptp_time_to_frame(time, frame, conn); + if (conn->timing_type == ts_ptp) + return local_ptp_time_to_frame(time, frame, conn); + else + return local_ntp_time_to_frame(time, frame, conn); } -void reset_anchor_info(rtsp_conn_info *conn) { reset_ptp_anchor_info(conn); } +void reset_anchor_info(rtsp_conn_info *conn) { + if (conn->timing_type == ts_ptp) + reset_ptp_anchor_info(conn); + else + reset_ntp_anchor_info(conn); +} int have_timestamp_timing_information(rtsp_conn_info *conn) { - return have_ptp_timing_information(conn); + if (conn->timing_type == ts_ptp) + return have_ptp_timing_information(conn); + else + return have_ntp_timestamp_timing_information(conn); } #else + int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { return frame_to_ntp_local_time(timestamp, time, conn); } diff --git a/rtsp.c b/rtsp.c index 41ddff22..962a7626 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2090,7 +2090,7 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * "OPTIONS, GET_PARAMETER, SET_PARAMETER, GET"); } -void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, +void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { debug(2, "Connection %d: TEARDOWN", conn->connection_number); @@ -2198,8 +2198,8 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message resp->respcode = 451; // don't know what to do here } } +#endif -#else void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { debug_log_rtsp_message(2, "TEARDOWN request", req); @@ -2220,7 +2220,7 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message resp->respcode = 451; } } -#endif + void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { // TODO -- don't know what this is for in AP2 @@ -3571,6 +3571,11 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag } } */ + // In AirPlay 2, an ANNOUNCE signifies the start of an AirPlay 1 session. +#ifdef CONFIG_AIRPLAY_2 + conn->airplay_type = ap_1; + conn->timing_type = ts_ntp; +#endif conn->stream.type = ast_unknown; resp->respcode = 456; // 456 - Header Field Not Valid for Resource @@ -3788,34 +3793,39 @@ out: } } +#ifdef CONFIG_AIRPLAY_2 static struct method_handler { char *method; - void (*handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); + void (*ap1_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 + void (*ap2_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 2 +} method_handlers[] = {{"OPTIONS", handle_options, handle_options}, + {"ANNOUNCE", handle_announce, handle_announce}, + {"FLUSH", handle_flush, handle_flush}, + {"TEARDOWN", handle_teardown, handle_teardown_2}, + {"SETUP", handle_setup, handle_setup_2}, + {"GET_PARAMETER", handle_get_parameter, handle_get_parameter}, + {"SET_PARAMETER", handle_set_parameter, handle_set_parameter}, + {"RECORD", handle_record, handle_record_2}, + {"GET", handle_get, handle_get}, + {"POST", handle_post, handle_post}, + {"SETPEERS", handle_setpeers, handle_setpeers}, + {"SETRATEANCHORTI", handle_setrateanchori, handle_setrateanchori}, + {"FLUSHBUFFERED", handle_flushbuffered, handle_flushbuffered}, + {NULL, NULL, NULL}}; +#else +static struct method_handler { + char *method; + void (*handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 only } method_handlers[] = {{"OPTIONS", handle_options}, {"ANNOUNCE", handle_announce}, {"FLUSH", handle_flush}, {"TEARDOWN", handle_teardown}, -#ifdef CONFIG_AIRPLAY_2 - {"SETUP", handle_setup_2}, -#else {"SETUP", handle_setup}, -#endif {"GET_PARAMETER", handle_get_parameter}, {"SET_PARAMETER", handle_set_parameter}, -#ifdef CONFIG_AIRPLAY_2 - {"RECORD", handle_record_2}, -#else {"RECORD", handle_record}, -#endif -#ifdef CONFIG_AIRPLAY_2 - - {"GET", handle_get}, - {"POST", handle_post}, - {"SETPEERS", handle_setpeers}, - {"SETRATEANCHORTI", handle_setrateanchori}, - {"FLUSHBUFFERED", handle_flushbuffered}, -#endif {NULL, NULL}}; +#endif static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { char *hdr = msg_get_header(req, "Apple-Challenge"); @@ -4247,7 +4257,14 @@ static void *rtsp_conversation_thread_func(void *pconn) { for (mh = method_handlers; mh->method; mh++) { if (!strcmp(mh->method, req->method)) { method_selected = 1; +#ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_1) + mh->ap1_handler(conn, req, resp); + else + mh->ap2_handler(conn, req, resp); +#else mh->handler(conn, req, resp); +#endif break; } } @@ -4613,6 +4630,11 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { die("Couldn't allocate memory for an rtsp_conn_info record."); memset(conn, 0, sizeof(rtsp_conn_info)); conn->connection_number = RTSP_connection_index++; +#ifdef CONFIG_AIRPLAY_2 + conn->airplay_type = ap_2; // changed if an ANNOUNCE is received + conn->timing_type = ts_ptp; // changed if an ANNOUNCE is received +#endif + socklen_t size_of_reply = sizeof(SOCKADDR); conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &size_of_reply); From 04c0d97685f583a0a9dbda5f604859ab4dcff6f0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 14:33:47 +0100 Subject: [PATCH 036/878] Resend requests now work in the AP1-compatible AP2 mode. --- rtp.c | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/rtp.c b/rtp.c index 50166543..cfe9135f 100644 --- a/rtp.c +++ b/rtp.c @@ -676,7 +676,7 @@ void *rtp_timing_receiver(void *arg) { if (packet[1] == 0xd3) { // timing reply return_time = arrival_time - conn->departure_time; - debug(1, "clock synchronisation request: return time is %8.3f milliseconds.", + debug(2, "clock synchronisation request: return time is %8.3f milliseconds.", 0.000001 * return_time); if (return_time < 200000000) { // must be less than 0.2 seconds @@ -1126,25 +1126,22 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { char req[8]; // *not* a standard RTCP NACK req[0] = 0x80; #ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_2) { if (conn->ap2_remote_control_socket_addr_length == 0) { debug(2, "No remote socket -- skipping the resend"); return; // hack } req[1] = 0xD5; // Airplay 2 'resend' -#else + } else { +#endif req[1] = (char)0x55 | (char)0x80; // Apple 'resend' +#ifdef CONFIG_AIRPLAY_2 + } #endif *(unsigned short *)(req + 2) = htons(1); // our sequence number *(unsigned short *)(req + 4) = htons(first); // missed seqnum *(unsigned short *)(req + 6) = htons(count); // count -#ifndef CONFIG_AIRPLAY_2 - socklen_t msgsize = sizeof(struct sockaddr_in); -#ifdef AF_INET6 - if (conn->rtp_client_control_socket.SAFAMILY == AF_INET6) { - msgsize = sizeof(struct sockaddr_in6); - } -#endif -#endif + uint64_t time_of_sending_ns = get_absolute_time_in_ns(); uint64_t resend_error_backoff_time = 300000000; // 0.3 seconds if ((conn->rtp_time_of_last_resend_request_error_ns == 0) || @@ -1157,21 +1154,33 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 100000; + int response; #ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_2) { if (setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) debug(1, "Can't set timeout on resend request socket."); - if (sendto(conn->ap2_control_socket, req, sizeof(req), 0, + response = sendto(conn->ap2_control_socket, req, sizeof(req), 0, (struct sockaddr *)&conn->ap2_remote_control_socket_addr, - conn->ap2_remote_control_socket_addr_length) == -1) { -#else + conn->ap2_remote_control_socket_addr_length); + } else { +#endif if (setsockopt(conn->control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) debug(1, "Can't set timeout on resend request socket."); - - if (sendto(conn->control_socket, req, sizeof(req), 0, - (struct sockaddr *)&conn->rtp_client_control_socket, msgsize) == -1) { + socklen_t msgsize = sizeof(struct sockaddr_in); +#ifdef AF_INET6 + if (conn->rtp_client_control_socket.SAFAMILY == AF_INET6) { + msgsize = sizeof(struct sockaddr_in6); + } #endif + response = sendto(conn->control_socket, req, sizeof(req), 0, + (struct sockaddr *)&conn->rtp_client_control_socket, msgsize); + +#ifdef CONFIG_AIRPLAY_2 + } +#endif + if (response == -1) { char em[1024]; strerror_r(errno, em, sizeof(em)); debug(2, "Error %d using sendto to request a resend: \"%s\".", errno, em); From 6a13062100d93b812c640c848695f25dff4a1a88 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 14:35:31 +0100 Subject: [PATCH 037/878] clang format --- player.c | 8 +++---- player.h | 9 ++++--- rtp.c | 72 ++++++++++++++++++++++++++++---------------------------- rtsp.c | 9 ++++--- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/player.c b/player.c index 7d6d026e..930cce54 100644 --- a/player.c +++ b/player.c @@ -1926,10 +1926,10 @@ void *player_thread_func(void *arg) { if (conn->timing_type == ts_ntp) { #endif - // create and start the timing, control and audio receiver threads - pthread_create(&conn->rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn); - pthread_create(&conn->rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn); - pthread_create(&conn->rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn); + // create and start the timing, control and audio receiver threads + pthread_create(&conn->rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn); + pthread_create(&conn->rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn); + pthread_create(&conn->rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn); #ifdef CONFIG_AIRPLAY_2 } diff --git a/player.h b/player.h index 5766f6fc..d6a3a785 100644 --- a/player.h +++ b/player.h @@ -81,7 +81,6 @@ typedef enum { ast_apple_lossless, } audio_stream_type; - typedef struct { int encrypted; uint8_t aesiv[16], aeskey[16]; @@ -104,9 +103,9 @@ typedef struct file_cipher_context { #endif typedef struct { - int connection_number; // for debug ID purposes, nothing else... - int resend_interval; // this is really just for debugging - char *UserAgent; // free this on teardown + int connection_number; // for debug ID purposes, nothing else... + int resend_interval; // this is really just for debugging + char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency uint32_t latency; // the actual latency used for this play session uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero @@ -242,7 +241,7 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? - timing_t timing_type; // are we using NTP or PTP on this connection? + timing_t timing_type; // are we using NTP or PTP on this connection? pthread_t rtp_event_thread; pthread_t rtp_ap2_control_thread; diff --git a/rtp.c b/rtp.c index cfe9135f..06c7324d 100644 --- a/rtp.c +++ b/rtp.c @@ -120,7 +120,7 @@ void rtp_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_audio_receiver(void *arg) { - debug(1,"rtp_audio_receiver start"); + debug(1, "rtp_audio_receiver start"); pthread_cleanup_push(rtp_audio_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -259,7 +259,7 @@ void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_control_receiver(void *arg) { - debug(1,"rtp_control_receiver start"); + debug(1, "rtp_control_receiver start"); pthread_cleanup_push(rtp_control_handler_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -508,7 +508,7 @@ void rtp_timing_sender_cleanup_handler(void *arg) { } void *rtp_timing_sender(void *arg) { - debug(1,"rtp_timing_sender start"); + debug(1, "rtp_timing_sender start"); pthread_cleanup_push(rtp_timing_sender_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; struct timing_request { @@ -610,7 +610,7 @@ void rtp_timing_receiver_cleanup_handler(void *arg) { } void *rtp_timing_receiver(void *arg) { - debug(1,"rtp_timing_receiver start"); + debug(1, "rtp_timing_receiver start"); pthread_cleanup_push(rtp_timing_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -1127,16 +1127,16 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { req[0] = 0x80; #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_type == ap_2) { - if (conn->ap2_remote_control_socket_addr_length == 0) { - debug(2, "No remote socket -- skipping the resend"); - return; // hack - } - req[1] = 0xD5; // Airplay 2 'resend' + if (conn->ap2_remote_control_socket_addr_length == 0) { + debug(2, "No remote socket -- skipping the resend"); + return; // hack + } + req[1] = 0xD5; // Airplay 2 'resend' } else { #endif - req[1] = (char)0x55 | (char)0x80; // Apple 'resend' + req[1] = (char)0x55 | (char)0x80; // Apple 'resend' #ifdef CONFIG_AIRPLAY_2 - } + } #endif *(unsigned short *)(req + 2) = htons(1); // our sequence number *(unsigned short *)(req + 4) = htons(first); // missed seqnum @@ -1156,29 +1156,29 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { timeout.tv_usec = 100000; int response; #ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_type == ap_2) { - if (setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, - sizeof(timeout)) < 0) - debug(1, "Can't set timeout on resend request socket."); - response = sendto(conn->ap2_control_socket, req, sizeof(req), 0, - (struct sockaddr *)&conn->ap2_remote_control_socket_addr, - conn->ap2_remote_control_socket_addr_length); - } else { + if (conn->airplay_type == ap_2) { + if (setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, + sizeof(timeout)) < 0) + debug(1, "Can't set timeout on resend request socket."); + response = sendto(conn->ap2_control_socket, req, sizeof(req), 0, + (struct sockaddr *)&conn->ap2_remote_control_socket_addr, + conn->ap2_remote_control_socket_addr_length); + } else { #endif - if (setsockopt(conn->control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, - sizeof(timeout)) < 0) - debug(1, "Can't set timeout on resend request socket."); - socklen_t msgsize = sizeof(struct sockaddr_in); + if (setsockopt(conn->control_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, + sizeof(timeout)) < 0) + debug(1, "Can't set timeout on resend request socket."); + socklen_t msgsize = sizeof(struct sockaddr_in); #ifdef AF_INET6 - if (conn->rtp_client_control_socket.SAFAMILY == AF_INET6) { - msgsize = sizeof(struct sockaddr_in6); - } + if (conn->rtp_client_control_socket.SAFAMILY == AF_INET6) { + msgsize = sizeof(struct sockaddr_in6); + } #endif - response = sendto(conn->control_socket, req, sizeof(req), 0, - (struct sockaddr *)&conn->rtp_client_control_socket, msgsize); + response = sendto(conn->control_socket, req, sizeof(req), 0, + (struct sockaddr *)&conn->rtp_client_control_socket, msgsize); #ifdef CONFIG_AIRPLAY_2 - } + } #endif if (response == -1) { char em[1024]; diff --git a/rtsp.c b/rtsp.c index 962a7626..4b6b7088 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2091,7 +2091,7 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * } void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - rtsp_message *resp) { + rtsp_message *resp) { debug(2, "Connection %d: TEARDOWN", conn->connection_number); // debug_log_rtsp_message(1, "TEARDOWN: ", req); @@ -2221,7 +2221,6 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message } } - void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { // TODO -- don't know what this is for in AP2 debug_log_rtsp_message(2, "FLUSH request", req); @@ -3815,7 +3814,8 @@ static struct method_handler { #else static struct method_handler { char *method; - void (*handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 only + void (*handler)(rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp); // for AirPlay 1 only } method_handlers[] = {{"OPTIONS", handle_options}, {"ANNOUNCE", handle_announce}, {"FLUSH", handle_flush}, @@ -4631,11 +4631,10 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { memset(conn, 0, sizeof(rtsp_conn_info)); conn->connection_number = RTSP_connection_index++; #ifdef CONFIG_AIRPLAY_2 - conn->airplay_type = ap_2; // changed if an ANNOUNCE is received + conn->airplay_type = ap_2; // changed if an ANNOUNCE is received conn->timing_type = ts_ptp; // changed if an ANNOUNCE is received #endif - socklen_t size_of_reply = sizeof(SOCKADDR); conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &size_of_reply); if (conn->fd < 0) { From 25a1303269be1a83283d1874061620d7267376b7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 15:34:52 +0100 Subject: [PATCH 038/878] Only stop the player if you still have the playing conn. --- player.c | 12 ++++++++++-- player.h | 2 ++ rtsp.c | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index 930cce54..e8d79115 100644 --- a/player.c +++ b/player.c @@ -1537,13 +1537,21 @@ void player_thread_initial_cleanup_handler(__attribute__((unused)) void *arg) { void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; + if (pthread_mutex_trylock(&playing_conn_lock) == 0) { + + pthread_cleanup_push(mutex_unlock,&playing_conn_lock); + if (playing_conn == conn) { + if (config.output->stop) + config.output->stop(); + } + pthread_cleanup_pop(1); // unlock the mutex + } + int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); debug(3, "Connection %d: player thread main loop exit via player_thread_cleanup_handler.", conn->connection_number); - if (config.output->stop) - config.output->stop(); if (config.statistics_requested) { int rawSeconds = (int)difftime(time(NULL), conn->playstart); diff --git a/player.h b/player.h index d6a3a785..f56562cb 100644 --- a/player.h +++ b/player.h @@ -342,6 +342,8 @@ typedef struct { uint64_t dac_buffer_queue_minimum_length; } rtsp_conn_info; +extern pthread_mutex_t playing_conn_lock; + void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy, rtsp_conn_info *conn); diff --git a/rtsp.c b/rtsp.c index 4b6b7088..1972ba6e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -112,7 +112,7 @@ rtsp_conn_info **conns; int metadata_running = 0; // always lock use this when accessing the playing conn value -static pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; // every time we want to retain or release a reference count, lock it with this // if a reference count is read as zero, it means the it's being deallocated. @@ -326,6 +326,9 @@ int have_play_lock(rtsp_conn_info *conn) { return response; } +// return 0 if the playing_conn isn't already locked by someone else and if +// it belongs to the conn passed in. +// remember to release it! int try_to_hold_play_lock(rtsp_conn_info *conn) { int response = -1; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { From 5173169c30ff9829d4d49aaa5e7b2ababfa5eb5a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:20:01 +0100 Subject: [PATCH 039/878] Create BUILDFORAP1.md --- BUILDFORAP1.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 BUILDFORAP1.md diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/BUILDFORAP1.md @@ -0,0 +1 @@ + From 268e93a83a4f84712e2d3eea13bf1ee562895ea6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:20:21 +0100 Subject: [PATCH 040/878] Create BUILDFORAP2.md --- BUILDFORAP2.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 BUILDFORAP2.md diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/BUILDFORAP2.md @@ -0,0 +1 @@ + From 6f5e6a96c0155f9bd504281e5eb74b7d59e7a38f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:20:47 +0100 Subject: [PATCH 041/878] Create MOREINFO.md --- MOREINFO.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 MOREINFO.md diff --git a/MOREINFO.md b/MOREINFO.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/MOREINFO.md @@ -0,0 +1 @@ + From 8dc13c8e6b62dd76a638326c0d6e151c21d37791 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:21:32 +0100 Subject: [PATCH 042/878] Update MOREINFO.md --- MOREINFO.md | 630 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 630 insertions(+) diff --git a/MOREINFO.md b/MOREINFO.md index 8b137891..1b024340 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -1 +1,631 @@ +More Information +---------- +Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). + +Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. + +For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. + +For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. + +Synchronisation, Latency, "Stuffing" +--------- +The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. + +As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. + +Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. + +Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. + +What else? +-------------- +* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. +* Hardware Mute — Shairport Sync can mute properly if the hardware supports it. +* Support for the Apple ALAC decoder. +* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. +* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. +* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`. +* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) +* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. +* Compiles on Linux, Cygwin, FreeBSD, OpenBSD. +* Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io). +* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. +* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. +* A native D-Bus interface, including access to metadata and artwork, some limited remote control and some system settings. + +Heritage +------- +Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. + +Status +------ +Shairport Sync works on a wide variety of Linux devices, FreeBSD and OpenBSD. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs. Shairport Sync runs well on the Raspberry Pi. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card. + +Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. + +Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system. + +Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora, FreeBSD and OpenBSD inside VMWare Fusion on a Mac, but synchronisation in inaccurate — possibly because the sound card is being emulated. + +For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. + +Building And Installing +--------------------- +See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. + +Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more: + +**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. + +**Debian:** shairport-sync is in the Debian archive. + +**OpenWrt:** There is a Shairport Sync package in OpenWrt trunk. Also, there's an OpenWrt package at https://github.com/mikebrady/shairport-sync-for-openwrt, including one that builds back to Barrier Breaker. + +**Arch Linux:** Shairport Sync is available for x86_64 and i686 platforms in the Arch Linux Community Repository -- search for `shairport-sync`. See also https://www.archlinux.org/packages/. + +To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda). + +**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: +``` +$brew install shairport-sync +``` +Note that the installation uses the libao library and so synchronisation is not available — playback glitches will occur occasionally, when the ao system's buffers overflow or underflow. + +**Fedora:** Please see the guide at [FEDORA.md](https://github.com/mikebrady/shairport-sync/blob/master/FEDORA.md). + +**Cygwin:** Please see the guide at [CYGWIN.md](https://github.com/mikebrady/shairport-sync/blob/master/CYGWIN.md). + +Sincere thanks to all package contributors! + +If you wish to build and install the latest version of Shairport Sync on Debian, Ubuntu, Fedora or Arch platforms, please continue to follow these instructions. When the program has been compiled and installed, refer to the section on Configuring Shairport Sync that follows. To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md). + +**Remove Old Versions of Shairport Sync and its Startup Scripts** + +You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. + +You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. + +If you removed any installations of Shairport Sync or any of its startup script files, you should reboot. + +**Determine The Configuration Needed** + +Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: +``` +$ pactl info +Server String: unix:/run/user/1000/pulse/native +Library Protocol Version: 30 +Server Protocol Version: 30 +Is Local: yes +Client Index: 9 +Tile Size: 65472 +User Name: mike +Host Name: ubuntu +Server Name: pulseaudio +Server Version: 8.0 +Default Sample Specification: s16le 2ch 44100Hz +Default Channel Map: front-left,front-right +Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo +Default Source: alsa_input.pci-0000_02_02.0.analog-stereo +Cookie: 96f9:3e8d +$ +``` +If PulseAudio in not installed, you'll get something like this: +``` +$ pactl info +-bash: pactl: command not found +$ +``` +If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. + +If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. + +**Building** + +To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. + +The following libraries are required: +* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) +* Avahi +* ALSA and/or PulseAudio +* libdaemon +* autoconf +* automake +* libtool +* libpopt +* libconfig + +Optional: +* libsoxr +* libalac (This is a library containing the Apple ALAC decoder.) + +Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power. + +Debian, Ubuntu and Raspbian users can get the basics with: + +- `# apt install build-essential git xmltoman` – these may already be installed. +- `# apt install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev` +- `# apt install libasound2-dev` for the ALSA libraries +- `# apt install libpulse-dev` for the PulseAudio libraries +- `# apt install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). +- `# apt install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. +- `# apt install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. +- `# apt install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in recent distributions; if not, instructions for how to build it from source on Linux are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). +- `# apt install libsndfile1-dev` if you want to use the convolution filter. + +If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. + +**Download Shairport Sync:** +``` +$ git clone https://github.com/mikebrady/shairport-sync.git +``` + +Next, `cd` into the shairport-sync directory and execute the following commands: +``` +$ autoreconf -i -f +``` +(Note that the `autoreconf...` step may take some time on less powerful machines.) + +**Choose the appropriate `--with-*` options:** + +(Don't worry -- there's a recommended set of configuration options further down.) + +- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. +- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. +- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). +- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. +- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system. +- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. +- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. +- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently. +- `--with-soxr` for libsoxr-based resampling. +- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. +- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. +- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. +- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. +- `--with-apple-alac` to include the Apple ALAC Decoder. +- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. +- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. +- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. + +**Determine if it's a `systemd` or a "System V" installation:** + +If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) + +There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. + +The easiest way is to look at the first few lines of the `init` manual. Enter the command: + +``` +$ man init +``` +In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): +``` +SYSTEMD(1) systemd SYSTEMD(1) + +NAME + systemd, init - systemd system and service manager +... +``` +Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): + +``` +init(8) System Manager's Manual init(8) + +NAME + init - Upstart process management daemon + +SYNOPSIS + init [OPTION]... +... +``` +In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): +``` +INIT(8) Linux System Administrator's Manual INIT(8) + +NAME + init, telinit - process control initialization + +SYNOPSIS + /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] + /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] + /sbin/telinit [ -e VAR[=VAL] ] + +... +``` +If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-libdaemon --with-systemv`. + +**Choose the location of the configuration file** + +A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. + +**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** + +Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Buster, Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: + +`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd` + +* Omit the `--with-soxr` if the libsoxr library is not available. +* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`. +* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`. + +**Build and Install the Application:** + +Enter: + +``` +$ make +``` + +to build the application. + +Assuming you have used the `./configure` settings suggested above, you can now install the application: + +``` +$ sudo make install +``` +The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. + + +**Complete installation into to a `systemd` system** + +If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: + +``` +$ sudo systemctl enable shairport-sync +``` +You should then reboot the system. + +**Complete installation into a System V system** + +If you have chosen the `--with-systemv` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: +``` +$ sudo update-rc.d shairport-sync defaults 90 10 +$ sudo update-rc.d shairport-sync enable +``` +You should then reboot the system. + +**Man Page** + +You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html + +Configuring Shairport Sync +-------- +There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. + +**(1) Starting and Stopping:** +Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. + +If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, if the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. + +**(2) Settings:** +To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. + +For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. + +Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card. + +Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` asd give the service the password `secret`, then you should do the following: + +``` +general = +{ + name = "Front Room"; + password = "secret"; + // ... other general settings +}; +``` +(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) + +**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable. + +No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: + +``` + output_backend = "pa"; +``` +to the `general` group. + +The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. + +The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. +* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. +* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: + +``` +alsa = +{ + output_device = "hw:0,5"; + mixer_device = "hw:0"; + mixer_control_name = "PCM"; + // ... other alsa settings +}; +``` + +The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. + +Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. + +Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) + +Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. + +Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. + +**Raspberry Pi** + +The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. + +To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. + +Do the usual update and upgrade: +``` +# apt update +# apt upgrade +``` +To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: +``` +alsa = +{ + output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. + mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. + // ... other alsa settings +``` +(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. + +A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur. + +From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. + +*Command Line Arguments* + +As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html. + +Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. + +* The `-c` option allows you to specify the location of the configuration file. +* The `-V` option gives you version information about Shairport Sync and then quits. +* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. +* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. + +The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : +`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. + +Examples +-------- + +Here are some examples of complete configuration files. + +``` +general = { + name = "Joe's Stereo"; +}; + +alsa = { + output_device = "hw:0"; +}; +``` + +This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used. + +For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. + +Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: +``` +general = { + name = "Mike's Boombox"; +}; + +alsa = { + output_device = "hw:0"; + mixer_control_name = "PCM"; +}; +``` + +Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: +``` +general = { + name = "Kitchen"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "PCM"; +}; +``` + +For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: +``` +general = { + name = "Front Room"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "Speaker"; +}; +``` + +For a first generation Griffin iMic on a Raspberry Pi: +``` +general = { + name = "Attic"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "PCM"; +}; +``` + +For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: + +On an NSLU2, to drive a first generation Griffin iMic: +``` +general = { + name = "Den"; +}; + +alsa = { + mixer_control_name = "PCM"; +}; +``` + +On an NSLU2, to drive the "3D Sound" USB card: +``` +general = { + name = "TV Room"; +}; + +alsa = { + mixer_control_name = "Speaker"; +}; +``` + +Finally, here is an example of using the PulseAudio backend: +``` +general = { + name = "Zoe's Computer"; + output_backend = "pa"; +}; + +``` + + +Metadata +-------- + +Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. + +**Metadata broadcasting over UDP** + +As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. + +The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. + +The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. + +Latency +------- +Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. + +Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. + +Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. + +Resynchronisation +------------- +Shairport Sync actively maintains synchronisation with the source. +If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. +* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. + +Tolerance +--------- +Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. +* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. + +Some Statistics +--------------- +If you turn on the `general` `statistics` setting, a heading like this will be output to the log file (or to `STDERR` if the `-u` command line option is chosen): +``` +sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm +``` +This will be followed by the statistics themselves at regular intervals, for example: +``` + 0.15, 0.0, 0.0, 1003, 0, 0, 0, 0, 8539, 208, 225, 44099.95, 44274.21, 44099.68, 0.00, 0, -6.12 + 0.34, 0.0, 0.0, 2006, 0, 0, 0, 0, 8591, 206, 225, 44099.99, 44169.96, 44099.76, 0.00, 0, -5.27 + 0.89, 0.0, 0.0, 3009, 0, 0, 0, 0, 8571, 218, 225, 44100.01, 44152.81, 44099.85, 0.00, 0, -3.66 + 0.85, 0.0, 0.0, 4012, 0, 0, 0, 0, 8619, 218, 225, 44100.00, 44142.80, 44099.80, 0.00, 0, -4.62 + 1.29, 0.0, 0.0, 5015, 0, 0, 0, 0, 8623, 218, 225, 44100.00, 44129.39, 44099.83, 0.00, 0, -3.85 + 1.41, 0.0, 0.0, 6018, 0, 0, 0, 0, 8619, 216, 225, 44099.99, 44127.74, 44099.86, 0.00, 0, -2.76 + 1.53, 0.0, 0.0, 7021, 0, 0, 0, 0, 8603, 218, 225, 44100.00, 44109.60, 44099.81, 0.00, 0, -4.21 + 1.81, 0.0, 0.0, 8024, 0, 0, 0, 0, 8655, 215, 225, 44100.00, 44110.01, 44099.82, 0.00, 0, -4.16 + 2.00, -82.1, 82.1, 9027, 0, 0, 0, 0, 8481, 208, 225, 44100.00, 44110.69, 44099.83, 0.00, 0, -3.97 + 1.96, -8.5, 8.5, 10030, 0, 0, 0, 0, 8599, 209, 225, 44099.99, 44108.84, 44099.86, 0.00, 0, -3.02 + 1.94, 0.0, 0.0, 11033, 0, 0, 0, 0, 8583, 217, 225, 44100.00, 44109.33, 44099.87, 0.00, 0, -2.93 + 1.90, -5.7, 5.7, 12036, 0, 0, 0, 0, 8601, 219, 225, 44100.00, 44110.01, 44099.85, 20.47, 10, -23.88 + 2.02, -36.8, 36.8, 13039, 0, 0, 0, 0, 8488, 216, 225, 44100.00, 44107.18, 44099.85, 20.39, 12, -23.83 + 1.96, -17.0, 17.0, 14042, 0, 0, 0, 0, 8554, 218, 226, 44100.00, 44108.80, 44099.84, 20.63, 15, -24.13 + 1.99, -31.2, 31.2, 15045, 0, 0, 0, 0, 8592, 216, 225, 44100.00, 44106.85, 44099.85, 20.95, 18, -24.39 + 1.98, -28.3, 28.3, 16048, 0, 0, 0, 0, 8627, 219, 225, 44099.99, 44109.01, 44099.88, 21.23, 20, -23.90 + 2.01, -31.2, 31.2, 17051, 0, 0, 0, 0, 8582, 217, 225, 44100.00, 44108.86, 44099.84, 21.57, 22, -25.10 + 1.96, -19.8, 19.8, 18054, 0, 0, 0, 0, 8556, 215, 226, 44100.00, 44103.79, 44099.86, 21.59, 24, -24.85 + 1.98, -25.5, 25.5, 19057, 0, 0, 0, 0, 8563, 219, 225, 44100.00, 44102.62, 44099.86, 22.26, 25, -25.47 + 2.00, -28.3, 28.3, 20060, 0, 0, 0, 0, 8569, 214, 225, 44100.00, 44103.22, 44099.88, 22.57, 27, -25.41 + 1.98, -42.5, 42.5, 21063, 0, 0, 0, 0, 8570, 216, 225, 44100.00, 44104.14, 44099.85, 22.74, 28, -26.08 + 1.89, -31.2, 31.2, 22066, 0, 0, 0, 0, 8524, 218, 225, 44100.00, 44104.66, 44099.87, 22.89, 30, -25.89 + 1.96, 0.0, 0.0, 23069, 0, 0, 0, 0, 8650, 218, 225, 44100.00, 44105.36, 44099.88, 22.92, 32, -25.73 + 1.98, -34.0, 34.0, 24072, 0, 0, 0, 0, 8475, 219, 225, 44100.00, 44106.04, 44099.88, 22.90, 34, -25.70 + 2.07, -138.8, 138.8, 25075, 0, 0, 0, 0, 8490, 218, 226, 44100.00, 44104.93, 44099.87, 23.87, 35, -26.92 + 1.33, 0.0, 0.0, 26078, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44104.66, 44099.88, 23.68, 38, -26.43 + 1.67, -22.7, 22.7, 27081, 0, 0, 0, 0, 8562, 217, 225, 44100.00, 44106.16, 44099.87, 25.06, 40, -27.93 + 1.98, -28.3, 28.3, 28084, 0, 0, 0, 0, 8544, 218, 225, 44100.00, 44105.21, 44099.87, 25.91, 41, -28.84 + 1.99, -22.7, 22.7, 29087, 0, 0, 0, 0, 8569, 219, 225, 44100.00, 44102.30, 44099.87, 25.91, 41, -28.74 + 1.66, -56.6, 56.6, 30090, 0, 0, 0, 0, 8554, 218, 225, 44100.00, 44103.07, 44099.86, 26.38, 44, -29.46 + 1.44, 0.0, 0.0, 31093, 0, 0, 0, 0, 8642, 218, 225, 44100.00, 44102.18, 44099.87, 26.08, 46, -28.98 + 1.56, 0.0, 0.0, 32096, 0, 0, 0, 0, 8647, 218, 225, 44100.00, 44102.76, 44099.87, 25.79, 48, -28.64 + 1.68, 0.0, 0.0, 33099, 0, 0, 0, 0, 8647, 217, 225, 44100.00, 44103.41, 44099.88, 25.66, 49, -28.51 + 1.92, -14.2, 14.2, 34102, 0, 0, 0, 0, 8639, 217, 225, 44100.00, 44102.38, 44099.87, 25.45, 51, -28.40 + 2.00, -45.3, 45.3, 35105, 0, 0, 0, 0, 8593, 216, 225, 44100.00, 44103.23, 44099.86, 25.35, 53, -28.61 + 1.98, -17.0, 17.0, 36108, 0, 0, 0, 0, 8609, 218, 225, 44100.00, 44102.54, 44099.87, 25.21, 55, -28.12 + 1.99, -31.2, 31.2, 37111, 0, 0, 0, 0, 8575, 217, 225, 44100.00, 44101.98, 44099.88, 25.11, 57, -27.88 + 1.99, -14.2, 14.2, 38114, 0, 0, 0, 0, 8624, 218, 225, 44100.00, 44104.44, 44099.87, 24.95, 60, -27.97 + 1.99, -28.3, 28.3, 39117, 0, 0, 0, 0, 8582, 209, 225, 44100.00, 44104.91, 44099.86, 24.88, 62, -27.98 + 1.86, -17.0, 17.0, 40120, 0, 0, 0, 0, 8504, 207, 225, 44100.00, 44104.52, 44099.88, 24.66, 63, -27.39 + 1.96, -56.6, 56.6, 41123, 0, 0, 0, 0, 8517, 212, 226, 44100.00, 44102.29, 44099.88, 24.54, 66, -27.34 + 1.90, -36.8, 36.8, 42126, 0, 0, 0, 0, 8363, 218, 240, 44100.00, 44116.79, 44099.87, 24.55, 68, -27.48 + 1.89, 0.0, 0.0, 43129, 0, 0, 0, 0, 8487, 227, 241, 44100.00, 44117.57, 44099.88, 24.56, 69, -27.32 + 1.99, -22.7, 22.7, 44132, 0, 0, 0, 0, 8576, 227, 240, 44100.00, 44118.53, 44099.87, 24.57, 70, -27.44 + 1.98, -39.7, 39.7, 45135, 0, 0, 0, 0, 8593, 228, 240, 44100.00, 44113.88, 44099.87, 24.60, 73, -27.43 + 2.00, -34.0, 34.0, 46138, 0, 0, 0, 0, 8577, 227, 240, 44100.00, 44117.10, 44099.88, 24.65, 75, -27.46 + 1.98, -22.7, 22.7, 47141, 0, 0, 0, 0, 8591, 204, 240, 44100.00, 44102.78, 44099.87, 24.67, 77, -27.59 + 1.98, -17.0, 17.0, 48144, 0, 0, 0, 0, 8587, 216, 225, 44100.00, 44102.38, 44099.88, 24.67, 79, -27.42 + 1.99, -28.3, 28.3, 49147, 0, 0, 0, 0, 8590, 214, 225, 44100.00, 44102.70, 44099.87, 24.66, 80, -27.57 + 1.85, -62.3, 62.3, 50150, 0, 0, 0, 0, 8518, 217, 226, 44100.00, 44103.16, 44099.87, 24.70, 83, -27.55 + 1.90, -28.3, 28.3, 51153, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44102.60, 44099.88, 24.66, 85, -27.42 + 1.89, -5.7, 5.7, 52156, 0, 0, 0, 0, 8615, 218, 225, 44100.00, 44103.37, 44099.88, 24.65, 86, -27.39 + 1.96, -19.8, 19.8, 53159, 0, 0, 0, 0, 8600, 219, 225, 44100.00, 44103.77, 44099.87, 24.62, 89, -27.53 + 1.99, -22.7, 22.7, 54162, 0, 0, 0, 0, 8567, 216, 225, 44100.00, 44103.12, 44099.87, 24.57, 92, -27.45 +``` + +"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output is on average 0.15 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made. + +"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. In this case, the drift is negligible, but it can routinely be up to 150 ppm, especially with older machines. + +"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections are being made. Third party programs tend to have much larger levels of corrections. + +"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.2 seconds = 8,820 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up. + +"Source nominal frames per second" is the rate at which audio is being sent to Shairport Sync according to information supplied by the source itself. + +"Source actual frames per second" is the rate at which frames of audio are actually received by Shairport Sync. This can vary a great deal due to network conditions, but over a long time (more than 30 minutes) it should settle down to an accurate value. It does not account for retransmitted packets. + +"Output frames per second" is the actual rate at which frames of audio are taken by the output device. On a system with a well-conditioned `ntp`-based clock (and without output underruns) this figure should be very accurate after playing material continuously for a period. + +"Source clock drift in ppm" is an estimate of the difference in timekeeping between the audio source and the Shairport Sync device. It is calculated from a linear regression of drift sample data. The number of samples the estimate is based on is given in the next column, "Source clock drift sample count". + +"Rough calculated correction in ppm" is a very crude estimate of the amount of interpolation that needs to applied, on average, to keep sync. It is not really to be relied on at this time. + +For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an iPhone XS Max to Shairport Sync running on a WiFi-connected Raspberry Pi 3 using an [IQaudIO Pi-DigiAMP+](http://iqaudio.co.uk/hats/9-pi-digiamp.html). + +It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway... + + +WiFi Issues +--------- +If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for more details. + +Troubleshooting +--------------- +Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for a few hints, contributed by users. + +MQTT +--------------- +Please refer to the [MQTT INFO](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md) page for additional info on building, configuring and using MQTT to interface shairport-sync with common home automation systems (contributed by users). + +Packaging status +---------------- + +[![Packaging status](https://repology.org/badge/vertical-allrepos/shairport-sync.svg)](https://repology.org/project/shairport-sync/versions) + From 9dc1c1dedf4f0e93691070d63de53ac8382a910f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:24:02 +0100 Subject: [PATCH 043/878] Update README.md --- README.md | 637 +----------------------------------------------------- 1 file changed, 9 insertions(+), 628 deletions(-) diff --git a/README.md b/README.md index 1934009b..7e9b7e6b 100644 --- a/README.md +++ b/README.md @@ -1,638 +1,19 @@ Shairport Sync ============= -Shairport Sync is an AirPlay audio player – it plays audio streamed from iTunes, iOS, Apple TV and macOS devices and AirPlay sources such as Quicktime Player and [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/), among others. +Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and AirPlay sources such a [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -Audio played by a Shairport Sync-powered device stays synchronised with the source and hence with similar devices playing the same source. In this way, synchronised multi-room audio is possible for players that support it, such as iTunes and the macOS Music app. +Shairport Sync supports AirPlay 1 or AirPlay 2. AirPlay 2 support is experimental and incomplete -- in particular, it does not support Home integration. -Shairport Sync runs on Linux, FreeBSD and OpenBSD. It does not support AirPlay video or photo streaming. +When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. AirPlay 2 support is only available on recent Linux builds. -More Information ----------- -Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). +Shairport Sync does not support AirPlay video or photo streaming. -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. +For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). -For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. +A guide to building for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. +To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). -Synchronisation, Latency, "Stuffing" ---------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. - -As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. - -Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. - -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. - -What else? --------------- -* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. -* Hardware Mute — Shairport Sync can mute properly if the hardware supports it. -* Support for the Apple ALAC decoder. -* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. -* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. -* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`. -* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) -* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. -* Compiles on Linux, Cygwin, FreeBSD, OpenBSD. -* Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io). -* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. -* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. -* A native D-Bus interface, including access to metadata and artwork, some limited remote control and some system settings. - -Heritage -------- -Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. - -Status ------- -Shairport Sync works on a wide variety of Linux devices, FreeBSD and OpenBSD. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs. Shairport Sync runs well on the Raspberry Pi. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card. - -Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. - -Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system. - -Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora, FreeBSD and OpenBSD inside VMWare Fusion on a Mac, but synchronisation in inaccurate — possibly because the sound card is being emulated. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more: - -**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. - -**Debian:** shairport-sync is in the Debian archive. - -**OpenWrt:** There is a Shairport Sync package in OpenWrt trunk. Also, there's an OpenWrt package at https://github.com/mikebrady/shairport-sync-for-openwrt, including one that builds back to Barrier Breaker. - -**Arch Linux:** Shairport Sync is available for x86_64 and i686 platforms in the Arch Linux Community Repository -- search for `shairport-sync`. See also https://www.archlinux.org/packages/. - -To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda). - -**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: -``` -$brew install shairport-sync -``` -Note that the installation uses the libao library and so synchronisation is not available — playback glitches will occur occasionally, when the ao system's buffers overflow or underflow. - -**Fedora:** Please see the guide at [FEDORA.md](https://github.com/mikebrady/shairport-sync/blob/master/FEDORA.md). - -**Cygwin:** Please see the guide at [CYGWIN.md](https://github.com/mikebrady/shairport-sync/blob/master/CYGWIN.md). - -Sincere thanks to all package contributors! - -If you wish to build and install the latest version of Shairport Sync on Debian, Ubuntu, Fedora or Arch platforms, please continue to follow these instructions. When the program has been compiled and installed, refer to the section on Configuring Shairport Sync that follows. To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md). - -**Remove Old Versions of Shairport Sync and its Startup Scripts** - -You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. - -You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. - -If you removed any installations of Shairport Sync or any of its startup script files, you should reboot. - -**Determine The Configuration Needed** - -Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: unix:/run/user/1000/pulse/native -Library Protocol Version: 30 -Server Protocol Version: 30 -Is Local: yes -Client Index: 9 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu -Server Name: pulseaudio -Server Version: 8.0 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 96f9:3e8d -$ -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. - -If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. - -**Building** - -To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. - -The following libraries are required: -* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) -* Avahi -* ALSA and/or PulseAudio -* libdaemon -* autoconf -* automake -* libtool -* libpopt -* libconfig - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) - -Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt install build-essential git xmltoman` – these may already be installed. -- `# apt install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev` -- `# apt install libasound2-dev` for the ALSA libraries -- `# apt install libpulse-dev` for the PulseAudio libraries -- `# apt install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in recent distributions; if not, instructions for how to build it from source on Linux are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). -- `# apt install libsndfile1-dev` if you want to use the convolution filter. - -If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. - -**Download Shairport Sync:** -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -``` - -Next, `cd` into the shairport-sync directory and execute the following commands: -``` -$ autoreconf -i -f -``` -(Note that the `autoreconf...` step may take some time on less powerful machines.) - -**Choose the appropriate `--with-*` options:** - -(Don't worry -- there's a recommended set of configuration options further down.) - -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. -- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. -- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). -- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. -- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system. -- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. -- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. -- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently. -- `--with-soxr` for libsoxr-based resampling. -- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. -- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. -- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. -- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. -- `--with-apple-alac` to include the Apple ALAC Decoder. -- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. -- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. -- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. - -**Determine if it's a `systemd` or a "System V" installation:** - -If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) - -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. - -The easiest way is to look at the first few lines of the `init` manual. Enter the command: - -``` -$ man init -``` -In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): -``` -SYSTEMD(1) systemd SYSTEMD(1) - -NAME - systemd, init - systemd system and service manager -... -``` -Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): - -``` -init(8) System Manager's Manual init(8) - -NAME - init - Upstart process management daemon - -SYNOPSIS - init [OPTION]... -... -``` -In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): -``` -INIT(8) Linux System Administrator's Manual INIT(8) - -NAME - init, telinit - process control initialization - -SYNOPSIS - /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] - /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] - /sbin/telinit [ -e VAR[=VAL] ] - -... -``` -If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-libdaemon --with-systemv`. - -**Choose the location of the configuration file** - -A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. - -**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** - -Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Buster, Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: - -`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd` - -* Omit the `--with-soxr` if the libsoxr library is not available. -* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`. -* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`. - -**Build and Install the Application:** - -Enter: - -``` -$ make -``` - -to build the application. - -Assuming you have used the `./configure` settings suggested above, you can now install the application: - -``` -$ sudo make install -``` -The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. - - -**Complete installation into to a `systemd` system** - -If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: - -``` -$ sudo systemctl enable shairport-sync -``` -You should then reboot the system. - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -$ sudo update-rc.d shairport-sync enable -``` -You should then reboot the system. - -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html - -Configuring Shairport Sync --------- -There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. - -**(1) Starting and Stopping:** -Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. - -If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, if the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. - -**(2) Settings:** -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. - -For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. - -Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card. - -Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` asd give the service the password `secret`, then you should do the following: - -``` -general = -{ - name = "Front Room"; - password = "secret"; - // ... other general settings -}; -``` -(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) - -**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable. - -No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: - -``` - output_backend = "pa"; -``` -to the `general` group. - -The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. - -The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. -* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. -* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: - -``` -alsa = -{ - output_device = "hw:0,5"; - mixer_device = "hw:0"; - mixer_control_name = "PCM"; - // ... other alsa settings -}; -``` - -The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. - -Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. - -Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) - -Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. - -Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. - -**Raspberry Pi** - -The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. - -To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. - -Do the usual update and upgrade: -``` -# apt update -# apt upgrade -``` -To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: -``` -alsa = -{ - output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. - mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. - // ... other alsa settings -``` -(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. - -A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur. - -From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. - -*Command Line Arguments* - -As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html. - -Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. - -* The `-c` option allows you to specify the location of the configuration file. -* The `-V` option gives you version information about Shairport Sync and then quits. -* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. - -The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : -`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. - -Examples --------- - -Here are some examples of complete configuration files. - -``` -general = { - name = "Joe's Stereo"; -}; - -alsa = { - output_device = "hw:0"; -}; -``` - -This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used. - -For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. - -Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: -``` -general = { - name = "Mike's Boombox"; -}; - -alsa = { - output_device = "hw:0"; - mixer_control_name = "PCM"; -}; -``` - -Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: -``` -general = { - name = "Kitchen"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: -``` -general = { - name = "Front Room"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "Speaker"; -}; -``` - -For a first generation Griffin iMic on a Raspberry Pi: -``` -general = { - name = "Attic"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: - -On an NSLU2, to drive a first generation Griffin iMic: -``` -general = { - name = "Den"; -}; - -alsa = { - mixer_control_name = "PCM"; -}; -``` - -On an NSLU2, to drive the "3D Sound" USB card: -``` -general = { - name = "TV Room"; -}; - -alsa = { - mixer_control_name = "Speaker"; -}; -``` - -Finally, here is an example of using the PulseAudio backend: -``` -general = { - name = "Zoe's Computer"; - output_backend = "pa"; -}; - -``` - - -Metadata --------- - -Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. - -**Metadata broadcasting over UDP** - -As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. - -The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. - -The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. - -Latency -------- -Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. - -Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. - -Resynchronisation -------------- -Shairport Sync actively maintains synchronisation with the source. -If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. -* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. - -Tolerance ---------- -Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. -* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. - -Some Statistics ---------------- -If you turn on the `general` `statistics` setting, a heading like this will be output to the log file (or to `STDERR` if the `-u` command line option is chosen): -``` -sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm -``` -This will be followed by the statistics themselves at regular intervals, for example: -``` - 0.15, 0.0, 0.0, 1003, 0, 0, 0, 0, 8539, 208, 225, 44099.95, 44274.21, 44099.68, 0.00, 0, -6.12 - 0.34, 0.0, 0.0, 2006, 0, 0, 0, 0, 8591, 206, 225, 44099.99, 44169.96, 44099.76, 0.00, 0, -5.27 - 0.89, 0.0, 0.0, 3009, 0, 0, 0, 0, 8571, 218, 225, 44100.01, 44152.81, 44099.85, 0.00, 0, -3.66 - 0.85, 0.0, 0.0, 4012, 0, 0, 0, 0, 8619, 218, 225, 44100.00, 44142.80, 44099.80, 0.00, 0, -4.62 - 1.29, 0.0, 0.0, 5015, 0, 0, 0, 0, 8623, 218, 225, 44100.00, 44129.39, 44099.83, 0.00, 0, -3.85 - 1.41, 0.0, 0.0, 6018, 0, 0, 0, 0, 8619, 216, 225, 44099.99, 44127.74, 44099.86, 0.00, 0, -2.76 - 1.53, 0.0, 0.0, 7021, 0, 0, 0, 0, 8603, 218, 225, 44100.00, 44109.60, 44099.81, 0.00, 0, -4.21 - 1.81, 0.0, 0.0, 8024, 0, 0, 0, 0, 8655, 215, 225, 44100.00, 44110.01, 44099.82, 0.00, 0, -4.16 - 2.00, -82.1, 82.1, 9027, 0, 0, 0, 0, 8481, 208, 225, 44100.00, 44110.69, 44099.83, 0.00, 0, -3.97 - 1.96, -8.5, 8.5, 10030, 0, 0, 0, 0, 8599, 209, 225, 44099.99, 44108.84, 44099.86, 0.00, 0, -3.02 - 1.94, 0.0, 0.0, 11033, 0, 0, 0, 0, 8583, 217, 225, 44100.00, 44109.33, 44099.87, 0.00, 0, -2.93 - 1.90, -5.7, 5.7, 12036, 0, 0, 0, 0, 8601, 219, 225, 44100.00, 44110.01, 44099.85, 20.47, 10, -23.88 - 2.02, -36.8, 36.8, 13039, 0, 0, 0, 0, 8488, 216, 225, 44100.00, 44107.18, 44099.85, 20.39, 12, -23.83 - 1.96, -17.0, 17.0, 14042, 0, 0, 0, 0, 8554, 218, 226, 44100.00, 44108.80, 44099.84, 20.63, 15, -24.13 - 1.99, -31.2, 31.2, 15045, 0, 0, 0, 0, 8592, 216, 225, 44100.00, 44106.85, 44099.85, 20.95, 18, -24.39 - 1.98, -28.3, 28.3, 16048, 0, 0, 0, 0, 8627, 219, 225, 44099.99, 44109.01, 44099.88, 21.23, 20, -23.90 - 2.01, -31.2, 31.2, 17051, 0, 0, 0, 0, 8582, 217, 225, 44100.00, 44108.86, 44099.84, 21.57, 22, -25.10 - 1.96, -19.8, 19.8, 18054, 0, 0, 0, 0, 8556, 215, 226, 44100.00, 44103.79, 44099.86, 21.59, 24, -24.85 - 1.98, -25.5, 25.5, 19057, 0, 0, 0, 0, 8563, 219, 225, 44100.00, 44102.62, 44099.86, 22.26, 25, -25.47 - 2.00, -28.3, 28.3, 20060, 0, 0, 0, 0, 8569, 214, 225, 44100.00, 44103.22, 44099.88, 22.57, 27, -25.41 - 1.98, -42.5, 42.5, 21063, 0, 0, 0, 0, 8570, 216, 225, 44100.00, 44104.14, 44099.85, 22.74, 28, -26.08 - 1.89, -31.2, 31.2, 22066, 0, 0, 0, 0, 8524, 218, 225, 44100.00, 44104.66, 44099.87, 22.89, 30, -25.89 - 1.96, 0.0, 0.0, 23069, 0, 0, 0, 0, 8650, 218, 225, 44100.00, 44105.36, 44099.88, 22.92, 32, -25.73 - 1.98, -34.0, 34.0, 24072, 0, 0, 0, 0, 8475, 219, 225, 44100.00, 44106.04, 44099.88, 22.90, 34, -25.70 - 2.07, -138.8, 138.8, 25075, 0, 0, 0, 0, 8490, 218, 226, 44100.00, 44104.93, 44099.87, 23.87, 35, -26.92 - 1.33, 0.0, 0.0, 26078, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44104.66, 44099.88, 23.68, 38, -26.43 - 1.67, -22.7, 22.7, 27081, 0, 0, 0, 0, 8562, 217, 225, 44100.00, 44106.16, 44099.87, 25.06, 40, -27.93 - 1.98, -28.3, 28.3, 28084, 0, 0, 0, 0, 8544, 218, 225, 44100.00, 44105.21, 44099.87, 25.91, 41, -28.84 - 1.99, -22.7, 22.7, 29087, 0, 0, 0, 0, 8569, 219, 225, 44100.00, 44102.30, 44099.87, 25.91, 41, -28.74 - 1.66, -56.6, 56.6, 30090, 0, 0, 0, 0, 8554, 218, 225, 44100.00, 44103.07, 44099.86, 26.38, 44, -29.46 - 1.44, 0.0, 0.0, 31093, 0, 0, 0, 0, 8642, 218, 225, 44100.00, 44102.18, 44099.87, 26.08, 46, -28.98 - 1.56, 0.0, 0.0, 32096, 0, 0, 0, 0, 8647, 218, 225, 44100.00, 44102.76, 44099.87, 25.79, 48, -28.64 - 1.68, 0.0, 0.0, 33099, 0, 0, 0, 0, 8647, 217, 225, 44100.00, 44103.41, 44099.88, 25.66, 49, -28.51 - 1.92, -14.2, 14.2, 34102, 0, 0, 0, 0, 8639, 217, 225, 44100.00, 44102.38, 44099.87, 25.45, 51, -28.40 - 2.00, -45.3, 45.3, 35105, 0, 0, 0, 0, 8593, 216, 225, 44100.00, 44103.23, 44099.86, 25.35, 53, -28.61 - 1.98, -17.0, 17.0, 36108, 0, 0, 0, 0, 8609, 218, 225, 44100.00, 44102.54, 44099.87, 25.21, 55, -28.12 - 1.99, -31.2, 31.2, 37111, 0, 0, 0, 0, 8575, 217, 225, 44100.00, 44101.98, 44099.88, 25.11, 57, -27.88 - 1.99, -14.2, 14.2, 38114, 0, 0, 0, 0, 8624, 218, 225, 44100.00, 44104.44, 44099.87, 24.95, 60, -27.97 - 1.99, -28.3, 28.3, 39117, 0, 0, 0, 0, 8582, 209, 225, 44100.00, 44104.91, 44099.86, 24.88, 62, -27.98 - 1.86, -17.0, 17.0, 40120, 0, 0, 0, 0, 8504, 207, 225, 44100.00, 44104.52, 44099.88, 24.66, 63, -27.39 - 1.96, -56.6, 56.6, 41123, 0, 0, 0, 0, 8517, 212, 226, 44100.00, 44102.29, 44099.88, 24.54, 66, -27.34 - 1.90, -36.8, 36.8, 42126, 0, 0, 0, 0, 8363, 218, 240, 44100.00, 44116.79, 44099.87, 24.55, 68, -27.48 - 1.89, 0.0, 0.0, 43129, 0, 0, 0, 0, 8487, 227, 241, 44100.00, 44117.57, 44099.88, 24.56, 69, -27.32 - 1.99, -22.7, 22.7, 44132, 0, 0, 0, 0, 8576, 227, 240, 44100.00, 44118.53, 44099.87, 24.57, 70, -27.44 - 1.98, -39.7, 39.7, 45135, 0, 0, 0, 0, 8593, 228, 240, 44100.00, 44113.88, 44099.87, 24.60, 73, -27.43 - 2.00, -34.0, 34.0, 46138, 0, 0, 0, 0, 8577, 227, 240, 44100.00, 44117.10, 44099.88, 24.65, 75, -27.46 - 1.98, -22.7, 22.7, 47141, 0, 0, 0, 0, 8591, 204, 240, 44100.00, 44102.78, 44099.87, 24.67, 77, -27.59 - 1.98, -17.0, 17.0, 48144, 0, 0, 0, 0, 8587, 216, 225, 44100.00, 44102.38, 44099.88, 24.67, 79, -27.42 - 1.99, -28.3, 28.3, 49147, 0, 0, 0, 0, 8590, 214, 225, 44100.00, 44102.70, 44099.87, 24.66, 80, -27.57 - 1.85, -62.3, 62.3, 50150, 0, 0, 0, 0, 8518, 217, 226, 44100.00, 44103.16, 44099.87, 24.70, 83, -27.55 - 1.90, -28.3, 28.3, 51153, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44102.60, 44099.88, 24.66, 85, -27.42 - 1.89, -5.7, 5.7, 52156, 0, 0, 0, 0, 8615, 218, 225, 44100.00, 44103.37, 44099.88, 24.65, 86, -27.39 - 1.96, -19.8, 19.8, 53159, 0, 0, 0, 0, 8600, 219, 225, 44100.00, 44103.77, 44099.87, 24.62, 89, -27.53 - 1.99, -22.7, 22.7, 54162, 0, 0, 0, 0, 8567, 216, 225, 44100.00, 44103.12, 44099.87, 24.57, 92, -27.45 -``` - -"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output is on average 0.15 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made. - -"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. In this case, the drift is negligible, but it can routinely be up to 150 ppm, especially with older machines. - -"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections are being made. Third party programs tend to have much larger levels of corrections. - -"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.2 seconds = 8,820 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up. - -"Source nominal frames per second" is the rate at which audio is being sent to Shairport Sync according to information supplied by the source itself. - -"Source actual frames per second" is the rate at which frames of audio are actually received by Shairport Sync. This can vary a great deal due to network conditions, but over a long time (more than 30 minutes) it should settle down to an accurate value. It does not account for retransmitted packets. - -"Output frames per second" is the actual rate at which frames of audio are taken by the output device. On a system with a well-conditioned `ntp`-based clock (and without output underruns) this figure should be very accurate after playing material continuously for a period. - -"Source clock drift in ppm" is an estimate of the difference in timekeeping between the audio source and the Shairport Sync device. It is calculated from a linear regression of drift sample data. The number of samples the estimate is based on is given in the next column, "Source clock drift sample count". - -"Rough calculated correction in ppm" is a very crude estimate of the amount of interpolation that needs to applied, on average, to keep sync. It is not really to be relied on at this time. - -For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an iPhone XS Max to Shairport Sync running on a WiFi-connected Raspberry Pi 3 using an [IQaudIO Pi-DigiAMP+](http://iqaudio.co.uk/hats/9-pi-digiamp.html). - -It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway... - - -WiFi Issues ---------- -If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for more details. - -Troubleshooting ---------------- -Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for a few hints, contributed by users. - -MQTT ---------------- -Please refer to the [MQTT INFO](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md) page for additional info on building, configuring and using MQTT to interface shairport-sync with common home automation systems (contributed by users). - -Packaging status ----------------- - -[![Packaging status](https://repology.org/badge/vertical-allrepos/shairport-sync.svg)](https://repology.org/project/shairport-sync/versions) +Acknowledgements +---- From 373603729db487a09f11bf7329edab904d9319d6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:24:37 +0100 Subject: [PATCH 044/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e9b7e6b..a3ca98b5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Shairport Sync does not support AirPlay video or photo streaming. For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). -A guide to building for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +A brief guide to building for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). From d981e5ce8874f8e88e0e9ccb8220423eaba12207 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:26:03 +0100 Subject: [PATCH 045/878] Delete BUILDFORAP1.md --- BUILDFORAP1.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 BUILDFORAP1.md diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md deleted file mode 100644 index 8b137891..00000000 --- a/BUILDFORAP1.md +++ /dev/null @@ -1 +0,0 @@ - From f74e71f3459c37448829504df90119d81bc66f7e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:26:36 +0100 Subject: [PATCH 046/878] Rename INSTALL.md to BUILDFORAP1.md --- INSTALL.md => BUILDFORAP1.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename INSTALL.md => BUILDFORAP1.md (100%) diff --git a/INSTALL.md b/BUILDFORAP1.md similarity index 100% rename from INSTALL.md rename to BUILDFORAP1.md From 16c5b1e5bb387431a685c14272510587e0d20e56 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:27:31 +0100 Subject: [PATCH 047/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 2d21b9c5..29bb9118 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -51,6 +51,8 @@ Next, download Shairport Sync, configure it, compile and install it: ``` $ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync +$ git submodule init +$ git submodule update $ autoreconf -fi $ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd $ make From 5bef3b2d4114e692b25b95d98e53a186e4274074 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:28:40 +0100 Subject: [PATCH 048/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 29bb9118..7bb44341 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -51,6 +51,7 @@ Next, download Shairport Sync, configure it, compile and install it: ``` $ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync +$ git checkout development $ git submodule init $ git submodule update $ autoreconf -fi @@ -71,7 +72,7 @@ general = alsa = { output_device = "hw:0"; - mixer_control_name = "PCM"; + mixer_control_name = "Headphone"; }; ``` From 89fe38c651fc5a21acd681e22a4b3950f0ba54e0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:31:22 +0100 Subject: [PATCH 049/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 7bb44341..fc66d1c9 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -1,6 +1,6 @@ -Simple Installation Instructions +Simple Build Instructions == -Here are simple instructions for building and installing Shairport Sync on a Raspberry Pi B, 2B, 3B, 3B+ or 4B. It is assumed that the Pi is running Raspbian Buster Lite – a GUI isn't needed, since Shairport Sync runs as a daemon program. For a more thorough treatment, please go to the [README.md](https://github.com/mikebrady/shairport-sync/blob/master/README.md#building-and-installing) page. +Here are simple instructions for building and installing Shairport Sync on a recent Linux based on Debian, including Ubiuntu and Raspberry Pi OS. In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. @@ -10,7 +10,6 @@ Do the usual update and upgrade: # apt-get update # apt-get upgrade ``` -(Separately, if you haven't done so already, consider using the `raspi-config` tool to expand the file system to use the entire card.) ### Turn Off WiFi Power Management If you are using WiFi, you should turn off WiFi Power Management: @@ -19,7 +18,7 @@ If you are using WiFi, you should turn off WiFi Power Management: ``` WiFi Power Management will put the WiFi system in low-power mode when the WiFi system considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) -Reboot the Pi. +Reboot the computer. ### Remove Old Copies Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: @@ -63,7 +62,7 @@ By the way, the `autoreconf` step may take quite a while on a Raspberry Pi -- be Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: ``` -// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC +// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC: general = { volume_range_db = 60; @@ -86,6 +85,6 @@ Finally, either reboot the Pi or start the `shairport-sync` service: ``` # systemctl start shairport-sync ``` -The Shairport Sync AirPlay service should now appear on the network with a service name made from the Pi's hostname with the first letter capitalised, e.g. hostname `raspberrypi` gives a service name `Raspberrypi`. You can change the service name and set a password in the configuration file. +The Shairport Sync AirPlay service should now appear on the network with a service name made from the computer's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ubuntu`. You can change the service name and set a password in the configuration file. Connect and enjoy... From 986b347926da8063ac64d84e687327ea418c7efd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:32:15 +0100 Subject: [PATCH 050/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index fc66d1c9..9e9f6a9b 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -1,6 +1,6 @@ -Simple Build Instructions +Build Instructions for AirPlay 1 == -Here are simple instructions for building and installing Shairport Sync on a recent Linux based on Debian, including Ubiuntu and Raspberry Pi OS. +Here are simple instructions for building and installing Shairport Sync to provide an AirPlay 1 service on a recent Linux based on Debian, including Ubuntu and Raspberry Pi OS. In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. From 58a4eb119d0516d0f2d68b3f6fbbd3ff97f2409c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:38:44 +0100 Subject: [PATCH 051/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 8b137891..aac72954 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1 +1,123 @@ +Experimental Build Instructions for AirPlay 2 +== +* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is critical! +* Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! +* Initially, at least, leave the settings in the configuration file at default except as noted below. +* At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. +* At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). + +Installation Instructions +== +In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. + +### Turn Off WiFi Power Management +If you are using WiFi, you should turn off WiFi Power Management: +``` +# iwconfig wlan0 power off +``` +WiFi Power Management will put the WiFi system in low-power mode when the WiFi system considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) + +### Configure and Update +Do the usual update and upgrade: +``` +# apt-get update +# apt-get upgrade +``` +Reboot. + +### Remove Old Copies +Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: +``` +$ which shairport-sync +/usr/local/bin/shairport-sync +``` +Remove it as follows: +``` +# rm /usr/local/bin/shairport-sync +``` +Do this until no more copies of `shairport-sync` are found. + +### Remove Old Startup Scripts +You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. + +### Reboot after Cleaning Up +If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. + +### Tools and Libraries +Okay, now let's get the tools and libraries for building and installing Shairport Sync. + +First, install the packages needed by Shairport Sync: +``` +# apt-get install build-essential git xmltoman autoconf automake libtool \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev +``` +(Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) + +### nqptp ### +Download, install and start/enable `nqptp` ("Not Quite PTP") from [here](https://github.com/mikebrady/nqptp). + +***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. + +The `nqptp` service monitors PTP clocks. It provides a POSIX Shared Memory Interface (SMI) at `/nqptp`. A shared +pthread mutex is contained within the interface, and to use it you need write access. This is available +to the `root` user and also to members of the unix group `nqptp`, which is created by the `nqptp` installer. + +You should include the relevant users in the `nqptp` group -- including the `shairport-sync` user if you are using the standard Shairport Sync installer. + +Next, download Shairport Sync, configure it, compile and install it: +``` +$ git clone git@github.com:mikebrady/sps2-ex1.git +$ cd sps2-ex1 +$ git checkout development +$ git submodule init +$ git submodule update +$ autoreconf -fi +$ CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata \ + --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 +$ make -j +# make install +``` +Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: +``` +// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC +general = +{ + volume_range_db = 60; +}; + +alsa = +{ + output_device = "hw:0"; + mixer_control_name = "Headphone"; +}; + +``` +The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range. + +### Automatic Start ### + +To enable Shairport Sync to start automatically on boot up: +``` +# systemctl enable shairport-sync +``` +Now, either reboot the Pi or start the `shairport-sync` service: +``` +# systemctl start shairport-sync +``` + +### Running From the Command Line ### + +You may wish to run Shairport Sync from the command line (but remember to ensure it is not already running as a daemon). To enable debug messages and statistics, use the following: + +``` +$ shairport-sync -vu --statistics +``` +The user doesn't need to be privileged, but should be a member of the `nqptp` group for access to the `nqptp` service, and it should also be a member of the `audio` group for access to the `alsa` subsystem. + +### Using Shairport Sync ### + +The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ububntu`. You can change the service name in the configuration file. + +Connect and enjoy... From b95cfb0c6613af8979f96afd6d9824a092a09d2a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:39:08 +0100 Subject: [PATCH 052/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index aac72954..f5306671 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,6 +1,6 @@ Experimental Build Instructions for AirPlay 2 == -* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is critical! +* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected! * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. From 51952a150f67d06450c07eb1931fc5dc8c605c21 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:51:04 +0100 Subject: [PATCH 053/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index f5306671..4b6a3aeb 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,6 +5,9 @@ Experimental Build Instructions for AirPlay 2 * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). +* AirPlay 2 uses a timing system based on the Precision Timing Protocol (PTP). Shairport Sync relies on a program called `nqptp` ("Not Quite PTP") to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. + In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use an NTP service to keep the system clock synchronised with world time.) + When Shairport Sync is updated, it may be necessary to update `nqptp` at the same time. Installation Instructions == @@ -67,7 +70,7 @@ You should include the relevant users in the `nqptp` group -- including the `sha Next, download Shairport Sync, configure it, compile and install it: ``` -$ git clone git@github.com:mikebrady/sps2-ex1.git +$ git clone git@github.com:aillwee/shairport-sync.git $ cd sps2-ex1 $ git checkout development $ git submodule init @@ -101,7 +104,7 @@ To enable Shairport Sync to start automatically on boot up: ``` # systemctl enable shairport-sync ``` -Now, either reboot the Pi or start the `shairport-sync` service: +Now, either reboot or start the `shairport-sync` service: ``` # systemctl start shairport-sync ``` From 85962089ecf84a2ab30f857f4d0d99014cbd9cf4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:54:14 +0100 Subject: [PATCH 054/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 4b6a3aeb..b24017da 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -7,7 +7,7 @@ Experimental Build Instructions for AirPlay 2 * At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). * AirPlay 2 uses a timing system based on the Precision Timing Protocol (PTP). Shairport Sync relies on a program called `nqptp` ("Not Quite PTP") to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use an NTP service to keep the system clock synchronised with world time.) - When Shairport Sync is updated, it may be necessary to update `nqptp` at the same time. +* When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. Installation Instructions == @@ -58,7 +58,7 @@ First, install the packages needed by Shairport Sync: (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) ### nqptp ### -Download, install and start/enable `nqptp` ("Not Quite PTP") from [here](https://github.com/mikebrady/nqptp). +Download, install and start/enable `nqptp` from [here](https://github.com/mikebrady/nqptp). ***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. From d4a8b7af549b07a9e1aa15e240daf13ae6e903a6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:55:03 +0100 Subject: [PATCH 055/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index b24017da..9484d90a 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -71,7 +71,7 @@ You should include the relevant users in the `nqptp` group -- including the `sha Next, download Shairport Sync, configure it, compile and install it: ``` $ git clone git@github.com:aillwee/shairport-sync.git -$ cd sps2-ex1 +$ cd shairport-sync $ git checkout development $ git submodule init $ git submodule update From 6bcd75ede0a5f8c5a1889163e2ddb17d45189843 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:56:08 +0100 Subject: [PATCH 056/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 9e9f6a9b..5d4e26db 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -48,7 +48,7 @@ First, install the packages needed by Shairport Sync: ``` Next, download Shairport Sync, configure it, compile and install it: ``` -$ git clone https://github.com/mikebrady/shairport-sync.git +$ git clone https://github.com/aillwee/shairport-sync.git $ cd shairport-sync $ git checkout development $ git submodule init From ce9ae69a2153fec59efa91e5e31f485a185f1ec8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:57:36 +0100 Subject: [PATCH 057/878] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index a3ca98b5..97bb6986 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,3 @@ For more information, and a complete account of how to build Shairport Sync for A brief guide to building for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). - -Acknowledgements ----- From 17112f62be268d30e25b09a970db59c9d3d6e79c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:58:03 +0100 Subject: [PATCH 058/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97bb6986..560e6277 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,6 @@ Shairport Sync does not support AirPlay video or photo streaming. For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). -A brief guide to building for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). From 7f775ba1dfafad73ff6a028d91901b758d72e43f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 17:59:38 +0100 Subject: [PATCH 059/878] Update README.md --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 560e6277..16cb786b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ Shairport Sync ============= -Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and AirPlay sources such a [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). - -Shairport Sync supports AirPlay 1 or AirPlay 2. AirPlay 2 support is experimental and incomplete -- in particular, it does not support Home integration. - -When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. AirPlay 2 support is only available on recent Linux builds. - -Shairport Sync does not support AirPlay video or photo streaming. +* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and AirPlay sources such a [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). +* Shairport Sync supports AirPlay 1 or AirPlay 2. +* **AirPlay 2 support is experimental and incomplete** -- in particular, it does not support Home integration. +* When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. +* AirPlay 2 support is only available on recent Linux builds. +* Shairport Sync does not support AirPlay video or photo streaming. +* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +* To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). - -A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). - -To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). From 33c497436b26e011d02beb77a65385ce50bedac8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 18:05:47 +0100 Subject: [PATCH 060/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 9484d90a..88ffd0a0 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,8 +5,9 @@ Experimental Build Instructions for AirPlay 2 * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). -* AirPlay 2 uses a timing system based on the Precision Timing Protocol (PTP). Shairport Sync relies on a program called `nqptp` ("Not Quite PTP") to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use an NTP service to keep the system clock synchronised with world time.) +* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called `nqptp` ("Not Quite PTP") to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. + In addition, `nqptp` must run with `root` privileges. + (FYI, most computers do not have a PTP clock running -- instead, they may use an NTP service to keep the system clock synchronised with world time.) * When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. Installation Instructions From ddd6929666bb28e6a87ed9390850cb346083610f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 18:08:15 +0100 Subject: [PATCH 061/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 88ffd0a0..d595029e 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,9 +5,9 @@ Experimental Build Instructions for AirPlay 2 * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). -* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called `nqptp` ("Not Quite PTP") to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. +* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp` ("Not Quite PTP")](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. - (FYI, most computers do not have a PTP clock running -- instead, they may use an NTP service to keep the system clock synchronised with world time.) + (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. Installation Instructions From 3c175f38dd78de93485e7b081b4de6ddb6c270f8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 May 2021 18:08:48 +0100 Subject: [PATCH 062/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index d595029e..b64ffa1f 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,7 +5,7 @@ Experimental Build Instructions for AirPlay 2 * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). -* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp` ("Not Quite PTP")](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. +* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. From 59b87c7d6640e1123080f7b985e8d2305df083e5 Mon Sep 17 00:00:00 2001 From: dasl- Date: Sun, 2 May 2021 21:33:26 -0400 Subject: [PATCH 063/878] update raspberry pi installation guide as per https://github.com/mikebrady/shairport-sync/issues/1183 --- INSTALL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 2d21b9c5..fa36f91b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -9,7 +9,7 @@ Do the usual update and upgrade: ``` # apt-get update # apt-get upgrade -``` +``` (Separately, if you haven't done so already, consider using the `raspi-config` tool to expand the file system to use the entire card.) ### Turn Off WiFi Power Management @@ -63,13 +63,13 @@ Now to configure Shairport Sync. Here are the important options for the Shairpor // Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC general = { - volume_range_db = 60; + volume_range_db = 60; }; alsa = { - output_device = "hw:0"; - mixer_control_name = "PCM"; + output_device = "hw:Headphones"; + mixer_control_name = "Headphone"; }; ``` From 0c732417bbe0d4e8aa66158908313aae96c22428 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 07:54:29 +0100 Subject: [PATCH 064/878] Update README.md --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16cb786b..24ad8d0b 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,23 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and AirPlay sources such a [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). * Shairport Sync supports AirPlay 1 or AirPlay 2. -* **AirPlay 2 support is experimental and incomplete** -- in particular, it does not support Home integration. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good and stable audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. -* Shairport Sync does not support AirPlay video or photo streaming. * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +Shairport Sync does not support AirPlay video or photo streaming. + For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). + +Acknowledgements +--- +Special thanks are due to: +* @ejurgensen for advice and code to deal with pairing and encryption. +* @ckdo for pointing the way, with a functional Python implementation. +* @jds for thorough testing, support and encouragement. +* @invano for showing what might be possible. +* @ccc for testing and enthusiasm. + +And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From 068eea65a7ecef9eac5ffb052254d57a68fc5de9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:02:59 +0100 Subject: [PATCH 065/878] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 24ad8d0b..06d672a5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Shairport Sync ============= -* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and AirPlay sources such a [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). +* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). * Shairport Sync supports AirPlay 1 or AirPlay 2. * **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good and stable audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. @@ -16,10 +16,10 @@ For more information, and a complete account of how to build Shairport Sync for Acknowledgements --- Special thanks are due to: -* @ejurgensen for advice and code to deal with pairing and encryption. -* @ckdo for pointing the way, with a functional Python implementation. -* @jds for thorough testing, support and encouragement. -* @invano for showing what might be possible. -* @ccc for testing and enthusiasm. +* [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement. +* [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). +* [ckdo](https://github.com/ckdo) for pointing the way, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. +* [invano](https://github.com/invano) for showing what might be possible and for initial Python development. +* [Charles Somer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From b6897bec5bd5d3df4d177a00b0dfb732f535906d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:05:37 +0100 Subject: [PATCH 066/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06d672a5..603cc31d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For more information, and a complete account of how to build Shairport Sync for Acknowledgements --- -Special thanks are due to: +For the development of AirPlay 2 support, special thanks are due to: * [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement. * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. From 67a5f5a6e3b2b0e38f415517f02b1334b43d6c7e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:06:06 +0100 Subject: [PATCH 067/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 603cc31d..d7fd4fab 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). * Shairport Sync supports AirPlay 1 or AirPlay 2. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good and stable audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good, stable, audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). From 01689866dca4e5ce7fb6cdd83a0981a9a99881a6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:07:21 +0100 Subject: [PATCH 068/878] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index d7fd4fab..b83f3bbf 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,18 @@ Shairport Sync * **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good, stable, audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. + +Guides +=== * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +Note +=== Shairport Sync does not support AirPlay video or photo streaming. +More Information +=== For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). Acknowledgements From f0d29d566d7e2bd1fe38af4d6c5aab818dc84e65 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:12:25 +0100 Subject: [PATCH 069/878] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b83f3bbf..9c649902 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync supports AirPlay 1 or AirPlay 2. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is to get a good, stable, audio experience. Thus, many features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. +* Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on a good, stable, audio experience. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. From 4a907543be860efc4effb6aa3504f2b46559fdea Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:13:50 +0100 Subject: [PATCH 070/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c649902..fe702aac 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Acknowledgements For the development of AirPlay 2 support, special thanks are due to: * [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement. * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). -* [ckdo](https://github.com/ckdo) for pointing the way, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. +* [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. * [Charles Somer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. From ed886b273aa8333a974c08d0ea6edced29d5de93 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:14:20 +0100 Subject: [PATCH 071/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fe702aac..20d9e84c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Shairport Sync does not support AirPlay video or photo streaming. More Information === -For more information, and a complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). +For more information, and for a more complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). Acknowledgements --- From c82ea911e6e3ac3dde6bd4ab8e141e6d0a61b924 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 08:14:46 +0100 Subject: [PATCH 072/878] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20d9e84c..ac852a70 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,16 @@ Shairport Sync * AirPlay 2 support is only available on recent Linux builds. Guides -=== +--- * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). Note -=== +--- Shairport Sync does not support AirPlay video or photo streaming. More Information -=== +--- For more information, and for a more complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). Acknowledgements From cc69cad64f11578ea5f0f6cdd76abc62d00c5fb9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 09:00:06 +0100 Subject: [PATCH 073/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac852a70..e334cea9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). * Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on a good, stable, audio experience. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on a good audio performance. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. From 45add4f32433ba398eca009a8a5a9b7ff5d174cf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 09:00:52 +0100 Subject: [PATCH 074/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e334cea9..ee22144c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). * Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on a good audio performance. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on good audio performance. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. * AirPlay 2 support is only available on recent Linux builds. From 9dcef9f88cb2f56d0a4333ad3ec47f9bf2d441a0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 09:02:55 +0100 Subject: [PATCH 075/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index b64ffa1f..6c205212 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -9,8 +9,9 @@ Experimental Build Instructions for AirPlay 2 In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. +* Build instructions are likely to change. -Installation Instructions +Build and Install Instructions == In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. From 073e551ce834fbee0de3f6bafe4e4e4d82656ea6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 May 2021 09:03:31 +0100 Subject: [PATCH 076/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 6c205212..47f002b4 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -13,7 +13,7 @@ Experimental Build Instructions for AirPlay 2 Build and Install Instructions == -In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. +In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. ### Turn Off WiFi Power Management If you are using WiFi, you should turn off WiFi Power Management: From 493e9604d3dcdaf41a9debb2e92cad8639d13c78 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 4 May 2021 20:34:37 +0200 Subject: [PATCH 077/878] Use pair_ap for pairing and rtsp encryption --- Makefile.am | 6 +- csrp/LICENSE | 21 -- csrp/README.md | 155 -------- csrp/srp.c | 955 ------------------------------------------------- csrp/srp.h | 193 ---------- pair_ap | 2 +- player.h | 26 +- rtsp.c | 700 ++++++------------------------------ 8 files changed, 141 insertions(+), 1917 deletions(-) delete mode 100644 csrp/LICENSE delete mode 100644 csrp/README.md delete mode 100644 csrp/srp.c delete mode 100644 csrp/srp.h diff --git a/Makefile.am b/Makefile.am index f2d20713..b20ffbc3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,7 +2,6 @@ SUBDIRS = man ARFLAGS = cr lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_OPENSSL -pthread -lib_csrp_a_CFLAGS = -pthread lib_tinyhttp_a_CFLAGS = -pthread lib_dbus_interface_a_CFLAGS = -pthread lib_mpris_interface_a_CFLAGS = -pthread @@ -116,10 +115,9 @@ endif if USE_AIRPLAY_2 shairport_sync_SOURCES += ptp-utilities.c plist_xml_strings.c -shairport_sync_LDADD += lib_csrp.a lib_pair_ap.a +shairport_sync_LDADD += lib_pair_ap.a lib_pair_ap_a_SOURCES = pair_ap/pair.c pair_ap/pair_fruit.c pair_ap/pair_homekit.c pair_ap/pair-tlv.c -lib_csrp_a_SOURCES = csrp/srp.c -noinst_LIBRARIES += lib_pair_ap.a lib_csrp.a +noinst_LIBRARIES += lib_pair_ap.a plist_xml_strings.h: plists/get_info_response.xml echo "// Do not edit!" > plist_xml_strings.h echo "// This file is automatically generated from files in the plists folder." >> plist_xml_strings.h diff --git a/csrp/LICENSE b/csrp/LICENSE deleted file mode 100644 index 030645de..00000000 --- a/csrp/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Tom Cocagne - -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. diff --git a/csrp/README.md b/csrp/README.md deleted file mode 100644 index 4e655bc5..00000000 --- a/csrp/README.md +++ /dev/null @@ -1,155 +0,0 @@ -csrp -==== -Tom Cocagne <tom.cocagne@gmail.com> - -csrp is a minimal C implementation of the [Secure Remote Password -protocol](http://srp.stanford.edu/). The project consists of a single -C file and is intended for direct inclusion into utilizing programs. -It's only dependency is OpenSSL. - -*NOTE* This SRP implementation was created before the hashing algoritim specified -in RFC 5054 became the de-facto standard for interoperable SRP implementations. -The rfc5054_compat branch of this repository uses the RFC 5054 hashing algorithms -and is known to be compatible with other SRP implementations. If this version works -for you, please consider submitting a patch to this library that implements both the -original default and the RFC 5054 implementation to allow a single mainline version -of this library going forward. - - -SRP Overview ------------- - -SRP is a cryptographically strong authentication -protocol for password-based, mutual authentication over an insecure -network connection. - -Unlike other common challenge-response autentication protocols, such -as Kerberos and SSL, SRP does not rely on an external infrastructure -of trusted key servers or certificate management. Instead, SRP server -applications use verification keys derived from each user's password -to determine the authenticity of a network connection. - -SRP provides mutual-authentication in that successful authentication -requires both sides of the connection to have knowledge of the -user's password. If the client side lacks the user's password or the -server side lacks the proper verification key, the authentication will -fail. - -Unlike SSL, SRP does not directly encrypt all data flowing through -the authenticated connection. However, successful authentication does -result in a cryptographically strong shared key that can be used -for symmetric-key encryption. - -This library serves as the basis for a compatible Python module called -[pysrp](https://github.com/cocagne/pysrp). The -[pysrp](https://github.com/cocagne/pysrp) project contains complete, -user-friendly API documentation as well as a comprehensive overview of the SRP -protocol. As the APIs are virtually identical, the [pysrp -documentation](http://pythonhosted.org/srp/) is an excellent reference for -understanding this library. - - -Usage Example -------------- - -```c -#include -#include -#include - -#include "srp.h" - - -int main( int argc, char * argv[] ) -{ - int auth_failed = 1; - - struct SRPVerifier * ver; - struct SRPUser * usr; - - const unsigned char * bytes_s = 0; - const unsigned char * bytes_v = 0; - const unsigned char * bytes_A = 0; - const unsigned char * bytes_B = 0; - - const unsigned char * bytes_M = 0; - const unsigned char * bytes_HAMK = 0; - - int len_s = 0; - int len_v = 0; - int len_A = 0; - int len_B = 0; - int len_M = 0; - - const char * username = "testuser"; - const char * password = "password"; - - const char * auth_username = 0; - - SRP_HashAlgorithm alg = SRP_SHA1; - SRP_NGType ng_type = SRP_NG_2048; - - /* Create a salt+verification key for the user's password. The salt and - * key need to be computed at the time the user's password is set and - * must be stored by the server-side application for use during the - * authentication process. - */ - srp_create_salted_verification_key( alg, ng_type, username, - (const unsigned char *)password, - strlen(password), - &bytes_s, &len_s, - &bytes_v, &len_v, - NULL, NULL ); - - /* Begin authentication process */ - usr = srp_user_new( alg, ng_type, username, - (const unsigned char *)password, - strlen(password), NULL, NULL ); - - srp_user_start_authentication( usr, &auth_username, &bytes_A, &len_A ); - - /* User -> Host: (username, bytes_A) */ - ver = srp_verifier_new( alg, ng_type, username, bytes_s, len_s, bytes_v, len_v, - bytes_A, len_A, & bytes_B, &len_B, NULL, NULL ); - - if ( !bytes_B ) { - printf("Verifier SRP-6a safety check violated!\n"); - goto auth_failed; - } - - /* Host -> User: (bytes_s, bytes_B) */ - srp_user_process_challenge( usr, bytes_s, len_s, bytes_B, len_B, &bytes_M, &len_M ); - - if ( !bytes_M ) { - printf("User SRP-6a safety check violation!\n"); - goto auth_failed; - } - - /* User -> Host: (bytes_M) */ - srp_verifier_verify_session( ver, bytes_M, &bytes_HAMK ); - - if ( !bytes_HAMK ) { - printf("User authentication failed!\n"); - goto auth_failed; - } - - /* Host -> User: (HAMK) */ - srp_user_verify_session( usr, bytes_HAMK ); - - if ( !srp_user_is_authenticated(usr) ) { - printf("Server authentication failed!\n"); - goto auth_failed; - } - - auth_failed = 0; /* auth success! */ - -auth_failed: - srp_verifier_delete( ver ); - srp_user_delete( usr ); - - free( (char *)bytes_s ); - free( (char *)bytes_v ); - - return auth_failed; -} -``` diff --git a/csrp/srp.c b/csrp/srp.c deleted file mode 100644 index 1bfd908b..00000000 --- a/csrp/srp.c +++ /dev/null @@ -1,955 +0,0 @@ -/* - * Secure Remote Password 6a implementation - * Copyright (c) 2010 Tom Cocagne. All rights reserved. - * https://github.com/cocagne/csrp - * - * The MIT License (MIT) - * - * Copyright (c) 2013 Tom Cocagne - * - * 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. - * - */ - -#ifdef WIN32 - #include -#else - #include -#endif - -#include -#include -#include - -#include -#include -#include -#include - - -#include "srp.h" - -static int g_initialized = 0; - -typedef struct -{ - BIGNUM * N; - BIGNUM * g; -} NGConstant; - -struct NGHex -{ - const char * n_hex; - const char * g_hex; -}; - -/* All constants here were pulled from Appendix A of RFC 5054 */ -static struct NGHex global_Ng_constants[] = { - { /* 1024 */ - "EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496" - "EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E" - "F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA" - "9AFD5138FE8376435B9FC61D2FC0EB06E3", - "2" - }, - { /* 2048 */ - "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" - "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" - "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" - "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" - "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" - "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" - "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", - "2" - }, - { /* 4096 */ - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" - "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" - "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" - "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" - "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" - "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" - "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" - "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" - "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" - "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" - "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" - "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" - "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" - "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" - "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" - "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" - "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" - "FFFFFFFFFFFFFFFF", - "5" - }, - { /* 8192 */ - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" - "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" - "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" - "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" - "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" - "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" - "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" - "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" - "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" - "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" - "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" - "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" - "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" - "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" - "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" - "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" - "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" - "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" - "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406" - "AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918" - "DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151" - "2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03" - "F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F" - "BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" - "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B" - "B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632" - "387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E" - "6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA" - "3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C" - "5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" - "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886" - "2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6" - "6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5" - "0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268" - "359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6" - "FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" - "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", - "13" - }, - {0,0} /* null sentinel */ -}; - - -static NGConstant * new_ng( SRP_NGType ng_type, const char * n_hex, const char * g_hex ) -{ - NGConstant * ng = (NGConstant *) malloc( sizeof(NGConstant) ); - ng->N = BN_new(); - ng->g = BN_new(); - - if( !ng || !ng->N || !ng->g ) - return 0; - - if ( ng_type != SRP_NG_CUSTOM ) - { - n_hex = global_Ng_constants[ ng_type ].n_hex; - g_hex = global_Ng_constants[ ng_type ].g_hex; - } - - BN_hex2bn( &ng->N, n_hex ); - BN_hex2bn( &ng->g, g_hex ); - - return ng; -} - -static void delete_ng( NGConstant * ng ) -{ - if (ng) - { - BN_free( ng->N ); - BN_free( ng->g ); - ng->N = 0; - ng->g = 0; - free(ng); - } -} - - - -typedef union -{ - SHA_CTX sha; - SHA256_CTX sha256; - SHA512_CTX sha512; -} HashCTX; - - -struct SRPVerifier -{ - SRP_HashAlgorithm hash_alg; - NGConstant *ng; - - const char * username; - const unsigned char * bytes_B; - int authenticated; - - unsigned char M [SHA512_DIGEST_LENGTH]; - unsigned char H_AMK [SHA512_DIGEST_LENGTH]; - unsigned char session_key [SHA512_DIGEST_LENGTH]; -}; - - -struct SRPUser -{ - SRP_HashAlgorithm hash_alg; - NGConstant *ng; - - BIGNUM *a; - BIGNUM *A; - BIGNUM *S; - - const unsigned char * bytes_A; - int authenticated; - - const char * username; - const unsigned char * password; - int password_len; - - unsigned char M [SHA512_DIGEST_LENGTH]; - unsigned char H_AMK [SHA512_DIGEST_LENGTH]; - unsigned char session_key [SHA512_DIGEST_LENGTH]; -}; - - -static int hash_init( SRP_HashAlgorithm alg, HashCTX *c ) -{ - switch (alg) - { - case SRP_SHA1 : return SHA1_Init( &c->sha ); - case SRP_SHA224: return SHA224_Init( &c->sha256 ); - case SRP_SHA256: return SHA256_Init( &c->sha256 ); - case SRP_SHA384: return SHA384_Init( &c->sha512 ); - case SRP_SHA512: return SHA512_Init( &c->sha512 ); - default: - return -1; - }; -} -static int hash_update( SRP_HashAlgorithm alg, HashCTX *c, const void *data, size_t len ) -{ - switch (alg) - { - case SRP_SHA1 : return SHA1_Update( &c->sha, data, len ); - case SRP_SHA224: return SHA224_Update( &c->sha256, data, len ); - case SRP_SHA256: return SHA256_Update( &c->sha256, data, len ); - case SRP_SHA384: return SHA384_Update( &c->sha512, data, len ); - case SRP_SHA512: return SHA512_Update( &c->sha512, data, len ); - default: - return -1; - }; -} -static int hash_final( SRP_HashAlgorithm alg, HashCTX *c, unsigned char *md ) -{ - switch (alg) - { - case SRP_SHA1 : return SHA1_Final( md, &c->sha ); - case SRP_SHA224: return SHA224_Final( md, &c->sha256 ); - case SRP_SHA256: return SHA256_Final( md, &c->sha256 ); - case SRP_SHA384: return SHA384_Final( md, &c->sha512 ); - case SRP_SHA512: return SHA512_Final( md, &c->sha512 ); - default: - return -1; - }; -} -static unsigned char * hash( SRP_HashAlgorithm alg, const unsigned char *d, size_t n, unsigned char *md ) -{ - switch (alg) - { - case SRP_SHA1 : return SHA1( d, n, md ); - case SRP_SHA224: return SHA224( d, n, md ); - case SRP_SHA256: return SHA256( d, n, md ); - case SRP_SHA384: return SHA384( d, n, md ); - case SRP_SHA512: return SHA512( d, n, md ); - default: - return 0; - }; -} -static int hash_length( SRP_HashAlgorithm alg ) -{ - switch (alg) - { - case SRP_SHA1 : return SHA_DIGEST_LENGTH; - case SRP_SHA224: return SHA224_DIGEST_LENGTH; - case SRP_SHA256: return SHA256_DIGEST_LENGTH; - case SRP_SHA384: return SHA384_DIGEST_LENGTH; - case SRP_SHA512: return SHA512_DIGEST_LENGTH; - default: - return -1; - }; -} - - -static BIGNUM * H_nn( SRP_HashAlgorithm alg, const BIGNUM * n1, const BIGNUM * n2 ) -{ - unsigned char buff[ SHA512_DIGEST_LENGTH ]; - int len_n1 = BN_num_bytes(n1); - int len_n2 = BN_num_bytes(n2); - int nbytes = len_n1 + len_n2; - unsigned char * bin = (unsigned char *) malloc( nbytes ); - if (!bin) - return 0; - BN_bn2bin(n1, bin); - BN_bn2bin(n2, bin + len_n1); - hash( alg, bin, nbytes, buff ); - free(bin); - return BN_bin2bn(buff, hash_length(alg), NULL); -} - -static BIGNUM * H_nn_pad( SRP_HashAlgorithm alg, const BIGNUM * n1, const BIGNUM * n2 ) -{ - unsigned char * bin; - unsigned char buff[ SHA512_DIGEST_LENGTH ]; - int len_n1 = BN_num_bytes(n1); - int len_n2 = BN_num_bytes(n2); - int nbytes = 2 * len_n1; - - if ((len_n2 < 1) || (len_n2 > len_n1)) - return 0; - bin = (unsigned char *) calloc( 1, nbytes ); - if (!bin) - return 0; - BN_bn2bin(n1, bin); - BN_bn2bin(n2, bin + nbytes - len_n2); - hash( alg, bin, nbytes, buff ); - free(bin); - return BN_bin2bn(buff, hash_length(alg), NULL); -} - -static BIGNUM * H_ns( SRP_HashAlgorithm alg, const BIGNUM * n, const unsigned char * bytes, int len_bytes ) -{ - unsigned char buff[ SHA512_DIGEST_LENGTH ]; - int len_n = BN_num_bytes(n); - int nbytes = len_n + len_bytes; - unsigned char * bin = (unsigned char *) malloc( nbytes ); - if (!bin) - return 0; - BN_bn2bin(n, bin); - memcpy( bin + len_n, bytes, len_bytes ); - hash( alg, bin, nbytes, buff ); - free(bin); - return BN_bin2bn(buff, hash_length(alg), NULL); -} - -static BIGNUM * calculate_x( SRP_HashAlgorithm alg, const BIGNUM * salt, const char * username, const unsigned char * password, int password_len ) -{ - unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; - HashCTX ctx; - - hash_init( alg, &ctx ); - - hash_update( alg, &ctx, username, strlen(username) ); - hash_update( alg, &ctx, ":", 1 ); - hash_update( alg, &ctx, password, password_len ); - - hash_final( alg, &ctx, ucp_hash ); - - return H_ns( alg, salt, ucp_hash, hash_length(alg) ); -} - -static void update_hash_n( SRP_HashAlgorithm alg, HashCTX *ctx, const BIGNUM * n ) -{ - unsigned long len = BN_num_bytes(n); - unsigned char * n_bytes = (unsigned char *) malloc( len ); - if (!n_bytes) - return; - BN_bn2bin(n, n_bytes); - hash_update(alg, ctx, n_bytes, len); - free(n_bytes); -} - -static void hash_num( SRP_HashAlgorithm alg, const BIGNUM * n, unsigned char * dest ) -{ - int nbytes = BN_num_bytes(n); - unsigned char * bin = (unsigned char *) malloc( nbytes ); - if(!bin) - return; - BN_bn2bin(n, bin); - hash( alg, bin, nbytes, dest ); - free(bin); -} - -static void calculate_M( SRP_HashAlgorithm alg, NGConstant *ng, unsigned char * dest, const char * I, const BIGNUM * s, - const BIGNUM * A, const BIGNUM * B, const unsigned char * K ) -{ - unsigned char H_N[ SHA512_DIGEST_LENGTH ]; - unsigned char H_g[ SHA512_DIGEST_LENGTH ]; - unsigned char H_I[ SHA512_DIGEST_LENGTH ]; - unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; - HashCTX ctx; - int i = 0; - int hash_len = hash_length(alg); - - hash_num( alg, ng->N, H_N ); - hash_num( alg, ng->g, H_g ); - - hash(alg, (const unsigned char *)I, strlen(I), H_I); - - - for (i=0; i < hash_len; i++ ) - H_xor[i] = H_N[i] ^ H_g[i]; - - hash_init( alg, &ctx ); - - hash_update( alg, &ctx, H_xor, hash_len ); - hash_update( alg, &ctx, H_I, hash_len ); - update_hash_n( alg, &ctx, s ); - update_hash_n( alg, &ctx, A ); - update_hash_n( alg, &ctx, B ); - hash_update( alg, &ctx, K, hash_len ); - - hash_final( alg, &ctx, dest ); -} - -static void calculate_H_AMK( SRP_HashAlgorithm alg, unsigned char *dest, const BIGNUM * A, const unsigned char * M, const unsigned char * K ) -{ - HashCTX ctx; - - hash_init( alg, &ctx ); - - update_hash_n( alg, &ctx, A ); - hash_update( alg, &ctx, M, hash_length(alg) ); - hash_update( alg, &ctx, K, hash_length(alg) ); - - hash_final( alg, &ctx, dest ); -} - - -static void init_random() -{ -#ifdef WIN32 - HCRYPTPROV wctx; -#else - FILE *fp = 0; -#endif - - unsigned char buff[64]; - - if (g_initialized) - return; - -#ifdef WIN32 - - CryptAcquireContext(&wctx, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT); - - CryptGenRandom(wctx, sizeof(buff), (BYTE*) buff); - - CryptReleaseContext(wctx, 0); - - g_initialized = 1; - -#else - fp = fopen("/dev/urandom", "r"); - - if (fp) - { - size_t read = fread(buff, sizeof(buff), 1, fp); - g_initialized = read == 1; - fclose(fp); - } -#endif - - if (g_initialized) - RAND_seed( buff, sizeof(buff) ); -} - - -/*********************************************************************************************************** - * - * Exported Functions - * - ***********************************************************************************************************/ - -void srp_random_seed( const unsigned char * random_data, int data_length ) -{ - g_initialized = 1; - - if (random_data) - RAND_seed( random_data, data_length ); -} - - -void srp_create_salted_verification_key( SRP_HashAlgorithm alg, - SRP_NGType ng_type, const char * username, - const unsigned char * password, int len_password, - const unsigned char ** bytes_s, int * len_s, - const unsigned char ** bytes_v, int * len_v, - const char * n_hex, const char * g_hex ) -{ - BIGNUM * s = BN_new(); - BIGNUM * v = BN_new(); - BIGNUM * x = 0; - BN_CTX * ctx = BN_CTX_new(); - NGConstant * ng = new_ng( ng_type, n_hex, g_hex ); - - if( !s || !v || !ctx || !ng ) - goto cleanup_and_exit; - - init_random(); /* Only happens once */ - - BN_rand(s, 4*32, -1, 0); // MODIFIED!!! - - x = calculate_x( alg, s, username, password, len_password ); - - if( !x ) - goto cleanup_and_exit; - - BN_mod_exp(v, ng->g, x, ng->N, ctx); - - *len_s = BN_num_bytes(s); - *len_v = BN_num_bytes(v); - - *bytes_s = (const unsigned char *) malloc( *len_s ); - *bytes_v = (const unsigned char *) malloc( *len_v ); - - if (!bytes_s || !bytes_v) - goto cleanup_and_exit; - - BN_bn2bin(s, (unsigned char *) *bytes_s); - BN_bn2bin(v, (unsigned char *) *bytes_v); - - cleanup_and_exit: - delete_ng( ng ); - BN_free(s); - BN_free(v); - BN_free(x); - BN_CTX_free(ctx); -} - -void -srp_verifier_get_B(SRP_HashAlgorithm alg, SRP_NGType ng_type, - const unsigned char * bytes_v, int len_v, - const unsigned char ** bytes_b, int * len_b, - const unsigned char ** bytes_B, int * len_B, - const char * n_hex, const char * g_hex) -{ - BIGNUM *v = BN_bin2bn(bytes_v, len_v, NULL); - BIGNUM *k = 0; - BIGNUM *b = BN_new(); - BIGNUM *B = BN_new(); - BIGNUM *tmp1 = BN_new(); - BIGNUM *tmp2 = BN_new(); - BN_CTX *ctx = BN_CTX_new(); - NGConstant *ng = new_ng( ng_type, n_hex, g_hex ); - - *len_b = 0; - *bytes_b = 0; - *len_B = 0; - *bytes_B = 0; - - init_random(); /* Only happens once */ - - BN_rand(b, 256, -1, 0); - - k = H_nn_pad(alg, ng->N, ng->g); //MODIFIED - - /* B = kv + g^b */ - BN_mul(tmp1, k, v, ctx); - BN_mod_exp(tmp2, ng->g, b, ng->N, ctx); - BN_mod_add(B, tmp1, tmp2, ng->N, ctx); - - *len_B = BN_num_bytes(B); - *bytes_B = (const unsigned char *) malloc( *len_B ); - BN_bn2bin( B, (unsigned char *) *bytes_B ); - - *len_b = BN_num_bytes(b); - *bytes_b = (const unsigned char *) malloc( *len_b ); - BN_bn2bin( b, (unsigned char *) *bytes_b ); - - BN_free(b); // Added -- not sure about these, but they look necessary - BN_free(B); // Added -- not sure about these, but they look necessary - BN_free(v); - BN_free(k); - BN_free(tmp1); - BN_free(tmp2); - BN_CTX_free(ctx); - delete_ng(ng); -} - -/* Out: bytes_B, len_B. - * - * On failure, bytes_B will be set to NULL and len_B will be set to 0 - */ - -struct SRPVerifier * srp_verifier_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, - const unsigned char * bytes_s, int len_s, - const unsigned char * bytes_v, int len_v, - const unsigned char * bytes_A, int len_A, - const unsigned char * bytes_b, int len_b, - const unsigned char * bytes_B, int len_B, - const char * n_hex, const char * g_hex ) -{ - BIGNUM *s = BN_bin2bn(bytes_s, len_s, NULL); - BIGNUM *v = BN_bin2bn(bytes_v, len_v, NULL); - BIGNUM *A = BN_bin2bn(bytes_A, len_A, NULL); - BIGNUM *u = 0; - BIGNUM *b = BN_bin2bn(bytes_b, len_b, NULL);; - BIGNUM *B = BN_bin2bn(bytes_B, len_B, NULL);; - BIGNUM *S = BN_new(); - BIGNUM *k = 0; - BIGNUM *tmp1 = BN_new(); - BIGNUM *tmp2 = BN_new(); - BN_CTX *ctx = BN_CTX_new(); - int ulen = strlen(username) + 1; - NGConstant *ng = new_ng( ng_type, n_hex, g_hex ); - struct SRPVerifier *ver = 0; - - if( !s || !v || !A || !B || !S || !b || !tmp1 || !tmp2 || !ctx || !ng ) - goto cleanup_and_exit; - - ver = (struct SRPVerifier *) malloc( sizeof(struct SRPVerifier) ); - - if (!ver) - goto cleanup_and_exit; - - init_random(); /* Only happens once */ - - ver->username = (char *) malloc( ulen ); - ver->hash_alg = alg; - ver->ng = ng; - - if (!ver->username) - { - free(ver); - ver = 0; - goto cleanup_and_exit; - } - - memcpy( (char*)ver->username, username, ulen ); - - ver->authenticated = 0; - - /* SRP-6a safety check */ - BN_mod(tmp1, A, ng->N, ctx); - if ( !BN_is_zero(tmp1) ) - { - k = H_nn_pad(alg, ng->N, ng->g); //MODIFIED - - u = H_nn(alg, A, B); - - /* S = (A *(v^u)) ^ b */ - BN_mod_exp(tmp1, v, u, ng->N, ctx); - BN_mul(tmp2, A, tmp1, ctx); - BN_mod_exp(S, tmp2, b, ng->N, ctx); - - hash_num(alg, S, ver->session_key); - - calculate_M( alg, ng, ver->M, username, s, A, B, ver->session_key ); - calculate_H_AMK( alg, ver->H_AMK, A, ver->M, ver->session_key ); - - ver->bytes_B = bytes_B; - } - - cleanup_and_exit: - BN_free(s); - BN_free(v); - BN_free(A); - if (u) BN_free(u); - if (k) BN_free(k); - BN_free(B); - BN_free(S); - BN_free(b); - BN_free(tmp1); - BN_free(tmp2); - BN_CTX_free(ctx); - - return ver; -} - - - - -void srp_verifier_delete( struct SRPVerifier * ver ) -{ - if (ver) - { - delete_ng( ver->ng ); - free( (char *) ver->username ); - free( (unsigned char *) ver->bytes_B ); - memset(ver, 0, sizeof(*ver)); - free( ver ); - } -} - - - -int srp_verifier_is_authenticated( struct SRPVerifier * ver ) -{ - return ver->authenticated; -} - - -const char * srp_verifier_get_username( struct SRPVerifier * ver ) -{ - return ver->username; -} - - -const unsigned char * srp_verifier_get_session_key( struct SRPVerifier * ver, int * key_length ) -{ - if (key_length) - *key_length = hash_length( ver->hash_alg ); - return ver->session_key; -} - - -int srp_verifier_get_session_key_length( struct SRPVerifier * ver ) -{ - return hash_length( ver->hash_alg ); -} - - -/* user_M must be exactly SHA512_DIGEST_LENGTH bytes in size */ -void srp_verifier_verify_session( struct SRPVerifier * ver, const unsigned char * user_M, const unsigned char ** bytes_HAMK ) -{ - if ( memcmp( ver->M, user_M, hash_length(ver->hash_alg) ) == 0 ) - { - ver->authenticated = 1; - *bytes_HAMK = ver->H_AMK; - } - else - *bytes_HAMK = NULL; -} - -/*******************************************************************************/ - -struct SRPUser * srp_user_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, - const unsigned char * bytes_password, int len_password, - const char * n_hex, const char * g_hex ) -{ - struct SRPUser *usr = (struct SRPUser *) malloc( sizeof(struct SRPUser) ); - int ulen = strlen(username) + 1; - - if (!usr) - goto err_exit; - - init_random(); /* Only happens once */ - - usr->hash_alg = alg; - usr->ng = new_ng( ng_type, n_hex, g_hex ); - - usr->a = BN_new(); - usr->A = BN_new(); - usr->S = BN_new(); - - if (!usr->ng || !usr->a || !usr->A || !usr->S) - goto err_exit; - - usr->username = (const char *) malloc(ulen); - usr->password = (const unsigned char *) malloc(len_password); - usr->password_len = len_password; - - if (!usr->username || !usr->password) - goto err_exit; - - memcpy((char *)usr->username, username, ulen); - memcpy((char *)usr->password, bytes_password, len_password); - - usr->authenticated = 0; - - usr->bytes_A = 0; - - return usr; - - err_exit: - if (usr) - { - BN_free(usr->a); - BN_free(usr->A); - BN_free(usr->S); - if (usr->username) - free((void*)usr->username); - if (usr->password) - { - memset((void*)usr->password, 0, usr->password_len); - free((void*)usr->password); - } - free(usr); - } - - return 0; -} - - - -void srp_user_delete( struct SRPUser * usr ) -{ - if( usr ) - { - BN_free( usr->a ); - BN_free( usr->A ); - BN_free( usr->S ); - - delete_ng( usr->ng ); - - memset((void*)usr->password, 0, usr->password_len); - - free((char *)usr->username); - free((char *)usr->password); - - if (usr->bytes_A) - free( (char *)usr->bytes_A ); - - memset(usr, 0, sizeof(*usr)); - free( usr ); - } -} - - - -int srp_user_is_authenticated( struct SRPUser * usr) -{ - return usr->authenticated; -} - - -const char * srp_user_get_username( struct SRPUser * usr ) -{ - return usr->username; -} - - - -const unsigned char * srp_user_get_session_key( struct SRPUser * usr, int * key_length ) -{ - if (key_length) - *key_length = hash_length( usr->hash_alg ); - return usr->session_key; -} - - -int srp_user_get_session_key_length( struct SRPUser * usr ) -{ - return hash_length( usr->hash_alg ); -} - - - -/* Output: username, bytes_A, len_A */ -void srp_user_start_authentication( struct SRPUser * usr, const char ** username, - const unsigned char ** bytes_A, int * len_A ) -{ - BN_CTX *ctx = BN_CTX_new(); - - BN_rand(usr->a, 256, -1, 0); - - BN_mod_exp(usr->A, usr->ng->g, usr->a, usr->ng->N, ctx); - - BN_CTX_free(ctx); - - *len_A = BN_num_bytes(usr->A); - *bytes_A = (const unsigned char *) malloc( *len_A ); - - if (!*bytes_A) - { - *len_A = 0; - *bytes_A = 0; - *username = 0; - return; - } - - BN_bn2bin( usr->A, (unsigned char *) *bytes_A ); - - usr->bytes_A = *bytes_A; - *username = usr->username; -} - - -/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ -void srp_user_process_challenge( struct SRPUser * usr, - const unsigned char * bytes_s, int len_s, - const unsigned char * bytes_B, int len_B, - const unsigned char ** bytes_M, int * len_M ) -{ - BIGNUM *s = BN_bin2bn(bytes_s, len_s, NULL); - BIGNUM *B = BN_bin2bn(bytes_B, len_B, NULL); - BIGNUM *u = 0; - BIGNUM *x = 0; - BIGNUM *k = 0; - BIGNUM *v = BN_new(); - BIGNUM *tmp1 = BN_new(); - BIGNUM *tmp2 = BN_new(); - BIGNUM *tmp3 = BN_new(); - BN_CTX *ctx = BN_CTX_new(); - - *len_M = 0; - *bytes_M = 0; - - if( !s || !B || !v || !tmp1 || !tmp2 || !tmp3 || !ctx ) - goto cleanup_and_exit; - - u = H_nn_pad(usr->hash_alg, usr->A, B); //MODIFIED - - if (!u) - goto cleanup_and_exit; - - x = calculate_x( usr->hash_alg, s, usr->username, usr->password, usr->password_len ); - - if (!x) - goto cleanup_and_exit; - - k = H_nn_pad(usr->hash_alg, usr->ng->N, usr->ng->g); //MODIFIED - - if (!k) - goto cleanup_and_exit; - - /* SRP-6a safety check */ - if ( !BN_is_zero(B) && !BN_is_zero(u) ) - { - BN_mod_exp(v, usr->ng->g, x, usr->ng->N, ctx); - - /* S = (B - k*(g^x)) ^ (a + ux) */ - BN_mul(tmp1, u, x, ctx); - BN_add(tmp2, usr->a, tmp1); /* tmp2 = (a + ux) */ - BN_mod_exp(tmp1, usr->ng->g, x, usr->ng->N, ctx); - BN_mul(tmp3, k, tmp1, ctx); /* tmp3 = k*(g^x) */ - BN_sub(tmp1, B, tmp3); /* tmp1 = (B - K*(g^x)) */ - BN_mod_exp(usr->S, tmp1, tmp2, usr->ng->N, ctx); - - hash_num(usr->hash_alg, usr->S, usr->session_key); - - calculate_M( usr->hash_alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key ); - calculate_H_AMK( usr->hash_alg, usr->H_AMK, usr->A, usr->M, usr->session_key ); - - *bytes_M = usr->M; - if (len_M) - *len_M = hash_length( usr->hash_alg ); - } - else - { - *bytes_M = NULL; - if (len_M) - *len_M = 0; - } - - cleanup_and_exit: - - BN_free(s); - BN_free(B); - BN_free(u); - BN_free(x); - BN_free(k); - BN_free(v); - BN_free(tmp1); - BN_free(tmp2); - BN_free(tmp3); - BN_CTX_free(ctx); -} - - -void srp_user_verify_session( struct SRPUser * usr, const unsigned char * bytes_HAMK ) -{ - if ( memcmp( usr->H_AMK, bytes_HAMK, hash_length(usr->hash_alg) ) == 0 ) - usr->authenticated = 1; -} diff --git a/csrp/srp.h b/csrp/srp.h deleted file mode 100644 index 42885691..00000000 --- a/csrp/srp.h +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Secure Remote Password 6a implementation - * Copyright (c) 2010 Tom Cocagne. All rights reserved. - * https://github.com/cocagne/csrp - * - * The MIT License (MIT) - * - * Copyright (c) 2013 Tom Cocagne - * - * 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. - * - */ - -/* - * - * Purpose: This is a direct implementation of the Secure Remote Password - * Protocol version 6a as described by - * http://srp.stanford.edu/design.html - * - * Author: tom.cocagne@gmail.com (Tom Cocagne) - * - * Dependencies: OpenSSL (and Advapi32.lib on Windows) - * - * Usage: Refer to test_srp.c for a demonstration - * - * Notes: - * This library allows multiple combinations of hashing algorithms and - * prime number constants. For authentication to succeed, the hash and - * prime number constants must match between - * srp_create_salted_verification_key(), srp_user_new(), - * and srp_verifier_new(). A recommended approach is to determine the - * desired level of security for an application and globally define the - * hash and prime number constants to the predetermined values. - * - * As one might suspect, more bits means more security. As one might also - * suspect, more bits also means more processing time. The test_srp.c - * program can be easily modified to profile various combinations of - * hash & prime number pairings. - */ - -#ifndef SRP_H -#define SRP_H - - -struct SRPVerifier; -struct SRPUser; - -typedef enum -{ - SRP_NG_1024, - SRP_NG_2048, - SRP_NG_4096, - SRP_NG_8192, - SRP_NG_CUSTOM -} SRP_NGType; - -typedef enum -{ - SRP_SHA1, - SRP_SHA224, - SRP_SHA256, - SRP_SHA384, - SRP_SHA512 -} SRP_HashAlgorithm; - - -/* This library will automatically seed the OpenSSL random number generator - * using cryptographically sound random data on Windows & Linux. If this is - * undesirable behavior or the host OS does not provide a /dev/urandom file, - * this function may be called to seed the random number generator with - * alternate data. - * - * The random data should include at least as many bits of entropy as the - * largest hash function used by the application. So, for example, if a - * 512-bit hash function is used, the random data requies at least 512 - * bits of entropy. - * - * Passing a null pointer to this function will cause this library to skip - * seeding the random number generator. This is only legitimate if it is - * absolutely known that the OpenSSL random number generator has already - * been sufficiently seeded within the running application. - * - * Notes: - * * This function is optional on Windows & Linux and mandatory on all - * other platforms. - */ -void srp_random_seed( const unsigned char * random_data, int data_length ); - - -/* Out: bytes_s, len_s, bytes_v, len_v - * - * The caller is responsible for freeing the memory allocated for bytes_s and bytes_v - * - * The n_hex and g_hex parameters should be 0 unless SRP_NG_CUSTOM is used for ng_type. - * If provided, they must contain ASCII text of the hexidecimal notation. - */ -void srp_create_salted_verification_key( SRP_HashAlgorithm alg, - SRP_NGType ng_type, const char * username, - const unsigned char * password, int len_password, - const unsigned char ** bytes_s, int * len_s, - const unsigned char ** bytes_v, int * len_v, - const char * n_hex, const char * g_hex ); - - -/* Out: bytes_B, len_B. - */ - -void -srp_verifier_get_B(SRP_HashAlgorithm alg, SRP_NGType ng_type, - const unsigned char * bytes_v, int len_v, - const unsigned char ** bytes_b, int * len_b, - const unsigned char ** bytes_B, int * len_B, - const char * n_hex, const char * g_hex); - -struct SRPVerifier * srp_verifier_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, - const unsigned char * bytes_s, int len_s, - const unsigned char * bytes_v, int len_v, - const unsigned char * bytes_A, int len_A, - const unsigned char * bytes_b, int len_b, - const unsigned char * bytes_B, int len_B, - const char * n_hex, const char * g_hex ); - - -void srp_verifier_delete( struct SRPVerifier * ver ); - - -int srp_verifier_is_authenticated( struct SRPVerifier * ver ); - - -const char * srp_verifier_get_username( struct SRPVerifier * ver ); - -/* key_length may be null */ -const unsigned char * srp_verifier_get_session_key( struct SRPVerifier * ver, int * key_length ); - - -int srp_verifier_get_session_key_length( struct SRPVerifier * ver ); - - -/* user_M must be exactly srp_verifier_get_session_key_length() bytes in size */ -void srp_verifier_verify_session( struct SRPVerifier * ver, - const unsigned char * user_M, - const unsigned char ** bytes_HAMK ); - -/*******************************************************************************/ - -/* The n_hex and g_hex parameters should be 0 unless SRP_NG_CUSTOM is used for ng_type */ -struct SRPUser * srp_user_new( SRP_HashAlgorithm alg, SRP_NGType ng_type, const char * username, - const unsigned char * bytes_password, int len_password, - const char * n_hex, const char * g_hex ); - -void srp_user_delete( struct SRPUser * usr ); - -int srp_user_is_authenticated( struct SRPUser * usr); - - -const char * srp_user_get_username( struct SRPUser * usr ); - -/* key_length may be null */ -const unsigned char * srp_user_get_session_key( struct SRPUser * usr, int * key_length ); - -int srp_user_get_session_key_length( struct SRPUser * usr ); - -/* Output: username, bytes_A, len_A */ -void srp_user_start_authentication( struct SRPUser * usr, const char ** username, - const unsigned char ** bytes_A, int * len_A ); - -/* Output: bytes_M, len_M (len_M may be null and will always be - * srp_user_get_session_key_length() bytes in size) */ -void srp_user_process_challenge( struct SRPUser * usr, - const unsigned char * bytes_s, int len_s, - const unsigned char * bytes_B, int len_B, - const unsigned char ** bytes_M, int * len_M ); - -/* bytes_HAMK must be exactly srp_user_get_session_key_length() bytes in size */ -void srp_user_verify_session( struct SRPUser * usr, const unsigned char * bytes_HAMK ); - -#endif /* Include Guard */ diff --git a/pair_ap b/pair_ap index ad2bc75e..1b9954cb 160000 --- a/pair_ap +++ b/pair_ap @@ -1 +1 @@ -Subproject commit ad2bc75edaf09816a484c39dfebf6de016247db4 +Subproject commit 1b9954cba18631c9671595fca85565308e8b0972 diff --git a/player.h b/player.h index f56562cb..ab7eda9d 100644 --- a/player.h +++ b/player.h @@ -22,7 +22,7 @@ #endif #ifdef CONFIG_AIRPLAY_2 -#include "pair_ap/pair-internal.h" +#include "pair_ap/pair.h" #include "plist/plist.h" #endif @@ -92,6 +92,23 @@ typedef struct { typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; +typedef struct { + uint8_t *data; + size_t len; + size_t size; +} ap2_buffer; + +typedef struct { + int is_encrypted; + struct pair_setup_context *setup_ctx; + struct pair_verify_context *verify_ctx; + struct pair_cipher_context *cipher_ctx; + + ap2_buffer encrypted_buf; + ap2_buffer plain_buf; +} ap2_pairing; + +/* typedef struct file_cipher_context { struct pair_cipher_context *cipher_context; int active; // can be created during a pair setup but not activated until next read @@ -100,6 +117,7 @@ typedef struct file_cipher_context { void *input_plaintext_buffer_toq; size_t input_plaintext_buffer_bytes_occupied; } file_cipher_context; +*/ #endif typedef struct { @@ -248,10 +266,6 @@ typedef struct { pthread_t rtp_realtime_audio_thread; pthread_t rtp_buffered_audio_thread; - int pairing_mode; - file_cipher_context control_cipher_context; - struct verifier_setup_context *server_setup_ctx; - int last_anchor_info_is_valid; uint64_t last_anchor_clock_offset; uint64_t last_anchor_time_of_update; @@ -264,6 +278,8 @@ typedef struct { int ap2_rate; // protect with flush mutex, 0 means don't play, 1 means play int ap2_play_enabled; // protect with flush mutex + ap2_pairing ap2_control_pairing; + int event_socket; SOCKADDR ap2_remote_control_socket_addr; // a socket pointing to the control port of the client socklen_t ap2_remote_control_socket_addr_length; diff --git a/rtsp.c b/rtsp.c index 1972ba6e..ef9a0245 100644 --- a/rtsp.c +++ b/rtsp.c @@ -85,12 +85,8 @@ #endif #ifdef CONFIG_AIRPLAY_2 -#include "csrp/srp.h" -#include "pair_ap/pair-tlv.h" #include "pair_ap/pair.h" #include "plist/plist.h" -#include -// #include "proxy_ap/server.h" #include "plist_xml_strings.h" #include "ptp-utilities.h" #endif @@ -803,408 +799,100 @@ void debug_log_rtsp_message(int level, char *prompt, rtsp_message *message) { } } -// this will read a block of the size specified to the buffer -// and will return either with the block or on error -ssize_t read_sized_block(int fd, void *buf, size_t count) { - ssize_t response, nread; - size_t inbuf = 0; // bytes already in the buffer - int keep_trying = 1; +#ifdef CONFIG_AIRPLAY_2 +static void buf_add(ap2_buffer *buf, uint8_t *in, size_t in_len) { + if (buf->len + in_len > buf->size) { + buf->size = buf->len + in_len + 2048; // Extra legroom to avoid future memcpy's + uint8_t *new = malloc(buf->size); + memcpy(new, buf->data, buf->len); + free(buf->data); + buf->data = new; + } + memcpy(buf->data + buf->len, in, in_len); + buf->len += in_len; +} + +static void buf_drain(ap2_buffer *buf, ssize_t len) { + if (len < 0 || (size_t)len >= buf->len) { + free(buf->data); + memset(buf, 0, sizeof(ap2_buffer)); + return; + } + memmove(buf->data, buf->data + len, buf->len - len); + buf->len -= len; +} + +static size_t buf_remove(ap2_buffer *buf, uint8_t *out, size_t out_len) { + size_t bytes = (buf->len > out_len) ? out_len : buf->len; + memcpy(out, buf->data, bytes); + buf_drain(buf, bytes); + return bytes; +} + +static ssize_t read_encrypted(int fd, ap2_pairing *ctx, void *buf, size_t count) { + uint8_t in[4096]; + uint8_t *plain; + size_t plain_len; + + // If there is leftover decoded content from the last pass just return that + if (ctx->plain_buf.len > 0) { + return buf_remove(&ctx->plain_buf, buf, count); + } do { - nread = read(fd, buf + inbuf, count - inbuf); - if (nread == 0) { - // a blocking read that returns zero means eof -- implies connection closed - debug(3, "read_sized_block connection closed."); - keep_trying = 0; - } else if (nread < 0) { - if (errno == EAGAIN) { - debug(1, "read_sized_block getting Error 11 -- EAGAIN from a blocking read!"); - } - if ((errno != ECONNRESET) && (errno != EAGAIN) && (errno != EINTR)) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "read_sized_block read error %d: \"%s\".", errno, (char *)errorstring); - keep_trying = 0; - } - } else { - inbuf += (size_t)nread; - } - } while ((keep_trying != 0) && (inbuf < count)); - if (nread <= 0) - response = nread; - else - response = inbuf; - return response; -} + ssize_t got = read(fd, in, sizeof(in)); + if (got <= 0) + return got; + buf_add(&ctx->encrypted_buf, in, got); -#ifdef CONFIG_AIRPLAY_2 + ssize_t consumed = pair_decrypt(&plain, &plain_len, ctx->encrypted_buf.data, ctx->encrypted_buf.len, ctx->cipher_ctx); + if (consumed < 0) + return -1; + buf_drain(&ctx->encrypted_buf, consumed); + } while (plain_len == 0); -/* ----------------------------- DEFINES ETC ------------------------------- */ - -#define USERNAME "Pair-Setup" -#define AUTHTAG_LENGTH 16 -#define NONCE_LENGTH 12 // 96 bits according to chacha poly1305 -#define RESPONSE_BUFSIZE 8192 -#define ENCRYPTED_LEN_MAX 0x400 -#define PASSWORD "3939" - -#define RTSP_VERSION "RTSP/1.0" -#define POST_PAIR_SETUP "POST /pair-setup" -#define OPTIONS "OPTIONS *" -#define GET_INFO "GET /info" -#define POST_AUTH_SETUP "POST /auth-setup" -#define POST_FP_SETUP "POST /fp-setup" - -/* Fairplay magic */ -/* -static uint8_t server_fp_reply1[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x00\x0f\x9f\x3f\x9e\x0a" - "\x25\x21\xdb\xdf\x31\x2a\xb2\xbf\xb2\x9e\x8d\x23\x2b\x63\x76\xa8\xc8\x18\x70" - "\x1d\x22\xae\x93\xd8\x27\x37\xfe\xaf\x9d\xb4\xfd\xf4\x1c\x2d\xba\x9d\x1f\x49" - "\xca\xaa\xbf\x65\x91\xac\x1f\x7b\xc6\xf7\xe0\x66\x3d\x21\xaf\xe0\x15\x65\x95" - "\x3e\xab\x81\xf4\x18\xce\xed\x09\x5a\xdb\x7c\x3d\x0e\x25\x49\x09\xa7\x98\x31" - "\xd4\x9c\x39\x82\x97\x34\x34\xfa\xcb\x42\xc6\x3a\x1c\xd9\x11\xa6\xfe\x94\x1a" - "\x8a\x6d\x4a\x74\x3b\x46\xc3\xa7\x64\x9e\x44\xc7\x89\x55\xe4\x9d\x81\x55\x00" - "\x95\x49\xc4\xe2\xf7\xa3\xf6\xd5\xba"; -static uint8_t server_fp_reply2[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x01\xcf\x32\xa2\x57\x14" - "\xb2\x52\x4f\x8a\xa0\xad\x7a\xf1\x64\xe3\x7b\xcf\x44\x24\xe2\x00\x04\x7e\xfc" - "\x0a\xd6\x7a\xfc\xd9\x5d\xed\x1c\x27\x30\xbb\x59\x1b\x96\x2e\xd6\x3a\x9c\x4d" - "\xed\x88\xba\x8f\xc7\x8d\xe6\x4d\x91\xcc\xfd\x5c\x7b\x56\xda\x88\xe3\x1f\x5c" - "\xce\xaf\xc7\x43\x19\x95\xa0\x16\x65\xa5\x4e\x19\x39\xd2\x5b\x94\xdb\x64\xb9" - "\xe4\x5d\x8d\x06\x3e\x1e\x6a\xf0\x7e\x96\x56\x16\x2b\x0e\xfa\x40\x42\x75\xea" - "\x5a\x44\xd9\x59\x1c\x72\x56\xb9\xfb\xe6\x51\x38\x98\xb8\x02\x27\x72\x19\x88" - "\x57\x16\x50\x94\x2a\xd9\x46\x68\x8a"; -static uint8_t server_fp_reply3[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x02\xc1\x69\xa3\x52\xee" - "\xed\x35\xb1\x8c\xdd\x9c\x58\xd6\x4f\x16\xc1\x51\x9a\x89\xeb\x53\x17\xbd\x0d" - "\x43\x36\xcd\x68\xf6\x38\xff\x9d\x01\x6a\x5b\x52\xb7\xfa\x92\x16\xb2\xb6\x54" - "\x82\xc7\x84\x44\x11\x81\x21\xa2\xc7\xfe\xd8\x3d\xb7\x11\x9e\x91\x82\xaa\xd7" - "\xd1\x8c\x70\x63\xe2\xa4\x57\x55\x59\x10\xaf\x9e\x0e\xfc\x76\x34\x7d\x16\x40" - "\x43\x80\x7f\x58\x1e\xe4\xfb\xe4\x2c\xa9\xde\xdc\x1b\x5e\xb2\xa3\xaa\x3d\x2e" - "\xcd\x59\xe7\xee\xe7\x0b\x36\x29\xf2\x2a\xfd\x16\x1d\x87\x73\x53\xdd\xb9\x9a" - "\xdc\x8e\x07\x00\x6e\x56\xf8\x50\xce"; -static uint8_t server_fp_reply4[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x03\x90\x01\xe1\x72\x7e" - "\x0f\x57\xf9\xf5\x88\x0d\xb1\x04\xa6\x25\x7a\x23\xf5\xcf\xff\x1a\xbb\xe1\xe9" - "\x30\x45\x25\x1a\xfb\x97\xeb\x9f\xc0\x01\x1e\xbe\x0f\x3a\x81\xdf\x5b\x69\x1d" - "\x76\xac\xb2\xf7\xa5\xc7\x08\xe3\xd3\x28\xf5\x6b\xb3\x9d\xbd\xe5\xf2\x9c\x8a" - "\x17\xf4\x81\x48\x7e\x3a\xe8\x63\xc6\x78\x32\x54\x22\xe6\xf7\x8e\x16\x6d\x18" - "\xaa\x7f\xd6\x36\x25\x8b\xce\x28\x72\x6f\x66\x1f\x73\x88\x93\xce\x44\x31\x1e" - "\x4b\xe6\xc0\x53\x51\x93\xe5\xef\x72\xe8\x68\x62\x33\x72\x9c\x22\x7d\x82\x0c" - "\x99\x94\x45\xd8\x92\x46\xc8\xc3\x59"; - -static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; + // Fast path, avoids some memcpy + allocs in case of the normal, small message +/* if (ctx->plain_buf.len == 0 && plain_len < count) { + memcpy(buf, plain, plain_len); + free(plain); + return plain_len; + } */ + buf_add(&ctx->plain_buf, plain, plain_len); + free(plain); -/* 3072 n and g for SRP*/ - -const char *nl_hex = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" - "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" - "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" - "E649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23" - "DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32" - "905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558" - "17183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521" - "ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D7" - "1E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B1817" - "7B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82" - "D120A93AD2CAFFFFFFFFFFFFFFFF"; -const char *gl_hex = "5"; - -enum pair_keys { - PAIR_SETUP_MSG01 = 0, - PAIR_SETUP_MSG02, - PAIR_SETUP_MSG03, - PAIR_SETUP_MSG04, - PAIR_SETUP_MSG05, - PAIR_SETUP_MSG06, - PAIR_SETUP_SIGN, - PAIR_VERIFY_MSG01, - PAIR_VERIFY_MSG02, - PAIR_VERIFY_MSG03, - PAIR_VERIFY_MSG04, - PAIR_CONTROL_WRITE, - PAIR_CONTROL_READ, - PAIR_EVENTS_WRITE, - PAIR_EVENTS_READ, -}; - -struct pair_keys_map { - uint8_t state; - const char *salt; - const char *info; - const char nonce[8]; -}; - -static struct pair_keys_map pair_keys_map[] = { - // Used for /pair-setup - {0x01, NULL, NULL, ""}, - {0x02, NULL, NULL, ""}, - {0x03, NULL, NULL, ""}, - {0x04, NULL, NULL, ""}, - {0x05, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg05"}, - {0x06, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg06"}, - {0, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", ""}, - - // Used for /pair-verify - {0x01, NULL, NULL, ""}, - {0x02, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg02"}, - {0x03, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg03"}, - {0x04, NULL, NULL, ""}, - - // Encryption/decryption of control channel - {0, "Control-Salt", "Control-Write-Encryption-Key", ""}, - {0, "Control-Salt", "Control-Read-Encryption-Key", ""}, - - // Encryption/decryption of event channel - {0, "Events-Salt", "Events-Write-Encryption-Key", ""}, - {0, "Events-Salt", "Events-Read-Encryption-Key", ""}, -}; - -enum pair_method { - PairingMethodPairSetup = 0x00, - PairingMethodPairSetupWithAuth = 0x01, - PairingMethodPairVerify = 0x02, - PairingMethodAddPairing = 0x03, - PairingMethodRemovePairing = 0x04, - PairingMethodListPairings = 0x05 -}; - -enum pair_flags { - PairingFlagsTransient = 0x10, -}; - -struct verifier_setup_context { - struct pair_definition *type; - - struct SRPVerifier *verifier; - - char pin[4]; - char device_id[17]; // Incl. zero term - - uint8_t *pkA; - uint64_t pkA_len; - - const uint8_t *pkB; - int pkB_len; - - const uint8_t *b; - int b_len; - - uint8_t *M1; - uint64_t M1_len; - - const uint8_t *M2; - int M2_len; - - const uint8_t *v; - int v_len; - - const uint8_t *salt; - int salt_len; - uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; - uint8_t private_key[crypto_sign_SECRETKEYBYTES]; - // Hex-formatet concatenation of public + private, 0-terminated - char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1]; - - // We don't actually use the server's epk and authtag for anything - uint8_t *epk; - uint64_t epk_len; - uint8_t *authtag; - uint64_t authtag_len; - - int setup_is_completed; - const char *errmsg; -}; - -// static struct verifier_setup_context server_setup_ctx; - -ssize_t write_encrypted(file_cipher_context *context, const void *buf, size_t count) { - // need to make this cancellable! - // encrypt the contents of the buffer - ssize_t response; - uint8_t nonce[NONCE_LENGTH] = {0}; - // uint8_t tag[AUTHTAG_LENGTH]; - uint8_t *plain_block; - uint8_t *cipher_block; - uint16_t block_len; - int nblocks; - int ret; - int i; - - if ((count == 0) || (buf == NULL)) { - debug(1, "encrypting a null character sequence"); - response = 0; - } else { - // Encryption is done in blocks, where each block consists of a short, the - // encrypted data and an auth tag. The short is the size of the encrypted - // data. The encrypted data in the block cannot exceed ENCRYPTED_LEN_MAX. - nblocks = 1 + ((count - 1) / ENCRYPTED_LEN_MAX); // Ceiling of division - - ssize_t ctl = nblocks * (sizeof(block_len) + AUTHTAG_LENGTH) + count; - // size_t hdh = 0; - void *ciphertext = malloc(ctl); - if (ciphertext != NULL) { - - context->cipher_context->encryption_counter_prev = - context->cipher_context->encryption_counter; - - for (i = 0, plain_block = (uint8_t *)buf, cipher_block = ciphertext; i < nblocks; i++) { - // If it is the last block we will encrypt only the remaining data - if (i == nblocks - 1) { - uint8_t *b = (uint8_t *)buf; - block_len = b + count - plain_block; - } else - block_len = ENCRYPTED_LEN_MAX; - - memcpy(nonce + 4, &(context->cipher_context->encryption_counter), - sizeof(context->cipher_context->encryption_counter)); // TODO BE or LE? - - // Write the ciphered block - memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE? - - unsigned long long cipher_length = 0; - ret = crypto_aead_chacha20poly1305_ietf_encrypt( - cipher_block + sizeof(block_len), // ciphertext - &cipher_length, - plain_block, // message - block_len, // message length - (unsigned char *)&block_len, // additional data - sizeof(block_len), // additional data length - NULL, - nonce, // nonce - context->cipher_context->encryption_key // key - ); - - if (ret < 0) { - debug(1, "Encryption with chacha poly1305 failed"); - context->cipher_context->encryption_counter = - context->cipher_context->encryption_counter_prev; - free(ciphertext); - } else { - plain_block += block_len; - cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; - context->cipher_context->encryption_counter++; - } - } - // maybe this really should be write_sized_block, similar to read_sized_block - ssize_t trywrite = write(context->fd, ciphertext, ctl); - if (trywrite != ctl) - debug(1, "write_encrypted write failure"); - free(ciphertext); - response = count; // pretend only the plaintext number of bytes were written - } else { - // can't allocate memory for the cyphertext - errno = ENOMEM; - response = -1; - } - } - return response; + return buf_remove(&ctx->plain_buf, buf, count); } -ssize_t read_encrypted(file_cipher_context *context, void *buf, size_t count) { - // need to make this cancellable! - // if the plaintext buffer is empty, this will - // read an encrypted block and decipher it to plaintext - // this will then return bytes in the plaintext buffer - // up to the limit of the count - ssize_t response = 0; - if (context->input_plaintext_buffer == NULL) { - response = -1; - uint16_t block_len; - if (read_sized_block(context->fd, &block_len, sizeof(block_len)) == sizeof(block_len)) { - void *cipher_block = malloc(block_len + AUTHTAG_LENGTH); - if (cipher_block != NULL) { - if (read_sized_block(context->fd, cipher_block, (block_len + AUTHTAG_LENGTH)) == - block_len + AUTHTAG_LENGTH) { - // here we have the encrypted block - uint8_t nonce[NONCE_LENGTH] = {0}; - uint8_t tag[AUTHTAG_LENGTH]; - context->input_plaintext_buffer = malloc(block_len); // this should be more than enough - if (context->input_plaintext_buffer != NULL) { - memcpy(tag, cipher_block + block_len, sizeof(tag)); - memcpy(nonce + 4, &(context->cipher_context->decryption_counter), - sizeof(context->cipher_context->decryption_counter)); // TODO BE or LE? - unsigned long long new_payload_length = 0; - int ret = crypto_aead_chacha20poly1305_ietf_decrypt( - context->input_plaintext_buffer, // m - &new_payload_length, // mlen_p - NULL, // nsec, - cipher_block, // cipher text - block_len + AUTHTAG_LENGTH, // length of the ciphertext - (unsigned char *)&block_len, // authenticated additional data - sizeof(block_len), // authenticated additional data length - nonce, // nonce - context->cipher_context->decryption_key // key - ); - free(cipher_block); - cipher_block = NULL; - if (ret == 0) { - response = 0; // decryption was successful - context->cipher_context->decryption_counter++; - context->input_plaintext_buffer_toq = context->input_plaintext_buffer; - context->input_plaintext_buffer_bytes_occupied = block_len; - } else { - // decryption failed - debug(1, "decryption failed."); - free(context->input_plaintext_buffer); - context->input_plaintext_buffer = NULL; - errno = EILSEQ; // illegal byte sequence - } - } else { - // failed to allocate a plaintext buffer - debug(1, "could not allocate a plaintext buffer"); - free(cipher_block); - cipher_block = NULL; - errno = ENOMEM; - } - } else { - // failed to read a block properly -- errno will be set - // debug(1, "could not read a block"); - free(cipher_block); - cipher_block = NULL; - } - } else { - // failed to allocate a ciphertext buffer - debug(1, "could not allocate a ciphertext buffer"); - errno = ENOMEM; - } - } else { - debug(2, "could not read the block_len"); - // failed to read the encrypted block size -- errno will be set - } +static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t count) { + uint8_t *encrypted; + size_t encrypted_len; + + ssize_t ret = pair_encrypt(&encrypted, &encrypted_len, buf, count, conn->ap2_control_pairing.cipher_ctx); + if (ret < 0) { + debug(1, pair_cipher_errmsg(conn->ap2_control_pairing.cipher_ctx)); + return -1; } - if (response == 0) { - // now, transfer bytes to the output buffer up to the limit of the count requested - if (context->input_plaintext_buffer_bytes_occupied) { - size_t bytes_to_transfer = context->input_plaintext_buffer_bytes_occupied; - if (bytes_to_transfer > count) - bytes_to_transfer = count; - if (bytes_to_transfer == 0) - debug(1, "Shome problem -- zero bytes to transfer!"); - memcpy(buf, context->input_plaintext_buffer_toq, bytes_to_transfer); - context->input_plaintext_buffer_bytes_occupied = - context->input_plaintext_buffer_bytes_occupied - bytes_to_transfer; - if (context->input_plaintext_buffer_bytes_occupied == 0) { - free(context->input_plaintext_buffer); - context->input_plaintext_buffer = NULL; - } else { - context->input_plaintext_buffer_toq = - context->input_plaintext_buffer_toq + bytes_to_transfer; - } - response = bytes_to_transfer; + + size_t remain = encrypted_len; + while (remain > 0) { + ssize_t wrote = write(conn->fd, encrypted + (encrypted_len - remain), remain); + if (wrote <= 0) { + free(encrypted); + return wrote; } + remain -= wrote; } - return response; + free(encrypted); + return count; } #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { #ifdef CONFIG_AIRPLAY_2 - if ((conn->control_cipher_context.cipher_context != NULL) && - (conn->control_cipher_context.active != 0)) { - return read_encrypted(&conn->control_cipher_context, buf, count); + if (conn->ap2_control_pairing.cipher_ctx) { + conn->ap2_control_pairing.is_encrypted = 1; + return read_encrypted(conn->fd, &conn->ap2_control_pairing, buf, count); } else { return read(conn->fd, buf, count); } @@ -1433,9 +1121,8 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 ssize_t reply; - if ((conn->control_cipher_context.cipher_context != NULL) && - (conn->control_cipher_context.active != 0)) { - reply = write_encrypted(&conn->control_cipher_context, pkt, p - pkt); + if (conn->ap2_control_pairing.is_encrypted) { + reply = write_encrypted(conn, pkt, p - pkt); } else { reply = write(conn->fd, pkt, p - pkt); } @@ -1731,183 +1418,44 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) #ifdef CONFIG_AIRPLAY_2 void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; - resp->respcode = 200; // assume everything works out okay + uint8_t *body; + size_t body_len; + struct pair_result *result; debug(2, "Connection %d: pair-setup Content-Length %d", conn->connection_number, req->contentlength); - pair_tlv_values_t *values = pair_tlv_new(); - pair_tlv_t *state; - pair_tlv_t *method; - pair_tlv_t *flags; - ret = pair_tlv_parse((const unsigned char *)req->content, req->contentlength, values); - if (ret < 0) - debug(1, "Could not parse TLV"); - state = pair_tlv_get_value(values, TLVType_State); - if (!state || state->size != 1) { - debug(1, "Missing/unexpected pairing state in TLV."); - } - if (state->value[0] == pair_keys_map[PAIR_SETUP_MSG01].state) { - debug(2, "pair-setup part 1"); - method = pair_tlv_get_value(values, TLVType_Method); - if (!method || method->size != 1 || method->value[0] != 0) { - debug(1, "Missing/unexpected pairing method in TLV."); - } - if (method->value[0] != PairingMethodPairSetup) - debug(1, "Unexpected method value %u.", method->value[0]); - flags = pair_tlv_get_value(values, TLVType_Flags); - if (!flags || flags->size != 1) { - debug(1, "Missing/unexpected pairing flags in TLV."); - } - if (flags->value[0] == PairingFlagsTransient) { - debug(2, "Transient pairing selected."); - conn->pairing_mode = - PairingFlagsTransient; // when pairing step 2 is finished, turn on encryption - } - - // Note this is modified to return a 16 byte salt - srp_create_salted_verification_key( - SRP_SHA512, SRP_NG_CUSTOM, USERNAME, (const unsigned char *)PASSWORD, strlen(PASSWORD), - &conn->server_setup_ctx->salt, &conn->server_setup_ctx->salt_len, - &conn->server_setup_ctx->v, &conn->server_setup_ctx->v_len, nl_hex, gl_hex); - - srp_verifier_get_B(SRP_SHA512, SRP_NG_CUSTOM, conn->server_setup_ctx->v, - conn->server_setup_ctx->v_len, &conn->server_setup_ctx->b, - &conn->server_setup_ctx->b_len, &conn->server_setup_ctx->pkB, - &conn->server_setup_ctx->pkB_len, nl_hex, gl_hex); - - pair_tlv_values_t *payload = pair_tlv_new(); - - pair_tlv_add_value(payload, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG02].state, - sizeof(pair_keys_map[PAIR_SETUP_MSG02].state)); - pair_tlv_add_value(payload, TLVType_Salt, conn->server_setup_ctx->salt, - conn->server_setup_ctx->salt_len); // 16 - pair_tlv_add_value(payload, TLVType_PublicKey, conn->server_setup_ctx->pkB, - conn->server_setup_ctx->pkB_len); // 384 - - // turn the tlv to binary form - - char *body = malloc(RESPONSE_BUFSIZE); - size_t body_len; - body_len = RESPONSE_BUFSIZE; - - ret = pair_tlv_format(payload, (uint8_t *)body, &body_len); - if (ret < 0) - debug(1, "Can't convert response to a binary stream"); - pair_tlv_free(payload); - resp->content = body; // these will be freed when the data is sent - resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-setup part 1 response", resp); - resp->respcode = 200; // it all worked out okay - - } else if (state->value[0] == pair_keys_map[PAIR_SETUP_MSG03].state) { - debug(2, "pair setup part 2"); - - // uint8_t *out; - // size_t out_len; - const uint8_t *key; - int key_len; - pair_tlv_t *pk; - pair_tlv_t *proof; - pair_tlv_values_t *payload; - - pk = pair_tlv_get_value(values, TLVType_PublicKey); - proof = pair_tlv_get_value(values, TLVType_Proof); - - if ((pk != NULL) && (proof != NULL)) { - conn->server_setup_ctx->pkA_len = pk->size; // 384 - conn->server_setup_ctx->pkA = malloc(conn->server_setup_ctx->pkA_len); - memcpy(conn->server_setup_ctx->pkA, pk->value, conn->server_setup_ctx->pkA_len); - - conn->server_setup_ctx->M1_len = proof->size; // 64 - conn->server_setup_ctx->M1 = malloc(conn->server_setup_ctx->M1_len); - memcpy(conn->server_setup_ctx->M1, proof->value, conn->server_setup_ctx->M1_len); - - conn->server_setup_ctx->verifier = srp_verifier_new( - SRP_SHA512, SRP_NG_CUSTOM, USERNAME, conn->server_setup_ctx->salt, - conn->server_setup_ctx->salt_len, conn->server_setup_ctx->v, - conn->server_setup_ctx->v_len, conn->server_setup_ctx->pkA, - conn->server_setup_ctx->pkA_len, conn->server_setup_ctx->b, conn->server_setup_ctx->b_len, - conn->server_setup_ctx->pkB, conn->server_setup_ctx->pkB_len, nl_hex, gl_hex); - if (conn->server_setup_ctx->verifier != NULL) { - if (!conn->server_setup_ctx->verifier) { - die("Error verifier"); - } - - conn->server_setup_ctx->M2_len = 64; // 512 bit hash - srp_verifier_verify_session(conn->server_setup_ctx->verifier, conn->server_setup_ctx->M1, - &conn->server_setup_ctx->M2); - if (conn->server_setup_ctx->M2 != NULL) { - - key = srp_verifier_get_session_key(conn->server_setup_ctx->verifier, &key_len); - - conn->control_cipher_context.cipher_context = - pair_cipher_new(PAIR_SERVER_HOMEKIT_TRANSIENT, 2, key, key_len); - conn->control_cipher_context.fd = conn->fd; - conn->control_cipher_context.input_plaintext_buffer = NULL; - conn->control_cipher_context.input_plaintext_buffer_toq = NULL; - conn->control_cipher_context.input_plaintext_buffer_bytes_occupied = 0; - conn->control_cipher_context.active = 0; - payload = pair_tlv_new(); - - pair_tlv_add_value(payload, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG04].state, - sizeof(pair_keys_map[PAIR_SETUP_MSG04].state)); - pair_tlv_add_value(payload, TLVType_Proof, conn->server_setup_ctx->M2, - conn->server_setup_ctx->M2_len); // 384 - - char *body = malloc(RESPONSE_BUFSIZE); - size_t body_len; - body_len = RESPONSE_BUFSIZE; - - ret = pair_tlv_format(payload, (uint8_t *)body, &body_len); - if (ret < 0) - debug(1, "Can't convert response to a binary stream"); - pair_tlv_free(payload); - - // free the verifier created by srp_verifier_new() - if (conn->server_setup_ctx->verifier != NULL) { - srp_verifier_delete(conn->server_setup_ctx->verifier); - } - - // free mallocs made here for pkA and M1 - if (conn->server_setup_ctx->pkA != NULL) - free((void *)conn->server_setup_ctx->pkA); - if (conn->server_setup_ctx->M1 != NULL) - free((void *)conn->server_setup_ctx->M1); - - // free mallocs made by srp_create_salted_verification_key() - if (conn->server_setup_ctx->salt != NULL) - free((void *)conn->server_setup_ctx->salt); - if (conn->server_setup_ctx->v != NULL) - free((void *)conn->server_setup_ctx->v); - - // free mallocs made by srp_verifier_get_B() - if (conn->server_setup_ctx->b != NULL) - free((void *)conn->server_setup_ctx->b); - // if (conn->server_setup_ctx->pkB != NULL) - // free((void *)conn->server_setup_ctx->pkB); - - resp->content = body; // these will be freed when the data is sent - resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-setup part 2 response", resp); - resp->respcode = 200; // it all worked out okay - } else { - debug(1, "Error M2"); - resp->respcode = 451; // 451 is "Parameter not understood" - } - } else { - debug(1, "Error verifier"); - resp->respcode = 451; - } - } else { - debug(1, "Error pkA ver"); + if (!conn->ap2_control_pairing.setup_ctx) { + conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); + if (!conn->ap2_control_pairing.setup_ctx) { + debug(1, "Error creating setup context"); resp->respcode = 451; + return; } - } else { - debug(1, "don't recognise pair setup message"); } - pair_tlv_free(values); + + ret = pair_setup(&body, &body_len, conn->ap2_control_pairing.setup_ctx, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, pair_setup_errmsg(conn->ap2_control_pairing.setup_ctx)); + resp->respcode = 451; + return; + } + + ret = pair_setup_result(NULL, &result, conn->ap2_control_pairing.setup_ctx); + if (ret == 0 && result->shared_secret_len > 0) { + // Transient pairing completed (pair-setup step 2), prepare encryption, but + // don't activate yet, the response to this request is still plaintext + conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + if (!conn->ap2_control_pairing.cipher_ctx) { + debug(1, "Error setting up rtsp control channel ciphering\n"); + resp->respcode = 451; + return; + } + } + + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-setup response", resp); } void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, @@ -4119,10 +3667,11 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { conn->client_setup_plist = NULL; } - // free the cipher context, if allocated - if (conn->control_cipher_context.cipher_context) - free(conn->control_cipher_context.cipher_context); - + buf_drain(&conn->ap2_control_pairing.plain_buf, -1); + buf_drain(&conn->ap2_control_pairing.encrypted_buf, -1); + pair_setup_free(conn->ap2_control_pairing.setup_ctx); + pair_verify_free(conn->ap2_control_pairing.verify_ctx); + pair_cipher_free(conn->ap2_control_pairing.cipher_ctx); #endif rtp_terminate(conn); @@ -4163,11 +3712,6 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Connection %d: Checking play lock.", conn->connection_number); release_play_lock(conn); -#ifdef CONFIG_AIRPLAY_2 - if (conn->server_setup_ctx) - free(conn->server_setup_ctx); -#endif - debug(1, "Connection %d: terminated.", conn->connection_number); conn->running = 0; pthread_setcancelstate(oldState, NULL); @@ -4213,20 +3757,10 @@ static void *rtsp_conversation_thread_func(void *pconn) { #ifdef CONFIG_AIRPLAY_2 conn->ap2_audio_buffer_size = 1024 * 1024 * 8; - conn->server_setup_ctx = - (struct verifier_setup_context *)malloc(sizeof(struct verifier_setup_context)); - if (conn->server_setup_ctx != NULL) - memset(conn->server_setup_ctx, 0, sizeof(struct verifier_setup_context)); #endif while (conn->stop == 0) { int debug_level = 3; // for printing the request and response -#ifdef CONFIG_AIRPLAY_2 - // transactions should be begin to be encrypted after a read/response cycle has - // been completed - conn->control_cipher_context.active = 1; - -#endif reply = rtsp_read_request(conn, &req); if (reply == rtsp_read_request_response_ok) { pthread_cleanup_push(msg_cleanup_function, (void *)&req); From ba73111a6f085b6a5d61ab4bf192996ac8028051 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 4 May 2021 20:38:35 +0200 Subject: [PATCH 078/878] Support for pair-verify --- common.h | 2 ++ rtsp.c | 47 +++++++++++++++++++++++++++++++++++++++++++---- shairport.c | 7 +++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/common.h b/common.h index 3c4fba65..2172e287 100644 --- a/common.h +++ b/common.h @@ -296,7 +296,9 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 uint64_t airplay_features; + uint32_t airplay_statusflags; char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList + char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing char *airplay_pk; // "pk" string in the Bonjour advertisement char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist char *airplay_gid; // UUID in the Bonjour advertisement -- initially the same as the pi diff --git a/rtsp.c b/rtsp.c index ef9a0245..ade66145 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1232,6 +1232,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r debug(1, "GET /info Stage 1: response plist not created from XML!"); } else { plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); @@ -1256,6 +1257,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, &response_plist); plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); @@ -1416,6 +1418,40 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) #endif #ifdef CONFIG_AIRPLAY_2 +void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + int ret; + uint8_t *body; + size_t body_len; + struct pair_result *result; + debug(2, "Connection %d: pair-verify Content-Length %d", conn->connection_number, + req->contentlength); + + if (!conn->ap2_control_pairing.verify_ctx) { + conn->ap2_control_pairing.verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); + if (!conn->ap2_control_pairing.verify_ctx) { + debug(1, "Error creating verify context"); + } + } + + ret = pair_verify(&body, &body_len, conn->ap2_control_pairing.verify_ctx, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, pair_verify_errmsg(conn->ap2_control_pairing.verify_ctx)); + } + + ret = pair_verify_result(&result, conn->ap2_control_pairing.verify_ctx); + if (ret == 0 && result->shared_secret_len > 0) { + conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + if (!conn->ap2_control_pairing.cipher_ctx) { + debug(1, "Error setting up rtsp control channel ciphering\n"); + } + } + + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-verify response", resp); +} + void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; uint8_t *body; @@ -1425,7 +1461,7 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re req->contentlength); if (!conn->ap2_control_pairing.setup_ctx) { - conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); + conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, NULL, NULL, config.airplay_device_id); if (!conn->ap2_control_pairing.setup_ctx) { debug(1, "Error creating setup context"); resp->respcode = 451; @@ -1564,6 +1600,8 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { handle_pair_setup(conn, req, resp); + } else if (strcmp(req->path, "/pair-verify") == 0) { + handle_pair_verify(conn, req, resp); } else if (strcmp(req->path, "/fp-setup") == 0) { handle_fp_setup(conn, req, resp); } else { @@ -4062,17 +4100,18 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { snprintf(deviceIdString, sizeof(deviceIdString) - 1, "deviceid=%s", config.airplay_device_id); *p++ = deviceIdString; // features is a 64 bit number, least significant 32 bits - char featuresString[64]; uint64_t features_hi = config.airplay_features; features_hi = (features_hi >> 32) & 0xffffffff; uint64_t features_lo = config.airplay_features; features_lo = features_lo & 0xffffffff; + // @mikebrady I don't think the -1 here (and other places) is necessary snprintf(featuresString, sizeof(featuresString) - 1, "features=0x%" PRIx64 ",0x%" PRIx64 "", features_lo, features_hi); - *p++ = featuresString; - *p++ = "flags=0x4"; + char statusflagsString[32]; + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIx32, config.airplay_statusflags); + *p++ = statusflagsString; *p++ = "protovers=1.1"; *p++ = "acl=0"; *p++ = "rsf=0x0"; diff --git a/shairport.c b/shairport.c index 2e9dff2a..303a282a 100644 --- a/shairport.c +++ b/shairport.c @@ -1577,6 +1577,13 @@ int main(int argc, char **argv) { // 496155702020608 this setting here is the source of both the plist features response and the // mDNS string. config.airplay_features = 0x1C340405F4A00; + // Advertised with mDNS and returned with GET /info, see https://openairplay.github.io/airplay-spec/status_flags.html + // 0x4: Audio cable attached, no PIN required (transient pairing), no Homekit access control + // 0x204: Audio cable attached, OneTimePairingRequired + // 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit access control + config.airplay_statusflags = 0x4; + // Set to NULL to work with transient pairing + config.airplay_pin = NULL; // get a device id -- the first non-local MAC address, not necessarily the one in use config.airplay_device_id = get_device_id(); if (config.airplay_device_id) { From c5b58343fb7c614d7dd7df8c34e356e7873f5bf7 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 13:09:07 +0100 Subject: [PATCH 079/878] Docker image start. --- .github/workflows/docker-build-master.yaml | 32 +++++ docker/Dockerfile | 137 +++++++++++++++++++++ docker/docker-compose.yaml | 12 ++ docker/shairport-sync.conf | 14 +++ docker/start.sh | 18 +++ 5 files changed, 213 insertions(+) create mode 100644 .github/workflows/docker-build-master.yaml create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yaml create mode 100644 docker/shairport-sync.conf create mode 100644 docker/start.sh diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml new file mode 100644 index 00000000..06163667 --- /dev/null +++ b/.github/workflows/docker-build-master.yaml @@ -0,0 +1,32 @@ +# Pushes to tag 'unstable' since it uses the master branch directly. +# Releases are pushed to latest and tagged - see docker-build-releases.yaml. + +name: Build and push docker image based on master branch. + +on: + push: + branches: master + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: registry.nova.do/shairport-sync:unstable \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..31f00d49 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,137 @@ +FROM alpine:3.12 AS builder-base +# Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard +# in debian/ubuntu. + +# General Build System: +RUN apk -U add \ + git \ + build-base \ + autoconf \ + automake \ + libtool \ + dbus \ + su-exec \ + alsa-lib-dev \ + libdaemon-dev \ + popt-dev \ + mbedtls-dev \ + soxr-dev \ + avahi-dev \ + libconfig-dev \ + libsndfile-dev \ + mosquitto-dev \ + xmltoman \ + openssh-client \ + libsodium-dev \ + ffmpeg-dev \ + xxd \ + libressl-dev \ + openssl-dev \ + libplist-dev + +# ALAC Build System: +FROM builder-base AS builder-alac + +RUN git clone https://github.com/mikebrady/alac +WORKDIR /alac +RUN git pull +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install + +# Shairport Sync Build System: +FROM builder-base AS builder-sps + +# This may be modified by the Github Action Workflow. +ARG SHAIRPORT_SYNC_BRANCH=development + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc +COPY --from=builder-alac /usr/local/include /usr/local/include + +RUN git clone git@github.com:charlesomer/nqptp.git +WORKDIR /nqptp +# Use development branch for now. +RUN git checkout development +RUN git pull +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install +WORKDIR / + +RUN git clone git@github.com:aillwee/shairport-sync.git +WORKDIR /shairport-sync +RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +RUN git pull +RUN autoreconf -fi +RUN git submodule init +RUN git submodule update +# RUN ./configure \ +# --with-alsa \ +# --with-dummy \ +# --with-pipe \ +# --with-stdout \ +# --with-avahi \ +# --with-ssl=mbedtls \ +# --with-soxr \ +# --sysconfdir=/etc \ +# --with-dbus-interface \ +# --with-mpris-interface \ +# --with-mqtt-client \ +# --with-apple-alac \ +# --with-convolution \ +# --with-airplay-2 +# RUN make -j $(nproc) +RUN CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata --with-dummy --with-pipe \ + --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-dbus-interface --with-stdout \ + --with-mpris-interface --with-mqtt-client --with-apple-alac --with-convolution --with-airplay-2 +RUN make -j +RUN make install + +# Shairport Sync Runtime System: +FROM alpine:3.12 + +RUN apk add \ + alsa-lib \ + dbus \ + popt \ + glib \ + mbedtls \ + soxr \ + avahi \ + libconfig \ + libsndfile \ + mosquitto-libs \ + su-exec \ + libuuid \ + ffmpeg-dev \ + ffmpeg-libs \ + libsodium-dev \ + libplist-dev + +RUN rm -rf /lib/apk/db/* + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-sps /etc/shairport-sync* /etc/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync +COPY --from=builder-sps /usr/local/bin/nqptp /usr/local/bin/nqptp + +# Create non-root user for running the container -- running as the user 'shairport-sync' also allows +# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container + +RUN addgroup shairport-sync +RUN adduser -D shairport-sync -G shairport-sync + +# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff +RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio + +COPY ./docker/shairport-sync.conf /etc/shairport-sync.conf +COPY ./docker/start.sh / + +RUN chmod +x ./start.sh + +ENTRYPOINT [ "/start.sh" ] \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..8adbd6df --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,12 @@ +# Example docker compose config. + +version: "3.8" +services: + shairport-sync: + image: registry.nova.do/shairport-sync:dev + network_mode: host + restart: unless-stopped + devices: + - "/dev/snd" + volumes: + - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf \ No newline at end of file diff --git a/docker/shairport-sync.conf b/docker/shairport-sync.conf new file mode 100644 index 00000000..bb447958 --- /dev/null +++ b/docker/shairport-sync.conf @@ -0,0 +1,14 @@ +# Temp sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC. +general = +{ + volume_range_db = 60; +}; + +alsa = +{ + output_device = "hw:0"; + # Could also be hw:1. aplay -l should help identify the correct one. + # e.g. card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones] + # card x where x is the number to use above in hw:x + mixer_control_name = "Headphone"; +}; \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 00000000..bb790283 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +rm -rf /var/run/dbus.pid +#mkdir -p /var/run/dbus + +dbus-uuidgen --ensure +dbus-daemon --system + +avahi-daemon --daemonize --no-chroot + +# TODO: This should probably be implemented in a better way and using the correct nqptp group. +nqptp & + +# su-exec shairport-sync shairport-sync $@ + +shairport-sync -vu --statistics \ No newline at end of file From 033d03821de735523cbf4a968d034af4b3b6014c Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 13:11:53 +0100 Subject: [PATCH 080/878] Update docker build. --- .github/workflows/docker-build-master.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 06163667..b16c5812 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -5,7 +5,7 @@ name: Build and push docker image based on master branch. on: push: - branches: master + branches: [ master, development ] jobs: main: @@ -29,4 +29,4 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable \ No newline at end of file + tags: registry.nova.do/shairport-sync:unstable-${GITHUB_REF##*/} # ${GITHUB_REF##*/} gets the current branch name. \ No newline at end of file From 34d4ca2155706691110d6eb612e2cfcab3b5a466 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 13:14:24 +0100 Subject: [PATCH 081/878] Added readme file. --- docker/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docker/README.md diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..90aaefa1 --- /dev/null +++ b/docker/README.md @@ -0,0 +1 @@ +# Shairport Sync Docker Image \ No newline at end of file From 38216253b7a124a5181987b8c823b775a2fa4a47 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 13:17:24 +0100 Subject: [PATCH 082/878] Corrected registry. --- .github/workflows/docker-build-master.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index b16c5812..d94037bb 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -17,9 +17,10 @@ jobs: uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub + - name: Login to Docker Registry uses: docker/login-action@v1 with: + registry: registry.nova.do username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Build and push From 6bdc4aa8a61a05736ed7dd5a9c2608ecbba426fa Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 13:22:00 +0100 Subject: [PATCH 083/878] Set branch env. --- .github/workflows/docker-build-master.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index d94037bb..de398537 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Set branch env. + run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx @@ -30,4 +32,6 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${GITHUB_REF##*/} # ${GITHUB_REF##*/} gets the current branch name. \ No newline at end of file + tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} + build-args: | + BRANCH=${{ env.BRANCH }} \ No newline at end of file From fd4cc98abaeb1c322e87fb56f348307bcfb8ab9b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 5 May 2021 18:33:21 +0100 Subject: [PATCH 084/878] Slightly better timing. Bumped version for ejurgensen's new pair_ap work. --- configure.ac | 2 +- player.c | 18 ++++++++++-------- rtp.c | 20 ++++++++++---------- rtsp.c | 1 + 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/configure.ac b/configure.ac index fdeaa139..087ef5a6 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0d1], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0d2], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) diff --git a/player.c b/player.c index e8d79115..24640c5a 100644 --- a/player.c +++ b/player.c @@ -442,6 +442,8 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta conn->ab_write = seqno; conn->ab_read = seqno; conn->ab_synced = 1; + conn->first_packet_timestamp = 0; + debug(1,"synced by first packet"); } else if (original_format == 0) { // if the packet is coming in original format, the sequence number is important // otherwise, ignore is by setting it equal to the expected sequence number in ab_write @@ -1279,7 +1281,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (do_wait == 0) if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! if (notified_buffer_empty == 0) { - debug(3, "Buffers exhausted."); + debug(1, "Buffers exhausted."); notified_buffer_empty = 1; // reset_input_flow_metrics(conn); // don't do a full flush parameters reset conn->initial_reference_time = 0; @@ -1559,11 +1561,11 @@ void player_thread_cleanup_handler(void *arg) { int elapsedMin = (rawSeconds / 60) % 60; int elapsedSec = rawSeconds % 60; if (conn->frame_rate_status) - inform("Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, output: %0.2f " - "frames per second.", + inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, output: %0.2f " + "frames per second.", conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate, conn->frame_rate); else - inform("Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f frames per second.", + inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f frames per second.", conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate); } @@ -2280,7 +2282,7 @@ void *player_thread_func(void *arg) { int64_t filler_length = (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { - debug(2, "Large positive sync error: %" PRId64 ".", sync_error); + debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", sync_error, (sync_error * 1.0)/config.output_rate); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; uint32_t frames_to_drop_sized = local_frames_to_drop; @@ -2292,10 +2294,10 @@ void *player_thread_func(void *arg) { debug_mutex_unlock(&conn->flush_mutex, 3); } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { - debug(2, - "Large negative sync error: %" PRId64 " with should_be_frame_32 of %" PRIu32 + debug(1, + "Large negative sync error of: %" PRId64 " frames (%f seconds), with should_be_frame_32 of %" PRIu32 ", nt of %" PRId64 " and current_delay of %" PRId64 ".", - sync_error, should_be_frame_32, + sync_error, (sync_error * 1.0)/config.output_rate, should_be_frame_32, inframe->given_timestamp * conn->output_sample_ratio, current_delay); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) diff --git a/rtp.c b/rtp.c index 06c7324d..182d19ba 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -2065,8 +2065,9 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); + double reqested_lead_time = 0.4; // if (player_buffer_occupancy > - ((0.25 + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. + ((reqested_lead_time + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while @@ -2089,8 +2090,7 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - // ask for 0.5 sec of leadtime - if ((lead_time >= (int64_t)(0.25 * 1000000000)) || (streaming_has_started == 1)) { + if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); streaming_has_started = 1; diff --git a/rtsp.c b/rtsp.c index ade66145..74a1249a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3163,6 +3163,7 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag #ifdef CONFIG_AIRPLAY_2 conn->airplay_type = ap_1; conn->timing_type = ts_ntp; + debug(1, "Connection %d. AirPlay 1 Audio Stream Detected.", conn->connection_number); #endif conn->stream.type = ast_unknown; From 9ae77b98418592e04238937f983f0fbf16f5fbca Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 5 May 2021 22:45:03 +0100 Subject: [PATCH 085/878] Updated docker build. --- .github/workflows/docker-build-master.yaml | 3 ++- docker/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index de398537..60ab6048 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -34,4 +34,5 @@ jobs: push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | - BRANCH=${{ env.BRANCH }} \ No newline at end of file + BRANCH=${{ env.BRANCH }} + ssh: ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 31f00d49..05382642 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,7 +50,7 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -RUN git clone git@github.com:charlesomer/nqptp.git +RUN --mount=type=ssh git clone git@github.com:charlesomer/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development @@ -61,7 +61,7 @@ RUN make RUN make install WORKDIR / -RUN git clone git@github.com:aillwee/shairport-sync.git +RUN --mount=type=ssh git clone git@github.com:aillwee/shairport-sync.git WORKDIR /shairport-sync RUN git checkout "$SHAIRPORT_SYNC_BRANCH" RUN git pull From 2331b109b02f719ab1956369f9288d53b5c7cdec Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 00:26:01 +0100 Subject: [PATCH 086/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 60ab6048..88277193 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -35,4 +35,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | BRANCH=${{ env.BRANCH }} - ssh: ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file + ssh: [ ${{ secrets.SSH_PRIVATE_KEY }} ] \ No newline at end of file From 2ba6d2a8799d549c1518593d22fed11737205315 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 00:28:10 +0100 Subject: [PATCH 087/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 88277193..f07a9959 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -35,4 +35,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | BRANCH=${{ env.BRANCH }} - ssh: [ ${{ secrets.SSH_PRIVATE_KEY }} ] \ No newline at end of file + ssh: default ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file From debb62cd27dc1183ba38f77897324fcf58e01f02 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 01:55:41 +0100 Subject: [PATCH 088/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- docker/Dockerfile | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index f07a9959..f751f7bd 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -35,4 +35,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | BRANCH=${{ env.BRANCH }} - ssh: default ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file + ssh: default=${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 05382642..584b73ec 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:experimental + FROM alpine:3.12 AS builder-base # Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard # in debian/ubuntu. @@ -34,7 +36,6 @@ FROM builder-base AS builder-alac RUN git clone https://github.com/mikebrady/alac WORKDIR /alac -RUN git pull RUN autoreconf -fi RUN ./configure RUN make @@ -50,11 +51,12 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include +RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts + RUN --mount=type=ssh git clone git@github.com:charlesomer/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development -RUN git pull RUN autoreconf -fi RUN ./configure RUN make @@ -64,7 +66,6 @@ WORKDIR / RUN --mount=type=ssh git clone git@github.com:aillwee/shairport-sync.git WORKDIR /shairport-sync RUN git checkout "$SHAIRPORT_SYNC_BRANCH" -RUN git pull RUN autoreconf -fi RUN git submodule init RUN git submodule update From 46742624862440f98cb04e03cfdc2b2b9cfb8c6c Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:00:43 +0100 Subject: [PATCH 089/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- docker/Dockerfile | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index f751f7bd..60ab6048 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -35,4 +35,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | BRANCH=${{ env.BRANCH }} - ssh: default=${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file + ssh: ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 584b73ec..b9098c8f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,3 @@ -# syntax=docker/dockerfile:experimental - FROM alpine:3.12 AS builder-base # Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard # in debian/ubuntu. @@ -51,8 +49,6 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - RUN --mount=type=ssh git clone git@github.com:charlesomer/nqptp.git WORKDIR /nqptp # Use development branch for now. From 2a06e349f420f765f296b1f4c50c2476dedb6ac7 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:07:05 +0100 Subject: [PATCH 090/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- docker/Dockerfile | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 60ab6048..be7f644e 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -35,4 +35,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | BRANCH=${{ env.BRANCH }} - ssh: ${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file + ssh: github=${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index b9098c8f..d12ad352 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -49,7 +49,10 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -RUN --mount=type=ssh git clone git@github.com:charlesomer/nqptp.git +# TODO: Remove when no longer pulling from private repos. +RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts + +RUN --mount=type=ssh,id=github git clone git@github.com:charlesomer/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development @@ -59,7 +62,7 @@ RUN make RUN make install WORKDIR / -RUN --mount=type=ssh git clone git@github.com:aillwee/shairport-sync.git +RUN --mount=type=ssh,id=github git clone git@github.com:aillwee/shairport-sync.git WORKDIR /shairport-sync RUN git checkout "$SHAIRPORT_SYNC_BRANCH" RUN autoreconf -fi From 21c6f93ae1103c48424cf30d4d511453ea12dcbd Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:08:47 +0100 Subject: [PATCH 091/878] Try this. --- .github/workflows/docker-build-master.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index be7f644e..f4054eb5 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,9 +30,9 @@ jobs: with: context: ./ file: ./docker/Dockerfile + ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | - BRANCH=${{ env.BRANCH }} - ssh: github=${{ secrets.SSH_PRIVATE_KEY }} \ No newline at end of file + BRANCH=${{ env.BRANCH }} \ No newline at end of file From 859ea80447343a9359dfe82d8e2cdf16f35c9094 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:10:28 +0100 Subject: [PATCH 092/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index f4054eb5..114a468e 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,7 +30,7 @@ jobs: with: context: ./ file: ./docker/Dockerfile - ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Can be removed when no longer pulling from private repos. + ssh: [github=${{ secrets.SSH_PRIVATE_KEY }}] # Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} From 4d05d4ab3b5eefd478ac2f14c002f2341f438f1d Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:16:21 +0100 Subject: [PATCH 093/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 114a468e..2671b155 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,7 +30,7 @@ jobs: with: context: ./ file: ./docker/Dockerfile - ssh: [github=${{ secrets.SSH_PRIVATE_KEY }}] # Can be removed when no longer pulling from private repos. + ssh: "github=${{ secrets.SSH_PRIVATE_KEY }}" # Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} From 0fe12907238f919aaaac7d5781818c7c87ee4616 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:17:25 +0100 Subject: [PATCH 094/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 2671b155..0533f728 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,7 +30,7 @@ jobs: with: context: ./ file: ./docker/Dockerfile - ssh: "github=${{ secrets.SSH_PRIVATE_KEY }}" # Can be removed when no longer pulling from private repos. + ssh: github=test # Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} From b35fa40cf46c91c387c077dfe66a3582f6bce0c8 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:20:57 +0100 Subject: [PATCH 095/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 0533f728..f4054eb5 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,7 +30,7 @@ jobs: with: context: ./ file: ./docker/Dockerfile - ssh: github=test # Can be removed when no longer pulling from private repos. + ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} From a53e80ed07a3db070b3bc7811806ed19dff4b8e2 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:22:07 +0100 Subject: [PATCH 096/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index f4054eb5..252a9ae6 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -30,7 +30,7 @@ jobs: with: context: ./ file: ./docker/Dockerfile - ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Can be removed when no longer pulling from private repos. + # ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Has to be added to repo without line breaks. Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} From dc510f1ebc87789a1180185144992739747191e9 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:46:14 +0100 Subject: [PATCH 097/878] Try this. --- .github/workflows/docker-build-master.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 252a9ae6..5524f846 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -15,6 +15,9 @@ jobs: uses: actions/checkout@v2 - name: Set branch env. run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - uses: webfactory/ssh-agent # Can be removed when no longer pulling from private repos. + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx @@ -30,9 +33,9 @@ jobs: with: context: ./ file: ./docker/Dockerfile - # ssh: github=${{ secrets.SSH_PRIVATE_KEY }} # Has to be added to repo without line breaks. Can be removed when no longer pulling from private repos. platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} build-args: | - BRANCH=${{ env.BRANCH }} \ No newline at end of file + BRANCH=${{ env.BRANCH }} + ssh: github=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file From 511eff799961cae2e3020762a66a227ffcc798c5 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:49:13 +0100 Subject: [PATCH 098/878] Try this. --- .github/workflows/docker-build-master.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 5524f846..3f9a87a8 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -15,7 +15,8 @@ jobs: uses: actions/checkout@v2 - name: Set branch env. run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - uses: webfactory/ssh-agent # Can be removed when no longer pulling from private repos. + - name: Set up ssh agent + uses: webfactory/ssh-agent # Can be removed when no longer pulling from private repos. with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Set up QEMU From 503e59220bdc0a020a59b5c2172e7dbced23942f Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:51:24 +0100 Subject: [PATCH 099/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 3f9a87a8..52ce2daf 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -16,7 +16,7 @@ jobs: - name: Set branch env. run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up ssh agent - uses: webfactory/ssh-agent # Can be removed when no longer pulling from private repos. + uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Set up QEMU From 942cf5026f968f22fdaaee30d64b06da2475a914 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 02:53:21 +0100 Subject: [PATCH 100/878] Try this. --- .github/workflows/docker-build-master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build-master.yaml index 52ce2daf..71ca9d87 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build-master.yaml @@ -1,4 +1,4 @@ -# Pushes to tag 'unstable' since it uses the master branch directly. +# Pushes to tag 'unstable-BRANCH'. # Releases are pushed to latest and tagged - see docker-build-releases.yaml. name: Build and push docker image based on master branch. From 233de61032f1846f5a0c681321e67374f4c4bafa Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 11:36:02 +0100 Subject: [PATCH 101/878] Test. --- docker/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 90aaefa1..74b986ca 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1 +1,3 @@ -# Shairport Sync Docker Image \ No newline at end of file +# Shairport Sync Docker Image + +https://hub.docker.com/r/mikebrady/shairport-sync \ No newline at end of file From 0ff054b9ce621de100cc5555c1ae2b0bbb23955e Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 6 May 2021 11:53:19 +0100 Subject: [PATCH 102/878] Updated docker compose. --- docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8adbd6df..4153577a 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -3,7 +3,7 @@ version: "3.8" services: shairport-sync: - image: registry.nova.do/shairport-sync:dev + image: registry.nova.do/shairport-sync:unstable-development network_mode: host restart: unless-stopped devices: From 888f292ddedfafaeb26ba18ce3d7955e9d1c8464 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 7 May 2021 07:03:14 +0100 Subject: [PATCH 103/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee22144c..edba6115 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. -* [Charles Somer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From 0aa3b3fee384fd3e42b14620705d82f1bfe57457 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 7 May 2021 19:10:07 +0200 Subject: [PATCH 104/878] Return same pairing errors as other Airplay devices - 470 on general errors - TLV-formatet authentication error on incorrect PIN/signature The latter makes sure clients clear saved credentials, so the user isn't stuck with a device that keeps using incorrect credentials. --- pair_ap | 2 +- rtsp.c | 32 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pair_ap b/pair_ap index 1b9954cb..1a83fd5c 160000 --- a/pair_ap +++ b/pair_ap @@ -1 +1 @@ -Subproject commit 1b9954cba18631c9671595fca85565308e8b0972 +Subproject commit 1a83fd5c501bbf8390b2b34bfb94c59f9889a98a diff --git a/rtsp.c b/rtsp.c index 74a1249a..0c2ca760 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1420,8 +1420,8 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) #ifdef CONFIG_AIRPLAY_2 void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; - uint8_t *body; - size_t body_len; + uint8_t *body = NULL; + size_t body_len = 0; struct pair_result *result; debug(2, "Connection %d: pair-verify Content-Length %d", conn->connection_number, req->contentlength); @@ -1430,12 +1430,16 @@ void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *r conn->ap2_control_pairing.verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); if (!conn->ap2_control_pairing.verify_ctx) { debug(1, "Error creating verify context"); + resp->respcode = 500; // Internal Server Error + goto out; } } ret = pair_verify(&body, &body_len, conn->ap2_control_pairing.verify_ctx, (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, pair_verify_errmsg(conn->ap2_control_pairing.verify_ctx)); + resp->respcode = 470; // Connection Authorization Required + goto out; } ret = pair_verify_result(&result, conn->ap2_control_pairing.verify_ctx); @@ -1443,19 +1447,22 @@ void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *r conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); if (!conn->ap2_control_pairing.cipher_ctx) { debug(1, "Error setting up rtsp control channel ciphering\n"); + goto out; } } + out: resp->content = (char *)body; // these will be freed when the data is sent resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); + if (body) + msg_add_header(resp, "Content-Type", "application/octet-stream"); debug_log_rtsp_message(2, "pair-verify response", resp); } void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; - uint8_t *body; - size_t body_len; + uint8_t *body = NULL; + size_t body_len = 0; struct pair_result *result; debug(2, "Connection %d: pair-setup Content-Length %d", conn->connection_number, req->contentlength); @@ -1464,16 +1471,16 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, NULL, NULL, config.airplay_device_id); if (!conn->ap2_control_pairing.setup_ctx) { debug(1, "Error creating setup context"); - resp->respcode = 451; - return; + resp->respcode = 500; // Internal Server Error + goto out; } } ret = pair_setup(&body, &body_len, conn->ap2_control_pairing.setup_ctx, (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, pair_setup_errmsg(conn->ap2_control_pairing.setup_ctx)); - resp->respcode = 451; - return; + resp->respcode = 470; // Connection Authorization Required + goto out; } ret = pair_setup_result(NULL, &result, conn->ap2_control_pairing.setup_ctx); @@ -1483,14 +1490,15 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); if (!conn->ap2_control_pairing.cipher_ctx) { debug(1, "Error setting up rtsp control channel ciphering\n"); - resp->respcode = 451; - return; + goto out; } } + out: resp->content = (char *)body; // these will be freed when the data is sent resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); + if (body) + msg_add_header(resp, "Content-Type", "application/octet-stream"); debug_log_rtsp_message(2, "pair-setup response", resp); } From f5cd4238f0e97279c84d79baae10ba5309c61dfb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 7 May 2021 19:26:48 +0200 Subject: [PATCH 105/878] Announce the actual public key (pk) in Bonjour and GET /info Same as other Airplay 2 devices do, even though it doesn't seem to be used by any clients. --- common.h | 3 +-- rtsp.c | 33 ++++++++++++++++++++++++++------- shairport.c | 15 --------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/common.h b/common.h index 2172e287..6cabfba5 100644 --- a/common.h +++ b/common.h @@ -299,8 +299,7 @@ typedef struct { uint32_t airplay_statusflags; char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing - char *airplay_pk; // "pk" string in the Bonjour advertisement - char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist + char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist char *airplay_gid; // UUID in the Bonjour advertisement -- initially the same as the pi #endif } shairport_cfg; diff --git a/rtsp.c b/rtsp.c index 0c2ca760..067749a8 100644 --- a/rtsp.c +++ b/rtsp.c @@ -886,6 +886,19 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou free(encrypted); return count; } + +static void pkString_make(char *str, size_t str_size, const char *device_id) { + uint8_t public_key[32]; + if (str_size < 2 * sizeof(public_key) + 1) { + warn("Insufficient string size"); + str[0] = '\0'; + return; + } + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); + char *ptr = str; + for (size_t i = 0; i < sizeof(public_key); i++) + ptr += sprintf(ptr, "%02x", public_key[i]); +} #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { @@ -1238,11 +1251,14 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); char *vs = get_version_string(); plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + free(vs); + char pkString[128]; + pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); plist_to_bin(response_plist, &resp->content, &resp->contentlength); if (resp->contentlength == 0) debug(1, "GET /info Stage 1: response bplist not created!"); plist_free(response_plist); - free(vs); } msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); debug_log_rtsp_message(2, "GET /info Stage 1 Response:", resp); @@ -1264,6 +1280,9 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r char *vs = get_version_string(); plist_dict_set_item(response_plist, "model", plist_new_string(vs)); free(vs); + char pkString[128]; + pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); plist_to_bin(response_plist, &resp->content, &resp->contentlength); plist_free(response_plist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); @@ -4106,7 +4125,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { #ifdef CONFIG_AIRPLAY_2 *p++ = "srcvers=366.0"; char deviceIdString[64]; - snprintf(deviceIdString, sizeof(deviceIdString) - 1, "deviceid=%s", config.airplay_device_id); + snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); *p++ = deviceIdString; // features is a 64 bit number, least significant 32 bits char featuresString[64]; @@ -4114,8 +4133,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { features_hi = (features_hi >> 32) & 0xffffffff; uint64_t features_lo = config.airplay_features; features_lo = features_lo & 0xffffffff; - // @mikebrady I don't think the -1 here (and other places) is necessary - snprintf(featuresString, sizeof(featuresString) - 1, "features=0x%" PRIx64 ",0x%" PRIx64 "", + snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIx64 ",0x%" PRIx64 "", features_lo, features_hi); *p++ = featuresString; char statusflagsString[32]; @@ -4127,14 +4145,15 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = "fv=p20.78000.12"; *p++ = "model=SPS"; char piString[64]; - snprintf(piString, sizeof(piString) - 1, "pi=%s", config.airplay_pi); + snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); *p++ = piString; char gidString[64]; - snprintf(gidString, sizeof(gidString) - 1, "gid=%s", config.airplay_gid); + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); *p++ = gidString; *p++ = "gcgl=0"; char pkString[128]; - snprintf(pkString, sizeof(pkString) - 1, "pk=%s", config.airplay_pk); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); *p++ = pkString; *p++ = NULL; #else diff --git a/shairport.c b/shairport.c index 303a282a..afe9ab4a 100644 --- a/shairport.c +++ b/shairport.c @@ -1591,21 +1591,6 @@ int main(int argc, char **argv) { } else debug(1, "Started in Airplay 2 mode!"); - // now make up a 32-byte "pk" string from random numbers - const ssize_t pk_size = 32; - char pk_bytes[pk_size]; - char pk_string[pk_size * 2 + 1]; - randombytes_buf(pk_bytes, pk_size); // using libsodium - char *obfp = pk_string; - int obfc; - for (obfc = 0; obfc < pk_size; obfc++) { - snprintf(obfp, 3, "%02X", pk_bytes[obfc]); - obfp += 2; - }; - *obfp = 0; - debug(1, "pk string: \"%s\"", pk_string); - config.airplay_pk = strdup(pk_string); - // now generate a UUID // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c // with thanks From e270f8a9a5307d34bde90cf3942593670eaefe02 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 8 May 2021 15:29:54 +0100 Subject: [PATCH 106/878] Update configure.ac Bumped for an update to `pair_ap`. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 087ef5a6..3336dcfc 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0d2], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0d3], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From 953ad8ac147e487e5af0c5189138261477cf14eb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 8 May 2021 17:46:13 +0100 Subject: [PATCH 107/878] Move AP2 player termination code from handle_teardown into the player cleanup function so that cancelling the player thread cleans up properly. --- player.c | 166 ++++++++++++++++++++++-------------- player.h | 4 +- rtp.c | 27 +++--- rtsp.c | 111 ++++++++---------------- rtsp.h | 5 +- scripts/shairport-sync.conf | 2 +- shairport.c | 9 +- 7 files changed, 161 insertions(+), 163 deletions(-) diff --git a/player.c b/player.c index 24640c5a..bd1f2ce0 100644 --- a/player.c +++ b/player.c @@ -443,7 +443,7 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta conn->ab_read = seqno; conn->ab_synced = 1; conn->first_packet_timestamp = 0; - debug(1,"synced by first packet"); + debug(1, "synced by first packet"); } else if (original_format == 0) { // if the packet is coming in original format, the sequence number is important // otherwise, ignore is by setting it equal to the expected sequence number in ab_write @@ -1541,12 +1541,12 @@ void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { - pthread_cleanup_push(mutex_unlock,&playing_conn_lock); - if (playing_conn == conn) { - if (config.output->stop) - config.output->stop(); - } - pthread_cleanup_pop(1); // unlock the mutex + pthread_cleanup_push(mutex_unlock, &playing_conn_lock); + if (playing_conn == conn) { + if (config.output->stop) + config.output->stop(); + } + pthread_cleanup_pop(1); // unlock the mutex } int oldState; @@ -1554,19 +1554,21 @@ void player_thread_cleanup_handler(void *arg) { debug(3, "Connection %d: player thread main loop exit via player_thread_cleanup_handler.", conn->connection_number); - if (config.statistics_requested) { int rawSeconds = (int)difftime(time(NULL), conn->playstart); int elapsedHours = rawSeconds / 3600; int elapsedMin = (rawSeconds / 60) % 60; int elapsedSec = rawSeconds % 60; if (conn->frame_rate_status) - inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, output: %0.2f " - "frames per second.", conn->connection_number, - elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate, conn->frame_rate); + inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, " + "output: %0.2f " + "frames per second.", + conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate, + conn->frame_rate); else - inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f frames per second.", conn->connection_number, - elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate); + inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f " + "frames per second.", + conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate); } #ifdef CONFIG_DACP_CLIENT @@ -1576,57 +1578,92 @@ void player_thread_cleanup_handler(void *arg) { mdns_dacp_monitor_set_id(NULL); // say we're not interested in following that DACP id any more #endif -#ifdef CONFIG_AIRPORT_2 - if (conn->rtp_ap2_control_thread) { - debug(3, "Cancelling rtp_ap2_control_thread"); - pthread_cancel(conn->rtp_ap2_control_thread); - pthread_join(conn->rtp_ap2_control_thread, NULL); - } - if (conn->ap2_control_socket) { - close(conn->ap2_control_socket); - conn->ap2_control_socket = 0; - } + // four possibilities + // 1 -- regular AirPlay 1 + // 2 -- AirPlay 2 in AirPlay 1 mode + // 3 -- AirPlay 2 in Buffered Audio Mode + // 4 -- AirPlay 3 in Realtime Audio Mode. - // realtime stuff - if (conn->rtp_realtime_audio_thread) { - debug(3, "Cancelling rtp_realtime_audio_thread"); - pthread_cancel(conn->rtp_realtime_audio_thread); - pthread_join(conn->rtp_realtime_audio_thread, NULL); - } - if (conn->realtime_audio_socket) { - close(conn->realtime_audio_socket); - conn->realtime_audio_socket = 0; - } +#ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_2) { + debug(1, "In AP2 mode"); - // buffered stuff - if (conn->rtp_buffered_audio_thread) { - debug(3, "Cancelling rtp_buffered_audio_thread"); - pthread_cancel(conn->rtp_buffered_audio_thread); - pthread_join(conn->rtp_buffered_audio_thread, NULL); - } - if (conn->buffered_audio_socket) { - close(conn->buffered_audio_socket); - conn->buffered_audio_socket = 0; + if (conn->airplay_stream_type == realtime_stream) { + debug(2, "Connection %d: TEARDOWN Delete Realtime Audio Stream Thread", + conn->connection_number); + pthread_cancel(conn->rtp_realtime_audio_thread); + pthread_join(conn->rtp_realtime_audio_thread, NULL); + if (conn->realtime_audio_socket) + close(conn->realtime_audio_socket); + } else if (conn->airplay_stream_type == buffered_stream) { + debug(2, "Connection %d: TEARDOWN Delete Buffered Audio Stream Thread", + conn->connection_number); + pthread_cancel(conn->rtp_buffered_audio_thread); + pthread_join(conn->rtp_buffered_audio_thread, NULL); + if (conn->buffered_audio_socket) + close(conn->buffered_audio_socket); + } else { + die("Unrecognised Stream Type"); + } + + if (conn->rtp_ap2_control_thread) { + debug(1, "Cancelling rtp_ap2_control_thread"); + pthread_cancel(conn->rtp_ap2_control_thread); + pthread_join(conn->rtp_ap2_control_thread, NULL); + } + if (conn->ap2_control_socket) { + close(conn->ap2_control_socket); + conn->ap2_control_socket = 0; + conn->ap2_remote_control_socket_addr_length = + 0; // indicates to the control receiver thread that the socket address need to be + // recreated. + } + + if (conn->airplay_stream_type == realtime_stream) { + // realtime stuff + if (conn->rtp_realtime_audio_thread) { + debug(1, "Cancelling rtp_realtime_audio_thread"); + pthread_cancel(conn->rtp_realtime_audio_thread); + pthread_join(conn->rtp_realtime_audio_thread, NULL); + } + if (conn->realtime_audio_socket) { + close(conn->realtime_audio_socket); + conn->realtime_audio_socket = 0; + } + } else if (conn->airplay_stream_type == buffered_stream) { + // buffered stuff + if (conn->rtp_buffered_audio_thread) { + debug(3, "Cancelling rtp_buffered_audio_thread"); + pthread_cancel(conn->rtp_buffered_audio_thread); + pthread_join(conn->rtp_buffered_audio_thread, NULL); + } + if (conn->buffered_audio_socket) { + close(conn->buffered_audio_socket); + conn->buffered_audio_socket = 0; + } + } + } else { +#endif + debug(1, "Cancelling AP1 timing, control and audio threads..."); + debug(3, "Cancel timing thread."); + pthread_cancel(conn->rtp_timing_thread); + debug(3, "Join timing thread."); + pthread_join(conn->rtp_timing_thread, NULL); + debug(3, "Timing thread terminated."); + debug(3, "Cancel control thread."); + pthread_cancel(conn->rtp_control_thread); + debug(3, "Join control thread."); + pthread_join(conn->rtp_control_thread, NULL); + debug(3, "Control thread terminated."); + debug(3, "Cancel audio thread."); + pthread_cancel(conn->rtp_audio_thread); + debug(3, "Join audio thread."); + pthread_join(conn->rtp_audio_thread, NULL); + debug(3, "Audio thread terminated."); +#ifdef CONFIG_AIRPLAY_2 } #endif - debug(3, "Cancelling timing, control and audio threads..."); - debug(3, "Cancel timing thread."); - pthread_cancel(conn->rtp_timing_thread); - debug(3, "Join timing thread."); - pthread_join(conn->rtp_timing_thread, NULL); - debug(3, "Timing thread terminated."); - debug(3, "Cancel control thread."); - pthread_cancel(conn->rtp_control_thread); - debug(3, "Join control thread."); - pthread_join(conn->rtp_control_thread, NULL); - debug(3, "Control thread terminated."); - debug(3, "Cancel audio thread."); - pthread_cancel(conn->rtp_audio_thread); - debug(3, "Join audio thread."); - pthread_join(conn->rtp_audio_thread, NULL); - debug(3, "Audio thread terminated."); - if (conn->outbuf) { free(conn->outbuf); conn->outbuf = NULL; @@ -1649,6 +1686,7 @@ void player_thread_cleanup_handler(void *arg) { terminate_decoders(conn); reset_anchor_info(conn); + release_play_lock(conn); conn->rtp_running = 0; pthread_setcancelstate(oldState, NULL); } @@ -2282,7 +2320,8 @@ void *player_thread_func(void *arg) { int64_t filler_length = (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { - debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", sync_error, (sync_error * 1.0)/config.output_rate); + debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", + sync_error, (sync_error * 1.0) / config.output_rate); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; uint32_t frames_to_drop_sized = local_frames_to_drop; @@ -2295,9 +2334,10 @@ void *player_thread_func(void *arg) { } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, - "Large negative sync error of: %" PRId64 " frames (%f seconds), with should_be_frame_32 of %" PRIu32 - ", nt of %" PRId64 " and current_delay of %" PRId64 ".", - sync_error, (sync_error * 1.0)/config.output_rate, should_be_frame_32, + "Large negative sync error of: %" PRId64 + " frames (%f seconds), with should_be_frame_32 of %" PRIu32 ", nt of %" PRId64 + " and current_delay of %" PRId64 ".", + sync_error, (sync_error * 1.0) / config.output_rate, should_be_frame_32, inframe->given_timestamp * conn->output_sample_ratio, current_delay); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) diff --git a/player.h b/player.h index ab7eda9d..25003e5b 100644 --- a/player.h +++ b/player.h @@ -91,6 +91,7 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; +typedef enum { realtime_stream, buffered_stream } airplay_stream_t; typedef struct { uint8_t *data; @@ -259,7 +260,8 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? - timing_t timing_type; // are we using NTP or PTP on this connection? + airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... + timing_t timing_type; // are we using NTP or PTP on this connection? pthread_t rtp_event_thread; pthread_t rtp_ap2_control_thread; diff --git a/rtp.c b/rtp.c index 182d19ba..84f0d800 100644 --- a/rtp.c +++ b/rtp.c @@ -120,7 +120,7 @@ void rtp_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_audio_receiver(void *arg) { - debug(1, "rtp_audio_receiver start"); + debug(3, "rtp_audio_receiver start"); pthread_cleanup_push(rtp_audio_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -259,7 +259,7 @@ void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) { } void *rtp_control_receiver(void *arg) { - debug(1, "rtp_control_receiver start"); + debug(2, "rtp_control_receiver start"); pthread_cleanup_push(rtp_control_handler_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -406,7 +406,7 @@ void *rtp_control_receiver(void *arg) { if (la != conn->latency) { conn->latency = la; - debug(1, + debug(2, "New latency: %" PRIu32 ", sync latency: %" PRIu32 ", minimum latency: %" PRIu32 ", maximum " "latency: %" PRIu32 ", fixed offset: %" PRIu32 @@ -508,7 +508,7 @@ void rtp_timing_sender_cleanup_handler(void *arg) { } void *rtp_timing_sender(void *arg) { - debug(1, "rtp_timing_sender start"); + debug(2, "rtp_timing_sender start"); pthread_cleanup_push(rtp_timing_sender_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; struct timing_request { @@ -610,7 +610,7 @@ void rtp_timing_receiver_cleanup_handler(void *arg) { } void *rtp_timing_receiver(void *arg) { - debug(1, "rtp_timing_receiver start"); + debug(3, "rtp_timing_receiver start"); pthread_cleanup_push(rtp_timing_receiver_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; @@ -2090,7 +2090,8 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { + if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || + (streaming_has_started == 1)) { if (streaming_has_started == 0) debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); streaming_has_started = 1; diff --git a/rtsp.c b/rtsp.c index 067749a8..dde6cff7 100644 --- a/rtsp.c +++ b/rtsp.c @@ -845,19 +845,20 @@ static ssize_t read_encrypted(int fd, ap2_pairing *ctx, void *buf, size_t count) return got; buf_add(&ctx->encrypted_buf, in, got); - ssize_t consumed = pair_decrypt(&plain, &plain_len, ctx->encrypted_buf.data, ctx->encrypted_buf.len, ctx->cipher_ctx); + ssize_t consumed = pair_decrypt(&plain, &plain_len, ctx->encrypted_buf.data, + ctx->encrypted_buf.len, ctx->cipher_ctx); if (consumed < 0) return -1; buf_drain(&ctx->encrypted_buf, consumed); } while (plain_len == 0); // Fast path, avoids some memcpy + allocs in case of the normal, small message -/* if (ctx->plain_buf.len == 0 && plain_len < count) { - memcpy(buf, plain, plain_len); - free(plain); - return plain_len; - } -*/ + /* if (ctx->plain_buf.len == 0 && plain_len < count) { + memcpy(buf, plain, plain_len); + free(plain); + return plain_len; + } + */ buf_add(&ctx->plain_buf, plain, plain_len); free(plain); @@ -868,7 +869,8 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou uint8_t *encrypted; size_t encrypted_len; - ssize_t ret = pair_encrypt(&encrypted, &encrypted_len, buf, count, conn->ap2_control_pairing.cipher_ctx); + ssize_t ret = + pair_encrypt(&encrypted, &encrypted_len, buf, count, conn->ap2_control_pairing.cipher_ctx); if (ret < 0) { debug(1, pair_cipher_errmsg(conn->ap2_control_pairing.cipher_ctx)); return -1; @@ -1245,7 +1247,8 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r debug(1, "GET /info Stage 1: response plist not created from XML!"); } else { plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); - plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); + plist_dict_set_item(response_plist, "statusFlags", + plist_new_uint(config.airplay_statusflags)); plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); @@ -1446,7 +1449,8 @@ void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *r req->contentlength); if (!conn->ap2_control_pairing.verify_ctx) { - conn->ap2_control_pairing.verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); + conn->ap2_control_pairing.verify_ctx = + pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); if (!conn->ap2_control_pairing.verify_ctx) { debug(1, "Error creating verify context"); resp->respcode = 500; // Internal Server Error @@ -1454,7 +1458,8 @@ void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *r } } - ret = pair_verify(&body, &body_len, conn->ap2_control_pairing.verify_ctx, (const uint8_t *)req->content, req->contentlength); + ret = pair_verify(&body, &body_len, conn->ap2_control_pairing.verify_ctx, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, pair_verify_errmsg(conn->ap2_control_pairing.verify_ctx)); resp->respcode = 470; // Connection Authorization Required @@ -1463,14 +1468,15 @@ void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *r ret = pair_verify_result(&result, conn->ap2_control_pairing.verify_ctx); if (ret == 0 && result->shared_secret_len > 0) { - conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + conn->ap2_control_pairing.cipher_ctx = + pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); if (!conn->ap2_control_pairing.cipher_ctx) { debug(1, "Error setting up rtsp control channel ciphering\n"); goto out; } } - out: +out: resp->content = (char *)body; // these will be freed when the data is sent resp->contentlength = body_len; if (body) @@ -1487,7 +1493,8 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re req->contentlength); if (!conn->ap2_control_pairing.setup_ctx) { - conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, NULL, NULL, config.airplay_device_id); + conn->ap2_control_pairing.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, + NULL, NULL, config.airplay_device_id); if (!conn->ap2_control_pairing.setup_ctx) { debug(1, "Error creating setup context"); resp->respcode = 500; // Internal Server Error @@ -1495,7 +1502,8 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re } } - ret = pair_setup(&body, &body_len, conn->ap2_control_pairing.setup_ctx, (const uint8_t *)req->content, req->contentlength); + ret = pair_setup(&body, &body_len, conn->ap2_control_pairing.setup_ctx, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, pair_setup_errmsg(conn->ap2_control_pairing.setup_ctx)); resp->respcode = 470; // Connection Authorization Required @@ -1506,14 +1514,15 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re if (ret == 0 && result->shared_secret_len > 0) { // Transient pairing completed (pair-setup step 2), prepare encryption, but // don't activate yet, the response to this request is still plaintext - conn->ap2_control_pairing.cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + conn->ap2_control_pairing.cipher_ctx = + pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); if (!conn->ap2_control_pairing.cipher_ctx) { debug(1, "Error setting up rtsp control channel ciphering\n"); goto out; } } - out: +out: resp->content = (char *)body; // these will be freed when the data is sent resp->contentlength = body_len; if (body) @@ -1722,68 +1731,12 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag if (streams) { // we are being asked to close a stream - // at present, we just close this one - debug(2, - "Connection %d: A \"streams\" array has been found -- stop the player, stop and delete " - "the control " - "and audio threads and close the ports", - conn->connection_number); - - // get stream[0] - plist_t stream0 = plist_array_get_item(streams, 0); - - plist_t item = plist_dict_get_item(stream0, "type"); - uint64_t item_value; - plist_get_uint_val(item, &item_value); - - switch (item_value) { - case 96: { - release_play_lock(conn); - debug(2, "Connection %d: TEARDOWN Delete Realtime Audio Stream Thread", - conn->connection_number); - pthread_cancel(conn->rtp_realtime_audio_thread); - pthread_join(conn->rtp_realtime_audio_thread, NULL); - if (conn->realtime_audio_socket) - close(conn->realtime_audio_socket); - } break; - case 103: { - release_play_lock(conn); // shoulda been released when stopped... - debug(2, "Connection %d: TEARDOWN Delete Buffered Audio Stream Thread", - conn->connection_number); - pthread_cancel(conn->rtp_buffered_audio_thread); - pthread_join(conn->rtp_buffered_audio_thread, NULL); - if (conn->buffered_audio_socket) - close(conn->buffered_audio_socket); - } break; - default: {} - } - - debug(2, "Connection %d: TEARDOWN Close Control Thread", conn->connection_number); - pthread_cancel(conn->rtp_ap2_control_thread); - pthread_join(conn->rtp_ap2_control_thread, NULL); - - debug(2, "Connection %d: TEARDOWN Close Control Socket.", conn->connection_number); - if (conn->ap2_control_socket) { - close(conn->ap2_control_socket); - conn->ap2_remote_control_socket_addr_length = - 0; // indicates to the control receiver thread that the socket address need to be - // recreated. - } - - debug(2, "Connection %d: TEARDOWN Stop the Player.", conn->connection_number); player_stop(conn); - if (conn->session_key) { free(conn->session_key); conn->session_key = NULL; } plist_free(streams); - - release_play_lock(conn); - - // if (ptp_shm_interface_close() != 0) - // warn("Error closing the nqptp interface."); - debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { // we are being asked to disconnect @@ -1939,7 +1892,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); get_play_lock(conn); - + conn->airplay_stream_type = realtime_stream; // bind a new UDP port and get a socket conn->local_realtime_audio_port = 0; // any port err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, @@ -1992,6 +1945,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) case 103: { debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); + // for buffered audio, delay getting the play lock until we actually start playing... + conn->airplay_stream_type = buffered_stream; // get needed stuff // bind a new TCP port and get a socket @@ -2128,7 +2083,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "Can't find timingPeerInfo addresses"); } } else { - debug(1, "Can't find timingPeerInfo"); + debug_log_rtsp_message(1, "Unrecognised SETUP incoming message", req); } // make up the timing peer info list part of the response... @@ -4137,7 +4092,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { features_lo, features_hi); *p++ = featuresString; char statusflagsString[32]; - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIx32, config.airplay_statusflags); + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIx32, + config.airplay_statusflags); *p++ = statusflagsString; *p++ = "protovers=1.1"; *p++ = "acl=0"; @@ -4153,7 +4109,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = "gcgl=0"; char pkString[128]; snprintf(pkString, sizeof(pkString), "pk="); - pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); *p++ = pkString; *p++ = NULL; #else diff --git a/rtsp.h b/rtsp.h index 89f5e151..5222b8e2 100644 --- a/rtsp.h +++ b/rtsp.h @@ -8,10 +8,7 @@ extern rtsp_conn_info **conns; void *rtsp_listen_loop(__attribute((unused)) void *arg); -// void rtsp_shutdown_stream(void); -void rtsp_request_shutdown_stream(void); - -void cancel_all_RTSP_threads(void); +void release_play_lock(rtsp_conn_info *conn); // initialise and completely delete the metadata stuff diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index e7f9e748..f377c221 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -40,7 +40,7 @@ general = // volume_control_profile = "standard" ; // use this advanced setting to specify how the airplay volume is transferred to the mixer volume. // "standard" makes the volume change more quickly at lower volumes and slower at higher volumes. // "flat" makes the volume change at the same rate at all volumes. -// volume_range_combined_hardware_priority = "no"; // when extending the volume range by combining the built-in software attenuator with the hardware mixer attenuator, set this to "yes" to reduce volume by using the hardware mixer first, then the built-in software attenuator. +// volume_control_combined_hardware_priority = "no"; // when extending the volume range by combining the built-in software attenuator with the hardware mixer attenuator, set this to "yes" to reduce volume by using the hardware mixer first, then the built-in software attenuator. // run_this_when_volume_is_set = "/full/path/to/application/and/args"; // Run the specified application whenever the volume control is set or changed. // The desired AirPlay volume is appended to the end of the command line – leave a space if you want it treated as an extra argument. // AirPlay volume goes from 0 to -30 and -144 means "mute". diff --git a/shairport.c b/shairport.c index afe9ab4a..89fa5858 100644 --- a/shairport.c +++ b/shairport.c @@ -1577,10 +1577,11 @@ int main(int argc, char **argv) { // 496155702020608 this setting here is the source of both the plist features response and the // mDNS string. config.airplay_features = 0x1C340405F4A00; - // Advertised with mDNS and returned with GET /info, see https://openairplay.github.io/airplay-spec/status_flags.html - // 0x4: Audio cable attached, no PIN required (transient pairing), no Homekit access control - // 0x204: Audio cable attached, OneTimePairingRequired - // 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit access control + // Advertised with mDNS and returned with GET /info, see + // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN + // required (transient pairing), no Homekit access control 0x204: Audio cable attached, + // OneTimePairingRequired 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit + // access control config.airplay_statusflags = 0x4; // Set to NULL to work with transient pairing config.airplay_pin = NULL; From 7ae72d795fe70d104351319c61cf81cee59e38fb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 09:01:25 +0100 Subject: [PATCH 108/878] add plist cleanup for pthread cleanup --- common.c | 11 ++++++++--- common.h | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common.c b/common.c index 2cfed813..068f78b8 100644 --- a/common.c +++ b/common.c @@ -1558,12 +1558,16 @@ int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexname, const cha void malloc_cleanup(void *arg) { // debug(1, "malloc cleanup called."); free(arg); - arg = NULL; +} + +void plist_cleanup(void *arg) { + // debug(1, "plist cleanup called."); + plist_free((plist_t)arg); } void socket_cleanup(void *arg) { - // debug(1, "socket_cleanup called."); intptr_t fdp = (intptr_t)arg; + debug(3, "socket_cleanup called for socket: %" PRIdPTR ".", fdp); close(fdp); } @@ -1582,13 +1586,14 @@ void mutex_cleanup(void *arg) { void mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); } void thread_cleanup(void *arg) { - // debug(1, "mutex_cleanup called."); + debug(3, "thread_cleanup called."); pthread_t *thread = (pthread_t *)arg; pthread_cancel(*thread); int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); pthread_join(*thread, NULL); pthread_setcancelstate(oldState, NULL); + debug(3, "thread_cleanup done."); } void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); } diff --git a/common.h b/common.h index 6cabfba5..8ff0978c 100644 --- a/common.h +++ b/common.h @@ -471,6 +471,7 @@ void mutex_unlock(void *arg); void mutex_cleanup(void *arg); void cv_cleanup(void *arg); void thread_cleanup(void *arg); +void plist_cleanup(void *arg); char *debug_malloc_hex_cstring(void *packet, size_t nread); From d2d35cc758614af5829ebf12031a68efbbe9fbe4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 09:36:14 +0100 Subject: [PATCH 109/878] Quieten speed and size debug message --- audio_alsa.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index d5757bef..f0c10987 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -525,7 +525,7 @@ int actual_open_alsa_device(int do_auto_setup) { } if (ret == 0) { config.output_format = trial_format; - debug(1, "alsa: output format chosen is \"%s\".", + debug(2, "alsa: output format chosen is \"%s\".", sps_format_description_string(config.output_format)); } else { warn("audio_alsa: Could not automatically set the output format for device \"%s\": %s", @@ -566,7 +566,7 @@ int actual_open_alsa_device(int do_auto_setup) { } if (ret == 0) { config.output_rate = actual_sample_rate; - debug(1, "alsa: output speed chosen is %d.", config.output_rate); + debug(2, "alsa: output speed chosen is %d.", config.output_rate); } else { warn("audio_alsa: Could not automatically set the output rate for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); From 829ea399fc13608d5594b10d1bc4f870f842d5f5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 09:37:42 +0100 Subject: [PATCH 110/878] Player thread always gains play lock and keeps ut until terminated. Clean up termination code, add extra diagnistic information and quieten some debug messages. --- player.c | 75 ++++++++++++++++++-------------------------------------- rtp.c | 66 +++++++++++++++++++++++++------------------------ rtsp.c | 34 +++++++++++++------------ 3 files changed, 76 insertions(+), 99 deletions(-) diff --git a/player.c b/player.c index bd1f2ce0..754d30d1 100644 --- a/player.c +++ b/player.c @@ -437,13 +437,11 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta if (conn->connection_state_to_output) { // if we are supposed to be processing these packets abuf_t *abuf = 0; if (!conn->ab_synced) { - // if this is the first packet... - debug(3, "syncing to seqno %u.", seqno); conn->ab_write = seqno; conn->ab_read = seqno; conn->ab_synced = 1; conn->first_packet_timestamp = 0; - debug(1, "synced by first packet"); + debug(2, "Connection %d: synced by first packet, seqno %u.", conn->connection_number, seqno); } else if (original_format == 0) { // if the packet is coming in original format, the sequence number is important // otherwise, ignore is by setting it equal to the expected sequence number in ab_write @@ -904,8 +902,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->flush_rtp_timestamp = 0; debug_mutex_unlock(&conn->flush_mutex, 0); } - debug_mutex_lock(&conn->flush_mutex, 1000, 0); + pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); if (conn->flush_requested == 1) { if (conn->flush_output_flushed == 0) if (config.output->flush) { @@ -999,7 +997,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->flush_rtp_timestamp = 0; conn->flush_output_flushed = 0; } - debug_mutex_unlock(&conn->flush_mutex, 0); + pthread_cleanup_pop(1); // unlock the conn->flush_mutex if (conn->ab_synced) { curframe = conn->audio_buffer + BUFIDX(conn->ab_read); @@ -1543,10 +1541,16 @@ void player_thread_cleanup_handler(void *arg) { pthread_cleanup_push(mutex_unlock, &playing_conn_lock); if (playing_conn == conn) { - if (config.output->stop) + if (config.output->stop) { + debug(3, "Connection %d: Stop the output backend.", conn->connection_number); config.output->stop(); + } + } else { + debug(1, "This is not the playing conn."); } pthread_cleanup_pop(1); // unlock the mutex + } else { + debug(1, "Can not acquire play lock."); } int oldState; @@ -1586,65 +1590,33 @@ void player_thread_cleanup_handler(void *arg) { #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_type == ap_2) { - debug(1, "In AP2 mode"); + debug(2, "Cancelling AP2 timing, control and audio threads..."); if (conn->airplay_stream_type == realtime_stream) { - debug(2, "Connection %d: TEARDOWN Delete Realtime Audio Stream Thread", + debug(2, "Connection %d: Delete Realtime Audio Stream thread", conn->connection_number); pthread_cancel(conn->rtp_realtime_audio_thread); pthread_join(conn->rtp_realtime_audio_thread, NULL); - if (conn->realtime_audio_socket) - close(conn->realtime_audio_socket); + } else if (conn->airplay_stream_type == buffered_stream) { - debug(2, "Connection %d: TEARDOWN Delete Buffered Audio Stream Thread", - conn->connection_number); + + debug(2, "Connection %d: Delete Buffered Audio Stream thread", conn->connection_number); pthread_cancel(conn->rtp_buffered_audio_thread); pthread_join(conn->rtp_buffered_audio_thread, NULL); - if (conn->buffered_audio_socket) - close(conn->buffered_audio_socket); + } else { die("Unrecognised Stream Type"); } - if (conn->rtp_ap2_control_thread) { - debug(1, "Cancelling rtp_ap2_control_thread"); - pthread_cancel(conn->rtp_ap2_control_thread); - pthread_join(conn->rtp_ap2_control_thread, NULL); - } - if (conn->ap2_control_socket) { - close(conn->ap2_control_socket); - conn->ap2_control_socket = 0; - conn->ap2_remote_control_socket_addr_length = - 0; // indicates to the control receiver thread that the socket address need to be - // recreated. - } + debug(2, "Connection %d: Delete AirPlay 2 Control thread"); + pthread_cancel(conn->rtp_ap2_control_thread); + pthread_join(conn->rtp_ap2_control_thread, NULL); - if (conn->airplay_stream_type == realtime_stream) { - // realtime stuff - if (conn->rtp_realtime_audio_thread) { - debug(1, "Cancelling rtp_realtime_audio_thread"); - pthread_cancel(conn->rtp_realtime_audio_thread); - pthread_join(conn->rtp_realtime_audio_thread, NULL); - } - if (conn->realtime_audio_socket) { - close(conn->realtime_audio_socket); - conn->realtime_audio_socket = 0; - } - } else if (conn->airplay_stream_type == buffered_stream) { - // buffered stuff - if (conn->rtp_buffered_audio_thread) { - debug(3, "Cancelling rtp_buffered_audio_thread"); - pthread_cancel(conn->rtp_buffered_audio_thread); - pthread_join(conn->rtp_buffered_audio_thread, NULL); - } - if (conn->buffered_audio_socket) { - close(conn->buffered_audio_socket); - conn->buffered_audio_socket = 0; - } - } } else { + debug(1, "Cancelling AP1-compatible timing, control and audio threads..."); +#else + debug(2, "Cancelling AP1 timing, control and audio threads..."); #endif - debug(1, "Cancelling AP1 timing, control and audio threads..."); debug(3, "Cancel timing thread."); pthread_cancel(conn->rtp_timing_thread); debug(3, "Join timing thread."); @@ -1689,6 +1661,7 @@ void player_thread_cleanup_handler(void *arg) { release_play_lock(conn); conn->rtp_running = 0; pthread_setcancelstate(oldState, NULL); + debug(2, "Connection %d: player terminated.", conn->connection_number); } void *player_thread_func(void *arg) { @@ -3029,7 +3002,7 @@ void player_volume(double airplay_volume, rtsp_conn_info *conn) { void do_flush(uint32_t timestamp, rtsp_conn_info *conn) { - debug(2, "do_flush: flush to %u.", timestamp); + debug(3, "do_flush: flush to %u.", timestamp); debug_mutex_lock(&conn->flush_mutex, 1000, 1); conn->flush_requested = 1; conn->flush_rtp_timestamp = timestamp; // flush all packets up to, but not including, this one. diff --git a/rtp.c b/rtp.c index 84f0d800..8877285d 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1330,12 +1330,12 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn void rtp_event_receiver_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; - debug(2, "Connection %d: Event Receiver Cleanup.", conn->connection_number); + debug(2, "Connection %d: AP2 Event Receiver Cleanup.", conn->connection_number); } void *rtp_event_receiver(void *arg) { - debug(2, "Event Receiver started"); rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(2, "Connection %d: AP2 Event Receiver started", conn->connection_number); pthread_cleanup_push(rtp_event_receiver_cleanup_handler, arg); listen(conn->event_socket, 5); @@ -1355,7 +1355,7 @@ void *rtp_event_receiver(void *arg) { if (nread < 0) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "error in rtp_event_receiver %d: \"%s\". Could not recv a packet.", errno, + debug(1, "Connection %d: error in ap2 rtp_event_receiver %d: \"%s\". Could not recv a packet.", conn->connection_number, errno, errorstring); // if ((config.diagnostic_drop_packet_fraction == 0.0) || // (drand48() > config.diagnostic_drop_packet_fraction)) { @@ -1364,10 +1364,10 @@ void *rtp_event_receiver(void *arg) { // ssize_t plen = nread; debug(1, "Packet Received on Event Port."); if (packet[1] == 0xD7) { - debug(1, "Event Receiver -- Time Announce RTP packet of type 0x%02X length %d received.", + debug(1, "Connection %d: AP2 Event Receiver -- Time Announce RTP packet of type 0x%02X length %d received.", conn->connection_number, packet[1], nread); } else { - debug(1, "Event Receiver -- Unknown RTP packet of type 0x%02X length %d received.", + debug(1, "Connection %d: AP2 Event Receiver -- Unknown RTP packet of type 0x%02X length %d received.", conn->connection_number, packet[1], nread); } // } else { @@ -1377,17 +1377,22 @@ void *rtp_event_receiver(void *arg) { finished = 1; } } while (finished == 0); - debug(1, "Event Receiver RTP thread \"normal\" exit."); + debug(1, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", conn->connection_number); pthread_cleanup_pop(1); // close the socket pthread_cleanup_pop(1); // do the cleanup - debug(1, "Connection %d: Event Receiver RTP thread exit.", conn->connection_number); + debug(1, "Connection %d: AP2 Event Receiver RTP thread exit.", conn->connection_number); pthread_exit(NULL); } void rtp_ap2_control_handler_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); + close(conn->ap2_control_socket); + conn->ap2_control_socket = 0; + conn->ap2_remote_control_socket_addr_length = + 0; // indicates to the control receiver thread that the socket address need to be + // recreated (needed for resend requests in the realtime mode) } int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread, @@ -1463,7 +1468,8 @@ void *rtp_ap2_control_receiver(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; uint8_t packet[4096]; ssize_t nread; - while (1) { + int keep_going = 1; + while (keep_going) { SOCKADDR from_sock_addr; socklen_t from_sock_addr_length = sizeof(SOCKADDR); memset(&from_sock_addr, 0, sizeof(SOCKADDR)); @@ -1471,7 +1477,7 @@ void *rtp_ap2_control_receiver(void *arg) { nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); - if (nread >= 0) { + if (nread > 0) { // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); if ((config.diagnostic_drop_packet_fraction == 0.0) || @@ -1552,22 +1558,25 @@ void *rtp_ap2_control_receiver(void *arg) { } else { debug(1, "AP2 Control Receiver -- dropping a packet."); } + } else if (nread == 0) { + debug(1, "AP2 Control Receiver -- connection closed."); + keep_going = 0; } else { debug(1, "AP2 Control Receiver -- error %d receiving a packet.", errno); } } debug(1, "AP2 Control RTP thread \"normal\" exit -- this can't happen. Hah!"); - pthread_cleanup_pop(0); // don't execute anything here. + pthread_cleanup_pop(1); debug(1, "AP2 Control RTP thread exit."); pthread_exit(NULL); } void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { - debug(1, "Realtime Audio Receiver Cleanup Start."); + debug(2, "Realtime Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->realtime_audio_socket); conn->realtime_audio_socket = 0; - debug(1, "Realtime Audio Receiver Cleanup Done."); + debug(2, "Realtime Audio Receiver Cleanup Done."); } void *rtp_realtime_audio_receiver(void *arg) { @@ -1627,14 +1636,6 @@ void *rtp_realtime_audio_receiver(void *arg) { pthread_exit(NULL); } -void rtp_buffered_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { - debug(1, "Buffered Audio Receiver Cleanup Start."); - rtsp_conn_info *conn = (rtsp_conn_info *)arg; - close(conn->buffered_audio_socket); - conn->buffered_audio_socket = 0; - debug(1, "Buffered Audio Receiver Cleanup Done."); -} - ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { ssize_t response; // usleep(1500000); @@ -1676,10 +1677,10 @@ ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { return response; } -#define STANDARD_PACKET_SIZE 65536 +#define STANDARD_PACKET_SIZE 4096 void buffered_tcp_reader_cleanup_handler(__attribute__((unused)) void *arg) { - debug(2, "Buffered TCP Reader Thread Exit via Cleanup."); + debug(1, "Buffered TCP Reader Thread Exit via Cleanup."); } void *buffered_tcp_reader(void *arg) { @@ -1849,11 +1850,11 @@ void addADTStoPacket(uint8_t *packet, int packetLen) { } void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { - debug(3, "Buffered Audio Receiver Cleanup Start."); + debug(1, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); conn->buffered_audio_socket = 0; - debug(2, "Buffered Audio Receiver Cleanup Done."); + debug(1, "Buffered Audio Receiver Cleanup Done."); } void *rtp_buffered_audio_processor(void *arg) { @@ -2093,7 +2094,7 @@ void *rtp_buffered_audio_processor(void *arg) { if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(1, "rtp lead time is %f ms.", 0.000001 * lead_time); + debug(2, "Connection %d: buffered audio lead time is %f seconds.", 0.000000001 * lead_time); streaming_has_started = 1; player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); @@ -2371,6 +2372,7 @@ void *rtp_buffered_audio_processor(void *arg) { pthread_cleanup_pop(1); // do the cleanup. pthread_exit(NULL); } + int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { if (conn->timing_type == ts_ptp) return frame_to_ptp_local_time(timestamp, time, conn); diff --git a/rtsp.c b/rtsp.c index dde6cff7..c82c6bd7 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1339,6 +1339,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message plist_t messagePlist = plist_from_rtsp_content(req); if (messagePlist != NULL) { + pthread_cleanup_push(plist_cleanup, (void *)messagePlist); plist_t item = plist_dict_get_item(messagePlist, "networkTimeSecs"); if (item != NULL) { plist_t item_2 = plist_dict_get_item(messagePlist, "networkTimeTimelineID"); @@ -1388,16 +1389,9 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message plist_get_uint_val(item, &rate); debug(2, "anchor rate 0x%016" PRIx64 ".", rate); debug_mutex_lock(&conn->flush_mutex, 1000, 1); + pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); conn->ap2_rate = rate; if ((rate & 1) != 0) { - - get_play_lock(conn); - - if (conn->ap2_timing_peer_list_message) - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - else - debug(1, "Connection %d: No timing peer list!", conn->connection_number); - debug(2, "Connection %d: Start playing.", conn->connection_number); conn->ap2_play_enabled = 1; } else { @@ -1405,13 +1399,21 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message conn->ap2_play_enabled = 0; // not sure this is needed yet reset_anchor_info(conn); // needed if the player resumes - release_play_lock(conn); } - debug_mutex_unlock(&conn->flush_mutex, 3); - if ((rate & 1) == 0) + pthread_cleanup_pop(1); // unlock the conn->flush_mutex + + if ((rate & 1) != 0) { + // keep this outside the flush_mutex lock + if (conn->ap2_timing_peer_list_message) { + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + } else { + debug(1, "Connection %d: No timing peer list!", conn->connection_number); + } + } else { player_full_flush(conn); + } } - plist_free(messagePlist); + pthread_cleanup_pop(1); // plist_free the messagePlist; } else { debug(1, "missing plist!"); } @@ -1945,7 +1947,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) case 103: { debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); - // for buffered audio, delay getting the play lock until we actually start playing... + get_play_lock(conn); conn->airplay_stream_type = buffered_stream; // get needed stuff @@ -3730,10 +3732,10 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Delete watchdog mutex."); pthread_mutex_destroy(&conn->watchdog_mutex); - debug(3, "Connection %d: Checking play lock.", conn->connection_number); - release_play_lock(conn); + // debug(3, "Connection %d: Checking play lock.", conn->connection_number); + // release_play_lock(conn); - debug(1, "Connection %d: terminated.", conn->connection_number); + debug(2, "Connection %d: Closed.", conn->connection_number); conn->running = 0; pthread_setcancelstate(oldState, NULL); } From 04c7f84564b2d8ff1fd30182573ad89b1aab2e24 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 14:16:57 +0100 Subject: [PATCH 111/878] Improve NQPTP clock diagnostics, tidy up some messages. --- player.c | 7 ++- player.h | 16 ++++++- ptp-utilities.c | 46 ++++++------------- rtp.c | 118 ++++++++++++++++++++++++++++++++---------------- rtsp.c | 8 ++-- 5 files changed, 113 insertions(+), 82 deletions(-) diff --git a/player.c b/player.c index 754d30d1..69172e3f 100644 --- a/player.c +++ b/player.c @@ -1593,8 +1593,7 @@ void player_thread_cleanup_handler(void *arg) { debug(2, "Cancelling AP2 timing, control and audio threads..."); if (conn->airplay_stream_type == realtime_stream) { - debug(2, "Connection %d: Delete Realtime Audio Stream thread", - conn->connection_number); + debug(2, "Connection %d: Delete Realtime Audio Stream thread", conn->connection_number); pthread_cancel(conn->rtp_realtime_audio_thread); pthread_join(conn->rtp_realtime_audio_thread, NULL); @@ -1613,9 +1612,9 @@ void player_thread_cleanup_handler(void *arg) { pthread_join(conn->rtp_ap2_control_thread, NULL); } else { - debug(1, "Cancelling AP1-compatible timing, control and audio threads..."); + debug(2, "Cancelling AP1-compatible timing, control and audio threads..."); #else - debug(2, "Cancelling AP1 timing, control and audio threads..."); + debug(2, "Cancelling AP1 timing, control and audio threads..."); #endif debug(3, "Cancel timing thread."); pthread_cancel(conn->rtp_timing_thread); diff --git a/player.h b/player.h index 25003e5b..eb210ae5 100644 --- a/player.h +++ b/player.h @@ -33,7 +33,6 @@ #define time_ping_history \ (1 << time_ping_history_power_of_two) // 2^7 is 128. At 1 per three seconds, approximately six // minutes of records - typedef struct time_ping_record { uint64_t dispersion; uint64_t local_time; @@ -42,6 +41,19 @@ typedef struct time_ping_record { int chosen; } time_ping_record; +// these are for reporting the status of the clock +typedef enum { + clock_no_anchor_info, + clock_ok, + clock_service_unavailable, + clock_access_error, + clock_data_unavailable, + clock_no_master, + clock_version_mismatch, + clock_not_synchronised, + clock_not_valid +} clock_status_t; + typedef uint16_t seq_t; typedef struct audio_buffer_entry { // decoded audio packets @@ -258,6 +270,8 @@ typedef struct { uint64_t anchor_time; // this is the time according to the clock uint32_t anchor_rtptime; + clock_status_t clock_status; + #ifdef CONFIG_AIRPLAY_2 airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... diff --git a/ptp-utilities.c b/ptp-utilities.c index 01d32c58..8dd412dc 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -42,8 +42,6 @@ int shm_fd; void *mapped_addr = NULL; -int failure_message_sent = 0; - static pthread_mutex_t ptp_access_mutex = PTHREAD_MUTEX_INITIALIZER; // returns a copy of the shared memory data from the nqptp @@ -63,9 +61,8 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { } int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { - int response = -1; + int response = clock_ok; pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 10000, 1); - // 0 -> valid and working; -1 -> can't connect to nqptp if (actual_clock_id != NULL) *actual_clock_id = 0; if (raw_offset != NULL) @@ -81,39 +78,28 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { *actual_clock_id = nqptp_data.master_clock_id; if (raw_offset != NULL) *raw_offset = nqptp_data.local_to_master_time_offset; - response = 0; } else { - debug(1, "clock not valid"); - response = -2; // clock info not valid + response = clock_no_master; } } else { - if (failure_message_sent == 0) { - warn("This version of Shairport Sync requires an NQPTP with a Shared Memory Interface " - "Version %u, but the installed version is %u. Please install the correct version of " - "NQPTP.", - NQPTP_SHM_STRUCTURES_VERSION, nqptp_data.version); - failure_message_sent = 1; - } + response = clock_version_mismatch; } + } else { + response = clock_data_unavailable; } - if (response != -1) - response = ptp_shm_interface_close(); + if (ptp_shm_interface_close() != 0) + response = clock_access_error; } else { - if (failure_message_sent == 0) { - warn("Can't open the interface to nqptp. Is the service running?"); - failure_message_sent = 1; - } + response = clock_service_unavailable; } pthread_cleanup_pop(1); // release the mutex - if (response == 0) - failure_message_sent = 0; return response; } int ptp_shm_interface_open() { mapped_addr = NULL; int shared_memory_file_descriptor = shm_open("/nqptp", O_RDWR, 0); - int response = -1; + int response = 0; if (shared_memory_file_descriptor >= 0) { mapped_addr = // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the @@ -121,19 +107,13 @@ int ptp_shm_interface_open() { mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, shared_memory_file_descriptor, 0); if (mapped_addr == MAP_FAILED) { - if (failure_message_sent == 0) { - debug(1, "unable to open the shared memory interface with nqptp. Is the service running?"); - failure_message_sent = 1; - } + response = -1; } if (close(shared_memory_file_descriptor) == -1) { - if (failure_message_sent == 0) { - debug(1, "error closing \"/nqptp\" after mapping it."); - failure_message_sent = 1; - } - } else { - response = 0; + response = -1; } + } else { + response = -1; } return response; } diff --git a/rtp.c b/rtp.c index 8877285d..1cc3b084 100644 --- a/rtp.c +++ b/rtp.c @@ -255,7 +255,7 @@ void *rtp_audio_receiver(void *arg) { } void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) { - debug(1, "Control Receiver Cleanup Done."); + debug(2, "Control Receiver Cleanup Done."); } void *rtp_control_receiver(void *arg) { @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1226,13 +1226,13 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { - int response = 0; + uint64_t actual_clock_id, actual_offset; + int response = ptp_get_clock_info(&actual_clock_id, &actual_offset); - int ptp_status = ptp_get_clock_info(&actual_clock_id, &actual_offset); - - if (ptp_status == 0) { - if (conn->anchor_remote_info_is_valid != 0) { + if (response == clock_ok) { + if (conn->anchor_remote_info_is_valid != + 0) { // i.e. if we have anchor clock ID and anchor time / rtptime if (actual_clock_id == conn->anchor_clock) { conn->last_anchor_clock_offset = actual_offset; conn->last_anchor_time_of_update = get_absolute_time_in_ns(); @@ -1263,29 +1263,60 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; } } else { - debug(2, "no clock and no old anchor times"); - response = -1; + response = clock_not_valid; // no current clock information and no previous clock info } } } else { - debug(2, "don't have anchor_remote_time_info"); - response = -1; + response = clock_no_anchor_info; // no anchor information } - } else if (ptp_status == -1) { - debug(3, "don't have the ptp clock interface"); - response = -1; - } else if (ptp_status == -2) { - debug(1, "ptp clock not valid"); - response = -1; - } else { - debug(3, "ptp clock error"); - response = -1; + } + + // here, check and update the clock status + if ((clock_status_t)response != conn->clock_status) { + switch (response) { + case clock_ok: + debug(1, "Connection %d: NQPTP clock is valid and available.", conn->connection_number); + break; + case clock_service_unavailable: + debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); + warn("Can't access the NQPTP clock. Is NQPTP running?"); + break; + case clock_access_error: + debug(1, "Connection %d: Error accessing the NQPTP clock interface.", + conn->connection_number); + break; + case clock_data_unavailable: + debug(1, "Connection %d: Can not access NQPTP clock information.", conn->connection_number); + break; + case clock_no_master: + debug(1, "Connection %d: No NQPTP master clock.", conn->connection_number); + break; + case clock_no_anchor_info: + debug(1, "Connection %d: No Clock Anchor.", conn->connection_number); + break; + case clock_version_mismatch: + debug(1, "Connection %d: NQPTP clock interface mismatch.", conn->connection_number); + warn("This version of Shairport Sync is not compatible with the installed version of NQPTP. " + "Please update."); + break; + case clock_not_synchronised: + debug(1, "Connection %d: NQPTP clock is not synchronised.", conn->connection_number); + break; + case clock_not_valid: + debug(1, "Connection %d: NQPTP clock information is not valid.", conn->connection_number); + break; + default: + debug(3, "Connection %d: NQPTP clock reports an unrecognised status: %u.", + conn->connection_number, response); + break; + } + conn->clock_status = response; } return response; } int have_ptp_timing_information(rtsp_conn_info *conn) { - if (get_ptp_anchor_local_time_info(conn, NULL, NULL) == 0) + if (get_ptp_anchor_local_time_info(conn, NULL, NULL) == clock_ok) return 1; else return 0; @@ -1295,7 +1326,7 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * int result = -1; uint32_t anchor_rtptime; uint64_t anchor_local_time; - if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) { int32_t frame_difference = timestamp - anchor_rtptime; int64_t time_difference = frame_difference; time_difference = time_difference * 1000000000; @@ -1313,7 +1344,7 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn int result = -1; uint32_t anchor_rtptime; uint64_t anchor_local_time; - if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == 0) { + if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) { int64_t time_difference = time - anchor_local_time; int64_t frame_difference = time_difference; frame_difference = frame_difference * conn->input_rate; // but this is by 10^9 @@ -1355,8 +1386,9 @@ void *rtp_event_receiver(void *arg) { if (nread < 0) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "Connection %d: error in ap2 rtp_event_receiver %d: \"%s\". Could not recv a packet.", conn->connection_number, errno, - errorstring); + debug(1, + "Connection %d: error in ap2 rtp_event_receiver %d: \"%s\". Could not recv a packet.", + conn->connection_number, errno, errorstring); // if ((config.diagnostic_drop_packet_fraction == 0.0) || // (drand48() > config.diagnostic_drop_packet_fraction)) { } else if (nread > 0) { @@ -1364,11 +1396,15 @@ void *rtp_event_receiver(void *arg) { // ssize_t plen = nread; debug(1, "Packet Received on Event Port."); if (packet[1] == 0xD7) { - debug(1, "Connection %d: AP2 Event Receiver -- Time Announce RTP packet of type 0x%02X length %d received.", conn->connection_number, - packet[1], nread); + debug(1, + "Connection %d: AP2 Event Receiver -- Time Announce RTP packet of type 0x%02X length " + "%d received.", + conn->connection_number, packet[1], nread); } else { - debug(1, "Connection %d: AP2 Event Receiver -- Unknown RTP packet of type 0x%02X length %d received.", conn->connection_number, - packet[1], nread); + debug(1, + "Connection %d: AP2 Event Receiver -- Unknown RTP packet of type 0x%02X length %d " + "received.", + conn->connection_number, packet[1], nread); } // } else { // debug(3, "Event Receiver Thread -- dropping incoming packet to simulate a bad network."); @@ -1377,7 +1413,8 @@ void *rtp_event_receiver(void *arg) { finished = 1; } } while (finished == 0); - debug(1, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", conn->connection_number); + debug(1, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", + conn->connection_number); pthread_cleanup_pop(1); // close the socket pthread_cleanup_pop(1); // do the cleanup @@ -1680,7 +1717,7 @@ ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { #define STANDARD_PACKET_SIZE 4096 void buffered_tcp_reader_cleanup_handler(__attribute__((unused)) void *arg) { - debug(1, "Buffered TCP Reader Thread Exit via Cleanup."); + debug(2, "Buffered TCP Reader Thread Exit via Cleanup."); } void *buffered_tcp_reader(void *arg) { @@ -1850,11 +1887,11 @@ void addADTStoPacket(uint8_t *packet, int packetLen) { } void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { - debug(1, "Buffered Audio Receiver Cleanup Start."); + debug(2, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); conn->buffered_audio_socket = 0; - debug(1, "Buffered Audio Receiver Cleanup Done."); + debug(2, "Buffered Audio Receiver Cleanup Done."); } void *rtp_buffered_audio_processor(void *arg) { @@ -2094,7 +2131,8 @@ void *rtp_buffered_audio_processor(void *arg) { if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(2, "Connection %d: buffered audio lead time is %f seconds.", 0.000000001 * lead_time); + debug(2, "Connection %d: buffered audio lead time is %f seconds.", + 0.000000001 * lead_time); streaming_has_started = 1; player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); diff --git a/rtsp.c b/rtsp.c index c82c6bd7..3f216eea 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1395,7 +1395,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(2, "Connection %d: Start playing.", conn->connection_number); conn->ap2_play_enabled = 1; } else { - debug(1, "Connection %d: Stop playing.", conn->connection_number); + debug(2, "Connection %d: Stop playing.", conn->connection_number); conn->ap2_play_enabled = 0; // not sure this is needed yet reset_anchor_info(conn); // needed if the player resumes @@ -1934,7 +1934,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->input_num_channels = conn->stream.fmtp[7]; conn->input_bit_depth = conn->stream.fmtp[3]; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - debug(1, "Realtime Stream Play"); + debug(2, "Realtime Stream Play"); if (conn->ap2_timing_peer_list_message) ptp_send_control_message_string(conn->ap2_timing_peer_list_message); else @@ -2215,7 +2215,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { msg_add_header(resp, "Session", "1"); resp->respcode = 200; // it all worked out okay - debug(1, + debug(2, "Connection %d: SETUP DACP-ID \"%s\" from %s to %s with UDP ports Control: " "%d, Timing: %d and Audio: %d.", conn->connection_number, conn->dacp_id, &conn->client_ip_string, @@ -4239,7 +4239,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(1, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(2, "Connection %d: new 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); } else { From a563d5e847699ba3fc5dd3ed3d4e454366b35fb6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 16:03:02 +0100 Subject: [PATCH 112/878] Quieten some more debug messages. Fix a misleading clock status return. --- player.c | 2 +- ptp-utilities.c | 6 +++++- rtp.c | 19 ++++++++----------- rtsp.c | 6 +++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/player.c b/player.c index 69172e3f..db46cd12 100644 --- a/player.c +++ b/player.c @@ -1279,7 +1279,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (do_wait == 0) if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! if (notified_buffer_empty == 0) { - debug(1, "Buffers exhausted."); + debug(3, "Buffers exhausted."); notified_buffer_empty = 1; // reset_input_flow_metrics(conn); // don't do a full flush parameters reset conn->initial_reference_time = 0; diff --git a/ptp-utilities.c b/ptp-utilities.c index 8dd412dc..73b778fa 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -82,7 +82,11 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { response = clock_no_master; } } else { - response = clock_version_mismatch; + // the version can not be zero. If zero is returned here, it means that the shared memory is not yet initialised, so not availalbe + if (nqptp_data.version == 0) + response = clock_service_unavailable; + else + response = clock_version_mismatch; } } else { response = clock_data_unavailable; diff --git a/rtp.c b/rtp.c index 1cc3b084..520e0c06 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1413,12 +1413,9 @@ void *rtp_event_receiver(void *arg) { finished = 1; } } while (finished == 0); - debug(1, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", - conn->connection_number); pthread_cleanup_pop(1); // close the socket - pthread_cleanup_pop(1); // do the cleanup - debug(1, "Connection %d: AP2 Event Receiver RTP thread exit.", conn->connection_number); + debug(2, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", conn->connection_number); pthread_exit(NULL); } diff --git a/rtsp.c b/rtsp.c index 3f216eea..9ea48512 100644 --- a/rtsp.c +++ b/rtsp.c @@ -379,7 +379,7 @@ int get_play_lock(rtsp_conn_info *conn) { #else } else if (config.allow_session_interruption == 1) { #endif - debug(1, "Connection %d: Asking Connection %d to stop playing.", conn->connection_number, + debug(2, "Connection %d: Asking Connection %d to stop playing.", conn->connection_number, playing_conn->connection_number); playing_conn->stop = 1; interrupting_current_session = 1; @@ -406,7 +406,7 @@ int get_play_lock(rtsp_conn_info *conn) { } if ((have_the_player == 1) && (interrupting_current_session == 1)) { - debug(1, "Connection %d: got player lock", conn->connection_number); + debug(2, "Connection %d: Got player lock", conn->connection_number); response = 1; } else { debug(1, "Connection %d: failed to get player lock", conn->connection_number); @@ -1398,7 +1398,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(2, "Connection %d: Stop playing.", conn->connection_number); conn->ap2_play_enabled = 0; // not sure this is needed yet - reset_anchor_info(conn); // needed if the player resumes + // reset_anchor_info(conn); // needed if the player resumes } pthread_cleanup_pop(1); // unlock the conn->flush_mutex From ca0c625b22283a7113f384e4cbb61ac067762abb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 10 May 2021 16:07:24 +0100 Subject: [PATCH 113/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 47f002b4..cd97f62b 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -54,7 +54,7 @@ Okay, now let's get the tools and libraries for building and installing Shairpor First, install the packages needed by Shairport Sync: ``` # apt-get install build-essential git xmltoman autoconf automake libtool \ - libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev ``` (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) From 2b88215f0955ad3d92aedd30ef1fff48718197e1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 11 May 2021 10:46:56 +0100 Subject: [PATCH 114/878] Clean up some of the sync timing. Ignore a flush if playing AP2. Set the requested lead time to zero. --- player.c | 28 +++++++++++++++------------- rtp.c | 2 +- rtsp.c | 14 +++++--------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/player.c b/player.c index db46cd12..dfedbecf 100644 --- a/player.c +++ b/player.c @@ -1059,12 +1059,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - debug(1, "Connection %d: Lead time: %f seconds.", conn->connection_number, + debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); - if (local_time_now > conn->first_packet_time_to_play) { - uint64_t lateness = local_time_now - conn->first_packet_time_to_play; - debug(2, "First packet is %" PRIu64 " nanoseconds late! Flushing 0.5 seconds", + int64_t lateness = local_time_now - conn->first_packet_time_to_play; + if (lateness > 0) { + debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", lateness); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); } @@ -1094,18 +1094,17 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (fabs(0.000001 * change_in_should_be_time) > 0.001) // the clock drift estimation might be nudging the estimate, and we can // ignore this unless if's more than a microsecond - debug(2, "Change in estimated first_packet_time: %8.4f milliseconds.", + debug(2, "Change in estimated first_packet_time: %f milliseconds for first_packet .", 0.000001 * change_in_should_be_time); conn->first_packet_time_to_play = should_be_time; - if (local_time_now > conn->first_packet_time_to_play) { - uint64_t lateness = local_time_now - conn->first_packet_time_to_play; - debug(2, "Gone past starting time by %" PRIu64 " nanoseconds.", lateness); + int64_t lead_time = conn->first_packet_time_to_play - local_time_now; // negative means late + if (lead_time < 0) { + debug(1, "Gone past starting time for %u by %" PRId64 " nanoseconds.", conn->first_packet_timestamp, -lead_time); conn->ab_buffering = 0; } else { // do some calculations - int64_t lead_time = conn->first_packet_time_to_play - local_time_now; if ((config.audio_backend_silent_lead_in_time_auto == 1) || (lead_time <= (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)1000000000))) { @@ -1131,7 +1130,6 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (exact_frame_gap <= frames_needed_to_maintain_desired_buffer) { fs = conn->max_frames_per_packet * 2; } - // if we are very close to the end of buffering, i.e. within two frame-lengths, // add the remaining silence needed and end buffering if (exact_frame_gap <= conn->max_frames_per_packet * 2) { @@ -1153,7 +1151,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { generate_zero_frames(silence, fs, config.output_format, conn->enable_dither, conn->previous_random_number); config.output->play(silence, fs); - // debug(1, "Sent %" PRId64 " frames of silence", fs); + debug(2, "Sent %" PRId64 " frames of silence", fs); free(silence); have_sent_prefiller_silence = 1; } @@ -1172,7 +1170,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } } } else { - debug(2, "Unexpected response to getting dac delay: %d.", resp); + debug(1, "Unexpected response to getting dac delay: %d.", resp); } } } @@ -1978,6 +1976,7 @@ void *player_thread_func(void *arg) { pthread_testcancel(); // allow a pthread_cancel request to take effect. abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not // guaranteed that they'll always be executed + if (inframe) { inbuf = inframe->data; inbuflength = inframe->length; @@ -2208,6 +2207,8 @@ void *player_thread_func(void *arg) { sync_error = delay; // int64_t from int64_t - int32_t, so okay + // debug(1, "Sync error on frame %u is %" PRId64 " frames.", inframe->given_timestamp, sync_error); + if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; @@ -2226,7 +2227,7 @@ void *player_thread_func(void *arg) { // remove the bias when reporting the error to make it the true error - debug(2, + debug(1, "first frame sync error (positive --> late): %" PRId64 " frames, %.3f mS at %d frames per second output.", sync_error + first_frame_early_bias, @@ -3025,6 +3026,7 @@ void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { } void player_full_flush(rtsp_conn_info *conn) { + debug(3,"player_full_flush"); // this basically flushes everything from the player // here, find the rtptime of the last from in the buffer and add 1 to it // so as to ask to flush everything diff --git a/rtp.c b/rtp.c index 520e0c06..d92390db 100644 --- a/rtp.c +++ b/rtp.c @@ -2100,7 +2100,7 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); - double reqested_lead_time = 0.4; // + double reqested_lead_time = 0.0; // if (player_buffer_occupancy > ((reqested_lead_time + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again diff --git a/rtsp.c b/rtsp.c index 9ea48512..a7ec0691 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1810,7 +1810,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 } } - // debug(1,"RTSP Flush Requested: %u.",rtptime); + debug(1,"RTSP Flush Requested: %u.",rtptime); if (have_play_lock(conn)) { #ifdef CONFIG_METADATA if (p) @@ -1818,19 +1818,15 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { else send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); #endif - player_flush(rtptime, conn); // will not crash even it there is no player thread. + // hack -- ignore it for airplay 2 + if (conn->airplay_type != ap_2) + player_flush(rtptime, conn); // will not crash even it there is no player thread. resp->respcode = 200; } else { - warn("Connection %d FLUSH %u received without having the player (no ANNOUNCE?)", + warn("Connection %d FLUSH %u received without having the player", conn->connection_number, rtptime); resp->respcode = 451; - // #ifdef CONFIG_AIRPLAY_2 - // debug(1, "Connection %d FLUSH %u received without having the player but do it anyway...", - // conn->connection_number, rtptime); - // player_flush(rtptime, conn); // will not crash even it there is no player thread. - // resp->respcode = 200; - // #endif } } From 3f77994948cb417d1edd72b8c18d7569ca7aebb3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 11 May 2021 18:52:02 +0100 Subject: [PATCH 115/878] more closely check for time information being available. --- player.c | 697 +++++++++++++++++++++++++----------------------- ptp-utilities.c | 3 +- rtp.c | 17 +- rtsp.c | 6 +- 4 files changed, 374 insertions(+), 349 deletions(-) diff --git a/player.c b/player.c index dfedbecf..799e9e25 100644 --- a/player.c +++ b/player.c @@ -87,6 +87,10 @@ #include "apple_alac.h" #endif +#ifdef CONFIG_AIRPLAY_2 +#include "ptp-utilities.h" +#endif + #include "loudness.h" #include "activity_monitor.h" @@ -876,344 +880,357 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { pthread_cleanup_push(buffer_get_frame_cleanup_handler, (void *)conn); // undo what's been done so far do { + // get the time local_time_now = get_absolute_time_in_ns(); // type okay // debug(3, "buffer_get_frame is iterating"); - int rco = get_requested_connection_state_to_output(); + if (have_timestamp_timing_information(conn)) { - if (conn->connection_state_to_output != rco) { - conn->connection_state_to_output = rco; - // change happening - if (conn->connection_state_to_output == 0) { // going off - debug(2, "request flush because connection_state_to_output is off"); - debug_mutex_lock(&conn->flush_mutex, 1000, 1); - conn->flush_requested = 1; - conn->flush_rtp_timestamp = 0; - debug_mutex_unlock(&conn->flush_mutex, 3); - } - } + int rco = get_requested_connection_state_to_output(); - if (config.output->is_running) - if (config.output->is_running() != 0) { // if the back end isn't running for any reason - debug(2, "request flush because back end is not running"); - debug_mutex_lock(&conn->flush_mutex, 1000, 0); - conn->flush_requested = 1; - conn->flush_rtp_timestamp = 0; - debug_mutex_unlock(&conn->flush_mutex, 0); - } - debug_mutex_lock(&conn->flush_mutex, 1000, 0); - pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); - if (conn->flush_requested == 1) { - if (conn->flush_output_flushed == 0) - if (config.output->flush) { - config.output->flush(); // no cancellation points - debug(2, "flush request: flush output device."); + if (conn->connection_state_to_output != rco) { + conn->connection_state_to_output = rco; + // change happening + if (conn->connection_state_to_output == 0) { // going off + debug(2, "request flush because connection_state_to_output is off"); + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + conn->flush_requested = 1; + conn->flush_rtp_timestamp = 0; + debug_mutex_unlock(&conn->flush_mutex, 3); } - conn->flush_output_flushed = 1; - } - // now check to see it the flush request is for frames in the buffer or not - // if the first_packet_timestamp is zero, don't check - int flush_needed = 0; - int drop_request = 0; - if ((conn->flush_requested == 1) && (conn->flush_rtp_timestamp == 0)) { - debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); - flush_needed = 1; - drop_request = 1; - } else if (conn->flush_rtp_timestamp != 0) { - if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { - abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); - abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); - if ((firstPacket != NULL) && (firstPacket->ready)) { - // discard flushes more than 10 seconds into the future -- they are probably bogus - uint32_t first_frame_in_buffer = firstPacket->given_timestamp; - int32_t offset_from_first_frame = - (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); - if (offset_from_first_frame > (int)conn->input_rate * 10) { - debug(1, - "flush request: sanity check -- flush frame %u is too far into the future from " - "the first frame %u -- discarded.", - conn->flush_rtp_timestamp, first_frame_in_buffer); - drop_request = 1; - } else { - if ((lastPacket != NULL) && (lastPacket->ready)) { - // we have enough information to check if the flush is needed or can be discarded - uint32_t last_frame_in_buffer = lastPacket->given_timestamp + lastPacket->length - 1; - // now we have to work out if the flush frame is in the buffer - // if it is later than the end of the buffer, flush everything and keep the request - // active. if it is in the buffer, we need to flush part of the buffer. Actually we - // flush the entire buffer and drop the request. if it is before the buffer, no flush - // is needed. Drop the request. - if (offset_from_first_frame > 0) { - int32_t offset_to_last_frame = - (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); - if (offset_to_last_frame >= 0) { - debug(2, - "flush request: flush frame %u active -- buffer contains %u frames, from " - "%u to %u", - conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, - first_frame_in_buffer, last_frame_in_buffer); - drop_request = 1; - flush_needed = 1; - } else { - debug(2, + } + + if (config.output->is_running) + if (config.output->is_running() != 0) { // if the back end isn't running for any reason + debug(2, "request flush because back end is not running"); + debug_mutex_lock(&conn->flush_mutex, 1000, 0); + conn->flush_requested = 1; + conn->flush_rtp_timestamp = 0; + debug_mutex_unlock(&conn->flush_mutex, 0); + } + debug_mutex_lock(&conn->flush_mutex, 1000, 0); + pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); + if (conn->flush_requested == 1) { + if (conn->flush_output_flushed == 0) + if (config.output->flush) { + config.output->flush(); // no cancellation points + debug(2, "flush request: flush output device."); + } + conn->flush_output_flushed = 1; + } + // now check to see it the flush request is for frames in the buffer or not + // if the first_packet_timestamp is zero, don't check + int flush_needed = 0; + int drop_request = 0; + if ((conn->flush_requested == 1) && (conn->flush_rtp_timestamp == 0)) { + debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); + flush_needed = 1; + drop_request = 1; + } else if (conn->flush_rtp_timestamp != 0) { + if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { + abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); + abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); + if ((firstPacket != NULL) && (firstPacket->ready)) { + // discard flushes more than 10 seconds into the future -- they are probably bogus + uint32_t first_frame_in_buffer = firstPacket->given_timestamp; + int32_t offset_from_first_frame = + (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); + if (offset_from_first_frame > (int)conn->input_rate * 10) { + debug(1, + "flush request: sanity check -- flush frame %u is too far into the future from " + "the first frame %u -- discarded.", + conn->flush_rtp_timestamp, first_frame_in_buffer); + drop_request = 1; + } else { + if ((lastPacket != NULL) && (lastPacket->ready)) { + // we have enough information to check if the flush is needed or can be discarded + uint32_t last_frame_in_buffer = + lastPacket->given_timestamp + lastPacket->length - 1; + // now we have to work out if the flush frame is in the buffer + // if it is later than the end of the buffer, flush everything and keep the request + // active. if it is in the buffer, we need to flush part of the buffer. Actually we + // flush the entire buffer and drop the request. if it is before the buffer, no + // flush is needed. Drop the request. + if (offset_from_first_frame > 0) { + int32_t offset_to_last_frame = + (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); + if (offset_to_last_frame >= 0) { + debug(2, + "flush request: flush frame %u active -- buffer contains %u frames, from " + "%u to %u", + conn->flush_rtp_timestamp, + last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, + last_frame_in_buffer); + drop_request = 1; + flush_needed = 1; + } else { + debug( + 2, "flush request: flush frame %u pending -- buffer contains %u frames, from " "%u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); - flush_needed = 1; - } - } else { - debug(2, + flush_needed = 1; + } + } else { + debug( + 2, "flush request: flush frame %u expired -- buffer contains %u frames, from %u " "to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); - drop_request = 1; + drop_request = 1; + } } } } - } - } else { - debug(3, + } else { + debug( + 3, "flush request: flush frame %u -- buffer not synced or empty: synced: %d, ab_read: " "%u, ab_write: %u", conn->flush_rtp_timestamp, conn->ab_synced, conn->ab_read, conn->ab_write); - // leave flush request pending and don't do a buffer flush, because there isn't one - } - } - if (flush_needed) { - debug(2, "flush request: flush done."); - ab_resync(conn); // no cancellation points - conn->first_packet_timestamp = 0; - conn->first_packet_time_to_play = 0; - conn->time_since_play_started = 0; - have_sent_prefiller_silence = 0; - dac_delay = 0; - } - if (drop_request) { - debug(2, "flush request: request dropped."); - conn->flush_requested = 0; - conn->flush_rtp_timestamp = 0; - conn->flush_output_flushed = 0; - } - pthread_cleanup_pop(1); // unlock the conn->flush_mutex - if (conn->ab_synced) { - curframe = conn->audio_buffer + BUFIDX(conn->ab_read); - - if ((conn->ab_read != conn->ab_write) && - (curframe->ready)) { // it could be synced and empty, under - // exceptional circumstances, with the - // frame unused, thus apparently ready - - if (curframe->sequence_number != conn->ab_read) { - // some kind of sync problem has occurred. - if (BUFIDX(curframe->sequence_number) == BUFIDX(conn->ab_read)) { - // it looks like aliasing has happened - // jump to the new incoming stuff... - conn->ab_read = curframe->sequence_number; - debug(1, "Aliasing of buffer index -- reset."); - } else { - debug(1, "Inconsistent sequence numbers detected"); - } + // leave flush request pending and don't do a buffer flush, because there isn't one } } + if (flush_needed) { + debug(2, "flush request: flush done."); + ab_resync(conn); // no cancellation points + conn->first_packet_timestamp = 0; + conn->first_packet_time_to_play = 0; + conn->time_since_play_started = 0; + have_sent_prefiller_silence = 0; + dac_delay = 0; + } + if (drop_request) { + debug(2, "flush request: request dropped."); + conn->flush_requested = 0; + conn->flush_rtp_timestamp = 0; + conn->flush_output_flushed = 0; + } + pthread_cleanup_pop(1); // unlock the conn->flush_mutex + if (conn->ab_synced) { + curframe = conn->audio_buffer + BUFIDX(conn->ab_read); - if ((curframe) && (curframe->ready)) { - notified_buffer_empty = 0; // at least one buffer now -- diagnostic only. - if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the - // player - if (conn->first_packet_timestamp == 0) { // if this is the very first packet - if (have_timestamp_timing_information(conn)) { // if we have a reference time - // debug(1,"First frame seen with timestamp..."); - conn->first_packet_timestamp = - curframe->given_timestamp; // we will keep buffering until we are - // supposed to start playing this -#ifdef CONFIG_METADATA - // say we have started receiving frames here - debug(2, "pffr"); - send_ssnc_metadata( - 'pffr', NULL, 0, - 0); // "first frame received", but don't wait if the queue is locked -#endif - // Here, calculate when we should start playing. We need to know when to allow the - // packets to be sent to the player. + if ((conn->ab_read != conn->ab_write) && + (curframe->ready)) { // it could be synced and empty, under + // exceptional circumstances, with the + // frame unused, thus apparently ready - // every second or so, we get a reference on when a particular packet should be - // played. - - // It probably won't be the timestamp of our first packet, however, so we might have - // to do some calculations. - - // To calculate when the first packet will be played, we figure out the exact time the - // packet should be played according to its timestamp and the reference time. - // The desired latency, typically 88200 frames, will be calculated for in rtp.c, - // and any desired backend latency offset included in it there. - - uint64_t should_be_time; - - frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 - &should_be_time, conn); - - conn->first_packet_time_to_play = should_be_time; - - int64_t lt = conn->first_packet_time_to_play - local_time_now; - - debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, - lt * 0.000000001); - - int64_t lateness = local_time_now - conn->first_packet_time_to_play; - if (lateness > 0) { - debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", - lateness); - do_flush(conn->first_packet_timestamp + 5 * 4410, conn); - } + if (curframe->sequence_number != conn->ab_read) { + // some kind of sync problem has occurred. + if (BUFIDX(curframe->sequence_number) == BUFIDX(conn->ab_read)) { + // it looks like aliasing has happened + // jump to the new incoming stuff... + conn->ab_read = curframe->sequence_number; + debug(1, "Aliasing of buffer index -- reset."); + } else { + debug(1, "Inconsistent sequence numbers detected"); } } + } - if (conn->first_packet_time_to_play != 0) { - // Now that we know the timing of the first packet... - if (config.output->delay) { - // and that the output device is capable of synchronization... + if ((curframe) && (curframe->ready)) { + notified_buffer_empty = 0; // at least one buffer now -- diagnostic only. + if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the + // player + if (conn->first_packet_timestamp == 0) { // if this is the very first packet + if (have_timestamp_timing_information(conn)) { // if we have a reference time + // debug(1,"First frame seen with timestamp..."); + conn->first_packet_timestamp = + curframe->given_timestamp; // we will keep buffering until we are + // supposed to start playing this +#ifdef CONFIG_METADATA + // say we have started receiving frames here + debug(2, "pffr"); + send_ssnc_metadata( + 'pffr', NULL, 0, + 0); // "first frame received", but don't wait if the queue is locked +#endif + // Here, calculate when we should start playing. We need to know when to allow the + // packets to be sent to the player. - // We may send packets of - // silence from now until the time the first audio packet should be sent - // and then we will send the first packet, which will be followed by - // the subsequent packets. - // here, we figure out whether and what silence to send. + // every second or so, we get a reference on when a particular packet should be + // played. - uint64_t should_be_time; + // It probably won't be the timestamp of our first packet, however, so we might + // have to do some calculations. - // readjust first packet time to play - frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 - &should_be_time, conn); + // To calculate when the first packet will be played, we figure out the exact time + // the packet should be played according to its timestamp and the reference time. + // The desired latency, typically 88200 frames, will be calculated for in rtp.c, + // and any desired backend latency offset included in it there. - int64_t change_in_should_be_time = - (int64_t)(should_be_time - conn->first_packet_time_to_play); + uint64_t should_be_time; - if (fabs(0.000001 * change_in_should_be_time) > - 0.001) // the clock drift estimation might be nudging the estimate, and we can - // ignore this unless if's more than a microsecond - debug(2, "Change in estimated first_packet_time: %f milliseconds for first_packet .", - 0.000001 * change_in_should_be_time); + frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 + &should_be_time, conn); - conn->first_packet_time_to_play = should_be_time; + conn->first_packet_time_to_play = should_be_time; - int64_t lead_time = conn->first_packet_time_to_play - local_time_now; // negative means late - if (lead_time < 0) { - debug(1, "Gone past starting time for %u by %" PRId64 " nanoseconds.", conn->first_packet_timestamp, -lead_time); - conn->ab_buffering = 0; - } else { - // do some calculations - if ((config.audio_backend_silent_lead_in_time_auto == 1) || - (lead_time <= - (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)1000000000))) { - // debug(1, "Lead time: %" PRId64 " nanoseconds.", lead_time); - int resp = 0; - dac_delay = 0; - if (have_sent_prefiller_silence != 0) - resp = config.output->delay( - &dac_delay); // we know the output device must have a delay function - if (resp == 0) { - int64_t gross_frame_gap = - ((conn->first_packet_time_to_play - local_time_now) * config.output_rate) / - 1000000000; - int64_t exact_frame_gap = gross_frame_gap - dac_delay; - int64_t frames_needed_to_maintain_desired_buffer = - (int64_t)(config.audio_backend_buffer_desired_length * config.output_rate) - - dac_delay; - // below, remember that exact_frame_gap and - // frames_needed_to_maintain_desired_buffer could both be negative - int64_t fs = frames_needed_to_maintain_desired_buffer; + int64_t lt = conn->first_packet_time_to_play - local_time_now; - // if there isn't enough time to have the desired buffer size - if (exact_frame_gap <= frames_needed_to_maintain_desired_buffer) { - fs = conn->max_frames_per_packet * 2; - } - // if we are very close to the end of buffering, i.e. within two frame-lengths, - // add the remaining silence needed and end buffering - if (exact_frame_gap <= conn->max_frames_per_packet * 2) { - fs = exact_frame_gap; - if (fs > first_frame_early_bias) - fs = fs - first_frame_early_bias; // deliberately make the first packet a - // tiny bit early so that the player may - // compensate for it at the last minute - conn->ab_buffering = 0; - } - void *silence; - if (fs > 0) { - silence = malloc(conn->output_bytes_per_frame * fs); - if (silence == NULL) - debug(1, "Failed to allocate %d byte silence buffer.", fs); - else { - // generate frames of silence with dither if necessary - conn->previous_random_number = - generate_zero_frames(silence, fs, config.output_format, - conn->enable_dither, conn->previous_random_number); - config.output->play(silence, fs); - debug(2, "Sent %" PRId64 " frames of silence", fs); - free(silence); - have_sent_prefiller_silence = 1; + debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + + int64_t lateness = local_time_now - conn->first_packet_time_to_play; + if (lateness > 0) { + debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", + lateness); + do_flush(conn->first_packet_timestamp + 5 * 4410, conn); + } + } + } + + if (conn->first_packet_time_to_play != 0) { + // Now that we know the timing of the first packet... + if (config.output->delay) { + // and that the output device is capable of synchronization... + + // We may send packets of + // silence from now until the time the first audio packet should be sent + // and then we will send the first packet, which will be followed by + // the subsequent packets. + // here, we figure out whether and what silence to send. + + uint64_t should_be_time; + + // readjust first packet time to play + frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 + &should_be_time, conn); + + int64_t change_in_should_be_time = + (int64_t)(should_be_time - conn->first_packet_time_to_play); + + if (fabs(0.000001 * change_in_should_be_time) > + 0.001) // the clock drift estimation might be nudging the estimate, and we can + // ignore this unless if's more than a microsecond + debug(2, + "Change in estimated first_packet_time: %f milliseconds for first_packet .", + 0.000001 * change_in_should_be_time); + + conn->first_packet_time_to_play = should_be_time; + + int64_t lead_time = + conn->first_packet_time_to_play - local_time_now; // negative means late + if (lead_time < 0) { + debug(1, "Gone past starting time for %u by %" PRId64 " nanoseconds.", + conn->first_packet_timestamp, -lead_time); + conn->ab_buffering = 0; + } else { + // do some calculations + if ((config.audio_backend_silent_lead_in_time_auto == 1) || + (lead_time <= + (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)1000000000))) { + // debug(1, "Lead time: %" PRId64 " nanoseconds.", lead_time); + int resp = 0; + dac_delay = 0; + if (have_sent_prefiller_silence != 0) + resp = config.output->delay( + &dac_delay); // we know the output device must have a delay function + if (resp == 0) { + int64_t gross_frame_gap = + ((conn->first_packet_time_to_play - local_time_now) * + config.output_rate) / + 1000000000; + int64_t exact_frame_gap = gross_frame_gap - dac_delay; + int64_t frames_needed_to_maintain_desired_buffer = + (int64_t)(config.audio_backend_buffer_desired_length * + config.output_rate) - + dac_delay; + // below, remember that exact_frame_gap and + // frames_needed_to_maintain_desired_buffer could both be negative + int64_t fs = frames_needed_to_maintain_desired_buffer; + + // if there isn't enough time to have the desired buffer size + if (exact_frame_gap <= frames_needed_to_maintain_desired_buffer) { + fs = conn->max_frames_per_packet * 2; } - } - } else { - - if (resp == sps_extra_code_output_stalled) { - if (conn->unfixable_error_reported == 0) { - conn->unfixable_error_reported = 1; - if (config.cmd_unfixable) { - command_execute(config.cmd_unfixable, "output_device_stalled", 1); - } else { - warn("an unrecoverable error, \"output_device_stalled\", has been " - "detected.", - conn->connection_number); + // if we are very close to the end of buffering, i.e. within two + // frame-lengths, add the remaining silence needed and end buffering + if (exact_frame_gap <= conn->max_frames_per_packet * 2) { + fs = exact_frame_gap; + if (fs > first_frame_early_bias) + fs = fs - first_frame_early_bias; // deliberately make the first packet a + // tiny bit early so that the player may + // compensate for it at the last minute + conn->ab_buffering = 0; + } + void *silence; + if (fs > 0) { + silence = malloc(conn->output_bytes_per_frame * fs); + if (silence == NULL) + debug(1, "Failed to allocate %d byte silence buffer.", fs); + else { + // generate frames of silence with dither if necessary + conn->previous_random_number = generate_zero_frames( + silence, fs, config.output_format, conn->enable_dither, + conn->previous_random_number); + config.output->play(silence, fs); + debug(2, "Sent %" PRId64 " frames of silence", fs); + free(silence); + have_sent_prefiller_silence = 1; } } } else { - debug(1, "Unexpected response to getting dac delay: %d.", resp); + + if (resp == sps_extra_code_output_stalled) { + if (conn->unfixable_error_reported == 0) { + conn->unfixable_error_reported = 1; + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, "output_device_stalled", 1); + } else { + warn("an unrecoverable error, \"output_device_stalled\", has been " + "detected.", + conn->connection_number); + } + } + } else { + debug(1, "Unexpected response to getting dac delay: %d.", resp); + } } } } - } - } else { - // if the output device doesn't have a delay, we simply send the lead-in - int64_t lead_time = - conn->first_packet_time_to_play - local_time_now; // negative if we are late - void *silence; - int64_t frame_gap = (lead_time * config.output_rate) / 1000000000; - // debug(1,"%d frames needed.",frame_gap); - while (frame_gap > 0) { - ssize_t fs = config.output_rate / 10; - if (fs > frame_gap) - fs = frame_gap; + } else { + // if the output device doesn't have a delay, we simply send the lead-in + int64_t lead_time = + conn->first_packet_time_to_play - local_time_now; // negative if we are late + void *silence; + int64_t frame_gap = (lead_time * config.output_rate) / 1000000000; + // debug(1,"%d frames needed.",frame_gap); + while (frame_gap > 0) { + ssize_t fs = config.output_rate / 10; + if (fs > frame_gap) + fs = frame_gap; - silence = malloc(conn->output_bytes_per_frame * fs); - if (silence == NULL) - debug(1, "Failed to allocate %d frame silence buffer.", fs); - else { - // debug(1, "No delay function -- outputting %d frames of silence.", fs); - conn->previous_random_number = - generate_zero_frames(silence, fs, config.output_format, conn->enable_dither, - conn->previous_random_number); - config.output->play(silence, fs); - free(silence); + silence = malloc(conn->output_bytes_per_frame * fs); + if (silence == NULL) + debug(1, "Failed to allocate %d frame silence buffer.", fs); + else { + // debug(1, "No delay function -- outputting %d frames of silence.", fs); + conn->previous_random_number = + generate_zero_frames(silence, fs, config.output_format, conn->enable_dither, + conn->previous_random_number); + config.output->play(silence, fs); + free(silence); + } + frame_gap -= fs; } - frame_gap -= fs; + conn->ab_buffering = 0; } - conn->ab_buffering = 0; } - } #ifdef CONFIG_METADATA - if (conn->ab_buffering == 0) { - debug(2, "prsm"); - send_ssnc_metadata('prsm', NULL, 0, - 0); // "resume", but don't wait if the queue is locked - } + if (conn->ab_buffering == 0) { + debug(2, "prsm"); + send_ssnc_metadata('prsm', NULL, 0, + 0); // "resume", but don't wait if the queue is locked + } #endif + } } } } - // Here, we work out whether to release a packet or wait // We release a packet when the time is right. @@ -1227,66 +1244,70 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // Note: the last three items are expressed in frames and must be converted to time. int do_wait = 0; // don't wait unless we can really prove we must - if ((conn->ab_synced) && (curframe) && (curframe->ready) && (curframe->given_timestamp)) { - do_wait = - 1; // if the current frame exists and is ready, then wait unless it's time to let it go... + if (have_timestamp_timing_information(conn)) { + if ((conn->ab_synced) && (curframe) && (curframe->ready) && (curframe->given_timestamp)) { + do_wait = 1; // if the current frame exists and is ready, then wait unless it's time to let + // it go... - // here, get the time to play the current frame. + // here, get the time to play the current frame. - if (have_timestamp_timing_information(conn)) { // if we have a reference time + if (have_timestamp_timing_information(conn)) { // if we have a reference time - uint64_t time_to_play; + uint64_t time_to_play; - // we must enable packets to be released early enough for the - // audio buffer to be filled to the desired length + // we must enable packets to be released early enough for the + // audio buffer to be filled to the desired length - uint32_t buffer_latency_offset = - (uint32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); - frame_to_local_time(curframe->given_timestamp - - buffer_latency_offset, // this will go modulo 2^32 - &time_to_play, conn); + uint32_t buffer_latency_offset = + (uint32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); + frame_to_local_time(curframe->given_timestamp - + buffer_latency_offset, // this will go modulo 2^32 + &time_to_play, conn); - if (local_time_now >= time_to_play) { - do_wait = 0; - } - // here, do a sanity check. if the time_to_play is not within a few seconds of the - // time now, the frame is probably not meant to be there, so let it go. - if (do_wait != 0) { - // this is a hack. - // we subtract two 2^n unsigned numbers and get a signed 2^n result. - // If we think of the calculation as occurring in modulo 2^n arithmetic - // then the signed result's magnitude represents the shorter distance around - // the modulo wheel of values from one number to the other. - // The sign indicates the direction: positive means clockwise (upwards) from the - // second number to the first (i.e. the first number comes "after" the second). + if (local_time_now >= time_to_play) { + do_wait = 0; + } + // here, do a sanity check. if the time_to_play is not within a few seconds of the + // time now, the frame is probably not meant to be there, so let it go. + if (do_wait != 0) { + // this is a hack. + // we subtract two 2^n unsigned numbers and get a signed 2^n result. + // If we think of the calculation as occurring in modulo 2^n arithmetic + // then the signed result's magnitude represents the shorter distance around + // the modulo wheel of values from one number to the other. + // The sign indicates the direction: positive means clockwise (upwards) from the + // second number to the first (i.e. the first number comes "after" the second). - int64_t time_difference = local_time_now - time_to_play; - if ((time_difference > 10000000000) || (time_difference < -10000000000)) { - debug(2, - "crazy time interval of %f seconds between time now: 0x%" PRIx64 - " and time of packet: %" PRIx64 ".", - 0.000000001 * time_difference, local_time_now, time_to_play); - debug(2, "packet rtptime: %u, reference_timestamp: %u", curframe->given_timestamp, - conn->anchor_rtptime); + int64_t time_difference = local_time_now - time_to_play; + if ((time_difference > 10000000000) || (time_difference < -10000000000)) { + debug(2, + "crazy time interval of %f seconds between time now: 0x%" PRIx64 + " and time of packet: %" PRIx64 ".", + 0.000000001 * time_difference, local_time_now, time_to_play); + debug(2, "packet rtptime: %u, reference_timestamp: %u", curframe->given_timestamp, + conn->anchor_rtptime); - do_wait = 0; // let it go + do_wait = 0; // let it go + } } } } - } - if (do_wait == 0) - if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! - if (notified_buffer_empty == 0) { - debug(3, "Buffers exhausted."); - notified_buffer_empty = 1; - // reset_input_flow_metrics(conn); // don't do a full flush parameters reset - conn->initial_reference_time = 0; - conn->initial_reference_timestamp = 0; + if (do_wait == 0) + if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! + if (notified_buffer_empty == 0) { + debug(3, "Buffers exhausted."); + notified_buffer_empty = 1; + // reset_input_flow_metrics(conn); // don't do a full flush parameters reset + conn->initial_reference_time = 0; + conn->initial_reference_timestamp = 0; + } + do_wait = 1; } - do_wait = 1; - } - wait = (conn->ab_buffering || (do_wait != 0) || (!conn->ab_synced)); - + wait = (conn->ab_buffering || (do_wait != 0) || (!conn->ab_synced)); + } else { + wait = 1; + // debug(1,"don't yet have timing information"); + } if (wait) { uint64_t time_to_wait_for_wakeup_ns = 1000000000 / conn->input_rate; // this is time period of one frame @@ -1631,6 +1652,7 @@ void player_thread_cleanup_handler(void *arg) { debug(3, "Audio thread terminated."); #ifdef CONFIG_AIRPLAY_2 } + ptp_send_control_message_string("T"); // remove all timing peers to force the master to 0 #endif if (conn->outbuf) { @@ -2191,7 +2213,7 @@ void *player_thread_func(void *arg) { conn->connection_number); } } else - debug(3, "Delay error %d when checking running latency.", resp); + debug(1, "Delay error %d when checking running latency.", resp); } } @@ -2207,7 +2229,8 @@ void *player_thread_func(void *arg) { sync_error = delay; // int64_t from int64_t - int32_t, so okay - // debug(1, "Sync error on frame %u is %" PRId64 " frames.", inframe->given_timestamp, sync_error); + // debug(1, "Sync error on frame %u is %" PRId64 " frames.", inframe->given_timestamp, + // sync_error); if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; @@ -3026,7 +3049,7 @@ void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { } void player_full_flush(rtsp_conn_info *conn) { - debug(3,"player_full_flush"); + debug(3, "player_full_flush"); // this basically flushes everything from the player // here, find the rtptime of the last from in the buffer and add 1 to it // so as to ask to flush everything diff --git a/ptp-utilities.c b/ptp-utilities.c index 73b778fa..6b6ffd1a 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -82,7 +82,8 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { response = clock_no_master; } } else { - // the version can not be zero. If zero is returned here, it means that the shared memory is not yet initialised, so not availalbe + // the version can not be zero. If zero is returned here, it means that the shared memory is + // not yet initialised, so not availalbe if (nqptp_data.version == 0) response = clock_service_unavailable; else diff --git a/rtp.c b/rtp.c index d92390db..3db75e1b 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1415,7 +1415,8 @@ void *rtp_event_receiver(void *arg) { } while (finished == 0); pthread_cleanup_pop(1); // close the socket pthread_cleanup_pop(1); // do the cleanup - debug(2, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", conn->connection_number); + debug(2, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.", + conn->connection_number); pthread_exit(NULL); } diff --git a/rtsp.c b/rtsp.c index a7ec0691..6eef9789 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1810,7 +1810,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 } } - debug(1,"RTSP Flush Requested: %u.",rtptime); + debug(1, "RTSP Flush Requested: %u.", rtptime); if (have_play_lock(conn)) { #ifdef CONFIG_METADATA if (p) @@ -1824,8 +1824,8 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; } else { - warn("Connection %d FLUSH %u received without having the player", - conn->connection_number, rtptime); + warn("Connection %d FLUSH %u received without having the player", conn->connection_number, + rtptime); resp->respcode = 451; } } From e8e81a8b079f2a14b5b1f7274bca555478368af6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 10:57:50 +0100 Subject: [PATCH 116/878] fix a couple of compilation errors. --- common.c | 2 ++ common.h | 2 ++ player.c | 91 ++++++++++++++++++++++++++------------------------------ rtsp.c | 1 + 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/common.c b/common.c index 068f78b8..69270cb9 100644 --- a/common.c +++ b/common.c @@ -1560,10 +1560,12 @@ void malloc_cleanup(void *arg) { free(arg); } +#ifdef CONFIG_AIRPLAY_2 void plist_cleanup(void *arg) { // debug(1, "plist cleanup called."); plist_free((plist_t)arg); } +#endif void socket_cleanup(void *arg) { intptr_t fdp = (intptr_t)arg; diff --git a/common.h b/common.h index 8ff0978c..4d8dcc1d 100644 --- a/common.h +++ b/common.h @@ -471,7 +471,9 @@ void mutex_unlock(void *arg); void mutex_cleanup(void *arg); void cv_cleanup(void *arg); void thread_cleanup(void *arg); +#ifdef CONFIG_AIRPLAY_2 void plist_cleanup(void *arg); +#endif char *debug_malloc_hex_cstring(void *packet, size_t nread); diff --git a/player.c b/player.c index 799e9e25..eb6e4b4a 100644 --- a/player.c +++ b/player.c @@ -1035,51 +1035,49 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { notified_buffer_empty = 0; // at least one buffer now -- diagnostic only. if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the // player - if (conn->first_packet_timestamp == 0) { // if this is the very first packet - if (have_timestamp_timing_information(conn)) { // if we have a reference time - // debug(1,"First frame seen with timestamp..."); - conn->first_packet_timestamp = - curframe->given_timestamp; // we will keep buffering until we are - // supposed to start playing this + if (conn->first_packet_timestamp == 0) { // if this is the very first packet + // debug(1,"First frame seen with timestamp..."); + conn->first_packet_timestamp = + curframe->given_timestamp; // we will keep buffering until we are + // supposed to start playing this #ifdef CONFIG_METADATA - // say we have started receiving frames here - debug(2, "pffr"); - send_ssnc_metadata( - 'pffr', NULL, 0, - 0); // "first frame received", but don't wait if the queue is locked + // say we have started receiving frames here + debug(2, "pffr"); + send_ssnc_metadata( + 'pffr', NULL, 0, + 0); // "first frame received", but don't wait if the queue is locked #endif - // Here, calculate when we should start playing. We need to know when to allow the - // packets to be sent to the player. + // Here, calculate when we should start playing. We need to know when to allow the + // packets to be sent to the player. - // every second or so, we get a reference on when a particular packet should be - // played. + // every second or so, we get a reference on when a particular packet should be + // played. - // It probably won't be the timestamp of our first packet, however, so we might - // have to do some calculations. + // It probably won't be the timestamp of our first packet, however, so we might + // have to do some calculations. - // To calculate when the first packet will be played, we figure out the exact time - // the packet should be played according to its timestamp and the reference time. - // The desired latency, typically 88200 frames, will be calculated for in rtp.c, - // and any desired backend latency offset included in it there. + // To calculate when the first packet will be played, we figure out the exact time + // the packet should be played according to its timestamp and the reference time. + // The desired latency, typically 88200 frames, will be calculated for in rtp.c, + // and any desired backend latency offset included in it there. - uint64_t should_be_time; + uint64_t should_be_time; - frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 - &should_be_time, conn); + frame_to_local_time(conn->first_packet_timestamp, // this will go modulo 2^32 + &should_be_time, conn); - conn->first_packet_time_to_play = should_be_time; + conn->first_packet_time_to_play = should_be_time; - int64_t lt = conn->first_packet_time_to_play - local_time_now; + int64_t lt = conn->first_packet_time_to_play - local_time_now; - debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); - int64_t lateness = local_time_now - conn->first_packet_time_to_play; - if (lateness > 0) { - debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", - lateness); - do_flush(conn->first_packet_timestamp + 5 * 4410, conn); - } + int64_t lateness = local_time_now - conn->first_packet_time_to_play; + if (lateness > 0) { + debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", + lateness); + do_flush(conn->first_packet_timestamp + 5 * 4410, conn); } } @@ -1230,21 +1228,19 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } } } - } - // Here, we work out whether to release a packet or wait - // We release a packet when the time is right. - // To work out when the time is right, we need to take account of (1) the actual time the packet - // should be released, - // (2) the latency requested, (3) the audio backend latency offset and (4) the desired length of - // the audio backend's buffer + // Here, we work out whether to release a packet or wait + // We release a packet when the time is right. - // The time is right if the current time is later or the same as - // The packet time + (latency + latency offset - backend_buffer_length). - // Note: the last three items are expressed in frames and must be converted to time. + // To work out when the time is right, we need to take account of (1) the actual time the + // packet should be released, (2) the latency requested, (3) the audio backend latency offset + // and (4) the desired length of the audio backend's buffer - int do_wait = 0; // don't wait unless we can really prove we must - if (have_timestamp_timing_information(conn)) { + // The time is right if the current time is later or the same as + // The packet time + (latency + latency offset - backend_buffer_length). + // Note: the last three items are expressed in frames and must be converted to time. + + int do_wait = 0; // don't wait unless we can really prove we must if ((conn->ab_synced) && (curframe) && (curframe->ready) && (curframe->given_timestamp)) { do_wait = 1; // if the current frame exists and is ready, then wait unless it's time to let // it go... @@ -1305,8 +1301,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } wait = (conn->ab_buffering || (do_wait != 0) || (!conn->ab_synced)); } else { - wait = 1; - // debug(1,"don't yet have timing information"); + wait = 1; // keep waiting until the timing information becomes available } if (wait) { uint64_t time_to_wait_for_wakeup_ns = diff --git a/rtsp.c b/rtsp.c index 6eef9789..a5564f1e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1411,6 +1411,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } } else { player_full_flush(conn); + ptp_send_control_message_string("T"); // ensure an obsolete clock isn't picked up later. } } pthread_cleanup_pop(1); // plist_free the messagePlist; From a27f43cf92e64f6cf63e101a1541d22282e5b0e1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 10:58:29 +0100 Subject: [PATCH 117/878] Remove some obsolete error checking code. --- player.c | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/player.c b/player.c index eb6e4b4a..c8eca4de 100644 --- a/player.c +++ b/player.c @@ -885,6 +885,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { local_time_now = get_absolute_time_in_ns(); // type okay // debug(3, "buffer_get_frame is iterating"); + // we must have timing information before we can do anything here if (have_timestamp_timing_information(conn)) { int rco = get_requested_connection_state_to_output(); @@ -1762,16 +1763,6 @@ void *player_thread_func(void *arg) { conn->dac_buffer_queue_minimum_length); conn->session_corrections = 0; - // conn->play_segment_reference_frame = 0; // zero signals that we are not in a play segment - - // check that there are enough buffers to accommodate the desired latency and the latency offset - - int maximum_latency = - conn->latency + (int)(config.audio_backend_latency_offset * config.output_rate); - if ((maximum_latency + (352 - 1)) / 352 + 10 > BUFFER_FRAMES) - die("Not enough buffers available for a total latency of %d frames. A maximum of %d 352-frame " - "packets may be accommodated.", - maximum_latency, BUFFER_FRAMES); conn->connection_state_to_output = get_requested_connection_state_to_output(); // this is about half a minute //#define trend_interval 3758 From 171001ffd85d1f6ad7dda76694ce1caf7f3f39ed Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 10:59:33 +0100 Subject: [PATCH 118/878] fix calculation of offset with a negative offset --- rtp.c | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/rtp.c b/rtp.c index 3db75e1b..3193eacb 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -364,17 +364,12 @@ void *rtp_control_receiver(void *arg) { // loop around in // modulo. Not sure if // you'll get an error! - // debug(3, "Latency derived just from the sync packet is %" PRIu32 " frames.", la); + // debug(1, "Latency from the sync packet is %" PRIu32 " frames.", la); if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) { la += config.fixedLatencyOffset; - // debug(3, "A fixed latency offset of %d frames has been added, giving a latency of - // " - // "%" PRId64 - // " frames with flags: %d and AirPlay version %d (triggers if 353 or - // less).", - // config.fixedLatencyOffset, la, flags, conn->AirPlayVersion); + // debug(1, "Latency offset by %" PRIu32" frames due to the source flags and version giving a latency of %" PRIu32 " frames.", config.fixedLatencyOffset, la); } if ((conn->maximum_latency) && (conn->maximum_latency < la)) la = conn->maximum_latency; @@ -394,7 +389,9 @@ void *rtp_control_receiver(void *arg) { // here we have the latency but it does not yet account for the // audio_backend_latency_offset int32_t latency_offset = - (uint32_t)(config.audio_backend_latency_offset * conn->input_rate); + (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + + // debug(1,"latency offset is %" PRId32 ", input rate is %u", latency_offset, conn->input_rate); int32_t adjusted_latency = latency_offset + (int32_t)la; if ((adjusted_latency < 0) || (adjusted_latency > @@ -411,7 +408,7 @@ void *rtp_control_receiver(void *arg) { ", minimum latency: %" PRIu32 ", maximum " "latency: %" PRIu32 ", fixed offset: %" PRIu32 ", audio_backend_latency_offset: %f.", - la, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, + conn->latency, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset, config.audio_backend_latency_offset); } From 4a0849b942739783ec64a9f2beb20fc5c36a9fda Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 11:00:09 +0100 Subject: [PATCH 119/878] Fix a compilation bug and quieten a debug message. --- rtsp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index a5564f1e..c670455b 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1811,7 +1811,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 } } - debug(1, "RTSP Flush Requested: %u.", rtptime); + debug(2, "RTSP Flush Requested: %u.", rtptime); if (have_play_lock(conn)) { #ifdef CONFIG_METADATA if (p) @@ -1819,8 +1819,11 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { else send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); #endif - // hack -- ignore it for airplay 2 + +// hack -- ignore it for airplay 2 +#ifdef CONFIG_AIRPLAY_2 if (conn->airplay_type != ap_2) +#endif player_flush(rtptime, conn); // will not crash even it there is no player thread. resp->respcode = 200; From fcf5c7f8d3b023c389485b39b63ada2f91b897cb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 11:00:41 +0100 Subject: [PATCH 120/878] clang --- rtp.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/rtp.c b/rtp.c index 3193eacb..c3cc3938 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -369,7 +369,8 @@ void *rtp_control_receiver(void *arg) { if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) { la += config.fixedLatencyOffset; - // debug(1, "Latency offset by %" PRIu32" frames due to the source flags and version giving a latency of %" PRIu32 " frames.", config.fixedLatencyOffset, la); + // debug(1, "Latency offset by %" PRIu32" frames due to the source flags and version + // giving a latency of %" PRIu32 " frames.", config.fixedLatencyOffset, la); } if ((conn->maximum_latency) && (conn->maximum_latency < la)) la = conn->maximum_latency; @@ -391,7 +392,8 @@ void *rtp_control_receiver(void *arg) { int32_t latency_offset = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); - // debug(1,"latency offset is %" PRId32 ", input rate is %u", latency_offset, conn->input_rate); + // debug(1,"latency offset is %" PRId32 ", input rate is %u", latency_offset, + // conn->input_rate); int32_t adjusted_latency = latency_offset + (int32_t)la; if ((adjusted_latency < 0) || (adjusted_latency > @@ -408,8 +410,8 @@ void *rtp_control_receiver(void *arg) { ", minimum latency: %" PRIu32 ", maximum " "latency: %" PRIu32 ", fixed offset: %" PRIu32 ", audio_backend_latency_offset: %f.", - conn->latency, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, - conn->maximum_latency, config.fixedLatencyOffset, + conn->latency, sync_rtp_timestamp - rtp_timestamp_less_latency, + conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset, config.audio_backend_latency_offset); } } From a3a7fb484925c69ac6ca619a824c9c731824fc9c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 12 May 2021 11:32:18 +0100 Subject: [PATCH 121/878] ask for at least 50 ms of a lead-in for buffered audio. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index c3cc3938..2dd5f185 100644 --- a/rtp.c +++ b/rtp.c @@ -2100,7 +2100,7 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); - double reqested_lead_time = 0.0; // + double reqested_lead_time = 0.050; // if (player_buffer_occupancy > ((reqested_lead_time + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again From 9f6f88d8d040e96c7b28638ba034128d08d346c4 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 13 May 2021 02:45:29 +0100 Subject: [PATCH 122/878] Smaller docker image & action change. --- .../{docker-build-master.yaml => docker-build.yaml} | 4 ++-- docker/Dockerfile | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) rename .github/workflows/{docker-build-master.yaml => docker-build.yaml} (92%) diff --git a/.github/workflows/docker-build-master.yaml b/.github/workflows/docker-build.yaml similarity index 92% rename from .github/workflows/docker-build-master.yaml rename to .github/workflows/docker-build.yaml index 71ca9d87..355d71b0 100644 --- a/.github/workflows/docker-build-master.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,11 +1,11 @@ # Pushes to tag 'unstable-BRANCH'. # Releases are pushed to latest and tagged - see docker-build-releases.yaml. -name: Build and push docker image based on master branch. +name: Build and push docker image based on specified branches. on: push: - branches: [ master, development ] + branches: [ master, development, charles-dev ] jobs: main: diff --git a/docker/Dockerfile b/docker/Dockerfile index d12ad352..1f9f0ca2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -52,7 +52,7 @@ COPY --from=builder-alac /usr/local/include /usr/local/include # TODO: Remove when no longer pulling from private repos. RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts -RUN --mount=type=ssh,id=github git clone git@github.com:charlesomer/nqptp.git +RUN --mount=type=ssh,id=github git clone git@github.com:mikebrady/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development @@ -103,13 +103,12 @@ RUN apk add \ avahi \ libconfig \ libsndfile \ - mosquitto-libs \ + mosquitto \ su-exec \ libuuid \ - ffmpeg-dev \ - ffmpeg-libs \ - libsodium-dev \ - libplist-dev + ffmpeg \ + libsodium \ + libplist RUN rm -rf /lib/apk/db/* From bb226a41844dafd052dfb41894f37019da1f9e52 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 20:10:38 +0100 Subject: [PATCH 123/878] fix flush of a large sync error, hopefully. --- player.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/player.c b/player.c index c8eca4de..1a4d855e 100644 --- a/player.c +++ b/player.c @@ -2305,15 +2305,8 @@ void *player_thread_func(void *arg) { debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", sync_error, (sync_error * 1.0) / config.output_rate); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; - uint32_t frames_to_drop_sized = local_frames_to_drop; - - debug_mutex_lock(&conn->flush_mutex, 1000, 1); - conn->flush_rtp_timestamp = - inframe->given_timestamp + - frames_to_drop_sized; // flush all packets up to (and including?) this - reset_input_flow_metrics(conn); - debug_mutex_unlock(&conn->flush_mutex, 3); - + uint32_t frames_to_drop_sized = local_frames_to_drop; + do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, "Large negative sync error of: %" PRId64 From 2180c7147f0170001d3c35a98d793bd078004525 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 20:12:12 +0100 Subject: [PATCH 124/878] If a PTP clock isn't available, use existing timing data if it's still valid. --- rtp.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index 2dd5f185..ab85cc09 100644 --- a/rtp.c +++ b/rtp.c @@ -1274,7 +1274,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP clock is valid and available.", conn->connection_number); + debug(1, "Connection %d: NQPTP clock % " PRIx64 " on line.", conn->connection_number, actual_clock_id); break; case clock_service_unavailable: debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); @@ -1311,6 +1311,14 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } conn->clock_status = response; } + + if ((response != clock_ok) && (conn->last_anchor_info_is_valid != 0)) { + if (anchorRTP != NULL) + *anchorRTP = conn->anchor_rtptime; + if (anchorLocalTime != NULL) + *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + response = clock_ok; + } return response; } From c7bdc2c40961e1257e051636b5f4797f1b4586bd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 20:58:03 +0100 Subject: [PATCH 125/878] Tidy up a message. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index ab85cc09..22424632 100644 --- a/rtp.c +++ b/rtp.c @@ -1274,7 +1274,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP clock % " PRIx64 " on line.", conn->connection_number, actual_clock_id); + debug(1, "Connection %d: NQPTP new master clock % " PRIx64 ".", conn->connection_number, actual_clock_id); break; case clock_service_unavailable: debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); From e41795f4dad1dd4a01f4eee4ed3fbd1ead0087d2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 20:59:38 +0100 Subject: [PATCH 126/878] clang --- player.c | 2 +- rtp.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index 1a4d855e..5996c5c4 100644 --- a/player.c +++ b/player.c @@ -2305,7 +2305,7 @@ void *player_thread_func(void *arg) { debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", sync_error, (sync_error * 1.0) / config.output_rate); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; - uint32_t frames_to_drop_sized = local_frames_to_drop; + uint32_t frames_to_drop_sized = local_frames_to_drop; do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, diff --git a/rtp.c b/rtp.c index 22424632..a78a1156 100644 --- a/rtp.c +++ b/rtp.c @@ -1274,7 +1274,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP new master clock % " PRIx64 ".", conn->connection_number, actual_clock_id); + debug(1, "Connection %d: NQPTP new master clock % " PRIx64 ".", conn->connection_number, + actual_clock_id); break; case clock_service_unavailable: debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); @@ -1311,7 +1312,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } conn->clock_status = response; } - + if ((response != clock_ok) && (conn->last_anchor_info_is_valid != 0)) { if (anchorRTP != NULL) *anchorRTP = conn->anchor_rtptime; From 7592e237d83924086eb9e781ff8958e3b0bd69b2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 21:07:56 +0100 Subject: [PATCH 127/878] quieten a debug message --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 5996c5c4..7c308b9a 100644 --- a/player.c +++ b/player.c @@ -1186,7 +1186,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } } } else { - debug(1, "Unexpected response to getting dac delay: %d.", resp); + debug(3, "Unexpected response to getting dac delay: %d.", resp); } } } From c8e45b3206fade3edc2cf63a326572666ad69bf6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 13 May 2021 21:16:58 +0100 Subject: [PATCH 128/878] Bump for changes to missing clock and flushing. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 3336dcfc..ca4cc228 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0d3], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0d4], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From db6cadb05c6d00a18b9384c1767f04703b08b1d4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 14 May 2021 12:26:25 +0100 Subject: [PATCH 129/878] Always update the systemd shairport-sync.service file. --- Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index b20ffbc3..16a0db74 100644 --- a/Makefile.am +++ b/Makefile.am @@ -214,13 +214,13 @@ if INSTALL_SYSTEMV getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null [ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d - [ -f $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ + cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ endif if INSTALL_SYSTEMD getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null [ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir) - [ -f $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) + cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) endif if INSTALL_FREEBSD_SERVICE # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." @@ -228,7 +228,7 @@ if INSTALL_FREEBSD_SERVICE pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1 [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync chown shairport-sync:shairport-sync /var/run/shairport-sync - [ -f /usr/local/etc/rc.d/shairport_sync ] || cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync + cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync chmod 555 /usr/local/etc/rc.d/shairport_sync endif if INSTALL_CYGWIN_SERVICE From 056b37020c02bbd485ea989b66e97b902b858509 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 14 May 2021 14:23:10 +0100 Subject: [PATCH 130/878] Was 4.0d4 with some extras. Moving to git-based semantic versioning. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index ca4cc228..aa082dee 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0d4], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.0-dev], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From 2203d6a335fe1883218a7a3037b2b01b44abfd5e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 14 May 2021 22:49:17 +0100 Subject: [PATCH 131/878] Start using automatic git-based versioning --- .gitignore | 4 ++++ Makefile.am | 12 ++++++++++++ common.c | 20 +++++++++++++++++--- configure.ac | 7 +++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5187a2b8..b78d32b7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ shairport-sync.core # macOS stuff .DS_Store shairport-sync.xcodeproj + +# version string generation +gitversion.h + diff --git a/Makefile.am b/Makefile.am index 16a0db74..d0a025e1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -32,6 +32,18 @@ else endif endif +# include information generated by 'git describe --dirty' if requested +if USE_GIT_VERSION +common.c: gitversion.h +gitversion.h: .git/index + echo "// Do not edit!" > gitversion.h + echo "// This file is automatically generated by 'git describe --dirty', if available." >> gitversion.h + echo -n " char git_version_string[] = \"" >> gitversion.h + git describe --dirty | tr -d '[[:space:]]' >> gitversion.h + echo "\";" >> gitversion.h +CLEANFILES += gitversion.h +endif + if USE_APPLE_ALAC shairport_sync_SOURCES += apple_alac.cpp endif diff --git a/common.c b/common.c index 69270cb9..ce43d004 100644 --- a/common.c +++ b/common.c @@ -2,7 +2,7 @@ * Utility routines. This file is part of Shairport. * Copyright (c) James Laird 2013 * The volume to attenuation function vol2attn copyright (c) Mike Brady 2014 - * Further changes and additions (c) Mike Brady 2014 -- 2019 + * Further changes and additions (c) Mike Brady 2014 -- 2021 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -27,6 +27,11 @@ */ #include "common.h" + +#ifdef CONFIG_USE_GIT_VERSION_STRING +#include "gitversion.h" +#endif + #include #include #include @@ -88,6 +93,8 @@ #include #endif + + #ifdef CONFIG_ALSA void set_alsa_out_dev(char *); #endif @@ -1602,9 +1609,16 @@ void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthre char *get_version_string() { char *version_string = malloc(1024); - if (version_string) { + if (version_string) { +#ifdef CONFIG_USE_GIT_VERSION_STRING + if (git_version_string[0] != '\0') + strcpy(version_string, git_version_string); + else +#endif strcpy(version_string, PACKAGE_VERSION); - +#ifdef CONFIG_AIRPLAY_2 + strcat(version_string, "-AirPlay2"); +#endif #ifdef CONFIG_APPLE_ALAC strcat(version_string, "-alac"); #endif diff --git a/configure.ac b/configure.ac index aa082dee..6b7c6d07 100644 --- a/configure.ac +++ b/configure.ac @@ -8,6 +8,13 @@ AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) AC_PROG_RANLIB +AC_CHECK_PROGS([GIT], [git]) +if test -n "$GIT"; then + AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.]) +fi +AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT"]) + + # Derived from the Avahi configure.ac file # Specifying the OS type, defaulting to linux. # From c645a039466e8f3ada91cd78ec88cc67744eed5d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 15 May 2021 12:18:37 +0100 Subject: [PATCH 132/878] handle volume control requests in AP2 correctly. Don't reset timing peer lists when buffered audio is paused. --- player.c | 9 ++++++--- player.h | 4 ++++ rtsp.c | 25 +++++++++++++++++-------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/player.c b/player.c index 7c308b9a..efa40c63 100644 --- a/player.c +++ b/player.c @@ -1974,10 +1974,13 @@ void *player_thread_func(void *arg) { #endif pthread_setcancelstate(oldState, NULL); - + + double initial_volume = config.airplay_volume; // default + if (conn->initial_airplay_volume_set) // if we have been given an initial volume + initial_volume = conn->initial_airplay_volume; // set the default volume to whatever it was before, as stored in the config airplay_volume - debug(2, "Set initial volume to %f.", config.airplay_volume); - player_volume(config.airplay_volume, conn); // will contain a cancellation point if asked to wait + debug(2, "Set initial volume to %f.", initial_volume); + player_volume(initial_volume, conn); // will contain a cancellation point if asked to wait debug(2, "Play begin"); while (1) { diff --git a/player.h b/player.h index eb210ae5..cc4dda2c 100644 --- a/player.h +++ b/player.h @@ -205,7 +205,11 @@ typedef struct { // mutexes and condition variables pthread_cond_t flowcontrol; pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex; + int fix_volume; + double initial_airplay_volume; + int initial_airplay_volume_set; + uint32_t timestamp_epoch, last_timestamp, maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2 // or 1. diff --git a/rtsp.c b/rtsp.c index c670455b..2828adec 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3,7 +3,7 @@ * Copyright (c) James Laird 2013 * Modifications associated with audio synchronization, mutithreading and - * metadata handling copyright (c) Mike Brady 2014-2020 + * metadata handling copyright (c) Mike Brady 2014-2021 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -1411,7 +1411,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } } else { player_full_flush(conn); - ptp_send_control_message_string("T"); // ensure an obsolete clock isn't picked up later. + // ptp_send_control_message_string("T"); // ensure an obsolete clock isn't picked up later. } } pthread_cleanup_pop(1); // plist_free the messagePlist; @@ -2281,8 +2281,16 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (!strncmp(cp, "volume: ", strlen("volume: "))) { float volume = atof(cp + strlen("volume: ")); - // debug(2, "AirPlay request to set volume to: %f.", volume); - player_volume(volume, conn); + debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); + // if we are playing, go ahead and change the volume + if (try_to_hold_play_lock(conn) == 0) { + player_volume(volume, conn); + release_hold_on_play_lock(conn); + } else { + // otherwise use the volume as the initial setting for when/if the player starts + conn->initial_airplay_volume = volume; + conn->initial_airplay_volume_set = 1; + } } else #ifdef CONFIG_METADATA if (!strncmp(cp, "progress: ", strlen("progress: "))) { @@ -2294,7 +2302,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else #endif { - debug(1, "unrecognised parameter: \"%s\" (%d)\n", cp, strlen(cp)); + debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, strlen(cp)); } cp = next; } @@ -3015,7 +3023,7 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - // debug(1,"Current volume sought"); + debug(2,"Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { resp->content = p; @@ -3110,10 +3118,11 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m // debug(2, "received parameters in SET_PARAMETER request."); handle_set_parameter_parameter(conn, req, resp); // this could be volume or progress } else { - debug(1, "received unknown Content-Type \"%s\" in SET_PARAMETER request.", ct); + debug(1, "Connection %d: received unknown Content-Type \"%s\" in SET_PARAMETER request.", conn->connection_number, ct); + debug_print_msg_headers(1,req); } } else { - debug(1, "missing Content-Type header in SET_PARAMETER request."); + debug(1, "Connection %d: missing Content-Type header in SET_PARAMETER request.", conn->connection_number); } resp->respcode = 200; } From 77e70e497fe5609fb6f89d83388b1779f4fc1aac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 16 May 2021 13:02:19 +0100 Subject: [PATCH 133/878] tighten up on the availability of a clock before feeding audio into the player. --- rtp.c | 4 ++-- rtsp.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rtp.c b/rtp.c index a78a1156..90362752 100644 --- a/rtp.c +++ b/rtp.c @@ -2128,7 +2128,7 @@ void *rtp_buffered_audio_processor(void *arg) { madly late int rtp_diff = pcm_buffer_read_point_rtptime - at_rtp; */ - if (play_enabled) { + if ((play_enabled) && (have_ptp_timing_information(conn) != 0)) { uint64_t buffer_should_be_time; if (frame_to_local_time(pcm_buffer_read_point_rtptime, &buffer_should_be_time, conn) == 0) { @@ -2137,7 +2137,7 @@ void *rtp_buffered_audio_processor(void *arg) { if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(2, "Connection %d: buffered audio lead time is %f seconds.", + debug(2, "Connection %d: buffered audio lead time is %f seconds.", conn->connection_number, 0.000000001 * lead_time); streaming_has_started = 1; player_put_packet(0, 0, pcm_buffer_read_point_rtptime, diff --git a/rtsp.c b/rtsp.c index 2828adec..da94052e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2281,7 +2281,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (!strncmp(cp, "volume: ", strlen("volume: "))) { float volume = atof(cp + strlen("volume: ")); - debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); + debug(1, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); // if we are playing, go ahead and change the volume if (try_to_hold_play_lock(conn) == 0) { player_volume(volume, conn); From bca004f7d1efa23e2ba019798e24303b8e62c43f Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 11:49:19 +0100 Subject: [PATCH 134/878] Moved autoreconf. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1f9f0ca2..1d5030d9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -65,9 +65,9 @@ WORKDIR / RUN --mount=type=ssh,id=github git clone git@github.com:aillwee/shairport-sync.git WORKDIR /shairport-sync RUN git checkout "$SHAIRPORT_SYNC_BRANCH" -RUN autoreconf -fi RUN git submodule init RUN git submodule update +RUN autoreconf -fi # RUN ./configure \ # --with-alsa \ # --with-dummy \ From 10aac5e5fca7c7ab78529385763bb7e84ccbbac8 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 12:11:59 +0100 Subject: [PATCH 135/878] Test copying SPS directory instead of cloning. --- docker/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1d5030d9..a15abb8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -62,9 +62,8 @@ RUN make RUN make install WORKDIR / -RUN --mount=type=ssh,id=github git clone git@github.com:aillwee/shairport-sync.git WORKDIR /shairport-sync -RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +COPY . . RUN git submodule init RUN git submodule update RUN autoreconf -fi From aa8fcbe7c8cfde484370a180a5a0a50d6e70b6df Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 12:22:23 +0100 Subject: [PATCH 136/878] Add information about git branch. --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a15abb8b..723b7f8c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -64,6 +64,7 @@ WORKDIR / WORKDIR /shairport-sync COPY . . +RUN git status RUN git submodule init RUN git submodule update RUN autoreconf -fi From b0b6500a37e586de381d789e5c91c2a6a5b8c5cf Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 12:42:58 +0100 Subject: [PATCH 137/878] Added branch switching for SPS docker build. --- .github/workflows/docker-build.yaml | 8 ++++---- docker/Dockerfile | 8 +++++--- docker/README.md | 11 ++++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 355d71b0..772586e1 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -13,8 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Set branch env. - run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Set SHAIRPORT_SYNC_BRANCH env. + run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up ssh agent uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. with: @@ -36,7 +36,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.BRANCH }} + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | - BRANCH=${{ env.BRANCH }} + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} ssh: github=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 723b7f8c..17ed2451 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,8 +42,10 @@ RUN make install # Shairport Sync Build System: FROM builder-base AS builder-sps -# This may be modified by the Github Action Workflow. -ARG SHAIRPORT_SYNC_BRANCH=development +# This will be modified by the Github Action Workflow and is required +# to ensure the correct branch is being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc @@ -64,7 +66,7 @@ WORKDIR / WORKDIR /shairport-sync COPY . . -RUN git status +RUN git checkout $SHAIRPORT_SYNC_BRANCH RUN git submodule init RUN git submodule update RUN autoreconf -fi diff --git a/docker/README.md b/docker/README.md index 74b986ca..8172ec08 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,3 +1,12 @@ # Shairport Sync Docker Image -https://hub.docker.com/r/mikebrady/shairport-sync \ No newline at end of file +Available at: https://hub.docker.com/r/mikebrady/shairport-sync + +## Build Example (for arm7 devices) + +TODO: remove `--ssh` when going public as this is only for private repos. +``` +docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . +``` + +`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file From 38ad90d410638665b35e1c1656e675e27697af60 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 12:45:38 +0100 Subject: [PATCH 138/878] Never mind, GitHub actions default to trigger branch. --- docker/Dockerfile | 6 ------ docker/README.md | 6 ++---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 17ed2451..c714462b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,11 +42,6 @@ RUN make install # Shairport Sync Build System: FROM builder-base AS builder-sps -# This will be modified by the Github Action Workflow and is required -# to ensure the correct branch is being used. -ARG SHAIRPORT_SYNC_BRANCH -RUN test -n "$SHAIRPORT_SYNC_BRANCH" - COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include @@ -66,7 +61,6 @@ WORKDIR / WORKDIR /shairport-sync COPY . . -RUN git checkout $SHAIRPORT_SYNC_BRANCH RUN git submodule init RUN git submodule update RUN autoreconf -fi diff --git a/docker/README.md b/docker/README.md index 8172ec08..2c221120 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,5 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . -``` - -`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file +docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile -t registry.nova.do/shairport-sync:unstable-development . +``` \ No newline at end of file From 87ee2aea8e243938fb56e34aa87cc497b7078e16 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 20 May 2021 13:10:23 +0100 Subject: [PATCH 139/878] Adding back branch specification just in case. --- docker/Dockerfile | 6 ++++++ docker/README.md | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c714462b..17ed2451 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,6 +42,11 @@ RUN make install # Shairport Sync Build System: FROM builder-base AS builder-sps +# This will be modified by the Github Action Workflow and is required +# to ensure the correct branch is being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" + COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include @@ -61,6 +66,7 @@ WORKDIR / WORKDIR /shairport-sync COPY . . +RUN git checkout $SHAIRPORT_SYNC_BRANCH RUN git submodule init RUN git submodule update RUN autoreconf -fi diff --git a/docker/README.md b/docker/README.md index 2c221120..4e10daae 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,5 +6,7 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile -t registry.nova.do/shairport-sync:unstable-development . -``` \ No newline at end of file +docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=charles-dev -t registry.nova.do/shairport-sync:unstable-development . +``` + +`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file From a6187fed12acf6316cf1cbb0503da624a2612088 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sat, 22 May 2021 12:47:14 +0100 Subject: [PATCH 140/878] Test. --- docker/shairport-sync.conf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker/shairport-sync.conf b/docker/shairport-sync.conf index bb447958..49338be1 100644 --- a/docker/shairport-sync.conf +++ b/docker/shairport-sync.conf @@ -1,9 +1,4 @@ # Temp sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC. -general = -{ - volume_range_db = 60; -}; - alsa = { output_device = "hw:0"; From 3143e9983a4fb0979990a256a3fcb864169b054f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 23 May 2021 10:13:14 +0100 Subject: [PATCH 141/878] Updates to pair_ap --- pair_ap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pair_ap b/pair_ap index 1a83fd5c..6d5933a2 160000 --- a/pair_ap +++ b/pair_ap @@ -1 +1 @@ -Subproject commit 1a83fd5c501bbf8390b2b34bfb94c59f9889a98a +Subproject commit 6d5933a2aa982fb8e7eb3f953ffa3bcf0e43a989 From e1034e11313dc67bd3522246f2bd444cb92059f0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 24 May 2021 12:07:05 +0100 Subject: [PATCH 142/878] Advertise the _raop._tcp service as well as the _airplay._tcp service. Use a real MAC as the basis for the device ID. --- common.c | 24 +++++------ common.h | 12 +++++- mdns.c | 13 +++--- mdns.h | 4 +- mdns_avahi.c | 43 +++++++++++++++----- rtsp.c | 111 ++++++++++++++++++++++++++++++++++----------------- shairport.c | 46 ++++++++++++++++----- 7 files changed, 173 insertions(+), 80 deletions(-) diff --git a/common.c b/common.c index ce43d004..4bdb78ab 100644 --- a/common.c +++ b/common.c @@ -1609,9 +1609,9 @@ void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthre char *get_version_string() { char *version_string = malloc(1024); - if (version_string) { + if (version_string) { #ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') + if (git_version_string[0] != '\0') strcpy(version_string, git_version_string); else #endif @@ -1939,29 +1939,29 @@ int32_t mod32Difference(uint32_t a, uint32_t b) { return result; } -char *get_device_id() { - char *response = NULL; +int get_device_id(uint8_t *id, int int_length) { + int response = 0; struct ifaddrs *ifaddr = NULL; struct ifaddrs *ifa = NULL; int i = 0; + uint8_t *t = id; + for (i = 0; i < int_length ; i++) { + *t++ = 0; + } if (getifaddrs(&ifaddr) == -1) { debug(1, "getifaddrs"); + response = -1; } else { + t = id; int found = 0; for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { - char obf[256] = {0}; - char *obfp = obf; struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; if ((strcmp(ifa->ifa_name, "lo") != 0) && (found == 0)) { - for (i = 0; i < s->sll_halen; i++) { - snprintf(obfp, 4, "%02x:", s->sll_addr[i]); - obfp += 3; + for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { + *t++ = s->sll_addr[i]; } - obfp -= 1; - *obfp = 0; - response = strdup(obf); found = 1; } } diff --git a/common.h b/common.h index 4d8dcc1d..48769527 100644 --- a/common.h +++ b/common.h @@ -209,7 +209,9 @@ typedef struct { // char *errfile; char *configfile; char *regtype; // The regtype is the service type followed by the protocol, separated by a dot, by - // default “_raop._tcp.”. + // default “_raop._tcp.” for AirPlay 1. + char *regtype2; // The regtype is the service type followed by the protocol, separated by a dot, by + // default “_raop._tcp.” for AirPlay 2. char *interface; // a string containg the interface name, or NULL if nothing specified int interface_index; // only valid if the interface string is non-NULL double audio_backend_buffer_desired_length; // this will be the length in seconds of the @@ -489,7 +491,13 @@ void *memdup(const void *mem, size_t size); // negative otherwise int32_t mod32Difference(uint32_t a, uint32_t b); -char *get_device_id(); + +int get_device_id(uint8_t *id, int int_length); + +#ifdef CONFIG_USE_GIT_VERSION_STRING +extern char git_version_string[]; +#endif + #endif // _COMMON_H diff --git a/mdns.c b/mdns.c index 7eb1f433..073e346c 100644 --- a/mdns.c +++ b/mdns.c @@ -64,17 +64,15 @@ static mdns_backend *mdns_backends[] = { #endif NULL}; -void mdns_register(char **txt_records) { - char *mdns_service_name = alloca(strlen(config.service_name) + 14); - char *p = mdns_service_name; -#ifndef CONFIG_AIRPLAY_2 +void mdns_register(char **txt_records, char **secondary_txt_records) { + char *ap1_service_name = alloca(strlen(config.service_name) + 14); + char *p = ap1_service_name; int i; for (i = 0; i < 6; i++) { snprintf(p, 3, "%02X", config.hw_addr[i]); p += 2; } *p++ = '@'; -#endif strcpy(p, config.service_name); mdns_backend **b = NULL; @@ -83,7 +81,7 @@ void mdns_register(char **txt_records) { for (b = mdns_backends; *b; b++) { if (strcmp((*b)->name, config.mdns_name) != 0) // Not the one we are looking for continue; - int error = (*b)->mdns_register(mdns_service_name, config.port, txt_records); + int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, txt_records, secondary_txt_records); if (error >= 0) { config.mdns = *b; } @@ -93,8 +91,9 @@ void mdns_register(char **txt_records) { if (*b == NULL) warn("%s mDNS backend not found"); } else { + // default -- pick the first back end for (b = mdns_backends; *b; b++) { - int error = (*b)->mdns_register(mdns_service_name, config.port, txt_records); + int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, txt_records, secondary_txt_records); if (error >= 0) { config.mdns = *b; break; diff --git a/mdns.h b/mdns.h index 73c15e8e..30900c78 100644 --- a/mdns.h +++ b/mdns.h @@ -8,7 +8,7 @@ extern int mdns_pid; void mdns_unregister(void); -void mdns_register(char **txt_records); +void mdns_register(char **txt_records, char **secondary_txt_records); void mdns_dacp_monitor_start(); void mdns_dacp_monitor_stop(void); void mdns_dacp_monitor_set_id(const char *dacp_id); @@ -17,7 +17,7 @@ void mdns_ls_backends(void); typedef struct { char *name; - int (*mdns_register)(char *apname, int port, char **txt_records); + int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records, char **secondary_txt_records); void (*mdns_unregister)(void); void (*mdns_dacp_monitor_start)(); void (*mdns_dacp_monitor_set_id)(const char *); diff --git a/mdns_avahi.c b/mdns_avahi.c index 4ab991c7..4873639c 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -62,6 +62,7 @@ typedef struct { dacp_browser_struct private_dbs; AvahiStringList *text_record_string_list = NULL; +AvahiStringList *ap2_text_record_string_list = NULL; // static AvahiServiceBrowser *sb = NULL; static AvahiClient *client = NULL; @@ -71,6 +72,8 @@ static AvahiThreadedPoll *tpoll = NULL; // static AvahiThreadedPoll *service_poll = NULL; static char *service_name = NULL; +char *ap2_service_name = NULL; + static int port = 0; static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIndex interface, @@ -187,7 +190,7 @@ static void egroup_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, /* A service name collision with a remote service * happened. Let's pick a new name */ - debug(2, "avahi name collision -- look for another"); + debug(1, "avahi name collision -- look for another"); n = avahi_alternative_service_name(service_name); if (service_name) avahi_free(service_name); @@ -238,10 +241,16 @@ static void register_service(AvahiClient *c) { selected_interface = config.interface_index; else selected_interface = AVAHI_IF_UNSPEC; - if (text_record_string_list) { + if (ap2_text_record_string_list) { + ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + ap2_service_name, config.regtype2, NULL, NULL, port, + ap2_text_record_string_list); + } + if ((ret == 0) && (text_record_string_list)) { ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, service_name, config.regtype, NULL, NULL, port, text_record_string_list); + } if (ret == 0) { ret = avahi_entry_group_commit(group); debug(2, "avahi: avahi_entry_group_commit %d", ret); @@ -252,9 +261,6 @@ static void register_service(AvahiClient *c) { } else { debug(1, "avahi: unexpected positive return"); } - } else { - debug(1, "Can't find a valid text_record_string_list"); - } } } @@ -312,12 +318,17 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } -static int avahi_register(char *srvname, int srvport, char **txt_records) { +static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records,char **secondary_txt_records) { // debug(1, "avahi_register."); - service_name = strdup(srvname); + service_name = strdup(ap1name); + if (ap2name != NULL) + ap2_service_name = strdup(ap2name); text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1); + if (secondary_txt_records != NULL) + ap2_text_record_string_list = avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); + port = srvport; int err; @@ -362,15 +373,29 @@ static void avahi_unregister(void) { if (service_name) { debug(2, "avahi: free the service name."); free(service_name); + service_name = NULL; } else debug(1, "avahi attempt to free NULL service name"); + if (ap2_service_name) { + debug(2, "avahi: free the ap2 service_name."); + free(ap2_service_name); + ap2_service_name = NULL; + } + if (text_record_string_list) { debug(2, "avahi free text_record_string_list"); avahi_string_list_free(text_record_string_list); - } + text_record_string_list = NULL; + } else + debug(1, "avahi attempt to free NULL text_record_string_list"); - service_name = NULL; + + if (ap2_text_record_string_list) { + debug(2, "avahi free ap_2text_record_string_list"); + avahi_string_list_free(ap2_text_record_string_list); + ap2_text_record_string_list = NULL; + } } void avahi_dacp_monitor_start(void) { diff --git a/rtsp.c b/rtsp.c index da94052e..ace0e98f 100644 --- a/rtsp.c +++ b/rtsp.c @@ -696,7 +696,7 @@ int msg_handle_line(rtsp_message **pmsg, char *line) { *p = 0; p += 2; msg_add_header(msg, line, p); - debug(3, " %s: %s.", line, p); + // debug(3, " %s: %s.", line, p); return -1; } else { char *cl = msg_get_header(msg, "Content-Length"); @@ -1714,8 +1714,8 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * resp->respcode = 200; msg_add_header(resp, "Public", "ANNOUNCE, SETUP, RECORD, " - "PAUSE, FLUSH, TEARDOWN, " - "OPTIONS, GET_PARAMETER, SET_PARAMETER, GET"); + "PAUSE, FLUSH, FLUSHBUFFERED, TEARDOWN, " + "OPTIONS, POST, GET, PUT"); } void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, @@ -3414,7 +3414,7 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { char *hdr = msg_get_header(req, "Apple-Challenge"); if (!hdr) return; - + debug(1,"Apple Challenge"); SOCKADDR fdsa; socklen_t sa_len = sizeof(fdsa); getsockname(fd, (struct sockaddr *)&fdsa, &sa_len); @@ -4084,50 +4084,58 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { maxfd = sockfd[i]; } + char **t1; // ap1 test records + char **t2; // two text records + + t1 = NULL; + t2 = NULL; + // make up a txt record char *txt_records[64]; char **p = txt_records; + t1 = p; // first set of text records + +// make up a firmware version + char firmware_version[64]; +#ifdef CONFIG_USE_GIT_VERSION_STRING + if (git_version_string[0] != '\0') + snprintf(firmware_version, sizeof(firmware_version), "fv=%s",git_version_string); + else +#endif + snprintf(firmware_version, sizeof(firmware_version), "fv=%s",PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 - *p++ = "srcvers=366.0"; - char deviceIdString[64]; - snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); - *p++ = deviceIdString; - // features is a 64 bit number, least significant 32 bits - char featuresString[64]; + char ap1_featuresString[64]; uint64_t features_hi = config.airplay_features; features_hi = (features_hi >> 32) & 0xffffffff; uint64_t features_lo = config.airplay_features; features_lo = features_lo & 0xffffffff; - snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIx64 ",0x%" PRIx64 "", + snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", features_lo, features_hi); - *p++ = featuresString; - char statusflagsString[32]; - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIx32, - config.airplay_statusflags); - *p++ = statusflagsString; - *p++ = "protovers=1.1"; - *p++ = "acl=0"; - *p++ = "rsf=0x0"; - *p++ = "fv=p20.78000.12"; - *p++ = "model=SPS"; - char piString[64]; - snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); - *p++ = piString; - char gidString[64]; - snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); - *p++ = gidString; - *p++ = "gcgl=0"; char pkString[128]; snprintf(pkString, sizeof(pkString), "pk="); pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); + +// the ap1 text record is different if it is set up for ap2 + *p++ = "cn=0,1"; + *p++ = "da=true"; + *p++ = "et=0,4"; + *p++ = ap1_featuresString; + *p++ = firmware_version; + *p++ = "md=2"; + *p++ = "am=SPS"; + *p++ = "sf=0x4"; + *p++ = "tp=UDP"; + *p++ = "vn=65537"; + *p++ = "vs=366.0"; *p++ = pkString; *p++ = NULL; + #else // here, just replicate what happens in mdns.h when using those #defines *p++ = "sf=0x4"; - *p++ = "fv=76400.10"; + *p++ = firmware_version; *p++ = "am=ShairportSync"; *p++ = "vs=105.1"; *p++ = "tp=TCP,UDP"; @@ -4146,23 +4154,52 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = "ek=1"; *p++ = "cn=0,1"; *p++ = "ch=2"; - *p++ = "am=ShairportSync"; *p++ = "txtvers=1"; if (config.password == 0) *p++ = "pw=false"; else *p++ = "pw=true"; + *p++ = NULL; #endif - /* - debug(1,"txt_record:"); - p = txt_records; - while (*p) - debug(1," %s", *p++); - */ - mdns_register(txt_records); +#ifdef CONFIG_AIRPLAY_2 + // make up a secondary set of text records + char *secondary_txt_records[64]; + p = secondary_txt_records; + t2 = p; // second set of text records in AirPlay 2 only + + *p++ = "srcvers=366.0"; + char deviceIdString[64]; + snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); + *p++ = deviceIdString; + char featuresString[64]; + snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + *p++ = featuresString; + char statusflagsString[32]; + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, + config.airplay_statusflags); + *p++ = statusflagsString; + *p++ = "protovers=1.1"; + *p++ = "acl=0"; + *p++ = "rsf=0x0"; + *p++ = firmware_version; + *p++ = "model=SPS"; + char piString[64]; + snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); + *p++ = piString; + char gidString[64]; + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); + *p++ = gidString; + *p++ = "gcgl=0"; + *p++ = pkString; + *p++ = NULL; +#endif + + + mdns_register(t1,t2); pthread_setcancelstate(oldState, NULL); int acceptfd; diff --git a/shairport.c b/shairport.c index 89fa5858..b2d8886c 100644 --- a/shairport.c +++ b/shairport.c @@ -1246,10 +1246,10 @@ int parse_options(int argc, char **argv) { /* if the regtype hasn't been set, do it now */ if (config.regtype == NULL) -#ifdef CONFIG_AIRPLAY_2 - config.regtype = strdup("_airplay._tcp"); -#else config.regtype = strdup("_raop._tcp"); +#ifdef CONFIG_AIRPLAY_2 + if (config.regtype2 == NULL) + config.regtype2 = strdup("_airplay._tcp"); #endif if (tdebuglev != 0) @@ -1416,6 +1416,11 @@ void exit_function() { if (config.regtype) free(config.regtype); +#ifdef CONFIG_AIRPLAY_2 + if (config.regtype2) + free(config.regtype2); +#endif + #ifdef CONFIG_LIBDAEMON if (this_is_the_daemon_process) { @@ -1515,6 +1520,9 @@ int main(int argc, char **argv) { // set defaults + // get a device id -- the first non-local MAC address + get_device_id((uint8_t *)&config.hw_addr, 6); + // get the endianness union { uint32_t u32; @@ -1582,15 +1590,27 @@ int main(int argc, char **argv) { // required (transient pairing), no Homekit access control 0x204: Audio cable attached, // OneTimePairingRequired 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit // access control + + // note: 0x300401F4A00 works but with weird delays and stuff + config.airplay_statusflags = 0x4; // Set to NULL to work with transient pairing config.airplay_pin = NULL; - // get a device id -- the first non-local MAC address, not necessarily the one in use - config.airplay_device_id = get_device_id(); - if (config.airplay_device_id) { - debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); - } else - debug(1, "Started in Airplay 2 mode!"); + + // use the config.hw_addr to generated an airplay_device_id + { + char obf[256] = {0}; + char *obfp = obf; + int i; + for (i = 0; i < 6; i++) { + snprintf(obfp, 4, "%02X:", config.hw_addr[i]); + obfp += 3; + } + obfp -= 1; + *obfp = 0; + config.airplay_device_id = strdup(obf); + } + debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); // now generate a UUID // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c @@ -2014,12 +2034,13 @@ int main(int argc, char **argv) { debug(1, "loudness is %d.", config.loudness); debug(1, "loudness reference level is %f", config.loudness_reference_volume_db); - uint8_t ap_md5[16]; - #ifdef CONFIG_SOXR pthread_create(&soxr_time_check_thread, NULL, &soxr_time_check, NULL); #endif +/* + uint8_t ap_md5[16]; + #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); @@ -2047,7 +2068,10 @@ int main(int argc, char **argv) { md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); md5_finish(&tctx, ap_md5); #endif + memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr)); +*/ + #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary #endif From 9d9c51d76513cd891cf63781ce2e5a9489bbfbde Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 24 May 2021 13:55:07 +0100 Subject: [PATCH 143/878] Removed .conf file. --- docker/Dockerfile | 1 - docker/docker-compose.yaml | 4 ++-- docker/shairport-sync.conf | 9 --------- 3 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 docker/shairport-sync.conf diff --git a/docker/Dockerfile b/docker/Dockerfile index 17ed2451..d7df1481 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -130,7 +130,6 @@ RUN adduser -D shairport-sync -G shairport-sync # Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio -COPY ./docker/shairport-sync.conf /etc/shairport-sync.conf COPY ./docker/start.sh / RUN chmod +x ./start.sh diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 4153577a..672639c9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -8,5 +8,5 @@ services: restart: unless-stopped devices: - "/dev/snd" - volumes: - - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf \ No newline at end of file + # volumes: + # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. \ No newline at end of file diff --git a/docker/shairport-sync.conf b/docker/shairport-sync.conf deleted file mode 100644 index 49338be1..00000000 --- a/docker/shairport-sync.conf +++ /dev/null @@ -1,9 +0,0 @@ -# Temp sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC. -alsa = -{ - output_device = "hw:0"; - # Could also be hw:1. aplay -l should help identify the correct one. - # e.g. card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones] - # card x where x is the number to use above in hw:x - mixer_control_name = "Headphone"; -}; \ No newline at end of file From 26ce13d51180f1f613561e330f5861b3ed08a657 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 26 May 2021 16:48:25 +0100 Subject: [PATCH 144/878] a slightly more rational clock handling regime, I hope. Wait for 400 ms when a clock becomes master. Wait five seconds when you get a new master clock. --- player.h | 6 ++-- ptp-utilities.c | 17 +++++++++--- rtp.c | 74 +++++++++++++++++++++++++++---------------------- rtsp.c | 2 +- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/player.h b/player.h index cc4dda2c..2a606a0e 100644 --- a/player.h +++ b/player.h @@ -209,7 +209,7 @@ typedef struct { int fix_volume; double initial_airplay_volume; int initial_airplay_volume_set; - + uint32_t timestamp_epoch, last_timestamp, maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2 // or 1. @@ -270,6 +270,7 @@ typedef struct { // this is what connects an rtp timestamp to the remote time int anchor_remote_info_is_valid; + int anchor_clock_is_new; uint64_t anchor_clock; uint64_t anchor_time; // this is the time according to the clock uint32_t anchor_rtptime; @@ -287,7 +288,8 @@ typedef struct { pthread_t rtp_buffered_audio_thread; int last_anchor_info_is_valid; - uint64_t last_anchor_clock_offset; + uint32_t last_anchor_rtptime; + uint64_t last_anchor_local_time; uint64_t last_anchor_time_of_update; uint64_t last_anchor_clock; diff --git a/ptp-utilities.c b/ptp-utilities.c index 6b6ffd1a..61041bd8 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -74,10 +74,19 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { // assuming a clock id can not be zero if (nqptp_data.master_clock_id != 0) { - if (actual_clock_id != NULL) - *actual_clock_id = nqptp_data.master_clock_id; - if (raw_offset != NULL) - *raw_offset = nqptp_data.local_to_master_time_offset; + // find out how long it's been since it became master + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_of_mastership = time_now - nqptp_data.master_clock_start_time; + // if (0) { + if (time_of_mastership < 700000000) { + // should get between 3 and 4 pings in this time -- less likely to get a bad start + response = clock_no_master; // not ready yet + } else { + if (actual_clock_id != NULL) + *actual_clock_id = nqptp_data.master_clock_id; + if (raw_offset != NULL) + *raw_offset = nqptp_data.local_to_master_time_offset; + } } else { response = clock_no_master; } diff --git a/rtp.c b/rtp.c index 90362752..4d5ba055 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1209,8 +1209,10 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { - if (conn->anchor_clock != clock_id) + if (conn->anchor_clock != clock_id) { debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); + conn->anchor_clock_is_new = 1; + } conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -1225,7 +1227,6 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { - uint64_t actual_clock_id, actual_offset; int response = ptp_get_clock_info(&actual_clock_id, &actual_offset); @@ -1233,36 +1234,38 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (conn->anchor_remote_info_is_valid != 0) { // i.e. if we have anchor clock ID and anchor time / rtptime if (actual_clock_id == conn->anchor_clock) { - conn->last_anchor_clock_offset = actual_offset; + // if the master clock and the anchor clock are the same + conn->last_anchor_clock = conn->anchor_clock; + conn->last_anchor_rtptime = conn->anchor_rtptime; + conn->last_anchor_local_time = conn->anchor_time - actual_offset; conn->last_anchor_time_of_update = get_absolute_time_in_ns(); conn->last_anchor_info_is_valid = 1; - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + if (conn->anchor_clock_is_new != 0) + debug(1,"new anchor recognised"); + conn->anchor_clock_is_new = 0; } else { - if (conn->last_anchor_info_is_valid != 0) { + // the anchor clock and the actual clock are different + // this could happen because + // the master clock has changed or + // because the anchor clock has changed + + // so, if the anchor has not changed, it must be that the master clock has changed + if (conn->anchor_clock_is_new != 0) + debug(1,"anchor has changed"); + + if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { debug(1, "change master clock to %" PRIx64 ".", actual_clock_id); - uint64_t new_anchor_time = conn->anchor_time; - new_anchor_time = new_anchor_time - conn->last_anchor_clock_offset; // to local - new_anchor_time = new_anchor_time + actual_offset; // to the next clock - conn->anchor_time = new_anchor_time; + // here we adjust the time of the anchor rtptime + // we know its local time, so we use the new clocks's offset to + // calculate what time that must be on the new clock + conn->anchor_time = conn->last_anchor_local_time + actual_offset; conn->anchor_clock = actual_clock_id; - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - actual_offset; - } else { - if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; - if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; } } else { - response = clock_not_valid; // no current clock information and no previous clock info + response = clock_not_valid; // no current clock information and no previous clock info } } } else { @@ -1313,11 +1316,12 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->clock_status = response; } - if ((response != clock_ok) && (conn->last_anchor_info_is_valid != 0)) { + if (conn->last_anchor_info_is_valid != 0) { if (anchorRTP != NULL) - *anchorRTP = conn->anchor_rtptime; + *anchorRTP = conn->last_anchor_rtptime; if (anchorLocalTime != NULL) - *anchorLocalTime = conn->anchor_time - conn->last_anchor_clock_offset; + *anchorLocalTime = conn->last_anchor_local_time; + response = clock_ok; } return response; @@ -2137,8 +2141,12 @@ void *rtp_buffered_audio_processor(void *arg) { if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(2, "Connection %d: buffered audio lead time is %f seconds.", conn->connection_number, + debug(1, "Connection %d: buffered audio starting lead time is %f seconds.", conn->connection_number, 0.000000001 * lead_time); + //else + // debug(1, "Connection %d: buffered audio lead time is %f seconds.", conn->connection_number, + // 0.000000001 * lead_time); + streaming_has_started = 1; player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); diff --git a/rtsp.c b/rtsp.c index ace0e98f..7ae9fc7b 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1298,7 +1298,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: FLUSHBUFFERED %s :: Content-Length %d", conn->connection_number, req->path, req->contentlength); - debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); + debug_log_rtsp_message(1, "FLUSHBUFFERED request", req); uint64_t flushUntilSeq = 0; uint64_t flushUntilTS = 0; From 637c8132e9d1ecac5ad2341569fed29e5982af1f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 26 May 2021 18:12:45 +0100 Subject: [PATCH 145/878] Move initial delay when using a new bus master to rtp.c and wait, if possible, whenever a new master comes along, to allow it to settle before using it. --- player.h | 3 ++- ptp-utilities.c | 21 +++++++-------------- ptp-utilities.h | 2 +- rtp.c | 43 +++++++++++++++++++++++++++++-------------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/player.h b/player.h index 2a606a0e..a7759c93 100644 --- a/player.h +++ b/player.h @@ -51,7 +51,8 @@ typedef enum { clock_no_master, clock_version_mismatch, clock_not_synchronised, - clock_not_valid + clock_not_valid, + clock_not_ready, } clock_status_t; typedef uint16_t seq_t; diff --git a/ptp-utilities.c b/ptp-utilities.c index 61041bd8..32225767 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -60,7 +60,7 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { return response; } -int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 10000, 1); if (actual_clock_id != NULL) @@ -74,19 +74,12 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset) { if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { // assuming a clock id can not be zero if (nqptp_data.master_clock_id != 0) { - // find out how long it's been since it became master - uint64_t time_now = get_absolute_time_in_ns(); - int64_t time_of_mastership = time_now - nqptp_data.master_clock_start_time; - // if (0) { - if (time_of_mastership < 700000000) { - // should get between 3 and 4 pings in this time -- less likely to get a bad start - response = clock_no_master; // not ready yet - } else { - if (actual_clock_id != NULL) - *actual_clock_id = nqptp_data.master_clock_id; - if (raw_offset != NULL) - *raw_offset = nqptp_data.local_to_master_time_offset; - } + if (actual_clock_id != NULL) + *actual_clock_id = nqptp_data.master_clock_id; + if (raw_offset != NULL) + *raw_offset = nqptp_data.local_to_master_time_offset; + if (mastership_start_time != NULL) + *mastership_start_time = nqptp_data.master_clock_start_time; } else { response = clock_no_master; } diff --git a/ptp-utilities.h b/ptp-utilities.h index 11a52c8f..1a7349fc 100644 --- a/ptp-utilities.h +++ b/ptp-utilities.h @@ -29,7 +29,7 @@ #include "config.h" #include -int ptp_get_clock_info(uint64_t *clock_id, uint64_t *offset); +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, uint64_t *mastership_start_time); void ptp_send_control_message_string(const char *msg); diff --git a/rtp.c b/rtp.c index 4d5ba055..d4f1e06a 100644 --- a/rtp.c +++ b/rtp.c @@ -1210,7 +1210,7 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { if (conn->anchor_clock != clock_id) { - debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); + debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; } conn->anchor_remote_info_is_valid = 1; @@ -1227,22 +1227,34 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { - uint64_t actual_clock_id, actual_offset; - int response = ptp_get_clock_info(&actual_clock_id, &actual_offset); + uint64_t actual_clock_id, actual_offset, start_of_mastership; + int response = ptp_get_clock_info(&actual_clock_id, &actual_offset, &start_of_mastership); if (response == clock_ok) { if (conn->anchor_remote_info_is_valid != 0) { // i.e. if we have anchor clock ID and anchor time / rtptime + // figure out how long the clock has been master + uint64_t time_now = get_absolute_time_in_ns(); + int64_t duration_of_mastership = time_now - start_of_mastership; + // if we have an alternative, i.e. if last anchor stuff is valid + // then we can wait for a long time to let the new master settle + // if we do not, we can wait for some different (shorter) time before + // using the master clock timing + if (actual_clock_id == conn->anchor_clock) { // if the master clock and the anchor clock are the same - conn->last_anchor_clock = conn->anchor_clock; - conn->last_anchor_rtptime = conn->anchor_rtptime; - conn->last_anchor_local_time = conn->anchor_time - actual_offset; - conn->last_anchor_time_of_update = get_absolute_time_in_ns(); - conn->last_anchor_info_is_valid = 1; - if (conn->anchor_clock_is_new != 0) - debug(1,"new anchor recognised"); - conn->anchor_clock_is_new = 0; + if (duration_of_mastership < 700000000) { + response = clock_not_ready; + } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { + conn->last_anchor_clock = conn->anchor_clock; + conn->last_anchor_rtptime = conn->anchor_rtptime; + conn->last_anchor_local_time = conn->anchor_time - actual_offset; + conn->last_anchor_time_of_update = get_absolute_time_in_ns(); + conn->last_anchor_info_is_valid = 1; + if (conn->anchor_clock_is_new != 0) + debug(1,"Connection %d: New anchor clock %" PRIx64 " recognised.", conn->connection_number, conn->anchor_clock); + conn->anchor_clock_is_new = 0; + } } else { // the anchor clock and the actual clock are different // this could happen because @@ -1251,13 +1263,13 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // so, if the anchor has not changed, it must be that the master clock has changed if (conn->anchor_clock_is_new != 0) - debug(1,"anchor has changed"); + debug(1,"Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 ".", conn->connection_number, conn->anchor_clock, actual_clock_id); if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { - debug(1, "change master clock to %" PRIx64 ".", actual_clock_id); + debug(1, "Connection %d: Master clock has changed to %" PRIx64 ".", conn->connection_number, actual_clock_id); // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to // calculate what time that must be on the new clock @@ -1277,9 +1289,12 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP new master clock % " PRIx64 ".", conn->connection_number, + debug(2, "Connection %d: NQPTP new master clock %" PRIx64 ".", conn->connection_number, actual_clock_id); break; + case clock_not_ready: + debug(2, "Connection %d: NQPTP master clock %" PRIx64 " is available but not ready.", conn->connection_number, actual_clock_id); + break; case clock_service_unavailable: debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); warn("Can't access the NQPTP clock. Is NQPTP running?"); From f4ba81ec5ffefddd4abc817dfdfdf55d9a45ef25 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 26 May 2021 19:36:31 +0100 Subject: [PATCH 146/878] remove redundant item --- player.h | 1 - rtp.c | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/player.h b/player.h index a7759c93..35a3db6e 100644 --- a/player.h +++ b/player.h @@ -292,7 +292,6 @@ typedef struct { uint32_t last_anchor_rtptime; uint64_t last_anchor_local_time; uint64_t last_anchor_time_of_update; - uint64_t last_anchor_clock; ssize_t ap2_audio_buffer_size; int ap2_flush_requested; diff --git a/rtp.c b/rtp.c index d4f1e06a..d7ad5472 100644 --- a/rtp.c +++ b/rtp.c @@ -1243,10 +1243,12 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (actual_clock_id == conn->anchor_clock) { // if the master clock and the anchor clock are the same + // wait at least this time before using the new master clock if (duration_of_mastership < 700000000) { response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { - conn->last_anchor_clock = conn->anchor_clock; + // use the master clock if it's at least this old or we have no alternative + // and at least it is the minimum age. conn->last_anchor_rtptime = conn->anchor_rtptime; conn->last_anchor_local_time = conn->anchor_time - actual_offset; conn->last_anchor_time_of_update = get_absolute_time_in_ns(); From e258cca33aed5ed2699d77bce1c0285bfc572dce Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 28 May 2021 19:16:37 +0100 Subject: [PATCH 147/878] recognise two types of flush -- one with from and until information, the other with just until information. A flush with "with" information is not followed by a new set anchor, whereas a flush without the " with" information is alwasy followed by a new setanchor. --- player.h | 7 +++++-- rtp.c | 59 +++++++++++++++++++++++++++++++++++++++----------------- rtsp.c | 39 +++++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/player.h b/player.h index 35a3db6e..2b5774a7 100644 --- a/player.h +++ b/player.h @@ -295,8 +295,11 @@ typedef struct { ssize_t ap2_audio_buffer_size; int ap2_flush_requested; - uint32_t ap2_flush_rtp_timestamp; - uint32_t ap2_flush_sequence_number; + int ap2_flush_from_valid; + uint32_t ap2_flush_from_rtp_timestamp; + uint32_t ap2_flush_from_sequence_number; + uint32_t ap2_flush_until_rtp_timestamp; + uint32_t ap2_flush_until_sequence_number; int ap2_rate; // protect with flush mutex, 0 means don't play, 1 means play int ap2_play_enabled; // protect with flush mutex diff --git a/rtp.c b/rtp.c index d7ad5472..3e1be921 100644 --- a/rtp.c +++ b/rtp.c @@ -1213,6 +1213,7 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; } + debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, rtptime, networktime); conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -2056,18 +2057,41 @@ void *rtp_buffered_audio_processor(void *arg) { int flush_newly_requested = 0; // are we in in flush mode, or just about to leave it? debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! - uint32_t flushUntilSeq = conn->ap2_flush_sequence_number; - uint32_t flushUntilTS = conn->ap2_flush_rtp_timestamp; - // if we are in flush mode + uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; + uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; + + int flush_request_active = 0; if (conn->ap2_flush_requested) { + if (conn->ap2_flush_from_valid == 0) {// i.e. a flush from right now + flush_request_active = 1; + } else { + int32_t blocks_to_start_of_flush = conn->ap2_flush_from_sequence_number - seq_no; + if (blocks_to_start_of_flush <= 0) { + flush_request_active = 1; + // debug(1, "delayed flush request going active"); + } + } + } + // if we are in flush mode + if (flush_request_active) { if (flush_requested == 0) { // here, a flush has been newly requested - debug(1, - "flush requested. flushUntilSeq = %u, flushUntilTS = %u, seq_no: %u, " - "pcm_buffer_read_point_rtptime = " - "%u, buffer_size: %u.", - flushUntilSeq, flushUntilTS, seq_no, pcm_buffer_read_point_rtptime, - buffered_audio->buffer_occupancy); + debug(1,"Flush requested:"); + if (conn->ap2_flush_from_valid) { + debug(1," fromTS: %u", conn->ap2_flush_from_rtp_timestamp); + debug(1," fromSeq: %u", conn->ap2_flush_from_sequence_number); + debug(1,"--"); + } + debug(1," untilTS: %u", conn->ap2_flush_until_rtp_timestamp); + debug(1," untilSeq: %u", conn->ap2_flush_until_sequence_number); + debug(1,"--"); + debug(1," currentTS_Start: %u", pcm_buffer_read_point_rtptime); + uint32_t fib = (pcm_buffer_occupancy - pcm_buffer_read_point) / 4; + debug(1," framesInBuffer: %u", fib); + uint32_t endTS = fib + pcm_buffer_read_point_rtptime; + debug(1," currentTS_End: %u", endTS); // a frame occupies 4 bytes + debug(1," currentSeq: %u", seq_no); + last_block_flushed_rtp_time = pcm_buffer_read_point_rtptime; last_block_flushed_seq_no = seq_no; flush_newly_requested = 1; @@ -2096,22 +2120,19 @@ void *rtp_buffered_audio_processor(void *arg) { pcm_buffer_read_point_rtptime = flushUntilTS; } conn->ap2_flush_requested = 0; + flush_request_active = 0; } else { if (seq_no > flushUntilSeq) { debug(1, "we have overshot the flush ending block. flushUntilSeq is %u, this block is: %u.", flushUntilSeq, seq_no); conn->ap2_flush_requested = 0; // but conn->ap2_play_enabled will remain at zero + flush_request_active = 0; } } } - flush_requested = conn->ap2_flush_requested; - - // this isn't always the case, i.e. that play is re-enabled after a flush - // if (flush_requested) - // conn->ap2_play_enabled = 0; - // play is disabled until an anchor time is sent and play is restarted - + flush_requested = flush_request_active; + // flush_requested = conn->ap2_flush_requested; int play_enabled = conn->ap2_play_enabled; debug_mutex_unlock(&conn->flush_mutex, 3); @@ -2237,6 +2258,7 @@ void *rtp_buffered_audio_processor(void *arg) { */ seq_no = packet[1] * (1 << 16) + packet[2] * (1 << 8) + packet[3]; uint32_t timestamp = nctohl(&packet[4]); + // debug(1, "immediately: block %u, rtptime %u", seq_no, timestamp); // uint32_t ssrc = nctohl(&packet[8]); // uint8_t marker = 0; // uint8_t payload_type = 0; @@ -2266,13 +2288,14 @@ void *rtp_buffered_audio_processor(void *arg) { if ((flush_requested) && (seq_no < flushUntilSeq)) { last_block_flushed_seq_no = seq_no; last_block_flushed_rtp_time = timestamp; - // debug(1, "flushing block %u, rtptime %u up to block %u, rtptime %u.", seq_no, - // timestamp, flushUntilSeq, flushUntilTS); + debug(1, "flush block %u, rtptime %u", seq_no, timestamp); } if (((flush_requested) && (seq_no == flushUntilSeq)) || (new_buffer_needed)) { if ((flush_requested) && (seq_no == flushUntilSeq)) { + if (conn->ap2_flush_from_valid != 0) + debug(1, "also flush block %u, rtptime %u", seq_no, timestamp); int32_t timestamp_difference = timestamp - flushUntilTS; debug(1, "end of flush reached. flushUntilSeq = %u, flushUntilTS = %u, last block flushed " diff --git a/rtsp.c b/rtsp.c index 7ae9fc7b..24d8ea14 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1300,11 +1300,35 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message req->path, req->contentlength); debug_log_rtsp_message(1, "FLUSHBUFFERED request", req); + uint64_t flushFromSeq = 0; + uint64_t flushFromTS = 0; uint64_t flushUntilSeq = 0; uint64_t flushUntilTS = 0; + int flushFromValid = 0; plist_t messagePlist = plist_from_rtsp_content(req); + plist_t item = plist_dict_get_item(messagePlist, "flushFromSeq"); + if (item == NULL) { + debug(2, "Can't find a flushFromSeq"); + } else { + flushFromValid = 1; + plist_get_uint_val(item, &flushFromSeq); + debug(2, "flushFromSeq is %" PRId64 ".", flushFromSeq); + } - plist_t item = plist_dict_get_item(messagePlist, "flushUntilSeq"); + item = plist_dict_get_item(messagePlist, "flushFromTS"); + if (item == NULL) { + if (conn->ap2_flush_from_valid != 0) + debug(1, "flushFromSeq without flushFromTS!"); + else + debug(2, "Can't find a flushFromTS"); + } else { + plist_get_uint_val(item, &flushFromTS); + if (flushFromValid == 0) + debug(1,"flushFromTS without flushFromSeq!"); + debug(2, "flushFromTS is %" PRId64 ".", flushFromTS); + } + + item = plist_dict_get_item(messagePlist, "flushUntilSeq"); if (item == NULL) { debug(1, "Can't find the flushUntilSeq"); } else { @@ -1321,8 +1345,15 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } debug_mutex_lock(&conn->flush_mutex, 1000, 1); - conn->ap2_flush_sequence_number = flushUntilSeq; - conn->ap2_flush_rtp_timestamp = flushUntilTS; + conn->ap2_flush_from_valid = flushFromValid; + // a flush with from... components will not be followed by a setanchoe (i.e. a play) + // if it's a flush that will be followed by a setanchor (i.e. a play) then stop play now. + if (flushFromValid == 0) + conn->ap2_play_enabled = 0; + conn->ap2_flush_from_sequence_number = flushFromSeq; + conn->ap2_flush_from_rtp_timestamp = flushFromTS; + conn->ap2_flush_until_sequence_number = flushUntilSeq; + conn->ap2_flush_until_rtp_timestamp = flushUntilTS; conn->ap2_flush_requested = 1; debug_mutex_unlock(&conn->flush_mutex, 3); @@ -3788,7 +3819,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { rtsp_message *req, *resp; #ifdef CONFIG_AIRPLAY_2 - conn->ap2_audio_buffer_size = 1024 * 1024 * 8; + conn->ap2_audio_buffer_size = 1024 * 1024 * 128; #endif while (conn->stop == 0) { From 2986b282d3139d4a22c472755c7f08f82aeccd96 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 28 May 2021 22:20:11 +0100 Subject: [PATCH 148/878] Does pretty well with flushing. Sometimes misses a flush on a repeat one. --- player.c | 11 +++--- rtp.c | 100 +++++++++++++++---------------------------------------- rtsp.c | 4 +-- 3 files changed, 35 insertions(+), 80 deletions(-) diff --git a/player.c b/player.c index efa40c63..a1bd0f40 100644 --- a/player.c +++ b/player.c @@ -924,11 +924,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // if the first_packet_timestamp is zero, don't check int flush_needed = 0; int drop_request = 0; - if ((conn->flush_requested == 1) && (conn->flush_rtp_timestamp == 0)) { + if (conn->flush_requested == 1) { + if (conn->flush_rtp_timestamp == 0) { debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); flush_needed = 1; drop_request = 1; - } else if (conn->flush_rtp_timestamp != 0) { + } else { if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); @@ -992,9 +993,11 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { "flush request: flush frame %u -- buffer not synced or empty: synced: %d, ab_read: " "%u, ab_write: %u", conn->flush_rtp_timestamp, conn->ab_synced, conn->ab_read, conn->ab_write); + conn->flush_requested = 0; // remove the request // leave flush request pending and don't do a buffer flush, because there isn't one } } + } if (flush_needed) { debug(2, "flush request: flush done."); ab_resync(conn); // no cancellation points @@ -1974,7 +1977,7 @@ void *player_thread_func(void *arg) { #endif pthread_setcancelstate(oldState, NULL); - + double initial_volume = config.airplay_volume; // default if (conn->initial_airplay_volume_set) // if we have been given an initial volume initial_volume = conn->initial_airplay_volume; @@ -3046,8 +3049,8 @@ void player_full_flush(rtsp_conn_info *conn) { abuf = conn->audio_buffer + BUFIDX(last_seqno_written); } while ((abuf->ready == 0) && (last_seqno_written != conn->ab_read)); if ((abuf != NULL) && (abuf->ready != 0)) { - debug(2, "full flush needed"); rtpTimestamp = abuf->given_timestamp + abuf->length + 1; + debug(2, "full flush needed to %u", rtpTimestamp); flush_needed = 1; } else { debug(2, "full flush not needed"); diff --git a/rtp.c b/rtp.c index 3e1be921..465f25b5 100644 --- a/rtp.c +++ b/rtp.c @@ -1213,7 +1213,7 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; } - debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, rtptime, networktime); + // debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, rtptime, networktime); conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -2033,12 +2033,9 @@ void *rtp_buffered_audio_processor(void *arg) { uint8_t *pcm_audio = NULL; // the S16 output unsigned char *data_to_process; ssize_t data_remaining; - uint32_t seq_no, last_block_flushed_seq_no; // audio packet number - uint32_t previous_seq_no = 0; + uint32_t seq_no; // audio packet number + // uint32_t previous_seq_no = 0; int new_buffer_needed; - - uint32_t last_block_flushed_rtp_time; - ssize_t nread; int finished = 0; @@ -2058,7 +2055,7 @@ void *rtp_buffered_audio_processor(void *arg) { // are we in in flush mode, or just about to leave it? debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; - uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; + // uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; int flush_request_active = 0; if (conn->ap2_flush_requested) { @@ -2076,6 +2073,7 @@ void *rtp_buffered_audio_processor(void *arg) { if (flush_request_active) { if (flush_requested == 0) { // here, a flush has been newly requested + /* debug(1,"Flush requested:"); if (conn->ap2_flush_from_valid) { debug(1," fromTS: %u", conn->ap2_flush_from_rtp_timestamp); @@ -2091,44 +2089,17 @@ void *rtp_buffered_audio_processor(void *arg) { uint32_t endTS = fib + pcm_buffer_read_point_rtptime; debug(1," currentTS_End: %u", endTS); // a frame occupies 4 bytes debug(1," currentSeq: %u", seq_no); - - last_block_flushed_rtp_time = pcm_buffer_read_point_rtptime; - last_block_flushed_seq_no = seq_no; - flush_newly_requested = 1; + */ + flush_newly_requested = 1; } - // if we have arrived at the nominated flush block - // (it must be the case that the block is in the pcm buffer) - if ((blocks_read != 0) && (seq_no == flushUntilSeq)) { - // we may need to flush the pcm_buffer up to flushUntilTS - int32_t flush_difference = flushUntilTS - pcm_buffer_read_point_rtptime; - if (flush_difference < 0) { - debug(1, "flushUntilTS is before the flushUntilSeq's start TS"); - } else if (flush_difference > pcm_buffer_occupancy) { - debug(1, - "flushUntilTS is after the flushUntilSeq's last TS. flushUntilTS = %u, " - "pcm_buffer_read_point_rtptime = %u, pcm_buffer_occupancy = %u.", - flushUntilTS, pcm_buffer_read_point_rtptime, pcm_buffer_occupancy); - } else { - // move the buffer contents from flush_difference to pcm_buffer_occupancy - // to the front of the pcm_buffer - memcpy(pcm_buffer, pcm_buffer + flush_difference * 4, - pcm_buffer_occupancy - flush_difference * 4); - pcm_buffer_occupancy = pcm_buffer_occupancy - flush_difference * 4; - pcm_buffer_read_point = 0; - debug(1, "set pcm_buffer_read_point_rtptime from %u to %u.", - pcm_buffer_read_point_rtptime, flushUntilTS); - pcm_buffer_read_point_rtptime = flushUntilTS; - } + // blocks_read to ensure seq_no is valid + if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { + // we have reached or overshot the flushUntilSeq block + if (flushUntilSeq != seq_no) + debug(1,"flushUntilSeq %u reached or overshot at %u.", flushUntilSeq, seq_no); conn->ap2_flush_requested = 0; flush_request_active = 0; - } else { - if (seq_no > flushUntilSeq) { - debug(1, - "we have overshot the flush ending block. flushUntilSeq is %u, this block is: %u.", - flushUntilSeq, seq_no); - conn->ap2_flush_requested = 0; // but conn->ap2_play_enabled will remain at zero - flush_request_active = 0; - } + flush_newly_requested = 0; } } flush_requested = flush_request_active; @@ -2138,7 +2109,6 @@ void *rtp_buffered_audio_processor(void *arg) { // do this outside the flush mutex if (flush_newly_requested) { - player_full_flush(conn); streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; @@ -2151,7 +2121,7 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); - double reqested_lead_time = 0.050; // + double reqested_lead_time = 0.10; // if (player_buffer_occupancy > ((reqested_lead_time + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again @@ -2179,11 +2149,8 @@ void *rtp_buffered_audio_processor(void *arg) { if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(1, "Connection %d: buffered audio starting lead time is %f seconds.", conn->connection_number, + debug(3, "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", conn->connection_number, pcm_buffer_read_point_rtptime, 0.000000001 * lead_time); - //else - // debug(1, "Connection %d: buffered audio lead time is %f seconds.", conn->connection_number, - // 0.000000001 * lead_time); streaming_has_started = 1; player_put_packet(0, 0, pcm_buffer_read_point_rtptime, @@ -2263,7 +2230,9 @@ void *rtp_buffered_audio_processor(void *arg) { // uint8_t marker = 0; // uint8_t payload_type = 0; +/* if ((blocks_read != 0) && (seq_no != previous_seq_no + 1)) { + if (previous_seq_no != 0) debug(1, "block discontinuity: from sequence number %u to sequence number %u.", previous_seq_no, seq_no); @@ -2274,41 +2243,24 @@ void *rtp_buffered_audio_processor(void *arg) { pcm_buffer_read_point_rtptime, previous_seq_no, timestamp, seq_no); pcm_buffer_occupancy = 0; } + // if still playing, this is a problem -- need to reset the player if (play_enabled) player_full_flush(conn); } +*/ - previous_seq_no = seq_no; + // previous_seq_no = seq_no; // at this point, we can check if we can to flush this packet -- we won't have // to decipher it first // debug(1,"seq_no %u, timestamp %u", seq_no, timestamp); - if ((flush_requested) && (seq_no < flushUntilSeq)) { - last_block_flushed_seq_no = seq_no; - last_block_flushed_rtp_time = timestamp; - debug(1, "flush block %u, rtptime %u", seq_no, timestamp); - } - if (((flush_requested) && (seq_no == flushUntilSeq)) || (new_buffer_needed)) { if ((flush_requested) && (seq_no == flushUntilSeq)) { - if (conn->ap2_flush_from_valid != 0) - debug(1, "also flush block %u, rtptime %u", seq_no, timestamp); - int32_t timestamp_difference = timestamp - flushUntilTS; - debug(1, - "end of flush reached. flushUntilSeq = %u, flushUntilTS = %u, last block flushed " - "seq_no = %u, last block flushed rtptime = %u, next block seq_no = %u, next " - "block timestamp = " - "%u, timestamp difference: %d, buffer_size: %u.", - flushUntilSeq, flushUntilTS, last_block_flushed_seq_no, - last_block_flushed_rtp_time, seq_no, timestamp, timestamp_difference, - buffered_audio->buffer_occupancy); - debug(2, - "pcm_buffer_read_point_rtptime = %u, pcm_buffer_occupancy = %u, " - "pcm_buffer_read_point = %u", - pcm_buffer_read_point_rtptime, pcm_buffer_occupancy, pcm_buffer_read_point); + player_full_flush(conn); + debug(1,"Flush completed."); } // if we are here because of a flush request, it must be the case that @@ -2322,12 +2274,12 @@ void *rtp_buffered_audio_processor(void *arg) { if (pcm_buffer_occupancy == 0) { // they should match and the read point should be zero - if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { - debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", - pcm_buffer_read_point_rtptime, timestamp); + // if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { + // debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", + // pcm_buffer_read_point_rtptime, timestamp); pcm_buffer_read_point_rtptime = timestamp; pcm_buffer_read_point = 0; - } + //} } unsigned char nonce[12]; diff --git a/rtsp.c b/rtsp.c index 24d8ea14..b9b38c25 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1298,7 +1298,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: FLUSHBUFFERED %s :: Content-Length %d", conn->connection_number, req->path, req->contentlength); - debug_log_rtsp_message(1, "FLUSHBUFFERED request", req); + debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); uint64_t flushFromSeq = 0; uint64_t flushFromTS = 0; @@ -1317,7 +1317,7 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message item = plist_dict_get_item(messagePlist, "flushFromTS"); if (item == NULL) { - if (conn->ap2_flush_from_valid != 0) + if (flushFromValid != 0) debug(1, "flushFromSeq without flushFromTS!"); else debug(2, "Can't find a flushFromTS"); From 9cf4ef032d01861f6ab4fcdaf5119be8031e2364 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 29 May 2021 10:54:44 +0100 Subject: [PATCH 149/878] Improving the flush behaviour. --- rtp.c | 53 +++++++++++++++++++++++++++++++++++++---------------- rtsp.c | 5 ++++- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/rtp.c b/rtp.c index 465f25b5..2c59b49c 100644 --- a/rtp.c +++ b/rtp.c @@ -2046,12 +2046,17 @@ void *rtp_buffered_audio_processor(void *arg) { int pcm_buffer_occupancy = 0; int pcm_buffer_read_point = 0; // offset to where the next buffer should come from uint32_t pcm_buffer_read_point_rtptime = 0; + uint32_t expected_rtptime; uint64_t blocks_read = 0; int flush_requested = 0; + int streaming_has_started = 0; + int flush_is_delayed; do { + int flush_newly_requested = 0; + int flush_newly_complete = 0; // are we in in flush mode, or just about to leave it? debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; @@ -2061,11 +2066,13 @@ void *rtp_buffered_audio_processor(void *arg) { if (conn->ap2_flush_requested) { if (conn->ap2_flush_from_valid == 0) {// i.e. a flush from right now flush_request_active = 1; + flush_is_delayed = 0; + } else { int32_t blocks_to_start_of_flush = conn->ap2_flush_from_sequence_number - seq_no; if (blocks_to_start_of_flush <= 0) { flush_request_active = 1; - // debug(1, "delayed flush request going active"); + flush_is_delayed = 1; } } } @@ -2102,16 +2109,29 @@ void *rtp_buffered_audio_processor(void *arg) { flush_newly_requested = 0; } } + if ((flush_requested) && (flush_request_active == 0)) + flush_newly_complete = 1; flush_requested = flush_request_active; // flush_requested = conn->ap2_flush_requested; int play_enabled = conn->ap2_play_enabled; debug_mutex_unlock(&conn->flush_mutex, 3); // do this outside the flush mutex + if (flush_newly_complete) { + debug(1,"Flush Complete."); + if (flush_is_delayed == 0) + player_full_flush(conn); + } if (flush_newly_requested) { - streaming_has_started = 0; - pcm_buffer_occupancy = 0; - pcm_buffer_read_point = 0; + if (flush_is_delayed == 0) { + debug(1,"Immediate Buffered Audio Flush Started."); +// player_full_flush(conn); +// streaming_has_started = 0; + pcm_buffer_occupancy = 0; + pcm_buffer_read_point = 0; + } else { + debug(1,"Delayed Buffered Audio Flush Started."); + } } // now, if a flush is not requested, we can do the normal stuff @@ -2121,9 +2141,9 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); - double reqested_lead_time = 0.10; // + double requested_lead_time = 0.10; // if (player_buffer_occupancy > - ((reqested_lead_time + 0.1) * 44100.0 / 352)) { // must be greater than the lead time. + ((requested_lead_time + 0.10) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while @@ -2146,15 +2166,21 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - if ((lead_time >= (int64_t)(reqested_lead_time * 1000000000)) || + if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || (streaming_has_started == 1)) { if (streaming_has_started == 0) - debug(3, "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", conn->connection_number, pcm_buffer_read_point_rtptime, + debug(1, "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", conn->connection_number, pcm_buffer_read_point_rtptime, 0.000000001 * lead_time); - + else { + if (expected_rtptime != pcm_buffer_read_point_rtptime) + debug(1,"actual rtptime is %u, expected rtptime is %u.", pcm_buffer_read_point_rtptime, expected_rtptime); + } streaming_has_started = 1; + expected_rtptime = pcm_buffer_read_point_rtptime + 352; + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); + usleep(2000); } pcm_buffer_read_point_rtptime += 352; @@ -2230,9 +2256,9 @@ void *rtp_buffered_audio_processor(void *arg) { // uint8_t marker = 0; // uint8_t payload_type = 0; + /* if ((blocks_read != 0) && (seq_no != previous_seq_no + 1)) { - if (previous_seq_no != 0) debug(1, "block discontinuity: from sequence number %u to sequence number %u.", previous_seq_no, seq_no); @@ -2243,13 +2269,13 @@ void *rtp_buffered_audio_processor(void *arg) { pcm_buffer_read_point_rtptime, previous_seq_no, timestamp, seq_no); pcm_buffer_occupancy = 0; } - // if still playing, this is a problem -- need to reset the player if (play_enabled) player_full_flush(conn); } */ + // previous_seq_no = seq_no; // at this point, we can check if we can to flush this packet -- we won't have @@ -2258,11 +2284,6 @@ void *rtp_buffered_audio_processor(void *arg) { if (((flush_requested) && (seq_no == flushUntilSeq)) || (new_buffer_needed)) { - if ((flush_requested) && (seq_no == flushUntilSeq)) { - player_full_flush(conn); - debug(1,"Flush completed."); - } - // if we are here because of a flush request, it must be the case that // flushing the pcm buffer wasn't enough, as the request would have been turned off by now // so we better indicate that the pcm buffer is empty and its contents invalid diff --git a/rtsp.c b/rtsp.c index b9b38c25..2e33f609 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1357,7 +1357,10 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message conn->ap2_flush_requested = 1; debug_mutex_unlock(&conn->flush_mutex, 3); - // player_full_flush(conn); + if (flushFromValid) + debug(1,"Deferred Flush Requested"); + else + debug(1,"Immediate Flush Requested"); resp->respcode = 200; } From 7c68f97d7f1614e09b27038192bfb6fadaafd4db Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 29 May 2021 15:59:11 +0100 Subject: [PATCH 150/878] flaky but some interesting stuff --- player.c | 11 +++++------ rtp.c | 52 ++++++++++++++++++++++++++++++++++++++++++---------- rtsp.c | 2 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/player.c b/player.c index a1bd0f40..46aa9d3e 100644 --- a/player.c +++ b/player.c @@ -930,6 +930,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { flush_needed = 1; drop_request = 1; } else { + debug(1,"flush"); if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); @@ -2308,18 +2309,16 @@ void *player_thread_func(void *arg) { int64_t filler_length = (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { - debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds).", - sync_error, (sync_error * 1.0) / config.output_rate); + debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds), with frame: %u.", + sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; uint32_t frames_to_drop_sized = local_frames_to_drop; do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, "Large negative sync error of: %" PRId64 - " frames (%f seconds), with should_be_frame_32 of %" PRIu32 ", nt of %" PRId64 - " and current_delay of %" PRId64 ".", - sync_error, (sync_error * 1.0) / config.output_rate, should_be_frame_32, - inframe->given_timestamp * conn->output_sample_ratio, current_delay); + " frames (%f seconds), with frame: %" PRIu32 ".", + sync_error, (sync_error * 1.0) / config.output_rate,inframe->given_timestamp); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) silence_length = filler_length * 5; diff --git a/rtp.c b/rtp.c index 2c59b49c..6e6b3995 100644 --- a/rtp.c +++ b/rtp.c @@ -2053,6 +2053,8 @@ void *rtp_buffered_audio_processor(void *arg) { int streaming_has_started = 0; int flush_is_delayed; + uint32_t flush_from_timestamp; + int dump_block_info = 0; do { int flush_newly_requested = 0; @@ -2067,12 +2069,13 @@ void *rtp_buffered_audio_processor(void *arg) { if (conn->ap2_flush_from_valid == 0) {// i.e. a flush from right now flush_request_active = 1; flush_is_delayed = 0; - } else { + flush_is_delayed = 1; + dump_block_info = 1; + flush_from_timestamp = conn->ap2_flush_from_rtp_timestamp; int32_t blocks_to_start_of_flush = conn->ap2_flush_from_sequence_number - seq_no; if (blocks_to_start_of_flush <= 0) { flush_request_active = 1; - flush_is_delayed = 1; } } } @@ -2080,7 +2083,7 @@ void *rtp_buffered_audio_processor(void *arg) { if (flush_request_active) { if (flush_requested == 0) { // here, a flush has been newly requested - /* + debug(1,"Flush requested:"); if (conn->ap2_flush_from_valid) { debug(1," fromTS: %u", conn->ap2_flush_from_rtp_timestamp); @@ -2096,7 +2099,7 @@ void *rtp_buffered_audio_processor(void *arg) { uint32_t endTS = fib + pcm_buffer_read_point_rtptime; debug(1," currentTS_End: %u", endTS); // a frame occupies 4 bytes debug(1," currentSeq: %u", seq_no); - */ + flush_newly_requested = 1; } // blocks_read to ensure seq_no is valid @@ -2119,18 +2122,19 @@ void *rtp_buffered_audio_processor(void *arg) { // do this outside the flush mutex if (flush_newly_complete) { debug(1,"Flush Complete."); - if (flush_is_delayed == 0) - player_full_flush(conn); } if (flush_newly_requested) { if (flush_is_delayed == 0) { debug(1,"Immediate Buffered Audio Flush Started."); -// player_full_flush(conn); -// streaming_has_started = 0; + player_full_flush(conn); + streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; + dump_block_info = 0; } else { debug(1,"Delayed Buffered Audio Flush Started."); + pcm_buffer_occupancy = 0; + pcm_buffer_read_point = 0; } } @@ -2143,7 +2147,7 @@ void *rtp_buffered_audio_processor(void *arg) { // player_buffer_occupancy); double requested_lead_time = 0.10; // if (player_buffer_occupancy > - ((requested_lead_time + 0.10) * 44100.0 / 352)) { // must be greater than the lead time. + ((requested_lead_time + 0.20) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while @@ -2256,6 +2260,18 @@ void *rtp_buffered_audio_processor(void *arg) { // uint8_t marker = 0; // uint8_t payload_type = 0; +/* + if (dump_block_info != 0) { + if ((flush_requested) && (seq_no < flushUntilSeq)) + debug(1, "block %u, rtptime %u, should be skipped.", seq_no, timestamp); + else + debug(1, "block %u, rtptime %u, should be decoded and used.", seq_no, timestamp); + + if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { + debug(1, "block %u, rtptime %u, will be decoded and used.", seq_no, timestamp); + } + } +*/ /* if ((blocks_read != 0) && (seq_no != previous_seq_no + 1)) { @@ -2282,7 +2298,13 @@ void *rtp_buffered_audio_processor(void *arg) { // to decipher it first // debug(1,"seq_no %u, timestamp %u", seq_no, timestamp); - if (((flush_requested) && (seq_no == flushUntilSeq)) || (new_buffer_needed)) { + if ((flush_requested) && (seq_no >= flushUntilSeq)) { + debug(1,"flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); + } + + + if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { + // if we are here because of a flush request, it must be the case that // flushing the pcm buffer wasn't enough, as the request would have been turned off by now @@ -2382,6 +2404,16 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1,"generated %d bytes of PCM", dst_bufsize); // copy the PCM audio into the PCM buffer. // make sure it's big enough first + + // also, check it if needs to be truncated but to an impending delayed flush_is_delayed + if (flush_is_delayed) { + // see if the flush_from_timestamp is in the buffer + int32_t samples_remaining = (flush_from_timestamp - pcm_buffer_read_point_rtptime); + if ((samples_remaining > 0) && ((samples_remaining * 4) < dst_bufsize)) { + debug(1,"samples remaining before flush: %d, number of samples %d. flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", samples_remaining, dst_bufsize/4, flush_from_timestamp, pcm_buffer_read_point_rtptime); + dst_bufsize = samples_remaining * 4; + } + } if ((pcm_buffer_size - pcm_buffer_occupancy) < dst_bufsize) { debug(1, "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " diff --git a/rtsp.c b/rtsp.c index 2e33f609..00bd3822 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3822,7 +3822,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { rtsp_message *req, *resp; #ifdef CONFIG_AIRPLAY_2 - conn->ap2_audio_buffer_size = 1024 * 1024 * 128; + conn->ap2_audio_buffer_size = 1024 * 1024 * 8; #endif while (conn->stop == 0) { From 2ea28c06617040dbfc73e218d8b4c78206a65951 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 29 May 2021 21:24:33 +0100 Subject: [PATCH 151/878] Much better flushing. Delayed flushing only works for one delay. Noice artifacts removed when skipping. Pretty solid. --- player.h | 2 ++ rtp.c | 52 ++++++++++++++++++++++++++++++++++++---------------- rtsp.c | 3 ++- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/player.h b/player.h index 2b5774a7..d3f4e63c 100644 --- a/player.h +++ b/player.h @@ -385,6 +385,8 @@ typedef struct { extern pthread_mutex_t playing_conn_lock; +void reset_buffer(rtsp_conn_info *conn); + void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy, rtsp_conn_info *conn); diff --git a/rtp.c b/rtp.c index 6e6b3995..b96bb5a4 100644 --- a/rtp.c +++ b/rtp.c @@ -2052,13 +2052,15 @@ void *rtp_buffered_audio_processor(void *arg) { int flush_requested = 0; int streaming_has_started = 0; - int flush_is_delayed; + int play_enabled = 0; uint32_t flush_from_timestamp; int dump_block_info = 0; + double requested_lead_time = 0.100; // normal lead time minimum do { - + int flush_is_delayed = 0; int flush_newly_requested = 0; int flush_newly_complete = 0; + int play_newly_stopped = 0; // are we in in flush mode, or just about to leave it? debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; @@ -2105,7 +2107,7 @@ void *rtp_buffered_audio_processor(void *arg) { // blocks_read to ensure seq_no is valid if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block - if (flushUntilSeq != seq_no) + // if (flushUntilSeq != seq_no) debug(1,"flushUntilSeq %u reached or overshot at %u.", flushUntilSeq, seq_no); conn->ap2_flush_requested = 0; flush_request_active = 0; @@ -2116,23 +2118,34 @@ void *rtp_buffered_audio_processor(void *arg) { flush_newly_complete = 1; flush_requested = flush_request_active; // flush_requested = conn->ap2_flush_requested; - int play_enabled = conn->ap2_play_enabled; + if ((play_enabled) && (conn->ap2_play_enabled == 0)) + play_newly_stopped = 1; + play_enabled = conn->ap2_play_enabled; debug_mutex_unlock(&conn->flush_mutex, 3); + // do this outside the flush mutex if (flush_newly_complete) { debug(1,"Flush Complete."); + blocks_read = 0; } + + if (play_newly_stopped != 0) + reset_buffer(conn); // stop play ASAP + if (flush_newly_requested) { + reset_buffer(conn); + if (flush_is_delayed == 0) { debug(1,"Immediate Buffered Audio Flush Started."); - player_full_flush(conn); + //player_full_flush(conn); streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; dump_block_info = 0; } else { debug(1,"Delayed Buffered Audio Flush Started."); + streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; } @@ -2145,9 +2158,8 @@ void *rtp_buffered_audio_processor(void *arg) { get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); - double requested_lead_time = 0.10; // if (player_buffer_occupancy > - ((requested_lead_time + 0.20) * 44100.0 / 352)) { // must be greater than the lead time. + ((requested_lead_time + 0.10) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while @@ -2170,22 +2182,23 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); + if (blocks_read > 1) { if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || - (streaming_has_started == 1)) { + (streaming_has_started != 0)) { if (streaming_has_started == 0) debug(1, "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", conn->connection_number, pcm_buffer_read_point_rtptime, 0.000000001 * lead_time); - else { - if (expected_rtptime != pcm_buffer_read_point_rtptime) - debug(1,"actual rtptime is %u, expected rtptime is %u.", pcm_buffer_read_point_rtptime, expected_rtptime); - } - streaming_has_started = 1; + //else { + //if (expected_rtptime != pcm_buffer_read_point_rtptime) + // debug(1,"actual rtptime is %u, expected rtptime is %u.", pcm_buffer_read_point_rtptime, expected_rtptime); + //} expected_rtptime = pcm_buffer_read_point_rtptime + 352; - player_put_packet(0, 0, pcm_buffer_read_point_rtptime, - pcm_buffer + pcm_buffer_read_point, 352, conn); + pcm_buffer + pcm_buffer_read_point, 352, conn); + streaming_has_started++; usleep(2000); } + } pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * 4; @@ -2299,10 +2312,17 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1,"seq_no %u, timestamp %u", seq_no, timestamp); if ((flush_requested) && (seq_no >= flushUntilSeq)) { + uint64_t should_be_time; + if ((frame_to_local_time(timestamp, &should_be_time, conn) == + 0) && (play_enabled)) { + // play enabled will be off when this is a full flush and the anchor information is not valid + int64_t lead_time = should_be_time - get_absolute_time_in_ns(); + debug(1,"flush completed to seq: %u with rtptime: %u, lead time: 0x%" PRIx64 " nanoseconds, i.e. %f sec.", seq_no, timestamp, lead_time, lead_time * 0.000000001); + } else { debug(1,"flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); + } } - if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { diff --git a/rtsp.c b/rtsp.c index 00bd3822..3ef47ba1 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1350,6 +1350,7 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message // if it's a flush that will be followed by a setanchor (i.e. a play) then stop play now. if (flushFromValid == 0) conn->ap2_play_enabled = 0; + conn->ap2_flush_from_sequence_number = flushFromSeq; conn->ap2_flush_from_rtp_timestamp = flushFromTS; conn->ap2_flush_until_sequence_number = flushUntilSeq; @@ -1444,7 +1445,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(1, "Connection %d: No timing peer list!", conn->connection_number); } } else { - player_full_flush(conn); + // player_full_flush(conn); // ptp_send_control_message_string("T"); // ensure an obsolete clock isn't picked up later. } } From a109b587ece049c1b2b26e99ac3d91c580832b2c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 30 May 2021 11:17:06 +0100 Subject: [PATCH 152/878] Flush seems to be working properly and reliably. Some audio glitches removed too by dropping the first two AAC blocks of any passage. Crude. Flush diagnostic linked list stuff left in place. --- common.c | 14 ++- common.h | 9 +- mdns.c | 6 +- mdns.h | 3 +- mdns_avahi.c | 31 +++---- player.c | 135 +++++++++++++++-------------- player.h | 22 ++--- ptp-utilities.c | 3 +- ptp-utilities.h | 3 +- rtp.c | 222 ++++++++++++++++++++++++++---------------------- rtsp.c | 125 +++++++++++++++++++++------ shairport.c | 54 ++++++------ 12 files changed, 363 insertions(+), 264 deletions(-) diff --git a/common.c b/common.c index 4bdb78ab..f87b7d29 100644 --- a/common.c +++ b/common.c @@ -93,8 +93,6 @@ #include #endif - - #ifdef CONFIG_ALSA void set_alsa_out_dev(char *); #endif @@ -1611,13 +1609,13 @@ char *get_version_string() { char *version_string = malloc(1024); if (version_string) { #ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') - strcpy(version_string, git_version_string); - else + if (git_version_string[0] != '\0') + strcpy(version_string, git_version_string); + else #endif - strcpy(version_string, PACKAGE_VERSION); + strcpy(version_string, PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 - strcat(version_string, "-AirPlay2"); + strcat(version_string, "-AirPlay2"); #endif #ifdef CONFIG_APPLE_ALAC strcat(version_string, "-alac"); @@ -1945,7 +1943,7 @@ int get_device_id(uint8_t *id, int int_length) { struct ifaddrs *ifa = NULL; int i = 0; uint8_t *t = id; - for (i = 0; i < int_length ; i++) { + for (i = 0; i < int_length; i++) { *t++ = 0; } diff --git a/common.h b/common.h index 48769527..26e9e17d 100644 --- a/common.h +++ b/common.h @@ -210,10 +210,10 @@ typedef struct { char *configfile; char *regtype; // The regtype is the service type followed by the protocol, separated by a dot, by // default “_raop._tcp.” for AirPlay 1. - char *regtype2; // The regtype is the service type followed by the protocol, separated by a dot, by - // default “_raop._tcp.” for AirPlay 2. - char *interface; // a string containg the interface name, or NULL if nothing specified - int interface_index; // only valid if the interface string is non-NULL + char *regtype2; // The regtype is the service type followed by the protocol, separated by a dot, + // by default “_raop._tcp.” for AirPlay 2. + char *interface; // a string containg the interface name, or NULL if nothing specified + int interface_index; // only valid if the interface string is non-NULL double audio_backend_buffer_desired_length; // this will be the length in seconds of the // audio backend buffer -- the DAC buffer for ALSA double audio_backend_buffer_interpolation_threshold_in_seconds; // below this, soxr interpolation @@ -498,7 +498,6 @@ int get_device_id(uint8_t *id, int int_length); extern char git_version_string[]; #endif - #endif // _COMMON_H #ifdef __cplusplus diff --git a/mdns.c b/mdns.c index 073e346c..4e44e38f 100644 --- a/mdns.c +++ b/mdns.c @@ -81,7 +81,8 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { for (b = mdns_backends; *b; b++) { if (strcmp((*b)->name, config.mdns_name) != 0) // Not the one we are looking for continue; - int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, txt_records, secondary_txt_records); + int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, + txt_records, secondary_txt_records); if (error >= 0) { config.mdns = *b; } @@ -93,7 +94,8 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { } else { // default -- pick the first back end for (b = mdns_backends; *b; b++) { - int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, txt_records, secondary_txt_records); + int error = (*b)->mdns_register(ap1_service_name, config.service_name, config.port, + txt_records, secondary_txt_records); if (error >= 0) { config.mdns = *b; break; diff --git a/mdns.h b/mdns.h index 30900c78..dc6e30d9 100644 --- a/mdns.h +++ b/mdns.h @@ -17,7 +17,8 @@ void mdns_ls_backends(void); typedef struct { char *name; - int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records, char **secondary_txt_records); + int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records, + char **secondary_txt_records); void (*mdns_unregister)(void); void (*mdns_dacp_monitor_start)(); void (*mdns_dacp_monitor_set_id)(const char *); diff --git a/mdns_avahi.c b/mdns_avahi.c index 4873639c..bf24ddba 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -243,24 +243,24 @@ static void register_service(AvahiClient *c) { selected_interface = AVAHI_IF_UNSPEC; if (ap2_text_record_string_list) { ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, - ap2_service_name, config.regtype2, NULL, NULL, port, - ap2_text_record_string_list); + ap2_service_name, config.regtype2, NULL, NULL, + port, ap2_text_record_string_list); } if ((ret == 0) && (text_record_string_list)) { ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, service_name, config.regtype, NULL, NULL, port, text_record_string_list); } - if (ret == 0) { - ret = avahi_entry_group_commit(group); - debug(2, "avahi: avahi_entry_group_commit %d", ret); - if (ret < 0) - debug(1, "avahi: avahi_entry_group_commit failed"); - } else if (ret < 0) { - debug(1, "avahi: avahi_entry_group_add_service failed"); - } else { - debug(1, "avahi: unexpected positive return"); - } + if (ret == 0) { + ret = avahi_entry_group_commit(group); + debug(2, "avahi: avahi_entry_group_commit %d", ret); + if (ret < 0) + debug(1, "avahi: avahi_entry_group_commit failed"); + } else if (ret < 0) { + debug(1, "avahi: avahi_entry_group_add_service failed"); + } else { + debug(1, "avahi: unexpected positive return"); + } } } @@ -318,7 +318,8 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } -static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records,char **secondary_txt_records) { +static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records, + char **secondary_txt_records) { // debug(1, "avahi_register."); service_name = strdup(ap1name); if (ap2name != NULL) @@ -327,7 +328,8 @@ static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_ text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1); if (secondary_txt_records != NULL) - ap2_text_record_string_list = avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); + ap2_text_record_string_list = + avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); port = srvport; @@ -390,7 +392,6 @@ static void avahi_unregister(void) { } else debug(1, "avahi attempt to free NULL text_record_string_list"); - if (ap2_text_record_string_list) { debug(2, "avahi free ap_2text_record_string_list"); avahi_string_list_free(ap2_text_record_string_list); diff --git a/player.c b/player.c index 46aa9d3e..c8402524 100644 --- a/player.c +++ b/player.c @@ -925,80 +925,83 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int flush_needed = 0; int drop_request = 0; if (conn->flush_requested == 1) { - if (conn->flush_rtp_timestamp == 0) { - debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); - flush_needed = 1; - drop_request = 1; - } else { - debug(1,"flush"); - if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { - abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); - abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); - if ((firstPacket != NULL) && (firstPacket->ready)) { - // discard flushes more than 10 seconds into the future -- they are probably bogus - uint32_t first_frame_in_buffer = firstPacket->given_timestamp; - int32_t offset_from_first_frame = - (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); - if (offset_from_first_frame > (int)conn->input_rate * 10) { - debug(1, + if (conn->flush_rtp_timestamp == 0) { + debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); + flush_needed = 1; + drop_request = 1; + } else { + if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { + abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); + abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); + if ((firstPacket != NULL) && (firstPacket->ready)) { + // discard flushes more than 10 seconds into the future -- they are probably bogus + uint32_t first_frame_in_buffer = firstPacket->given_timestamp; + int32_t offset_from_first_frame = + (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); + if (offset_from_first_frame > (int)conn->input_rate * 10) { + debug( + 1, "flush request: sanity check -- flush frame %u is too far into the future from " "the first frame %u -- discarded.", conn->flush_rtp_timestamp, first_frame_in_buffer); - drop_request = 1; - } else { - if ((lastPacket != NULL) && (lastPacket->ready)) { - // we have enough information to check if the flush is needed or can be discarded - uint32_t last_frame_in_buffer = - lastPacket->given_timestamp + lastPacket->length - 1; - // now we have to work out if the flush frame is in the buffer - // if it is later than the end of the buffer, flush everything and keep the request - // active. if it is in the buffer, we need to flush part of the buffer. Actually we - // flush the entire buffer and drop the request. if it is before the buffer, no - // flush is needed. Drop the request. - if (offset_from_first_frame > 0) { - int32_t offset_to_last_frame = - (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); - if (offset_to_last_frame >= 0) { - debug(2, + drop_request = 1; + } else { + if ((lastPacket != NULL) && (lastPacket->ready)) { + // we have enough information to check if the flush is needed or can be discarded + uint32_t last_frame_in_buffer = + lastPacket->given_timestamp + lastPacket->length - 1; + // now we have to work out if the flush frame is in the buffer + // if it is later than the end of the buffer, flush everything and keep the + // request active. if it is in the buffer, we need to flush part of the buffer. + // Actually we flush the entire buffer and drop the request. if it is before the + // buffer, no flush is needed. Drop the request. + if (offset_from_first_frame > 0) { + int32_t offset_to_last_frame = + (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); + if (offset_to_last_frame >= 0) { + debug( + 2, "flush request: flush frame %u active -- buffer contains %u frames, from " "%u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); - drop_request = 1; - flush_needed = 1; + drop_request = 1; + flush_needed = 1; + } else { + debug(2, + "flush request: flush frame %u pending -- buffer contains %u frames, " + "from " + "%u to %u", + conn->flush_rtp_timestamp, + last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, + last_frame_in_buffer); + flush_needed = 1; + } } else { - debug( - 2, - "flush request: flush frame %u pending -- buffer contains %u frames, from " - "%u to %u", - conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, - first_frame_in_buffer, last_frame_in_buffer); - flush_needed = 1; + debug(2, + "flush request: flush frame %u expired -- buffer contains %u frames, " + "from %u " + "to %u", + conn->flush_rtp_timestamp, + last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, + last_frame_in_buffer); + drop_request = 1; } - } else { - debug( - 2, - "flush request: flush frame %u expired -- buffer contains %u frames, from %u " - "to %u", - conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, - first_frame_in_buffer, last_frame_in_buffer); - drop_request = 1; } } } + } else { + debug(3, + "flush request: flush frame %u -- buffer not synced or empty: synced: %d, " + "ab_read: " + "%u, ab_write: %u", + conn->flush_rtp_timestamp, conn->ab_synced, conn->ab_read, conn->ab_write); + conn->flush_requested = 0; // remove the request + // leave flush request pending and don't do a buffer flush, because there isn't one } - } else { - debug( - 3, - "flush request: flush frame %u -- buffer not synced or empty: synced: %d, ab_read: " - "%u, ab_write: %u", - conn->flush_rtp_timestamp, conn->ab_synced, conn->ab_read, conn->ab_write); - conn->flush_requested = 0; // remove the request - // leave flush request pending and don't do a buffer flush, because there isn't one } } - } if (flush_needed) { debug(2, "flush request: flush done."); ab_resync(conn); // no cancellation points @@ -1075,7 +1078,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); int64_t lateness = local_time_now - conn->first_packet_time_to_play; @@ -1980,7 +1983,7 @@ void *player_thread_func(void *arg) { pthread_setcancelstate(oldState, NULL); double initial_volume = config.airplay_volume; // default - if (conn->initial_airplay_volume_set) // if we have been given an initial volume + if (conn->initial_airplay_volume_set) // if we have been given an initial volume initial_volume = conn->initial_airplay_volume; // set the default volume to whatever it was before, as stored in the config airplay_volume debug(2, "Set initial volume to %f.", initial_volume); @@ -2243,7 +2246,7 @@ void *player_thread_func(void *arg) { // remove the bias when reporting the error to make it the true error - debug(1, + debug(2, "first frame sync error (positive --> late): %" PRId64 " frames, %.3f mS at %d frames per second output.", sync_error + first_frame_early_bias, @@ -2309,8 +2312,11 @@ void *player_thread_func(void *arg) { int64_t filler_length = (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { - debug(1, "Large positive sync error of: %" PRId64 " frames (%f seconds), with frame: %u.", - sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); + debug(1, + "Large positive sync error of: %" PRId64 + " frames (%f seconds), with frame: %u.", + sync_error, (sync_error * 1.0) / config.output_rate, + inframe->given_timestamp); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; uint32_t frames_to_drop_sized = local_frames_to_drop; do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); @@ -2318,7 +2324,8 @@ void *player_thread_func(void *arg) { debug(1, "Large negative sync error of: %" PRId64 " frames (%f seconds), with frame: %" PRIu32 ".", - sync_error, (sync_error * 1.0) / config.output_rate,inframe->given_timestamp); + sync_error, (sync_error * 1.0) / config.output_rate, + inframe->given_timestamp); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) silence_length = filler_length * 5; diff --git a/player.h b/player.h index d3f4e63c..fbffb04e 100644 --- a/player.h +++ b/player.h @@ -122,16 +122,17 @@ typedef struct { ap2_buffer plain_buf; } ap2_pairing; -/* -typedef struct file_cipher_context { - struct pair_cipher_context *cipher_context; - int active; // can be created during a pair setup but not activated until next read - int fd; - void *input_plaintext_buffer; - void *input_plaintext_buffer_toq; - size_t input_plaintext_buffer_bytes_occupied; -} file_cipher_context; -*/ +// flush requests are stored in order of flushFromSeq +// on the basis that block numbers are monotonic modulo 2^24 +typedef struct flush_request_t { + int flushNow; // if true, the flushFrom stuff is invalid + uint32_t flushFromSeq; + uint32_t flushFromTS; + uint32_t flushUntilSeq; + uint32_t flushUntilTS; + struct flush_request_t *next; +} flush_request_t; + #endif typedef struct { @@ -294,6 +295,7 @@ typedef struct { uint64_t last_anchor_time_of_update; ssize_t ap2_audio_buffer_size; + flush_request_t *flush_requests; // if non-null, there are flush requests, mutex protected int ap2_flush_requested; int ap2_flush_from_valid; uint32_t ap2_flush_from_rtp_timestamp; diff --git a/ptp-utilities.c b/ptp-utilities.c index 32225767..12a0890c 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -60,7 +60,8 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { return response; } -int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, uint64_t *mastership_start_time) { +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, + uint64_t *mastership_start_time) { int response = clock_ok; pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 10000, 1); if (actual_clock_id != NULL) diff --git a/ptp-utilities.h b/ptp-utilities.h index 1a7349fc..a0ce1604 100644 --- a/ptp-utilities.h +++ b/ptp-utilities.h @@ -29,7 +29,8 @@ #include "config.h" #include -int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, uint64_t *mastership_start_time); +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, + uint64_t *mastership_start_time); void ptp_send_control_message_string(const char *msg); diff --git a/rtp.c b/rtp.c index b96bb5a4..a4994301 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1213,7 +1213,8 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; } - // debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, rtptime, networktime); + // debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, + // rtptime, networktime); conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -1247,7 +1248,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // wait at least this time before using the new master clock if (duration_of_mastership < 700000000) { response = clock_not_ready; - } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { + } else if ((duration_of_mastership > 5000000000) || + (conn->last_anchor_info_is_valid == 0)) { // use the master clock if it's at least this old or we have no alternative // and at least it is the minimum age. conn->last_anchor_rtptime = conn->anchor_rtptime; @@ -1255,7 +1257,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_time_of_update = get_absolute_time_in_ns(); conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) - debug(1,"Connection %d: New anchor clock %" PRIx64 " recognised.", conn->connection_number, conn->anchor_clock); + debug(2, "Connection %d: New anchor clock %" PRIx64 " recognised.", + conn->connection_number, conn->anchor_clock); conn->anchor_clock_is_new = 0; } } else { @@ -1266,13 +1269,17 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // so, if the anchor has not changed, it must be that the master clock has changed if (conn->anchor_clock_is_new != 0) - debug(1,"Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 ".", conn->connection_number, conn->anchor_clock, actual_clock_id); + debug(1, + "Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 + ".", + conn->connection_number, conn->anchor_clock, actual_clock_id); if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { - debug(1, "Connection %d: Master clock has changed to %" PRIx64 ".", conn->connection_number, actual_clock_id); + debug(1, "Connection %d: Master clock has changed to %" PRIx64 ".", + conn->connection_number, actual_clock_id); // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to // calculate what time that must be on the new clock @@ -1280,7 +1287,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->anchor_clock = actual_clock_id; } } else { - response = clock_not_valid; // no current clock information and no previous clock info + response = clock_not_valid; // no current clock information and no previous clock info } } } else { @@ -1296,7 +1303,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, actual_clock_id); break; case clock_not_ready: - debug(2, "Connection %d: NQPTP master clock %" PRIx64 " is available but not ready.", conn->connection_number, actual_clock_id); + debug(2, "Connection %d: NQPTP master clock %" PRIx64 " is available but not ready.", + conn->connection_number, actual_clock_id); break; case clock_service_unavailable: debug(1, "Connection %d: NQPTP clock is not available.", conn->connection_number); @@ -1310,7 +1318,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, debug(1, "Connection %d: Can not access NQPTP clock information.", conn->connection_number); break; case clock_no_master: - debug(1, "Connection %d: No NQPTP master clock.", conn->connection_number); + debug(2, "Connection %d: No NQPTP master clock.", conn->connection_number); break; case clock_no_anchor_info: debug(1, "Connection %d: No Clock Anchor.", conn->connection_number); @@ -1922,6 +1930,37 @@ void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Buffered Audio Receiver Cleanup Done."); } +// not used right now, but potentially useful for understanding flush requests +void display_flush_requests(int activeOnly, uint32_t currentSeq, uint32_t currentTS, + rtsp_conn_info *conn) { + if (conn->flush_requests == NULL) { + if (activeOnly == 0) + debug(1, "No flush requests."); + } else { + flush_request_t *t = conn->flush_requests; + do { + if (t->flushNow) { + debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, + t->flushUntilTS); + } else { + if (activeOnly == 0) + debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, + t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); + else if ((activeOnly == 1) && + (currentSeq >= + (t->flushFromSeq - + 1))) // the -1 is because you might have to trim the end of the previous block + debug(1, + "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u, with currentSeq: %u, " + "currentTS: %u.", + t->flushFromSeq, t->flushFromTS, t->flushUntilSeq, t->flushUntilTS, currentSeq, + currentTS); + } + t = t->next; + } while (t != NULL); + } +} + void *rtp_buffered_audio_processor(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; pthread_cleanup_push(rtp_buffered_audio_cleanup_handler, arg); @@ -2046,7 +2085,7 @@ void *rtp_buffered_audio_processor(void *arg) { int pcm_buffer_occupancy = 0; int pcm_buffer_read_point = 0; // offset to where the next buffer should come from uint32_t pcm_buffer_read_point_rtptime = 0; - uint32_t expected_rtptime; + // uint32_t expected_rtptime; uint64_t blocks_read = 0; int flush_requested = 0; @@ -2054,8 +2093,8 @@ void *rtp_buffered_audio_processor(void *arg) { int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; - int dump_block_info = 0; double requested_lead_time = 0.100; // normal lead time minimum + reset_buffer(conn); // in case there is any garbage in the player do { int flush_is_delayed = 0; int flush_newly_requested = 0; @@ -2068,12 +2107,11 @@ void *rtp_buffered_audio_processor(void *arg) { int flush_request_active = 0; if (conn->ap2_flush_requested) { - if (conn->ap2_flush_from_valid == 0) {// i.e. a flush from right now + if (conn->ap2_flush_from_valid == 0) { // i.e. a flush from right now flush_request_active = 1; flush_is_delayed = 0; } else { flush_is_delayed = 1; - dump_block_info = 1; flush_from_timestamp = conn->ap2_flush_from_rtp_timestamp; int32_t blocks_to_start_of_flush = conn->ap2_flush_from_sequence_number - seq_no; if (blocks_to_start_of_flush <= 0) { @@ -2086,29 +2124,29 @@ void *rtp_buffered_audio_processor(void *arg) { if (flush_requested == 0) { // here, a flush has been newly requested - debug(1,"Flush requested:"); + debug(2, "Flush requested."); if (conn->ap2_flush_from_valid) { - debug(1," fromTS: %u", conn->ap2_flush_from_rtp_timestamp); - debug(1," fromSeq: %u", conn->ap2_flush_from_sequence_number); - debug(1,"--"); + debug(2, " fromTS: %u", conn->ap2_flush_from_rtp_timestamp); + debug(2, " fromSeq: %u", conn->ap2_flush_from_sequence_number); + debug(2, "--"); } - debug(1," untilTS: %u", conn->ap2_flush_until_rtp_timestamp); - debug(1," untilSeq: %u", conn->ap2_flush_until_sequence_number); - debug(1,"--"); - debug(1," currentTS_Start: %u", pcm_buffer_read_point_rtptime); - uint32_t fib = (pcm_buffer_occupancy - pcm_buffer_read_point) / 4; - debug(1," framesInBuffer: %u", fib); - uint32_t endTS = fib + pcm_buffer_read_point_rtptime; - debug(1," currentTS_End: %u", endTS); // a frame occupies 4 bytes - debug(1," currentSeq: %u", seq_no); + debug(2, " untilTS: %u", conn->ap2_flush_until_rtp_timestamp); + debug(2, " untilSeq: %u", conn->ap2_flush_until_sequence_number); + debug(2, "--"); + debug(2, " currentTS_Start: %u", pcm_buffer_read_point_rtptime); + uint32_t fib = (pcm_buffer_occupancy - pcm_buffer_read_point) / 4; + debug(2, " framesInBuffer: %u", fib); + uint32_t endTS = fib + pcm_buffer_read_point_rtptime; + debug(2, " currentTS_End: %u", endTS); // a frame occupies 4 bytes + debug(2, " currentSeq: %u", seq_no); - flush_newly_requested = 1; + flush_newly_requested = 1; } // blocks_read to ensure seq_no is valid if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block - // if (flushUntilSeq != seq_no) - debug(1,"flushUntilSeq %u reached or overshot at %u.", flushUntilSeq, seq_no); + if (flushUntilSeq != seq_no) + debug(1, "flushUntilSeq %u reached or overshot at %u.", flushUntilSeq, seq_no); conn->ap2_flush_requested = 0; flush_request_active = 0; flush_newly_requested = 0; @@ -2123,10 +2161,9 @@ void *rtp_buffered_audio_processor(void *arg) { play_enabled = conn->ap2_play_enabled; debug_mutex_unlock(&conn->flush_mutex, 3); - // do this outside the flush mutex if (flush_newly_complete) { - debug(1,"Flush Complete."); + debug(2, "Flush Complete."); blocks_read = 0; } @@ -2137,14 +2174,13 @@ void *rtp_buffered_audio_processor(void *arg) { reset_buffer(conn); if (flush_is_delayed == 0) { - debug(1,"Immediate Buffered Audio Flush Started."); - //player_full_flush(conn); + debug(2, "Immediate Buffered Audio Flush Started."); + // player_full_flush(conn); streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; - dump_block_info = 0; } else { - debug(1,"Delayed Buffered Audio Flush Started."); + debug(2, "Delayed Buffered Audio Flush Started."); streaming_has_started = 0; pcm_buffer_occupancy = 0; pcm_buffer_read_point = 0; @@ -2182,22 +2218,26 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - if (blocks_read > 1) { - if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || - (streaming_has_started != 0)) { - if (streaming_has_started == 0) - debug(1, "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", conn->connection_number, pcm_buffer_read_point_rtptime, + if (blocks_read > 2) { + if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || + (streaming_has_started != 0)) { + if (streaming_has_started == 0) + debug( + 2, + "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", + conn->connection_number, pcm_buffer_read_point_rtptime, 0.000000001 * lead_time); - //else { - //if (expected_rtptime != pcm_buffer_read_point_rtptime) - // debug(1,"actual rtptime is %u, expected rtptime is %u.", pcm_buffer_read_point_rtptime, expected_rtptime); - //} - expected_rtptime = pcm_buffer_read_point_rtptime + 352; - player_put_packet(0, 0, pcm_buffer_read_point_rtptime, - pcm_buffer + pcm_buffer_read_point, 352, conn); - streaming_has_started++; - usleep(2000); - } + // else { + // if (expected_rtptime != pcm_buffer_read_point_rtptime) + // debug(1,"actual rtptime is %u, expected rtptime is %u.", + // pcm_buffer_read_point_rtptime, expected_rtptime); + //} + // expected_rtptime = pcm_buffer_read_point_rtptime + 352; + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, + pcm_buffer + pcm_buffer_read_point, 352, conn); + streaming_has_started++; + usleep(2000); + } } pcm_buffer_read_point_rtptime += 352; @@ -2273,38 +2313,6 @@ void *rtp_buffered_audio_processor(void *arg) { // uint8_t marker = 0; // uint8_t payload_type = 0; -/* - if (dump_block_info != 0) { - if ((flush_requested) && (seq_no < flushUntilSeq)) - debug(1, "block %u, rtptime %u, should be skipped.", seq_no, timestamp); - else - debug(1, "block %u, rtptime %u, should be decoded and used.", seq_no, timestamp); - - if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { - debug(1, "block %u, rtptime %u, will be decoded and used.", seq_no, timestamp); - } - } -*/ - -/* - if ((blocks_read != 0) && (seq_no != previous_seq_no + 1)) { - if (previous_seq_no != 0) - debug(1, "block discontinuity: from sequence number %u to sequence number %u.", - previous_seq_no, seq_no); - if (pcm_buffer_occupancy != 0) { - debug(1, - "rtptime discontinuity! Existing pcm buffer contents with timestamp " - "%u, seq_no %u to new block with timestamp %u, seq_no %u", - pcm_buffer_read_point_rtptime, previous_seq_no, timestamp, seq_no); - pcm_buffer_occupancy = 0; - } - // if still playing, this is a problem -- need to reset the player - if (play_enabled) - player_full_flush(conn); - } -*/ - - // previous_seq_no = seq_no; // at this point, we can check if we can to flush this packet -- we won't have @@ -2313,18 +2321,21 @@ void *rtp_buffered_audio_processor(void *arg) { if ((flush_requested) && (seq_no >= flushUntilSeq)) { uint64_t should_be_time; - if ((frame_to_local_time(timestamp, &should_be_time, conn) == - 0) && (play_enabled)) { - // play enabled will be off when this is a full flush and the anchor information is not valid + if ((frame_to_local_time(timestamp, &should_be_time, conn) == 0) && (play_enabled)) { + // play enabled will be off when this is a full flush and the anchor information is not + // valid int64_t lead_time = should_be_time - get_absolute_time_in_ns(); - debug(1,"flush completed to seq: %u with rtptime: %u, lead time: 0x%" PRIx64 " nanoseconds, i.e. %f sec.", seq_no, timestamp, lead_time, lead_time * 0.000000001); + debug(2, + "flush completed to seq: %u with rtptime: %u, lead time: 0x%" PRIx64 + " nanoseconds, i.e. %f sec.", + seq_no, timestamp, lead_time, lead_time * 0.000000001); } else { - debug(1,"flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); + debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); } } - if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { - + if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || + ((flush_requested == 0) && (new_buffer_needed))) { // if we are here because of a flush request, it must be the case that // flushing the pcm buffer wasn't enough, as the request would have been turned off by now @@ -2337,11 +2348,11 @@ void *rtp_buffered_audio_processor(void *arg) { if (pcm_buffer_occupancy == 0) { // they should match and the read point should be zero - // if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { + // if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { // debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", // pcm_buffer_read_point_rtptime, timestamp); - pcm_buffer_read_point_rtptime = timestamp; - pcm_buffer_read_point = 0; + pcm_buffer_read_point_rtptime = timestamp; + pcm_buffer_read_point = 0; //} } @@ -2425,12 +2436,19 @@ void *rtp_buffered_audio_processor(void *arg) { // copy the PCM audio into the PCM buffer. // make sure it's big enough first - // also, check it if needs to be truncated but to an impending delayed flush_is_delayed + // also, check it if needs to be truncated but to an impending delayed + // flush_is_delayed if (flush_is_delayed) { // see if the flush_from_timestamp is in the buffer - int32_t samples_remaining = (flush_from_timestamp - pcm_buffer_read_point_rtptime); - if ((samples_remaining > 0) && ((samples_remaining * 4) < dst_bufsize)) { - debug(1,"samples remaining before flush: %d, number of samples %d. flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", samples_remaining, dst_bufsize/4, flush_from_timestamp, pcm_buffer_read_point_rtptime); + int32_t samples_remaining = + (flush_from_timestamp - pcm_buffer_read_point_rtptime); + if ((samples_remaining > 0) && + ((samples_remaining * 4) < dst_bufsize)) { + debug(2, + "samples remaining before flush: %d, number of samples %d. " + "flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", + samples_remaining, dst_bufsize / 4, flush_from_timestamp, + pcm_buffer_read_point_rtptime); dst_bufsize = samples_remaining * 4; } } diff --git a/rtsp.c b/rtsp.c index 3ef47ba1..88bf2125 100644 --- a/rtsp.c +++ b/rtsp.c @@ -513,6 +513,49 @@ void cleanup_threads(void) { } } +void add_flush_request(int flushNow, uint32_t flushFromSeq, uint32_t flushFromTS, + uint32_t flushUntilSeq, uint32_t flushUntilTS, rtsp_conn_info *conn) { + // immediate flush requests are added sequentially. Don't know how more than one could arise, TBH + flush_request_t **t = &conn->flush_requests; + int done = 0; + do { + flush_request_t *u = *t; + if ((u == NULL) || ((u->flushNow == 0) && (flushNow != 0)) || + (flushFromSeq < u->flushFromSeq) || + ((flushFromSeq == u->flushFromSeq) && (flushFromTS < u->flushFromTS))) { + flush_request_t *n = (flush_request_t *)calloc(sizeof(flush_request_t), 1); + n->flushNow = flushNow; + n->flushFromSeq = flushFromSeq; + n->flushFromTS = flushFromTS; + n->flushUntilSeq = flushUntilSeq; + n->flushUntilTS = flushUntilTS; + n->next = u; + *t = n; + done = 1; + } else { + t = &u->next; + } + } while (done == 0); +} + +void display_all_flush_requests(rtsp_conn_info *conn) { + if (conn->flush_requests == NULL) { + debug(1, "No flush requests."); + } else { + flush_request_t *t = conn->flush_requests; + do { + if (t->flushNow) { + debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, + t->flushUntilTS); + } else { + debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, + t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); + } + t = t->next; + } while (t != NULL); + } +} + // park a null at the line ending, and return the next line pointer // accept \r, \n, or \r\n static char *nextline(char *in, int inbuf) { @@ -1324,7 +1367,7 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } else { plist_get_uint_val(item, &flushFromTS); if (flushFromValid == 0) - debug(1,"flushFromTS without flushFromSeq!"); + debug(1, "flushFromTS without flushFromSeq!"); debug(2, "flushFromTS is %" PRId64 ".", flushFromTS); } @@ -1345,23 +1388,52 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } debug_mutex_lock(&conn->flush_mutex, 1000, 1); - conn->ap2_flush_from_valid = flushFromValid; - // a flush with from... components will not be followed by a setanchoe (i.e. a play) + // a flush with from... components will not be followed by a setanchor (i.e. a play) // if it's a flush that will be followed by a setanchor (i.e. a play) then stop play now. if (flushFromValid == 0) conn->ap2_play_enabled = 0; - conn->ap2_flush_from_sequence_number = flushFromSeq; - conn->ap2_flush_from_rtp_timestamp = flushFromTS; + // add the exact request as made to the linked list (not used for anything but diagnostics now) + // int flushNow = 0; + // if (flushFromValid == 0) + // flushNow = 1; + // add_flush_request(flushNow, flushFromSeq, flushFromTS, flushUntilSeq, flushUntilTS, conn); + + // now, if it's an immediate flush, replace the existing request, if any + // but it if's a deferred flush and there is an existing deferred request, + // only update the flushUntil stuff -- that seems to preserve + // the intended semantics + + // so, always replace these conn->ap2_flush_until_sequence_number = flushUntilSeq; conn->ap2_flush_until_rtp_timestamp = flushUntilTS; + + 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 + } else { + conn->ap2_flush_from_sequence_number = flushFromSeq; + conn->ap2_flush_from_rtp_timestamp = flushFromTS; + } + + conn->ap2_flush_from_valid = flushFromValid; conn->ap2_flush_requested = 1; + + // reflect the possibly updated flush request + // add_flush_request(flushNow, conn->ap2_flush_from_sequence_number, + // conn->ap2_flush_from_rtp_timestamp, conn->ap2_flush_until_sequence_number, + // conn->ap2_flush_until_rtp_timestamp, conn); + debug_mutex_unlock(&conn->flush_mutex, 3); if (flushFromValid) - debug(1,"Deferred Flush Requested"); + debug(2, "Deferred Flush Requested"); else - debug(1,"Immediate Flush Requested"); + debug(2, "Immediate Flush Requested"); + + // display_all_flush_requests(conn); resp->respcode = 200; } @@ -2316,7 +2388,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (!strncmp(cp, "volume: ", strlen("volume: "))) { float volume = atof(cp + strlen("volume: ")); - debug(1, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); + debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, + volume); // if we are playing, go ahead and change the volume if (try_to_hold_play_lock(conn) == 0) { player_volume(volume, conn); @@ -2337,7 +2410,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else #endif { - debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, strlen(cp)); + debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, + strlen(cp)); } cp = next; } @@ -3058,7 +3132,8 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(2,"Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); + debug(2, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { resp->content = p; @@ -3153,11 +3228,13 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m // debug(2, "received parameters in SET_PARAMETER request."); handle_set_parameter_parameter(conn, req, resp); // this could be volume or progress } else { - debug(1, "Connection %d: received unknown Content-Type \"%s\" in SET_PARAMETER request.", conn->connection_number, ct); - debug_print_msg_headers(1,req); + debug(1, "Connection %d: received unknown Content-Type \"%s\" in SET_PARAMETER request.", + conn->connection_number, ct); + debug_print_msg_headers(1, req); } } else { - debug(1, "Connection %d: missing Content-Type header in SET_PARAMETER request.", conn->connection_number); + debug(1, "Connection %d: missing Content-Type header in SET_PARAMETER request.", + conn->connection_number); } resp->respcode = 200; } @@ -3449,7 +3526,7 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { char *hdr = msg_get_header(req, "Apple-Challenge"); if (!hdr) return; - debug(1,"Apple Challenge"); + debug(1, "Apple Challenge"); SOCKADDR fdsa; socklen_t sa_len = sizeof(fdsa); getsockname(fd, (struct sockaddr *)&fdsa, &sa_len); @@ -4119,8 +4196,8 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { maxfd = sockfd[i]; } - char **t1; // ap1 test records - char **t2; // two text records + char **t1; // ap1 test records + char **t2; // two text records t1 = NULL; t2 = NULL; @@ -4130,14 +4207,14 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { char **p = txt_records; t1 = p; // first set of text records -// make up a firmware version + // make up a firmware version char firmware_version[64]; #ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') - snprintf(firmware_version, sizeof(firmware_version), "fv=%s",git_version_string); - else + if (git_version_string[0] != '\0') + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); + else #endif - snprintf(firmware_version, sizeof(firmware_version), "fv=%s",PACKAGE_VERSION); + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 char ap1_featuresString[64]; @@ -4152,7 +4229,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); -// the ap1 text record is different if it is set up for ap2 + // the ap1 text record is different if it is set up for ap2 *p++ = "cn=0,1"; *p++ = "da=true"; *p++ = "et=0,4"; @@ -4198,7 +4275,6 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = NULL; #endif - #ifdef CONFIG_AIRPLAY_2 // make up a secondary set of text records char *secondary_txt_records[64]; @@ -4233,8 +4309,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = NULL; #endif - - mdns_register(t1,t2); + mdns_register(t1, t2); pthread_setcancelstate(oldState, NULL); int acceptfd; diff --git a/shairport.c b/shairport.c index b2d8886c..d143de66 100644 --- a/shairport.c +++ b/shairport.c @@ -1421,7 +1421,6 @@ void exit_function() { free(config.regtype2); #endif - #ifdef CONFIG_LIBDAEMON if (this_is_the_daemon_process) { daemon_retval_send(0); @@ -2038,39 +2037,34 @@ int main(int argc, char **argv) { pthread_create(&soxr_time_check_thread, NULL, &soxr_time_check, NULL); #endif -/* - uint8_t ap_md5[16]; + /* + 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 + #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 -#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_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_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_finish(&tctx, ap_md5); + #endif - memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr)); -*/ + memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr)); + */ #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary From 4b5e9787b78c4b7141882c0db028649a244af337 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 30 May 2021 12:04:06 +0100 Subject: [PATCH 153/878] Log information when anchor or master clock changes occur. --- rtp.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rtp.c b/rtp.c index a4994301..a78c16e1 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1257,7 +1257,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_time_of_update = get_absolute_time_in_ns(); conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) - debug(2, "Connection %d: New anchor clock %" PRIx64 " recognised.", + debug(1, "Connection %d: New anchor clock %" PRIx64 " recognised.", conn->connection_number, conn->anchor_clock); conn->anchor_clock_is_new = 0; } @@ -1299,7 +1299,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(2, "Connection %d: NQPTP new master clock %" PRIx64 ".", conn->connection_number, + debug(1, "Connection %d: NQPTP new master clock %" PRIx64 ".", conn->connection_number, actual_clock_id); break; case clock_not_ready: From 5e498173e1b85818f228b39e79126b8f509de443 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 30 May 2021 12:12:40 +0100 Subject: [PATCH 154/878] Add a little information and remove a little ambiguity from a diagnostic message about anchor and master clocks especially relevant in Realtime Audio streams. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index a78c16e1..bfff886a 100644 --- a/rtp.c +++ b/rtp.c @@ -1257,7 +1257,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_time_of_update = get_absolute_time_in_ns(); conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) - debug(1, "Connection %d: New anchor clock %" PRIx64 " recognised.", + debug(1, "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock.", conn->connection_number, conn->anchor_clock); conn->anchor_clock_is_new = 0; } From ac515bbfa3cb6a3af5a63ad26dee5660316167e3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 May 2021 09:49:15 +0100 Subject: [PATCH 155/878] Deal with the possibility that the anchor information can be changed suddenly at the start of a Buffered Audio play. If the anchor info changes before the local_anchor info is mature enough (5 seconds), invalidate the local anchor info to force it to be immediately updated with the new anchor info. --- player.h | 1 + rtp.c | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/player.h b/player.h index fbffb04e..fc8e9bce 100644 --- a/player.h +++ b/player.h @@ -293,6 +293,7 @@ typedef struct { uint32_t last_anchor_rtptime; uint64_t last_anchor_local_time; uint64_t last_anchor_time_of_update; + uint64_t last_anchor_validity_start_time; ssize_t ap2_audio_buffer_size; flush_request_t *flush_requests; // if non-null, there are flush requests, mutex protected diff --git a/rtp.c b/rtp.c index bfff886a..0540306b 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1215,6 +1215,23 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti } // debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, // rtptime, networktime); + + // if the clock is the same but any details change, and if the last_anchor_info has not been + // valid for some minimum time (and thus may not be reliable), we need to invalidate + // last_anchor_info + + if ((clock_id == conn->anchor_clock) && + ((conn->anchor_rtptime != rtptime) || (conn->anchor_time != networktime))) { + uint64_t time_now = get_absolute_time_in_ns(); + int64_t last_anchor_validity_duration = time_now - conn->last_anchor_validity_start_time; + if (last_anchor_validity_duration < 5000000000) { + debug(1, + "Connection %d: Note: anchor parameters have changed before clock %" PRIx64 + " has stabilised.", + conn->connection_number, clock_id); + conn->last_anchor_info_is_valid = 0; + } + } conn->anchor_remote_info_is_valid = 1; conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; @@ -1250,14 +1267,17 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { - // use the master clock if it's at least this old or we have no alternative - // and at least it is the minimum age. + // use the master clock if it's at least this old or if we have no alternative + // and it at least is the minimum age. conn->last_anchor_rtptime = conn->anchor_rtptime; conn->last_anchor_local_time = conn->anchor_time - actual_offset; conn->last_anchor_time_of_update = get_absolute_time_in_ns(); + if (conn->last_anchor_info_is_valid == 0) + conn->last_anchor_validity_start_time = start_of_mastership; conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) - debug(1, "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock.", + debug(1, + "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock.", conn->connection_number, conn->anchor_clock); conn->anchor_clock_is_new = 0; } From 1464c4ed04ee2b0b970a8b2b3e061422a2cd6633 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 May 2021 17:12:20 +0100 Subject: [PATCH 156/878] Restore ability to compile without AIRPLAY-2 flag (duh). Begin to split player_play into player_prepare and player_play. --- player.c | 11 ++++++- player.h | 3 +- rtsp.c | 92 +++++++++++++++++++++++++++++--------------------------- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/player.c b/player.c index c8402524..c307a9cc 100644 --- a/player.c +++ b/player.c @@ -3039,6 +3039,7 @@ void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { #endif } +/* void player_full_flush(rtsp_conn_info *conn) { debug(3, "player_full_flush"); // this basically flushes everything from the player @@ -3068,8 +3069,13 @@ void player_full_flush(rtsp_conn_info *conn) { if (flush_needed) player_flush(rtpTimestamp, conn); } +*/ -int player_play(rtsp_conn_info *conn) { +// perpare_to_play and play are split so that we can get the capabilities of the +// dac etc. before initialising any decoders etc. +// for example, if we have 32-bit DACs, we can ask for 32 bit decodes + +int player_prepare_to_play(rtsp_conn_info *conn) { // need to use conn in place of stream below. Need to put the stream as a parameter to he if (conn->player_thread != NULL) die("Trying to create a second player thread for this RTSP session"); @@ -3082,7 +3088,10 @@ int player_play(rtsp_conn_info *conn) { // call on the output device to prepare itself if ((config.output) && (config.output->prepare)) config.output->prepare(); + return 0; +} +int player_play(rtsp_conn_info *conn) { pthread_t *pt = malloc(sizeof(pthread_t)); if (pt == NULL) die("Couldn't allocate space for pthread_t"); diff --git a/player.h b/player.h index fc8e9bce..a21deec6 100644 --- a/player.h +++ b/player.h @@ -395,13 +395,14 @@ void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occup int32_t modulo_32_offset(uint32_t from, uint32_t to); +int player_prepare_to_play(rtsp_conn_info *conn); int player_play(rtsp_conn_info *conn); int player_stop(rtsp_conn_info *conn); void player_volume(double f, rtsp_conn_info *conn); void player_volume_without_notification(double f, rtsp_conn_info *conn); void player_flush(uint32_t timestamp, rtsp_conn_info *conn); -void player_full_flush(rtsp_conn_info *conn); +// void player_full_flush(rtsp_conn_info *conn); void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn); int64_t monotonic_timestamp(uint32_t timestamp, diff --git a/rtsp.c b/rtsp.c index 88bf2125..079d6df6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -513,49 +513,6 @@ void cleanup_threads(void) { } } -void add_flush_request(int flushNow, uint32_t flushFromSeq, uint32_t flushFromTS, - uint32_t flushUntilSeq, uint32_t flushUntilTS, rtsp_conn_info *conn) { - // immediate flush requests are added sequentially. Don't know how more than one could arise, TBH - flush_request_t **t = &conn->flush_requests; - int done = 0; - do { - flush_request_t *u = *t; - if ((u == NULL) || ((u->flushNow == 0) && (flushNow != 0)) || - (flushFromSeq < u->flushFromSeq) || - ((flushFromSeq == u->flushFromSeq) && (flushFromTS < u->flushFromTS))) { - flush_request_t *n = (flush_request_t *)calloc(sizeof(flush_request_t), 1); - n->flushNow = flushNow; - n->flushFromSeq = flushFromSeq; - n->flushFromTS = flushFromTS; - n->flushUntilSeq = flushUntilSeq; - n->flushUntilTS = flushUntilTS; - n->next = u; - *t = n; - done = 1; - } else { - t = &u->next; - } - } while (done == 0); -} - -void display_all_flush_requests(rtsp_conn_info *conn) { - if (conn->flush_requests == NULL) { - debug(1, "No flush requests."); - } else { - flush_request_t *t = conn->flush_requests; - do { - if (t->flushNow) { - debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, - t->flushUntilTS); - } else { - debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, - t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); - } - t = t->next; - } while (t != NULL); - } -} - // park a null at the line ending, and return the next line pointer // accept \r, \n, or \r\n static char *nextline(char *in, int inbuf) { @@ -758,6 +715,49 @@ fail: #ifdef CONFIG_AIRPLAY_2 +void add_flush_request(int flushNow, uint32_t flushFromSeq, uint32_t flushFromTS, + uint32_t flushUntilSeq, uint32_t flushUntilTS, rtsp_conn_info *conn) { + // immediate flush requests are added sequentially. Don't know how more than one could arise, TBH + flush_request_t **t = &conn->flush_requests; + int done = 0; + do { + flush_request_t *u = *t; + if ((u == NULL) || ((u->flushNow == 0) && (flushNow != 0)) || + (flushFromSeq < u->flushFromSeq) || + ((flushFromSeq == u->flushFromSeq) && (flushFromTS < u->flushFromTS))) { + flush_request_t *n = (flush_request_t *)calloc(sizeof(flush_request_t), 1); + n->flushNow = flushNow; + n->flushFromSeq = flushFromSeq; + n->flushFromTS = flushFromTS; + n->flushUntilSeq = flushUntilSeq; + n->flushUntilTS = flushUntilTS; + n->next = u; + *t = n; + done = 1; + } else { + t = &u->next; + } + } while (done == 0); +} + +void display_all_flush_requests(rtsp_conn_info *conn) { + if (conn->flush_requests == NULL) { + debug(1, "No flush requests."); + } else { + flush_request_t *t = conn->flush_requests; + do { + if (t->flushNow) { + debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, + t->flushUntilTS); + } else { + debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, + t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); + } + t = t->next; + } while (t != NULL); + } +} + int rtsp_message_contains_plist(rtsp_message *message) { int reply = 0; // assume there is no plist in the message if ((message->contentlength >= strlen("bplist00")) && @@ -1213,8 +1213,10 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (have_play_lock(conn)) { if (conn->player_thread) warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); - else + else { + player_prepare_to_play(conn); player_play(conn); // the thread better be 0 + } resp->respcode = 200; // I think this is for telling the client what the absolute minimum latency @@ -2047,6 +2049,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) else debug(1, "No timing peer list!"); + player_prepare_to_play(conn); player_play(conn); conn->rtp_running = 1; // hack! @@ -2082,6 +2085,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->input_bit_depth = 16; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + player_prepare_to_play(conn); player_play(conn); conn->rtp_running = 1; // hack! From d4ea91aa6532105aa80647831437c269c7c96068 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Jun 2021 08:56:47 +0100 Subject: [PATCH 157/878] Rough and hacky but works with the bit depth of the output DAC to automatically choose the correct AAC decoding sample size. Allocation of rotating buffers is fixed and profligate. --- audio_alsa.c | 4 +-- player.c | 59 +++++++++++++++++++++++++++++++++++---- rtp.c | 78 +++++++++++++++++++++++++++++++++++++--------------- rtsp.c | 17 ++++++------ 4 files changed, 121 insertions(+), 37 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index f0c10987..d5757bef 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -525,7 +525,7 @@ int actual_open_alsa_device(int do_auto_setup) { } if (ret == 0) { config.output_format = trial_format; - debug(2, "alsa: output format chosen is \"%s\".", + debug(1, "alsa: output format chosen is \"%s\".", sps_format_description_string(config.output_format)); } else { warn("audio_alsa: Could not automatically set the output format for device \"%s\": %s", @@ -566,7 +566,7 @@ int actual_open_alsa_device(int do_auto_setup) { } if (ret == 0) { config.output_rate = actual_sample_rate; - debug(2, "alsa: output speed chosen is %d.", config.output_rate); + debug(1, "alsa: output speed chosen is %d.", config.output_rate); } else { warn("audio_alsa: Could not automatically set the output rate for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); diff --git a/player.c b/player.c index c307a9cc..a0e10d94 100644 --- a/player.c +++ b/player.c @@ -384,10 +384,12 @@ static void terminate_decoders(rtsp_conn_info *conn) { } 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++) - conn->audio_buffer[i].data = malloc(conn->input_bytes_per_frame * conn->max_frames_per_packet); - ab_resync(conn); +// 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 } static void free_audio_buffers(rtsp_conn_info *conn) { @@ -1692,7 +1694,6 @@ void *player_thread_func(void *arg) { conn->packet_count = 0; conn->packet_count_since_flush = 0; conn->previous_random_number = 0; - conn->input_bytes_per_frame = 4; conn->decoder_in_use = 0; conn->ab_buffering = 1; conn->ab_synced = 0; @@ -1711,6 +1712,7 @@ void *player_thread_func(void *arg) { // No pthread cancellation point in here // This must be after init_alac_decoder init_buffer(conn); // will need a corresponding deallocation. No cancellation points in here + ab_resync(conn); if (conn->stream.encrypted) { #ifdef CONFIG_MBEDTLS @@ -2114,10 +2116,56 @@ void *player_thread_func(void *arg) { *outpl++ = rl; } } - } break; + case 32: { + int i, j; + int32_t ls, rs; + int32_t ll = 0, rl = 0; + int32_t *inps = (int32_t*) inbuf; + int32_t *outpl = (int32_t *)conn->tbuf; + for (i = 0; i < inbuflength; i++) { + ls = *inps++; + rs = *inps++; + + // here, do the mode stuff -- mono / reverse stereo / leftonly / rightonly + + switch (config.playback_mode) { + case ST_mono: { + int64_t both = ls + rs; + both = both >> 1; + uint32_t both32 = both; + ll = both32; + rl = both32; + } break; + case ST_reverse_stereo: { + ll = rs; + rl = ls; + } break; + case ST_left_only: + rl = ls; + ll = ls; + break; + case ST_right_only: + ll = rs; + rl = rs; + break; + case ST_stereo: + ll = ls; + rl = rs; + break; // nothing extra to do + } + + // here, replicate the samples if you're upsampling + + for (j = 0; j < conn->output_sample_ratio; j++) { + *outpl++ = ll; + *outpl++ = rl; + } + } + } break; + default: - die("Shairport Sync only supports 16 bit input"); + die("Shairport Sync only supports 16 or 32 bit input"); } inbuflength *= conn->output_sample_ratio; @@ -3085,6 +3133,7 @@ int player_prepare_to_play(rtsp_conn_info *conn) { activity_monitor_signify_activity( 1); // active, and should be before play's command hook, command_start() command_start(); + conn->input_bytes_per_frame = 4; // default -- may be changed later // call on the output device to prepare itself if ((config.output) && (config.output->prepare)) config.output->prepare(); diff --git a/rtp.c b/rtp.c index 0540306b..dc3ba0aa 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -2082,9 +2082,45 @@ void *rtp_buffered_audio_processor(void *arg) { av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "in_sample_rate", 44100, 0); - av_opt_set_int(swr, "out_sample_rate", 44100, 0); + av_opt_set_int(swr, "out_sample_rate", config.output_rate, 0); av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); - av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); + + enum AVSampleFormat av_format; + switch (config.output_format) { + case SPS_FORMAT_S32: + case SPS_FORMAT_S32_LE: + case SPS_FORMAT_S32_BE: + case SPS_FORMAT_S24: + case SPS_FORMAT_S24_LE: + case SPS_FORMAT_S24_BE: + case SPS_FORMAT_S24_3LE: + case SPS_FORMAT_S24_3BE: + av_format = AV_SAMPLE_FMT_S32; + conn->input_bytes_per_frame = 8; // the output from the decoder will be input to the player + conn->input_bit_depth = 32; + debug(1,"32-bit output format chosen"); + break; + case SPS_FORMAT_S16: + case SPS_FORMAT_S16_LE: + case SPS_FORMAT_S16_BE: + av_format = AV_SAMPLE_FMT_S16; + conn->input_bytes_per_frame = 4; + conn->input_bit_depth = 16; + break; + case SPS_FORMAT_U8: + av_format = AV_SAMPLE_FMT_U8; + conn->input_bytes_per_frame = 2; + conn->input_bit_depth = 8; + break; + default: + debug(1,"Unsupported DAC output format %u. AV_SAMPLE_FMT_S16 decoding chosen. Good luck!", config.output_format); + av_format = AV_SAMPLE_FMT_S16; + conn->input_bytes_per_frame = 4; // the output from the decoder will be input to the player + conn->input_bit_depth = 16; + break; + }; + + av_opt_set_sample_fmt(swr, "out_sample_fmt", av_format, 0); swr_init(swr); uint8_t packet[16 * 1024]; @@ -2099,7 +2135,7 @@ void *rtp_buffered_audio_processor(void *arg) { int finished = 0; int pcm_buffer_size = - (1024 + 352) * 8; // This seems to be right. 8 is for 2 * 32-bit samples per frame + (1024 + 352) * conn->input_bytes_per_frame; uint8_t pcm_buffer[pcm_buffer_size]; int pcm_buffer_occupancy = 0; @@ -2220,7 +2256,7 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1,"sleep for 20 ms"); usleep(20000); // wait for a while } else { - if ((pcm_buffer_occupancy - pcm_buffer_read_point) >= (352 * 4)) { + if ((pcm_buffer_occupancy - pcm_buffer_read_point) >= (352 * conn->input_bytes_per_frame)) { new_buffer_needed = 0; // send a frame to the player if allowed // it it's way too late, it means that a new anchor time is needed @@ -2238,7 +2274,7 @@ void *rtp_buffered_audio_processor(void *arg) { 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - if (blocks_read > 2) { + if (blocks_read > 3) { if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || (streaming_has_started != 0)) { if (streaming_has_started == 0) @@ -2261,7 +2297,7 @@ void *rtp_buffered_audio_processor(void *arg) { } pcm_buffer_read_point_rtptime += 352; - pcm_buffer_read_point += 352 * 4; + pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; } // usleep(2000); // let other stuff happens } else { @@ -2271,7 +2307,7 @@ void *rtp_buffered_audio_processor(void *arg) { new_buffer_needed = 1; if (pcm_buffer_read_point != 0) { // debug(1,"pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy (frames): %u", - // pcm_buffer_read_point/4, pcm_buffer_occupancy/4); + // pcm_buffer_read_point/conn->input_bytes_per_frame, pcm_buffer_occupancy/conn->input_bytes_per_frame); // if there is anything to move down to the front of the buffer, do it now; if ((pcm_buffer_occupancy - pcm_buffer_read_point) > 0) { // move the remaining frames down to the start of the buffer @@ -2314,8 +2350,6 @@ void *rtp_buffered_audio_processor(void *arg) { strerror_r(errno, (char *)errorstring, sizeof(errorstring)); debug(1, "error in rtp_buffered_audio_processor %d: \"%s\". Could not recv a data packet.", errno, errorstring); - // if ((config.diagnostic_drop_packet_fraction == 0.0) || - // (drand48() > config.diagnostic_drop_packet_fraction)) { } else if (nread > 0) { blocks_read++; // note, this doesn't mean they are valid audio blocks // debug(1, "Realtime Audio Receiver Packet of length %d received.", nread); @@ -2445,13 +2479,13 @@ void *rtp_buffered_audio_processor(void *arg) { debug(1, "error %d during decoding", ret); } else { av_samples_alloc(&pcm_audio, &dst_linesize, codec_context->channels, - decoded_frame->nb_samples, AV_SAMPLE_FMT_S16, 1); + decoded_frame->nb_samples, av_format, 1); // remember to free pcm_audio ret = swr_convert(swr, &pcm_audio, decoded_frame->nb_samples, (const uint8_t **)decoded_frame->extended_data, decoded_frame->nb_samples); dst_bufsize = av_samples_get_buffer_size( - &dst_linesize, codec_context->channels, ret, AV_SAMPLE_FMT_S16, 1); + &dst_linesize, codec_context->channels, ret, av_format, 1); // debug(1,"generated %d bytes of PCM", dst_bufsize); // copy the PCM audio into the PCM buffer. // make sure it's big enough first @@ -2463,20 +2497,20 @@ void *rtp_buffered_audio_processor(void *arg) { int32_t samples_remaining = (flush_from_timestamp - pcm_buffer_read_point_rtptime); if ((samples_remaining > 0) && - ((samples_remaining * 4) < dst_bufsize)) { + ((samples_remaining * conn->input_bytes_per_frame) < dst_bufsize)) { debug(2, "samples remaining before flush: %d, number of samples %d. " "flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", - samples_remaining, dst_bufsize / 4, flush_from_timestamp, + samples_remaining, dst_bufsize / conn->input_bytes_per_frame, flush_from_timestamp, pcm_buffer_read_point_rtptime); - dst_bufsize = samples_remaining * 4; + dst_bufsize = samples_remaining * conn->input_bytes_per_frame; } } if ((pcm_buffer_size - pcm_buffer_occupancy) < dst_bufsize) { debug(1, "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " "(frames): %u", - pcm_buffer_read_point / 4, pcm_buffer_occupancy / 4); + pcm_buffer_read_point / conn->input_bytes_per_frame, pcm_buffer_occupancy / conn->input_bytes_per_frame); pcm_buffer_size = dst_bufsize + pcm_buffer_occupancy; debug(1, "fatal error! pcm buffer too small at %d bytes.", pcm_buffer_size); diff --git a/rtsp.c b/rtsp.c index 079d6df6..c50b7679 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2070,6 +2070,15 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) die("Error %d: could not find a TCP port to use as a buffered_audio port", err); } + // hack. + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + + player_prepare_to_play(conn); // get capabilities of DAC before creating the buffered audio thread + pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, (void *)conn); @@ -2078,14 +2087,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(stream0dict, "audioBufferSize", plist_new_uint(conn->ap2_audio_buffer_size)); - // hack. - conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; - conn->input_num_channels = 2; - conn->input_bit_depth = 16; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - - player_prepare_to_play(conn); player_play(conn); conn->rtp_running = 1; // hack! From 7df6ff0a5521b0ec1360f9431c36dd28a4417cfa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Jun 2021 09:51:14 +0100 Subject: [PATCH 158/878] Exit if the requested output format is not available. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index d5757bef..c5794348 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -501,7 +501,7 @@ int actual_open_alsa_device(int do_auto_setup) { } ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf); if (ret < 0) { - warn("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf, + die("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf, alsa_out_dev, snd_strerror(ret)); return ret; } From c5dafbd28b78ed30b041a4b9889339bc05f42b8d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Jun 2021 14:51:17 +0100 Subject: [PATCH 159/878] Remember the original anchor information so that if the master clock become equal to it in the future, it can be restored to those exact numbers and eliminate any intervening approximations. Add some diagnostics. --- audio_alsa.c | 4 +- player.c | 9 ++-- player.h | 8 +++ rtp.c | 137 ++++++++++++++++++++++++++++++++------------------- rtsp.c | 3 +- 5 files changed, 103 insertions(+), 58 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index c5794348..2d07c8f4 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -501,8 +501,8 @@ int actual_open_alsa_device(int do_auto_setup) { } ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf); if (ret < 0) { - die("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf, - alsa_out_dev, snd_strerror(ret)); + die("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf, alsa_out_dev, + snd_strerror(ret)); return ret; } } else { // auto format diff --git a/player.c b/player.c index a0e10d94..a9822d68 100644 --- a/player.c +++ b/player.c @@ -384,11 +384,12 @@ static void terminate_decoders(rtsp_conn_info *conn) { } 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); + // 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++) -// conn->audio_buffer[i].data = malloc(conn->input_bytes_per_frame * conn->max_frames_per_packet); + // 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 } @@ -2121,7 +2122,7 @@ void *player_thread_func(void *arg) { int i, j; int32_t ls, rs; int32_t ll = 0, rl = 0; - int32_t *inps = (int32_t*) inbuf; + int32_t *inps = (int32_t *)inbuf; int32_t *outpl = (int32_t *)conn->tbuf; for (i = 0; i < inbuflength; i++) { ls = *inps++; diff --git a/player.h b/player.h index a21deec6..c35e5726 100644 --- a/player.h +++ b/player.h @@ -273,10 +273,18 @@ typedef struct { int anchor_remote_info_is_valid; int anchor_clock_is_new; + + // these can be modified if the master clock changes over time uint64_t anchor_clock; uint64_t anchor_time; // this is the time according to the clock uint32_t anchor_rtptime; + // these are used to identify when the master clock becomes equal to the + // actual anchor clock information, so it can be used to avoid accumulating errors + uint64_t actual_anchor_clock; + uint64_t actual_anchor_time; + uint32_t actual_anchor_rtptime; + clock_status_t clock_status; #ifdef CONFIG_AIRPLAY_2 diff --git a/rtp.c b/rtp.c index dc3ba0aa..9fb8481c 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1220,22 +1220,45 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti // valid for some minimum time (and thus may not be reliable), we need to invalidate // last_anchor_info + if ((conn->airplay_stream_type == buffered_stream) && + ((clock_id != conn->anchor_clock) || (conn->anchor_rtptime != rtptime) || + (conn->anchor_time != networktime))) { + uint64_t master_clock_id = 0; + ptp_get_clock_info(&master_clock_id, NULL, NULL); + debug(1, + "Connection %d: Note: anchor parameters have changed. Old clock: %" PRIx64 + ", rtptime: %u, networktime: %" PRIu64 ". New clock: %" PRIx64 + ", rtptime: %u, networktime: %" PRIu64 ". Current master clock: %" PRIx64 ".", + conn->connection_number, conn->anchor_clock, conn->anchor_rtptime, conn->anchor_time, + clock_id, rtptime, networktime, master_clock_id); + } + if ((clock_id == conn->anchor_clock) && ((conn->anchor_rtptime != rtptime) || (conn->anchor_time != networktime))) { uint64_t time_now = get_absolute_time_in_ns(); int64_t last_anchor_validity_duration = time_now - conn->last_anchor_validity_start_time; if (last_anchor_validity_duration < 5000000000) { - debug(1, - "Connection %d: Note: anchor parameters have changed before clock %" PRIx64 - " has stabilised.", - conn->connection_number, clock_id); + if (conn->airplay_stream_type == buffered_stream) + debug(1, + "Connection %d: Note: anchor parameters have changed before clock %" PRIx64 + " has stabilised.", + conn->connection_number, clock_id); conn->last_anchor_info_is_valid = 0; } } + conn->anchor_remote_info_is_valid = 1; + + // these can be modified if the master clock changes over time conn->anchor_rtptime = rtptime; conn->anchor_time = networktime; conn->anchor_clock = clock_id; + + // these are used to identify when the master clock becomes equal to the + // actual anchor clock information, so it can be used to avoid accumulating errors + conn->actual_anchor_rtptime = rtptime; + conn->actual_anchor_time = networktime; + conn->actual_anchor_clock = clock_id; } void reset_ptp_anchor_info(rtsp_conn_info *conn) { @@ -1303,8 +1326,18 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to // calculate what time that must be on the new clock - conn->anchor_time = conn->last_anchor_local_time + actual_offset; - conn->anchor_clock = actual_clock_id; + + // but if the master clock has become equal to the actual anchor clock + // then we can reinstate it all + if (actual_clock_id == conn->actual_anchor_clock) { + debug(1, "Master clock has become equal to the anchor clock"); + conn->anchor_clock = conn->actual_anchor_clock; + conn->anchor_time = conn->actual_anchor_time; + conn->anchor_rtptime = conn->actual_anchor_rtptime; + } else { + conn->anchor_time = conn->last_anchor_local_time + actual_offset; + conn->anchor_clock = actual_clock_id; + } } } else { response = clock_not_valid; // no current clock information and no previous clock info @@ -2087,37 +2120,38 @@ void *rtp_buffered_audio_processor(void *arg) { enum AVSampleFormat av_format; switch (config.output_format) { - case SPS_FORMAT_S32: - case SPS_FORMAT_S32_LE: - case SPS_FORMAT_S32_BE: - case SPS_FORMAT_S24: - case SPS_FORMAT_S24_LE: - case SPS_FORMAT_S24_BE: - case SPS_FORMAT_S24_3LE: - case SPS_FORMAT_S24_3BE: - av_format = AV_SAMPLE_FMT_S32; - conn->input_bytes_per_frame = 8; // the output from the decoder will be input to the player - conn->input_bit_depth = 32; - debug(1,"32-bit output format chosen"); - break; - case SPS_FORMAT_S16: - case SPS_FORMAT_S16_LE: - case SPS_FORMAT_S16_BE: - av_format = AV_SAMPLE_FMT_S16; - conn->input_bytes_per_frame = 4; - conn->input_bit_depth = 16; - break; - case SPS_FORMAT_U8: - av_format = AV_SAMPLE_FMT_U8; - conn->input_bytes_per_frame = 2; - conn->input_bit_depth = 8; - break; - default: - debug(1,"Unsupported DAC output format %u. AV_SAMPLE_FMT_S16 decoding chosen. Good luck!", config.output_format); - av_format = AV_SAMPLE_FMT_S16; - conn->input_bytes_per_frame = 4; // the output from the decoder will be input to the player - conn->input_bit_depth = 16; - break; + case SPS_FORMAT_S32: + case SPS_FORMAT_S32_LE: + case SPS_FORMAT_S32_BE: + case SPS_FORMAT_S24: + case SPS_FORMAT_S24_LE: + case SPS_FORMAT_S24_BE: + case SPS_FORMAT_S24_3LE: + case SPS_FORMAT_S24_3BE: + av_format = AV_SAMPLE_FMT_S32; + conn->input_bytes_per_frame = 8; // the output from the decoder will be input to the player + conn->input_bit_depth = 32; + debug(1, "32-bit output format chosen"); + break; + case SPS_FORMAT_S16: + case SPS_FORMAT_S16_LE: + case SPS_FORMAT_S16_BE: + av_format = AV_SAMPLE_FMT_S16; + conn->input_bytes_per_frame = 4; + conn->input_bit_depth = 16; + break; + case SPS_FORMAT_U8: + av_format = AV_SAMPLE_FMT_U8; + conn->input_bytes_per_frame = 2; + conn->input_bit_depth = 8; + break; + default: + debug(1, "Unsupported DAC output format %u. AV_SAMPLE_FMT_S16 decoding chosen. Good luck!", + config.output_format); + av_format = AV_SAMPLE_FMT_S16; + conn->input_bytes_per_frame = 4; // the output from the decoder will be input to the player + conn->input_bit_depth = 16; + break; }; av_opt_set_sample_fmt(swr, "out_sample_fmt", av_format, 0); @@ -2134,8 +2168,7 @@ void *rtp_buffered_audio_processor(void *arg) { ssize_t nread; int finished = 0; - int pcm_buffer_size = - (1024 + 352) * conn->input_bytes_per_frame; + int pcm_buffer_size = (1024 + 352) * conn->input_bytes_per_frame; uint8_t pcm_buffer[pcm_buffer_size]; int pcm_buffer_occupancy = 0; @@ -2307,8 +2340,9 @@ void *rtp_buffered_audio_processor(void *arg) { new_buffer_needed = 1; if (pcm_buffer_read_point != 0) { // debug(1,"pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy (frames): %u", - // pcm_buffer_read_point/conn->input_bytes_per_frame, pcm_buffer_occupancy/conn->input_bytes_per_frame); - // if there is anything to move down to the front of the buffer, do it now; + // pcm_buffer_read_point/conn->input_bytes_per_frame, + // pcm_buffer_occupancy/conn->input_bytes_per_frame); if there is anything to move down + // to the front of the buffer, do it now; if ((pcm_buffer_occupancy - pcm_buffer_read_point) > 0) { // move the remaining frames down to the start of the buffer // debug(1,"move the remaining frames down to the start of the pcm_buffer"); @@ -2501,8 +2535,8 @@ void *rtp_buffered_audio_processor(void *arg) { debug(2, "samples remaining before flush: %d, number of samples %d. " "flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", - samples_remaining, dst_bufsize / conn->input_bytes_per_frame, flush_from_timestamp, - pcm_buffer_read_point_rtptime); + samples_remaining, dst_bufsize / conn->input_bytes_per_frame, + flush_from_timestamp, pcm_buffer_read_point_rtptime); dst_bufsize = samples_remaining * conn->input_bytes_per_frame; } } @@ -2510,7 +2544,8 @@ void *rtp_buffered_audio_processor(void *arg) { debug(1, "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " "(frames): %u", - pcm_buffer_read_point / conn->input_bytes_per_frame, pcm_buffer_occupancy / conn->input_bytes_per_frame); + pcm_buffer_read_point / conn->input_bytes_per_frame, + pcm_buffer_occupancy / conn->input_bytes_per_frame); pcm_buffer_size = dst_bufsize + pcm_buffer_occupancy; debug(1, "fatal error! pcm buffer too small at %d bytes.", pcm_buffer_size); diff --git a/rtsp.c b/rtsp.c index c50b7679..a808c96f 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2077,7 +2077,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->input_bit_depth = 16; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - player_prepare_to_play(conn); // get capabilities of DAC before creating the buffered audio thread + player_prepare_to_play( + conn); // get capabilities of DAC before creating the buffered audio thread pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, (void *)conn); From deeabbfbc576e0708a66aec31521fa222d0ba341 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:24:31 +0100 Subject: [PATCH 160/878] Quieten a rather chatty message. --- rtp.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rtp.c b/rtp.c index 9fb8481c..8d8e0680 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1220,7 +1220,7 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti // valid for some minimum time (and thus may not be reliable), we need to invalidate // last_anchor_info - if ((conn->airplay_stream_type == buffered_stream) && + if ((conn->airplay_stream_type == buffered_stream) && (conn->ap2_play_enabled != 0) && ((clock_id != conn->anchor_clock) || (conn->anchor_rtptime != rtptime) || (conn->anchor_time != networktime))) { uint64_t master_clock_id = 0; From 5b8d76cfbe45edf36e3be7f776d96b8c69e3b14f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Jun 2021 22:30:31 +0100 Subject: [PATCH 161/878] When the master clock returns to what the real anchor clock is, the difference between the actual and estimate is calculated and logged. Might have to actually use it... --- rtp.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index 8d8e0680..270866f5 100644 --- a/rtp.c +++ b/rtp.c @@ -1329,13 +1329,19 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // but if the master clock has become equal to the actual anchor clock // then we can reinstate it all + // first, calculate the cumulative offset after swapping all the clocks... + conn->anchor_time = conn->last_anchor_local_time + actual_offset; + // we can check how much of a deviation there was going from clock to clock and bakc around to the master clock + + if (actual_clock_id == conn->actual_anchor_clock) { - debug(1, "Master clock has become equal to the anchor clock"); + int64_t cumulative_deviation = conn->actual_anchor_time - conn->anchor_time; + debug(1, "Master clock has become equal to the anchor clock. The actual clock is %f ms ahead/behind (+/-) the calculated clock.", 0.000001 * cumulative_deviation); conn->anchor_clock = conn->actual_anchor_clock; conn->anchor_time = conn->actual_anchor_time; conn->anchor_rtptime = conn->actual_anchor_rtptime; } else { - conn->anchor_time = conn->last_anchor_local_time + actual_offset; + // conn->anchor_time = conn->last_anchor_local_time + actual_offset; // already done conn->anchor_clock = actual_clock_id; } } From 8bee92a4e650bd786c2a0899e7f2605b9c304dd0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:17:54 +0100 Subject: [PATCH 162/878] Clean up the statistics output to make it provide information for the stream type. Parameterise it so that it's a bit easier to manage. --- player.c | 251 ++++++++++++++++++++++++++++--------------------------- rtp.c | 48 +++++++---- 2 files changed, 161 insertions(+), 138 deletions(-) diff --git a/player.c b/player.c index a9822d68..f16c5021 100644 --- a/player.c +++ b/player.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -1560,6 +1561,54 @@ void player_thread_initial_cleanup_handler(__attribute__((unused)) void *arg) { conn->connection_number); } +char line_of_stats[1024]; +int statistics_row; // statistics_line 0 means print the headings; anything else 1 means print the + // values. Set to 0 the first time out. +int statistics_column; // used to index through the statistics_print_profile array to check if it + // should be printed +int was_a_previous_column; +int *statistics_print_profile; + +// these arrays specify which of the statistics specified by the statistics_item calls will actually +// be printed -- 1 means print, 0 means skip +int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; +int ap1_nosync_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; +int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0}; + +int ap2_realtime_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 1, 0, 0, 0}; +int ap2_realtime_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, + 1, 1, 0, 0, 0, 0, 0, 0}; + +int ap2_buffered_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 1, 0, 0, 0}; +int ap2_buffered_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 0, 0}; + +void statistics_item(const char *heading, const char *format, ...) { + if (statistics_print_profile[statistics_column] == 1) { // include this column? + if (was_a_previous_column != 0) + strcat(line_of_stats, ", "); + if (statistics_row == 0) { + strcat(line_of_stats, heading); + } else { + char b[1024]; + b[0] = 0; + va_list args; + va_start(args, format); + vsnprintf(b, sizeof(b), format, args); + va_end(args); + strcat(line_of_stats, b); + } + was_a_previous_column = 1; + } + statistics_column++; +} + void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { @@ -1907,55 +1956,45 @@ void *player_thread_func(void *arg) { conn->framesProcessedInThisEpoch = 0; conn->framesGeneratedInThisEpoch = 0; conn->correctionsRequestedInThisEpoch = 0; - + statistics_row = 0; // statistics_line 0 means print the headings; anything else 1 means print the + // values. Set to 0 the first time out. if (config.statistics_requested) { - if ((config.output->delay)) { - if (config.no_sync == 0) { - inform("sync error in milliseconds, " - "net correction in ppm, " - "corrections in ppm, " - "total packets, " - "missing packets, " - "late packets, " - "too late packets, " - "resend requests, " - "min DAC queue size, " - "min buffer occupancy, " - "max buffer occupancy, " - "source nominal frames per second, " - "source actual frames per second, " - "output frames per second, " - "source clock drift in ppm, " - "source clock drift sample count, " - "rough calculated correction in ppm"); + // decide on what statistics profile to use +#ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_2) { + if (conn->airplay_stream_type == realtime_stream) { + if (config.output->delay) { + if (config.no_sync == 0) + statistics_print_profile = ap2_realtime_synced_stream_statistics_print_profile; + else + statistics_print_profile = ap2_realtime_nosync_stream_statistics_print_profile; + } else { + statistics_print_profile = ap2_realtime_nodelay_stream_statistics_print_profile; + } } else { - inform("sync error in milliseconds, " - "total packets, " - "missing packets, " - "late packets, " - "too late packets, " - "resend requests, " - "min DAC queue size, " - "min buffer occupancy, " - "max buffer occupancy, " - "source nominal frames per second, " - "source actual frames per second, " - "source clock drift in ppm, " - "source clock drift sample count"); + if (config.output->delay) { + if (config.no_sync == 0) + statistics_print_profile = ap2_buffered_synced_stream_statistics_print_profile; + else + statistics_print_profile = ap2_buffered_nosync_stream_statistics_print_profile; + } else { + statistics_print_profile = ap2_buffered_nodelay_stream_statistics_print_profile; + } } } else { - inform("total packets, " - "missing packets, " - "late packets, " - "too late packets, " - "resend requests, " - "min buffer occupancy, " - "max buffer occupancy, " - "source nominal frames per second, " - "source actual frames per second, " - "source clock drift in ppm, " - "source clock drift sample count"); +#endif + if (config.output->delay) { + if (config.no_sync == 0) + statistics_print_profile = ap1_synced_statistics_print_profile; + else + statistics_print_profile = ap1_nosync_statistics_print_profile; + } else { + statistics_print_profile = ap1_nodelay_statistics_print_profile; + } +// airplay 1 stuff here +#ifdef CONFIG_AIRPLAY_2 } +#endif } #ifdef CONFIG_AIRPLAY_2 @@ -2726,87 +2765,51 @@ void *player_thread_func(void *arg) { (1.0 * tsum_of_insertions_and_deletions) / number_of_statistics; // double moving_average_drift = (1.0 * tsum_of_drifts) / number_of_statistics; // if ((play_number/print_interval)%20==0) - if (config.statistics_requested) { - if (at_least_one_frame_seen) { + // figure out which statistics profile to use, depending on the kind of stream - if ((config.output->delay)) { - if (config.no_sync == 0) { - inform( - "%*.2f," /* Sync error in milliseconds */ - "%*.1f," /* net correction in ppm */ - "%*.1f," /* corrections in ppm */ - "%*d," /* total packets */ - "%*" PRIu64 "," /* missing packets */ - "%*" PRIu64 "," /* late packets */ - "%*" PRIu64 "," /* too late packets */ - "%*" PRIu64 "," /* resend requests */ - "%*" PRIu64 "," /* min DAC queue size */ - "%*" PRId32 "," /* min buffer occupancy */ - "%*" PRId32 "," /* max buffer occupancy */ - "%*.2f," /* source nominal frame rate */ - "%*.2f," /* source actual (average) frame rate */ - "%*.2f," /* output frame rate */ - "%*.2f," /* source clock drift */ - "%*d," /* source clock drift sample count */ - "%*.2f", /* rough calculated correction in ppm */ - 10, 1000 * moving_average_sync_error / config.output_rate, 10, - moving_average_correction * 1000000 / (352 * conn->output_sample_ratio), 10, - moving_average_insertions_plus_deletions * 1000000 / - (352 * conn->output_sample_ratio), - 12, play_number, 7, conn->missing_packets, 7, conn->late_packets, 7, - conn->too_late_packets, 7, conn->resend_requests, 7, minimum_dac_queue_size, - 5, minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, - conn->remote_frame_rate, 11, conn->input_frame_rate, 11, conn->frame_rate, 10, - (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, - conn->local_to_remote_time_gradient_sample_count, 10, - (conn->frame_rate > 0.0) - ? ((conn->frame_rate - conn->remote_frame_rate * - conn->output_sample_ratio * - conn->local_to_remote_time_gradient) * - 1000000) / - conn->frame_rate - : 0.0); - } else { - inform("%*.2f," /* Sync error in milliseconds */ - "%*d," /* total packets */ - "%*" PRIu64 "," /* missing packets */ - "%*" PRIu64 "," /* late packets */ - "%*" PRIu64 "," /* too late packets */ - "%*" PRIu64 "," /* resend requests */ - "%*" PRIu64 "," /* min DAC queue size */ - "%*" PRId32 "," /* min buffer occupancy */ - "%*" PRId32 "," /* max buffer occupancy */ - "%*.2f," /* source nominal frame rate */ - "%*.2f," /* source actual (average) frame rate */ - "%*.2f," /* source clock drift */ - "%*d", /* source clock drift sample count */ - 10, 1000 * moving_average_sync_error / config.output_rate, 12, play_number, - 7, conn->missing_packets, 7, conn->late_packets, 7, conn->too_late_packets, - 7, conn->resend_requests, 7, minimum_dac_queue_size, 5, - minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, - conn->remote_frame_rate, 11, conn->input_frame_rate, 10, - (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, - conn->local_to_remote_time_gradient_sample_count); - } - } else { - inform("%*d," /* total packets */ - "%*" PRIu64 "," /* missing packets */ - "%*" PRIu64 "," /* late packets */ - "%*" PRIu64 "," /* too late packets */ - "%*" PRIu64 "," /* resend requests */ - "%*" PRId32 "," /* min buffer occupancy */ - "%*" PRId32 "," /* max buffer occupancy */ - "%*.2f," /* source nominal frame rate */ - "%*.2f," /* source actual (average) frame rate */ - "%*.2f," /* source clock drift */ - "%*d", /* source clock drift sample count */ - 12, play_number, 7, conn->missing_packets, 7, conn->late_packets, 7, - conn->too_late_packets, 7, conn->resend_requests, 5, - minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, - conn->remote_frame_rate, 11, conn->input_frame_rate, 10, - (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, - conn->local_to_remote_time_gradient_sample_count); - } + if (config.statistics_requested) { + + if (at_least_one_frame_seen) { + do { + line_of_stats[0] = '\0'; + statistics_column = 0; + was_a_previous_column = 0; + statistics_item("sync error ms", "%*.2f", 13, + 1000 * moving_average_sync_error / config.output_rate); + statistics_item("net sync ppm", "%*.1f", 12, + moving_average_correction * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item("all sync ppm", "%*.1f", 12, + moving_average_insertions_plus_deletions * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item(" packets", "%*d", 11, play_number); + statistics_item("missing", "%*" PRIu64 "", 7, conn->missing_packets); + statistics_item(" late", "%*" PRIu64 "", 6, conn->late_packets); + statistics_item("too late", "%*" PRIu64 "", 8, conn->too_late_packets); + statistics_item("resend reqs", "%*" PRIu64 "", 11, conn->resend_requests); + statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); + statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); + statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); + statistics_item("source nominal frames per second", "%*.2f", 10, + conn->remote_frame_rate); + statistics_item("source actual frames per second", "%*.2f", 10, + conn->input_frame_rate); + statistics_item("output fps", "%*.2f", 10, conn->frame_rate); + statistics_item("source clock drift ppm", "%*.2f", 9, + (conn->local_to_remote_time_gradient - 1.0) * 1000000); + statistics_item("source clock drift samples", "%*d", 5, + conn->local_to_remote_time_gradient_sample_count); + statistics_item( + "rough calculated correction in ppm", "%*.2f", 9, + (conn->frame_rate > 0.0) + ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * + conn->local_to_remote_time_gradient) * + 1000000) / + conn->frame_rate + : 0.0); + statistics_row++; + inform(line_of_stats); + } while (statistics_row < 2); } else { inform("No frames received in the last sampling interval."); } diff --git a/rtp.c b/rtp.c index 270866f5..91624463 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1327,16 +1327,36 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // we know its local time, so we use the new clocks's offset to // calculate what time that must be on the new clock - // but if the master clock has become equal to the actual anchor clock - // then we can reinstate it all - // first, calculate the cumulative offset after swapping all the clocks... - conn->anchor_time = conn->last_anchor_local_time + actual_offset; - // we can check how much of a deviation there was going from clock to clock and bakc around to the master clock + // Now, the thing is that while the anchor clock and master clock for a + // buffered session starts off the same, + // the master clock can change without the anchor clock changing. + // SPS allows the new master clock time to settle down and then + // calculates the appropriate offset to it by + // calculating back from the local anchor information and the new clock's + // advertised offset. Of course, small errors will occur. + // More importantly, the new master clock(s) and the original one will + // drift at different rates. So, after all this, if the original master + // clock becomes the master again, then there could be quite a difference + // in the time information that was calculated through all the clock + // changes and the actual master clock's time information. + // What do we do? We can hardly ignore this new and reliable information + // so we'll take it. Maybe we should add code to slowly correct towards it + // but at present, we just take it. + // So, if the master clock has again become equal to the actual anchor clock + // then we can reinstate it all. + // first, let us calculate the cumulative offset after swapping all the clocks... + conn->anchor_time = conn->last_anchor_local_time + actual_offset; + + // we can check how much of a deviation there was going from clock to clock and back + // around to the master clock if (actual_clock_id == conn->actual_anchor_clock) { - int64_t cumulative_deviation = conn->actual_anchor_time - conn->anchor_time; - debug(1, "Master clock has become equal to the anchor clock. The actual clock is %f ms ahead/behind (+/-) the calculated clock.", 0.000001 * cumulative_deviation); + int64_t cumulative_deviation = conn->anchor_time - conn->actual_anchor_time; + debug(1, + "Master clock has become equal to the anchor clock. The estimated clock time " + "was %f ms ahead(+) or behind (-) the real clock time.", + 0.000001 * cumulative_deviation); conn->anchor_clock = conn->actual_anchor_clock; conn->anchor_time = conn->actual_anchor_time; conn->anchor_rtptime = conn->actual_anchor_rtptime; From bcad9d5998cc64ac811380e27c3329e8f045bb98 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Jun 2021 09:21:14 +0100 Subject: [PATCH 163/878] Add 100 ms to the amount to flush after a large positive sync error to give the system a better chance to recover in one attempt. --- player.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/player.c b/player.c index f16c5021..4c13b2ef 100644 --- a/player.c +++ b/player.c @@ -2406,8 +2406,13 @@ void *player_thread_func(void *arg) { sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; + + // add 100 ms to give the pipeline a chance to recover -- a bit hacky + local_frames_to_drop += (conn->input_rate) * 100 / 1000; + uint32_t frames_to_drop_sized = local_frames_to_drop; do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); + } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, "Large negative sync error of: %" PRId64 From 7a26139c44519f3b717ff0a1287a8812d6e7a254 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Jun 2021 09:24:26 +0100 Subject: [PATCH 164/878] Shorten some arbitrary delays, quieten some debug messages, add (and comment out) disgnostic code to introduce a timing error. Make the buffer size 0.4 sec -- so that if a timing error of 150 ms is detected, enough frames are immediately available to be used. --- rtp.c | 54 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/rtp.c b/rtp.c index 91624463..8956ce08 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1790,7 +1790,6 @@ void *rtp_realtime_audio_receiver(void *arg) { ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { ssize_t response; - // usleep(1500000); if (pthread_mutex_lock(&descriptor->mutex) != 0) debug(1, "problem with mutex"); pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); @@ -2157,7 +2156,7 @@ void *rtp_buffered_audio_processor(void *arg) { av_format = AV_SAMPLE_FMT_S32; conn->input_bytes_per_frame = 8; // the output from the decoder will be input to the player conn->input_bit_depth = 32; - debug(1, "32-bit output format chosen"); + debug(2, "32-bit output format chosen"); break; case SPS_FORMAT_S16: case SPS_FORMAT_S16_LE: @@ -2208,8 +2207,9 @@ void *rtp_buffered_audio_processor(void *arg) { int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; - double requested_lead_time = 0.100; // normal lead time minimum - reset_buffer(conn); // in case there is any garbage in the player + double requested_lead_time = 0.10; // normal lead time minimum + reset_buffer(conn); // in case there is any garbage in the player + // int not_first_time_out = 0; do { int flush_is_delayed = 0; int flush_newly_requested = 0; @@ -2261,7 +2261,7 @@ void *rtp_buffered_audio_processor(void *arg) { if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block if (flushUntilSeq != seq_no) - debug(1, "flushUntilSeq %u reached or overshot at %u.", flushUntilSeq, seq_no); + debug(1, "flushUntilSeq %u overshot at %u.", flushUntilSeq, seq_no); conn->ap2_flush_requested = 0; flush_request_active = 0; flush_newly_requested = 0; @@ -2310,15 +2310,14 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, // player_buffer_occupancy); if (player_buffer_occupancy > - ((requested_lead_time + 0.10) * 44100.0 / 352)) { // must be greater than the lead time. + ((requested_lead_time + 0.4) * 44100.0 / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again - // debug(1,"sleep for 20 ms"); - usleep(20000); // wait for a while + usleep(1000); // wait for a while } else { if ((pcm_buffer_occupancy - pcm_buffer_read_point) >= (352 * conn->input_bytes_per_frame)) { new_buffer_needed = 0; // send a frame to the player if allowed - // it it's way too late, it means that a new anchor time is needed + // it it's way too late, it probably means that a new anchor time is needed /* uint32_t at_rtp = conn->reference_timestamp; @@ -2348,19 +2347,35 @@ void *rtp_buffered_audio_processor(void *arg) { // pcm_buffer_read_point_rtptime, expected_rtptime); //} // expected_rtptime = pcm_buffer_read_point_rtptime + 352; + + // this is a diagnostic for introducing a timing error that will force the + // processing chain to resync + // clang-format off + /* + if ((not_first_time_out == 0) && (blocks_read >= 20)) { + int timing_error = 150; + debug(1, "Connection %d: Introduce a timing error of %d milliseconds.", + conn->connection_number, timing_error); + if (timing_error >= 0) + pcm_buffer_read_point_rtptime += (conn->input_rate * timing_error) / 1000; + else + pcm_buffer_read_point_rtptime -= (conn->input_rate * (-timing_error)) / 1000; + not_first_time_out = 1; + } + */ + // clang-format on + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, pcm_buffer + pcm_buffer_read_point, 352, conn); streaming_has_started++; - usleep(2000); } } pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; } - // usleep(2000); // let other stuff happens } else { - usleep(20000); // wait before asking if play is enabled again + usleep(1000); // wait before asking if play is enabled again } } else { new_buffer_needed = 1; @@ -2599,7 +2614,6 @@ void *rtp_buffered_audio_processor(void *arg) { } } else { // nread is 0 -- the port has been closed - // finished = 1; debug(1, "buffered audio port closed!"); finished = 1; } From 13eccc30398f2d27df908f7ba9b913aaa428e884 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Jun 2021 09:52:05 +0100 Subject: [PATCH 165/878] Quieten a debug message. --- rtsp.c | 1 - 1 file changed, 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index a808c96f..ff855c3b 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3532,7 +3532,6 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { char *hdr = msg_get_header(req, "Apple-Challenge"); if (!hdr) return; - debug(1, "Apple Challenge"); SOCKADDR fdsa; socklen_t sa_len = sizeof(fdsa); getsockname(fd, (struct sockaddr *)&fdsa, &sa_len); From 516a6b2105500dd637599fef1fac488e523e1a51 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Jun 2021 09:52:38 +0100 Subject: [PATCH 166/878] Align some of the statistics better. --- player.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/player.c b/player.c index 4c13b2ef..e42dde96 100644 --- a/player.c +++ b/player.c @@ -2795,17 +2795,17 @@ void *player_thread_func(void *arg) { statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); - statistics_item("source nominal frames per second", "%*.2f", 10, + statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); - statistics_item("source actual frames per second", "%*.2f", 10, + statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); - statistics_item("output fps", "%*.2f", 10, conn->frame_rate); - statistics_item("source clock drift ppm", "%*.2f", 9, + statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); + statistics_item("source drift ppm", "%*.2f", 16, (conn->local_to_remote_time_gradient - 1.0) * 1000000); - statistics_item("source clock drift samples", "%*d", 5, + statistics_item("drift samples", "%*d", 13, conn->local_to_remote_time_gradient_sample_count); statistics_item( - "rough calculated correction in ppm", "%*.2f", 9, + "estimated (unused) correction ppm", "%*.2f", strlen("estimated (unused) correction ppm"), (conn->frame_rate > 0.0) ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * conn->local_to_remote_time_gradient) * From 2c930c959c81776052f11ba66f12ed960983812c Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 3 Jun 2021 16:46:31 +0100 Subject: [PATCH 167/878] Updated readme. --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 4e10daae..1ea8f984 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,7 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=charles-dev -t registry.nova.do/shairport-sync:unstable-development . +docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=unstable-development -t registry.nova.do/shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file From 65e66c4845878d7fbf8e19709ae8be60def94022 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:38:33 +0100 Subject: [PATCH 168/878] Updates to docker build process. --- .github/workflows/docker-build.yaml | 2 +- docker/Dockerfile | 2 +- docker/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 772586e1..98a4041e 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -39,4 +39,4 @@ jobs: tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: github=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file + ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index d7df1481..9393f072 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -54,7 +54,7 @@ COPY --from=builder-alac /usr/local/include /usr/local/include # TODO: Remove when no longer pulling from private repos. RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts -RUN --mount=type=ssh,id=github git clone git@github.com:mikebrady/nqptp.git +RUN --mount=type=ssh git clone git@github.com:mikebrady/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development diff --git a/docker/README.md b/docker/README.md index 1ea8f984..8172ec08 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,7 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=unstable-development -t registry.nova.do/shairport-sync:unstable-development . +docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file From b67e437fcc25c643d441847d678e0f0e2728034c Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Fri, 4 Jun 2021 00:50:52 +0100 Subject: [PATCH 169/878] Updated docker build action. --- .github/workflows/docker-build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 98a4041e..fc670487 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up ssh agent From d33de05ae9bcb5b508b7981c594521d4e65fdc5d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 4 Jun 2021 09:40:24 +0100 Subject: [PATCH 170/878] change some debug messages --- activity_monitor.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 5d178e5d..2f5efddf 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -169,11 +169,11 @@ void *activity_monitor_thread_code(void *arg) { do { switch (state) { case am_inactive: - // debug(1,"am_state: am_inactive"); + debug(2,"am_state: am_inactive"); while (player_state != ps_active) pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex); state = am_active; - // going_active(); // this is done in activity_monitor_signify_activity + debug(2,"am_state: going active"); break; case am_active: // debug(1,"am_state: am_active"); @@ -181,7 +181,6 @@ void *activity_monitor_thread_code(void *arg) { pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex); if (config.active_state_timeout == 0.0) { state = am_inactive; - // going_inactive(); // this is done in activity_monitor_signify_activity } else { state = am_timing_out; @@ -204,7 +203,6 @@ void *activity_monitor_thread_code(void *arg) { } break; case am_timing_out: - // debug(1,"am_state: am_timing_out"); rc = 0; while ((player_state != ps_active) && (rc != ETIMEDOUT)) { #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD From e4c82b7700c2bf78454323fbee5adf4e9751ee0c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 4 Jun 2021 09:41:15 +0100 Subject: [PATCH 171/878] Add some comments and a new debug message about underrun --- audio_alsa.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 2d07c8f4..31b18da8 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1659,14 +1659,15 @@ int do_play(void *buf, int samples) { if ((samples != 0) && (buf != NULL)) { - // jut check the state of the DAC + // just check the state of the DAC if ((state != SND_PCM_STATE_PREPARED) && (state != SND_PCM_STATE_RUNNING) && (state != SND_PCM_STATE_XRUN)) { debug(1, "alsa: DAC in odd SND_PCM_STATE_* %d prior to writing.", state); } - // debug(3, "write %d frames.", samples); + snd_pcm_state_t prior_state = state; // keep this for afterwards.... + debug(3, "alsa: write %d frames.", samples); ret = alsa_pcm_write(alsa_handle, buf, samples); if (ret == samples) { stall_monitor_frame_count += samples; @@ -1699,7 +1700,25 @@ int do_play(void *buf, int samples) { frame_index = 0; measurement_data_is_valid = 0; if (ret == -EPIPE) { /* underrun */ - debug(1, "alsa: underrun while writing %d samples to alsa device.", samples); + + // It could be that the DAC was in the SND_PCM_STATE_XRUN state before + // sending the samples to be output. If so, it will still be in + // the SND_PCM_STATE_XRUN state after the call and it needs to be recovered. + + // The underrun occurred in the past, so flagging an + // error at this point is misleading. + + // In fact, having put samples in the buffer, we are about to fix it by now + // issuing a snd_pcm_recover(). + + // So, if state is SND_PCM_STATE_XRUN now, only report it if the state was + // not SND_PCM_STATE_XRUN prior to the call, i.e. report it only + // if we are not trying to recover from a previous underrun. + + if (prior_state == SND_PCM_STATE_XRUN) + debug(2, "alsa: recovering from a previous underrun."); + else + debug(1, "alsa: underrun while writing %d samples to alsa device.", samples); int tret = snd_pcm_recover(alsa_handle, ret, 1); if (tret < 0) { warn("alsa: can't recover from SND_PCM_STATE_XRUN: %s.", snd_strerror(tret)); From 142478933ffdbc7f924f3178aade1222bc58d2ff Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 4 Jun 2021 09:42:31 +0100 Subject: [PATCH 172/878] Move the activity monitor calls to enable active mode correctly for buffered audio. --- player.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/player.c b/player.c index e42dde96..59acd7d5 100644 --- a/player.c +++ b/player.c @@ -3139,8 +3139,7 @@ int player_prepare_to_play(rtsp_conn_info *conn) { if (config.buffer_start_fill > BUFFER_FRAMES) die("specified buffer starting fill %d > buffer size %d", config.buffer_start_fill, BUFFER_FRAMES); - activity_monitor_signify_activity( - 1); // active, and should be before play's command hook, command_start() + // active, and should be before play's command hook, command_start() command_start(); conn->input_bytes_per_frame = 4; // default -- may be changed later // call on the output device to prepare itself @@ -3190,7 +3189,6 @@ int player_stop(rtsp_conn_info *conn) { #endif // debuglev = dl; command_stop(); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() return 0; } else { debug(3, "Connection %d: player thread already deleted.", conn->connection_number); From 0f1118c142b356d5185c634a0b30313c44e382b4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 6 Jun 2021 15:48:09 +0100 Subject: [PATCH 173/878] Exit immediately if the speed or format cannot be set. --- audio_alsa.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 31b18da8..6bc1d97d 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -559,7 +559,7 @@ int actual_open_alsa_device(int do_auto_setup) { if (ret == 0) { speed_found = 1; if (actual_sample_rate != speeds[i]) - warn("Speed requested: %d. Speed available: %d.", speeds[i], actual_sample_rate); + die("The frame rate of the output DAC can not be set to a multiple of 44100 fps. (The nearest speed available is: %d fps.)", speeds[i], actual_sample_rate); } else { i++; } @@ -641,7 +641,7 @@ int actual_open_alsa_device(int do_auto_setup) { } if (actual_sample_rate != config.output_rate) { - warn("Can't set the D/A converter to sample rate %d.", config.output_rate); + die("Can't set the output DAC to the requested frame rate of %d fps.", config.output_rate); return -EINVAL; } From 6edaa585958d38c27f8ea6a6c84c5933116881db Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 6 Jun 2021 15:49:03 +0100 Subject: [PATCH 174/878] increase the wait time for the shm access mutex. --- ptp-utilities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 12a0890c..9890028d 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -63,7 +63,7 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; - pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 10000, 1); + pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 20000, 1); if (actual_clock_id != NULL) *actual_clock_id = 0; if (raw_offset != NULL) From 93aefbd8eaccc04edbb16973a27355403a734f81 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 6 Jun 2021 15:50:05 +0100 Subject: [PATCH 175/878] get the activity monitor working for the different types of stream --- rtsp.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index ff855c3b..22c6e611 100644 --- a/rtsp.c +++ b/rtsp.c @@ -51,6 +51,7 @@ #include #include "config.h" +#include "activity_monitor.h" #ifdef CONFIG_OPENSSL #include @@ -1214,6 +1215,7 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (conn->player_thread) warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); else { + activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); // the thread better be 0 } @@ -1502,9 +1504,11 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message conn->ap2_rate = rate; if ((rate & 1) != 0) { debug(2, "Connection %d: Start playing.", conn->connection_number); + activity_monitor_signify_activity(1); conn->ap2_play_enabled = 1; } else { debug(2, "Connection %d: Stop playing.", conn->connection_number); + activity_monitor_signify_activity(0); conn->ap2_play_enabled = 0; // not sure this is needed yet // reset_anchor_info(conn); // needed if the player resumes @@ -1844,6 +1848,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag if (streams) { // we are being asked to close a stream player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() if (conn->session_key) { free(conn->session_key); conn->session_key = NULL; @@ -1893,6 +1898,7 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message "TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).", conn->connection_number); player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", conn->connection_number); } else { @@ -2048,7 +2054,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) ptp_send_control_message_string(conn->ap2_timing_peer_list_message); else debug(1, "No timing peer list!"); - + activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); @@ -2072,11 +2078,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // hack. conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; + conn->input_rate = 44100; // we are stuck with this for the moment. conn->input_num_channels = 2; conn->input_bit_depth = 16; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - + activity_monitor_signify_activity(1); player_prepare_to_play( conn); // get capabilities of DAC before creating the buffered audio thread @@ -2088,6 +2094,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(stream0dict, "audioBufferSize", plist_new_uint(conn->ap2_audio_buffer_size)); + + // this should be cancelled by an activity_monitor_signify_activity(1) + // call in the SETRATEANCHORI handler, which should come up right away + activity_monitor_signify_activity(0); + player_play(conn); conn->rtp_running = 1; // hack! @@ -3788,8 +3799,10 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); - if (conn->player_thread) + if (conn->player_thread) { player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + } debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", conn->connection_number); From 5c48580db95cb12716e60bbe53e08f127283bfbe Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 6 Jun 2021 15:51:39 +0100 Subject: [PATCH 176/878] Get it working for multiples of 44100 again. It's stuck accepting only 44,100 fps input for the moment. --- player.c | 74 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/player.c b/player.c index 59acd7d5..3da839df 100644 --- a/player.c +++ b/player.c @@ -96,6 +96,29 @@ #include "activity_monitor.h" +// sign extending rtptime calculations to 64 bit is needed from time to time. + +// the standard rtptime is unsigned 32 bits, +// so you can do modulo 2^32 difference calculations +// and get a signed result simply by typing the result as a signed 32-bit number + +// So long as you can be sure the numbers are within 2^31 of each other, +// the sign of the result calculated in this way indicates which comes after which +// so if you subtract a from b and the result is positive, +// b is the same as or comes after a in module 2^32 order. + +// We want to do the same with the rtptime calculations for multiples of +// the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit +// so we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 but unsigned +// numbers on the same basis + +// See http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend +// and https://stackoverflow.com/a/31655073/2942008 +// of which this is a slight variation + +#define SIGNEX(v, sb) (((v) & ((1 << (sb + 1)) - 1)) | (((v) & (1 << (sb))) ? ~((1 << (sb))-1) : 0)) + + // make the first audio packet deliberately early to bias the sync error of // the very first packet, making the error more likely to be too early // rather than too late. It it's too early, @@ -1082,7 +1105,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); int64_t lateness = local_time_now - conn->first_packet_time_to_play; @@ -1787,7 +1810,28 @@ void *player_thread_func(void *arg) { conn->output_sample_ratio = config.output_rate / conn->input_rate; - // debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); + int output_rtptime_sign_bit; + switch (conn->output_sample_ratio) { + case 1: + output_rtptime_sign_bit = 31; + break; + case 2: + output_rtptime_sign_bit = 32; + break; + case 4: + output_rtptime_sign_bit = 33; + break; + case 8: + output_rtptime_sign_bit = 34; + break; + default: + debug(1, "error with output ratio -- can't calculate sign bit number"); + output_rtptime_sign_bit = 31; + break; + } + + debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); + debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); conn->max_frame_size_change = 1 * conn->output_sample_ratio; // we add or subtract one frame at the nominal @@ -2304,17 +2348,19 @@ void *player_thread_func(void *arg) { if (resp == 0) { uint32_t should_be_frame_32; + // this is denominated in the frame rate of the incoming stream local_time_to_frame(local_time_now, &should_be_frame_32, conn); - int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; - int64_t delay = - should_be_frame - (inframe->given_timestamp * conn->output_sample_ratio - - current_delay); // all int64_t + int64_t should_be_frame = should_be_frame_32; + should_be_frame = should_be_frame * conn->output_sample_ratio; - sync_error = delay; // int64_t from int64_t - int32_t, so okay + // current_delay is denominated in the frame rate of the outgoing stream + int64_t will_be_frame = inframe->given_timestamp; + will_be_frame = will_be_frame * conn->output_sample_ratio; + will_be_frame = (will_be_frame - current_delay) & ((1 << (output_rtptime_sign_bit + 1)) - 1); - // debug(1, "Sync error on frame %u is %" PRId64 " frames.", inframe->given_timestamp, - // sync_error); + sync_error = + SIGNEX(should_be_frame - will_be_frame, output_rtptime_sign_bit) ; // all int64_t if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; @@ -2376,16 +2422,6 @@ void *player_thread_func(void *arg) { if ((config.no_sync == 0) && (inframe->given_timestamp != 0) && (config.resyncthreshold > 0.0) && (abs_sync_error > config.resyncthreshold * config.output_rate)) { - /* - if (abs_sync_error > 3 * config.output_rate) { - - warn("Very large sync error: %" PRId64 " frames, with should_be_frame: %" PRId64 - ", nt: %" PRId64 ", current_delay: %" PRId64 ", given timestamp %" PRIX32 - ", reference timestamp %" PRIX32 ", should_be_frame %" PRIX32 ".", - sync_error, should_be_frame, nt, current_delay, inframe->given_timestamp, - reference_timestamp, should_be_frame_32); - } - */ sync_error_out_of_bounds++; } else { sync_error_out_of_bounds = 0; From f6a9f634c7eab0f547e46876b3e81f04595762ac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 6 Jun 2021 15:53:55 +0100 Subject: [PATCH 177/878] Accept a new master clock immediately. Don't truncate the first three blocks if it's a normal start. Hacky. --- rtp.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/rtp.c b/rtp.c index 8956ce08..f363fa00 100644 --- a/rtp.c +++ b/rtp.c @@ -1284,9 +1284,10 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // using the master clock timing if (actual_clock_id == conn->anchor_clock) { - // if the master clock and the anchor clock are the same - // wait at least this time before using the new master clock - if (duration_of_mastership < 700000000) { + //if the master clock and the anchor clock are the same + //wait at least this time before using the new master clock + if (duration_of_mastership < 0000000) { + debug(1,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { @@ -1414,7 +1415,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, debug(1, "Connection %d: NQPTP clock information is not valid.", conn->connection_number); break; default: - debug(3, "Connection %d: NQPTP clock reports an unrecognised status: %u.", + debug(1, "Connection %d: NQPTP clock reports an unrecognised status: %u.", conn->connection_number, response); break; } @@ -2139,8 +2140,8 @@ void *rtp_buffered_audio_processor(void *arg) { av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); - av_opt_set_int(swr, "in_sample_rate", 44100, 0); - av_opt_set_int(swr, "out_sample_rate", config.output_rate, 0); + av_opt_set_int(swr, "in_sample_rate", conn->input_rate, 0); + av_opt_set_int(swr, "out_sample_rate", conn->input_rate, 0); // must match or the timing will be wrong` av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); enum AVSampleFormat av_format; @@ -2202,12 +2203,13 @@ void *rtp_buffered_audio_processor(void *arg) { // uint32_t expected_rtptime; uint64_t blocks_read = 0; + uint64_t blocks_read_since_flush = 0; int flush_requested = 0; int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; - double requested_lead_time = 0.10; // normal lead time minimum + double requested_lead_time = 0.3; // normal lead time minimum reset_buffer(conn); // in case there is any garbage in the player // int not_first_time_out = 0; do { @@ -2279,7 +2281,8 @@ void *rtp_buffered_audio_processor(void *arg) { // do this outside the flush mutex if (flush_newly_complete) { debug(2, "Flush Complete."); - blocks_read = 0; + blocks_read_since_flush = 0; + } if (play_newly_stopped != 0) @@ -2307,10 +2310,9 @@ void *rtp_buffered_audio_processor(void *arg) { // is there space in the player thread's buffer system? unsigned int player_buffer_size, player_buffer_occupancy; get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); - // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, - // player_buffer_occupancy); + //debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, player_buffer_occupancy); if (player_buffer_occupancy > - ((requested_lead_time + 0.4) * 44100.0 / 352)) { // must be greater than the lead time. + ((requested_lead_time + 0.4) * conn->input_rate / 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again usleep(1000); // wait for a while } else { @@ -2331,8 +2333,10 @@ void *rtp_buffered_audio_processor(void *arg) { if (frame_to_local_time(pcm_buffer_read_point_rtptime, &buffer_should_be_time, conn) == 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); - // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); - if (blocks_read > 3) { + // debug(3,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); + // if it's the very first block (thus no priming needed) + if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { + //if (1) { if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || (streaming_has_started != 0)) { if (streaming_has_started == 0) @@ -2373,6 +2377,8 @@ void *rtp_buffered_audio_processor(void *arg) { pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; + } else { + debug(1,"frame to local time error"); } } else { usleep(1000); // wait before asking if play is enabled again @@ -2427,6 +2433,7 @@ void *rtp_buffered_audio_processor(void *arg) { errno, errorstring); } else if (nread > 0) { blocks_read++; // note, this doesn't mean they are valid audio blocks + blocks_read_since_flush++; // debug(1, "Realtime Audio Receiver Packet of length %d received.", nread); // now get hold of its various bits and pieces /* From aa8d251aefff0ce1875918d218eff869d7076060 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 8 Jun 2021 14:14:23 +0100 Subject: [PATCH 178/878] Check for a version of libplist that is recent enough. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 6b7c6d07..6adaab22 100644 --- a/configure.ac +++ b/configure.ac @@ -383,7 +383,7 @@ 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_MSG_RESULT(>>Include libraries required for AirPlay 2) - AC_CHECK_LIB([plist], [plist_from_memory], , AC_MSG_ERROR(AirPlay 2 support requires libplist -- libplist-dev suggested!)) + PKG_CHECK_MODULES([plist], [libplist >= 2.0.0],[CFLAGS="${plist_CFLAGS} ${CFLAGS}" LIBS="${plist_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- libplist-dev suggested!)]) AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [AC_MSG_ERROR([AirPlay 2 support requires libavcodec -- libavcodec-dev suggested])]) From c37ee4542dd31c1ec84146648c0043930d7b33a1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 9 Jun 2021 19:35:08 +0100 Subject: [PATCH 179/878] Add code to skip packets of buffered audio that are late or too early. --- configure.ac | 2 +- player.c | 4 ++-- rtp.c | 25 ++++++++++++++++++------- rtsp.c | 3 +-- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/configure.ac b/configure.ac index 6adaab22..5849019a 100644 --- a/configure.ac +++ b/configure.ac @@ -382,7 +382,7 @@ 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_MSG_RESULT(>>Include libraries required for AirPlay 2) + AC_MSG_RESULT(>>Include libraries required for AirPlay 2) PKG_CHECK_MODULES([plist], [libplist >= 2.0.0],[CFLAGS="${plist_CFLAGS} ${CFLAGS}" LIBS="${plist_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- libplist-dev suggested!)]) AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) diff --git a/player.c b/player.c index 3da839df..f693d528 100644 --- a/player.c +++ b/player.c @@ -1830,8 +1830,8 @@ void *player_thread_func(void *arg) { break; } - debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); - debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); +// debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); +// debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); conn->max_frame_size_change = 1 * conn->output_sample_ratio; // we add or subtract one frame at the nominal diff --git a/rtp.c b/rtp.c index f363fa00..2f3ddd24 100644 --- a/rtp.c +++ b/rtp.c @@ -2206,6 +2206,7 @@ void *rtp_buffered_audio_processor(void *arg) { uint64_t blocks_read_since_flush = 0; int flush_requested = 0; + uint32_t timestamp; int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; @@ -2220,7 +2221,7 @@ void *rtp_buffered_audio_processor(void *arg) { // are we in in flush mode, or just about to leave it? debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; - // uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; + uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; int flush_request_active = 0; if (conn->ap2_flush_requested) { @@ -2263,7 +2264,9 @@ void *rtp_buffered_audio_processor(void *arg) { if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block if (flushUntilSeq != seq_no) - debug(1, "flushUntilSeq %u overshot at %u.", flushUntilSeq, seq_no); + debug(2, "flush request ended with flushUntilSeq %u overshot at %u, flushUntilTS: %u, incoming timestamp: %u.", flushUntilSeq, seq_no, flushUntilTS, timestamp); + else + debug(2, "flush request ended with flushUntilSeq, flushUntilTS: %u, incoming timestamp: %u", flushUntilSeq, flushUntilTS, timestamp); conn->ap2_flush_requested = 0; flush_request_active = 0; flush_newly_requested = 0; @@ -2333,7 +2336,12 @@ void *rtp_buffered_audio_processor(void *arg) { if (frame_to_local_time(pcm_buffer_read_point_rtptime, &buffer_should_be_time, conn) == 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); - // debug(3,"lead time in buffered_audio is %f milliseconds.", lead_time * 0.000001); + double lead_time_ms = lead_time * 0.000001; + // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time_ms); + + // it seems that some garbage blocks can be left after the flush, so if they + // don't have sensible lead times, drop them + if ((lead_time_ms < 5000.0) && (lead_time > -1000.0)) { // if it's the very first block (thus no priming needed) if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { //if (1) { @@ -2374,6 +2382,9 @@ void *rtp_buffered_audio_processor(void *arg) { streaming_has_started++; } } + } else { + debug(2,"Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); + } pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; @@ -2443,7 +2454,7 @@ void *rtp_buffered_audio_processor(void *arg) { uint8_t csrc_count = packet[0] & 0b00001111; */ seq_no = packet[1] * (1 << 16) + packet[2] * (1 << 8) + packet[3]; - uint32_t timestamp = nctohl(&packet[4]); + timestamp = nctohl(&packet[4]); // debug(1, "immediately: block %u, rtptime %u", seq_no, timestamp); // uint32_t ssrc = nctohl(&packet[8]); // uint8_t marker = 0; @@ -2461,10 +2472,10 @@ void *rtp_buffered_audio_processor(void *arg) { // play enabled will be off when this is a full flush and the anchor information is not // valid int64_t lead_time = should_be_time - get_absolute_time_in_ns(); - debug(2, - "flush completed to seq: %u with rtptime: %u, lead time: 0x%" PRIx64 + debug(1, + "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: 0x%" PRIx64 " nanoseconds, i.e. %f sec.", - seq_no, timestamp, lead_time, lead_time * 0.000000001); + seq_no, flushUntilTS, timestamp, lead_time, lead_time * 0.000000001); } else { debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); } diff --git a/rtsp.c b/rtsp.c index 22c6e611..d0cc762c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1445,7 +1445,6 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: SETRATEANCHORI %s :: Content-Length %d", conn->connection_number, req->path, req->contentlength); - debug_log_rtsp_message(2, "SETRATEANCHORI request", req); plist_t messagePlist = plist_from_rtsp_content(req); @@ -1953,7 +1952,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); - + // debug_log_rtsp_message(1, "Connection %d: SETUP (AirPlay 2) SETUP incoming message", req); // we need to get the timing peer interfaces. // I'm guessing they are all this device's adresses that are on the same subnets as // the timing peer info in the accompanying plist From 0ac00359f7bc98298b7d2505ca27a572acb5f61d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 17 Jun 2021 18:16:15 +0100 Subject: [PATCH 180/878] remove those warnings --- player.c | 66 +++++++++++++++++--------------------------------------- 1 file changed, 20 insertions(+), 46 deletions(-) diff --git a/player.c b/player.c index f693d528..cd09bf8b 100644 --- a/player.c +++ b/player.c @@ -159,39 +159,13 @@ static void ab_resync(rtsp_conn_info *conn) { conn->ab_buffering = 1; } -// given starting and ending points as unsigned 16-bit integers running modulo 2^16, returns the -// position of x in the interval in *pos -// returns true if x is actually within the buffer - -int position_in_modulo_uint16_t_buffer(uint16_t x, uint16_t start, uint16_t end, uint16_t *pos) { - int response = 0; - - int16_t x_disp = x - start; - int16_t b_size = - end - start; // this will be positive as long as (a) start and end are within 2^15 of each - // other and (b) end is at or after start on the modulo wheel - if ((x_disp >= 0) && (x_disp < b_size)) - response = 1; - return response; -} -// wrapped number between two seq_t. -// as long as the two seq_t numbers are supposed to be within 2^15 of one another, -// that is, you go the shorter way around the 2^16 modulo wheel, -// this will return positive if b is equal to or after a and negative otherwise -int32_t seq_diff(seq_t a, seq_t b, seq_t base) { - int16_t d = b - a; - return d; -} - // the sequence numbers will wrap pretty often. // this returns true if the second arg is strictly after the first -static inline int seq_order(seq_t a, seq_t b, seq_t base) { +static inline int is_after(seq_t a, seq_t b) { int16_t d = b - a; return d > 0; } -static inline seq_t seq_sum(seq_t a, seq_t b) { return a + b; } - void reset_input_flow_metrics(rtsp_conn_info *conn) { conn->play_number_after_flush = 0; conn->packet_count_since_flush = 0; @@ -491,13 +465,13 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; abuf = conn->audio_buffer + BUFIDX(seqno); conn->ab_write = seqno + 1; // move the write pointer to the next free space - } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected - int32_t gap = seq_diff(conn->ab_write, seqno, conn->ab_read); + } else if (is_after(conn->ab_write, seqno)) { // newer than expected + int32_t gap = seqno - conn->ab_write; if (gap <= 0) debug(1, "Unexpected gap size: %d.", gap); int i; for (i = 0; i < gap; i++) { - abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); + abuf = conn->audio_buffer + BUFIDX(conn->ab_write + i); abuf->ready = 0; // to be sure, to be sure abuf->resend_request_number = 0; abuf->initialisation_time = @@ -507,13 +481,11 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta abuf->given_timestamp = 0; abuf->sequence_number = 0; } - // debug(1,"N %d s %u.",seq_diff(ab_write,PREDECESSOR(seqno))+1,ab_write); abuf = conn->audio_buffer + BUFIDX(seqno); // rtp_request_resend(ab_write, gap); // resend_requests++; conn->ab_write = seqno + 1; - } else if (seq_order(conn->ab_read, seqno, - conn->ab_read)) { // older than expected but not too late + } else if (is_after(conn->ab_read, seqno)) { // older than expected but not too late conn->late_packets++; abuf = conn->audio_buffer + BUFIDX(seqno); } else { // too late. @@ -563,17 +535,19 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta (uint64_t)1000000000); uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)1000000000); latency_time = latency_time / (uint64_t)conn->input_rate; - - int x; // this is the first frame to be checked - // if we detected a first empty frame before and if it's still in the buffer! - if ((first_possibly_missing_frame >= 0) && - (position_in_modulo_uint16_t_buffer(first_possibly_missing_frame, conn->ab_read, - conn->ab_write, NULL))) { - x = first_possibly_missing_frame; - } else { - x = conn->ab_read; + + // find the first frame that is missing, if known + int x = conn->ab_read; + if (first_possibly_missing_frame >= 0) { + // if it's within the range + int16_t buffer_size = conn->ab_write - conn->ab_read; // must be positive + if (buffer_size >= 0) { + int16_t position_in_buffer = first_possibly_missing_frame - conn->ab_read; + if ((position_in_buffer >=0) && (position_in_buffer < buffer_size)) + x = first_possibly_missing_frame; + } } - + first_possibly_missing_frame = -1; // has not been set int missing_frame_run_count = 0; @@ -1389,7 +1363,7 @@ static inline int32_t mean_32(int32_t a, int32_t b) { int64_t mean = (al + bl) / 2; int32_t r = (int32_t)mean; if (r != mean) - debug(1, "Error calculating average of two int32_ts"); + debug(1, "Error calculating average of two int32_t values: %d, %d.", a, b); return r; } @@ -2290,8 +2264,8 @@ void *player_thread_func(void *arg) { } } - conn->buffer_occupancy = - seq_diff(conn->ab_read, conn->ab_write, conn->ab_read); // int32_t from int32 + int16_t bo = conn->ab_write - conn->ab_read; // do this in 16 bits + conn->buffer_occupancy = bo; // 32 bits if (conn->buffer_occupancy < minimum_buffer_occupancy) minimum_buffer_occupancy = conn->buffer_occupancy; From ad3412f5c2986e197f17b3e5e465d17c2420d275 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 17 Jun 2021 18:17:24 +0100 Subject: [PATCH 181/878] Ensure a new master clock is at least 1.1 seconds in mastership before using it. Remember that the mastership can be backdated. --- rtp.c | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/rtp.c b/rtp.c index 2f3ddd24..74110cfe 100644 --- a/rtp.c +++ b/rtp.c @@ -1284,10 +1284,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // using the master clock timing if (actual_clock_id == conn->anchor_clock) { - //if the master clock and the anchor clock are the same - //wait at least this time before using the new master clock - if (duration_of_mastership < 0000000) { - debug(1,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); + // if the master clock and the anchor clock are the same + // wait at least this time before using the new master clock + // note that mastershipo may be backdated + if (duration_of_mastership < 1100000000) { + debug(3,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { @@ -1365,6 +1366,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // conn->anchor_time = conn->last_anchor_local_time + actual_offset; // already done conn->anchor_clock = actual_clock_id; } + conn->anchor_clock_is_new = 0; } } else { response = clock_not_valid; // no current clock information and no previous clock info @@ -1442,8 +1444,8 @@ int have_ptp_timing_information(rtsp_conn_info *conn) { int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { int result = -1; - uint32_t anchor_rtptime; - uint64_t anchor_local_time; + uint32_t anchor_rtptime = 0; + uint64_t anchor_local_time = 0; if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) { int32_t frame_difference = timestamp - anchor_rtptime; int64_t time_difference = frame_difference; @@ -1460,8 +1462,8 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { int result = -1; - uint32_t anchor_rtptime; - uint64_t anchor_local_time; + uint32_t anchor_rtptime = 0; + uint64_t anchor_local_time = 0; if (get_ptp_anchor_local_time_info(conn, &anchor_rtptime, &anchor_local_time) == clock_ok) { int64_t time_difference = time - anchor_local_time; int64_t frame_difference = time_difference; @@ -2130,7 +2132,7 @@ void *rtp_buffered_audio_processor(void *arg) { int dst_linesize; int dst_bufsize; - // Prepare software resampler to convert floating point (?) to S16 + // Prepare software resampler to convert floating point (?) SwrContext *swr = swr_alloc(); if (swr == NULL) { debug(1, "can not allocate a swr context"); @@ -2211,7 +2213,7 @@ void *rtp_buffered_audio_processor(void *arg) { int play_enabled = 0; uint32_t flush_from_timestamp; double requested_lead_time = 0.3; // normal lead time minimum - reset_buffer(conn); // in case there is any garbage in the player + reset_buffer(conn); // in case there is any garbage in the player // int not_first_time_out = 0; do { int flush_is_delayed = 0; @@ -2338,13 +2340,12 @@ void *rtp_buffered_audio_processor(void *arg) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); double lead_time_ms = lead_time * 0.000001; // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time_ms); - - // it seems that some garbage blocks can be left after the flush, so if they - // don't have sensible lead times, drop them + + // it seems that some garbage blocks can be left after the flush, so + // only accept them if they have sensible lead times if ((lead_time_ms < 5000.0) && (lead_time > -1000.0)) { // if it's the very first block (thus no priming needed) if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { - //if (1) { if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || (streaming_has_started != 0)) { if (streaming_has_started == 0) From e925e196c837df5ec7e876aff13eb90d8054dfcd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:49:11 +0100 Subject: [PATCH 182/878] Changes to allow it to compile in FreeBSD and fix a few issues that were picked up by clang 10. --- common.c | 29 ++++++++++++++++++++++++++++- configure.ac | 14 ++++++++++---- dacp.c | 2 +- definitions.h | 17 ++++++++++++----- pair_ap | 2 +- ptp-utilities.c | 5 +++++ rtp.c | 10 ++++++---- shairport.c | 5 +++++ 8 files changed, 68 insertions(+), 16 deletions(-) diff --git a/common.c b/common.c index f87b7d29..fcbf4fb5 100644 --- a/common.c +++ b/common.c @@ -49,7 +49,16 @@ #include #include + +#ifdef COMPILE_FOR_LINUX #include +#endif + +#ifdef COMPILE_FOR_FREEBSD +#include +#include +#include +#endif #ifdef COMPILE_FOR_OSX #include @@ -1954,6 +1963,7 @@ int get_device_id(uint8_t *id, int int_length) { t = id; int found = 0; for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + #ifdef AF_PACKET if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; if ((strcmp(ifa->ifa_name, "lo") != 0) && (found == 0)) { @@ -1963,8 +1973,25 @@ int get_device_id(uint8_t *id, int int_length) { found = 1; } } + #else + #ifdef AF_LINK + struct sockaddr_dl * sdl = (struct sockaddr_dl *) ifa->ifa_addr; + if ((sdl) && (sdl->sdl_family == AF_LINK)) { + if (sdl->sdl_type == IFT_ETHER) { + char *s = LLADDR(sdl); + for (i = 0; i < sdl->sdl_alen; i++) { + debug(1,"char %d: \"%c\".", i, *s); + *t++ = (uint8_t)*s++; + } + found = 1; + } + } + #endif + #endif + } freeifaddrs(ifaddr); } return response; -} \ No newline at end of file +} + diff --git a/configure.ac b/configure.ac index 5849019a..4b107b33 100644 --- a/configure.ac +++ b/configure.ac @@ -25,7 +25,7 @@ fi with_os=`echo ${with_os} | tr '[[:upper:]]' '[[:lower:]]' ` if test "x${with_os}" != xlinux && "x${with_os}" != xfreebsd && "x${with_os}" != xdarwin ; then - AC_MSG_ERROR(--with-os= argument must linux, freebsd, openbsd or darwin) + AC_MSG_ERROR(--with-os= argument must linux freebsd openbsd or darwin) fi # Checks for programs. @@ -44,11 +44,11 @@ if test "x${with_os}" = xopenbsd ; then AC_CHECK_LIB([c],[clock_gettime], , AC_MSG_ERROR(libc needed)) fi +# Pass the conditionals to automake AM_CONDITIONAL([BUILD_FOR_LINUX], [test "x${with_os}" = xlinux ]) AM_CONDITIONAL([BUILD_FOR_FREEBSD], [test "x${with_os}" = xfreebsd ]) AM_CONDITIONAL([BUILD_FOR_OPENBSD], [test "x${with_os}" = xopenbsd ]) - ##### Some build systems are not fully using pkg-config, so we can use the flag ${with_pkg_config} on a case-by-case basis ##### to control how to deal with them ##### Note -- this flag is sometimes ignored, especially for newer packages @@ -383,8 +383,14 @@ 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_MSG_RESULT(>>Include libraries required for AirPlay 2) - PKG_CHECK_MODULES([plist], [libplist >= 2.0.0],[CFLAGS="${plist_CFLAGS} ${CFLAGS}" LIBS="${plist_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- libplist-dev suggested!)]) - AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) + if test "x${with_os}" = xlinux ; then + PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- libplist-dev suggested!)]) + fi + if test "x${with_os}" = xfreebsd ; then + 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-2.2.0 or later!)]) + fi + PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) +# AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [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])]) diff --git a/dacp.c b/dacp.c index 906519fd..35dbabd0 100644 --- a/dacp.c +++ b/dacp.c @@ -465,7 +465,7 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) { "dacp_monitor_port_update_callback with Remote ID \"%s\", target ID \"%s\" and port " "number %d.", dacp_id, dacp_server.dacp_id, port); - if ((dacp_id == NULL) || (dacp_server.dacp_id == NULL)) { + if ((dacp_id == NULL) || (dacp_server.dacp_id[0] == '\0')) { warn("dacp_id or dacp_server.dacp_id NULL detected"); } else { if (strcmp(dacp_id, dacp_server.dacp_id) == 0) { diff --git a/definitions.h b/definitions.h index 02b09eb8..f6263026 100644 --- a/definitions.h +++ b/definitions.h @@ -1,14 +1,9 @@ #ifndef _DEFINITIONS_H #define _DEFINITIONS_H -//#include -//#include -//#include #include -//#include "audio.h" #include "config.h" -//#include "mdns.h" #if defined(__APPLE__) && defined(__MACH__) /* Apple OSX and iOS (Darwin). ------------------------------ */ @@ -23,6 +18,18 @@ #define COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD 1 #endif +#if defined(__linux__) +#define COMPILE_FOR_LINUX 1 +#endif + +#if defined(__FreeBSD__) || defined(__OpenBSD__) +#define COMPILE_FOR_BSD 1 +#endif + +#if defined(__FreeBSD__) +#define COMPILE_FOR_FREEBSD 1 +#endif + // struct sockaddr_in6 is bigger than struct sockaddr. derp #ifdef AF_INET6 #define SOCKADDR struct sockaddr_storage diff --git a/pair_ap b/pair_ap index 6d5933a2..d955c447 160000 --- a/pair_ap +++ b/pair_ap @@ -1 +1 @@ -Subproject commit 6d5933a2aa982fb8e7eb3f953ffa3bcf0e43a989 +Subproject commit d955c447dac0545803dee9c90312c9d42310e9b1 diff --git a/ptp-utilities.c b/ptp-utilities.c index 9890028d..bda461ff 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -24,6 +24,7 @@ * OTHER DEALINGS IN THE SOFTWARE. */ +#include "definitions.h" #include #include #include @@ -31,6 +32,10 @@ #include #include #include +#ifdef COMPILE_FOR_FREEBSD +#include +#include +#endif #define __STDC_FORMAT_MACROS #include "common.h" #include diff --git a/rtp.c b/rtp.c index 74110cfe..5050ea7b 100644 --- a/rtp.c +++ b/rtp.c @@ -1498,7 +1498,8 @@ void *rtp_event_receiver(void *arg) { socklen_t addr_size = sizeof(remote_addr); int fd = accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size); - pthread_cleanup_push(socket_cleanup, (void *)fd); + intptr_t pfd = fd; + pthread_cleanup_push(socket_cleanup, (void *)pfd); int finished = 0; do { nread = recv(fd, packet, sizeof(packet), 0); @@ -1792,11 +1793,11 @@ void *rtp_realtime_audio_receiver(void *arg) { } ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { - ssize_t response; + ssize_t response = -1; if (pthread_mutex_lock(&descriptor->mutex) != 0) debug(1, "problem with mutex"); pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); - if ((descriptor->closed == 0)) { + if (descriptor->closed == 0) { while ((descriptor->buffer_occupancy == 0) && (descriptor->error_code == 0)) { if (pthread_cond_wait(&descriptor->not_empty_cv, &descriptor->mutex)) debug(1, "Error waiting for buffered read"); @@ -1848,7 +1849,8 @@ void *buffered_tcp_reader(void *arg) { socklen_t addr_size = sizeof(remote_addr); int finished = 0; int fd = accept(descriptor->sock_fd, (struct sockaddr *)&remote_addr, &addr_size); - pthread_cleanup_push(socket_cleanup, (void *)fd); + intptr_t pfd = fd; + pthread_cleanup_push(socket_cleanup, (void *)pfd); do { if (pthread_mutex_lock(&descriptor->mutex) != 0) diff --git a/shairport.c b/shairport.c index d143de66..45a08c7f 100644 --- a/shairport.c +++ b/shairport.c @@ -115,6 +115,10 @@ pid_t pid; int this_is_the_daemon_process = 0; #endif +#ifndef UUID_STR_LEN +#define UUID_STR_LEN 36 +#endif + pthread_t rtsp_listener_thread; int killOption = 0; @@ -1616,6 +1620,7 @@ int main(int argc, char **argv) { // with thanks uuid_t binuuid; uuid_generate_random(binuuid); + char *uuid = malloc(UUID_STR_LEN); /* Produces a UUID string at uuid consisting of lower-case letters. */ uuid_unparse_lower(binuuid, uuid); From 0c7d385f3510b31c9c2ad634c0d5b05284b5ec32 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:09:09 +0100 Subject: [PATCH 183/878] Keep an eye on DAC underrun recoveries --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 6bc1d97d..aa1258f8 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1716,7 +1716,7 @@ int do_play(void *buf, int samples) { // if we are not trying to recover from a previous underrun. if (prior_state == SND_PCM_STATE_XRUN) - debug(2, "alsa: recovering from a previous underrun."); + debug(1, "alsa: recovering from a previous underrun."); else debug(1, "alsa: underrun while writing %d samples to alsa device.", samples); int tret = snd_pcm_recover(alsa_handle, ret, 1); From 6761a4e382e820b7a2fb3b40c71cc6548701489d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:06:42 +0100 Subject: [PATCH 184/878] Update README.md --- README.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index edba6115..1294e45c 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,49 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on good audio performance. Thus, many out-of-scope features are missing or broken. So integration with Apple's Home app is missing; remote control doesn't work. +* Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. AirPlay 2 also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on getting the best possible audio performance. Thus, many features that are outside that scope are missing or broken. So, for examples, integration with Apple's Home app is missing; remote control doesn't work. +* For AirPlay 2, Shairport Sync requires the services of another application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. -* AirPlay 2 support is only available on recent Linux builds. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. FreeBSD implementation has not been extensively tested. +* OpenBSD and Cygwin are not supported. +* AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use NQPTP, which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. + +AirPlay 2 Issues +==== +Did we mention AirPlay 2 support is experimental? You should also note that it is entirely possible that changes could be made that would stop Shairport Sync AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there is little prospect of it having any kind of commercial significance. + +What Works +--- +* AirPlay 2 audio for players hosted on macOS and iOS. +* AirPlay 2 audio for System Sound Output from macOS. +* The `ALSA`, `stdout` and `pipe` backends definitely work, as does the `sndio` backend for FreeBSD. + +What Does Not Work +--- +* Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. +* There is no integration with the Home app or HomeKit, so nothing to do with the Home app works. +* There is no integration with Siri. +* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to NQPTP, and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) +* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` interface is working, but anything requiring sending commands to the player doesn't work. +* Artwork is not downloaded. + +More About What Works +--- +* Two types of audio are received -- CD quality ALAC (like AirPlay 1) and AAC stereo at 44,100 frames per seconds. The selection of encoding is made by the player. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. + +What You Need +--- +* A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run on an original Pi or a Pi Zero. +* An extra program, NQPTP, is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for PTP clocks. If you are using PTP clock services for something else, you can't install NQPTP and so you can't use Shairport Sync. Guides --- * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* To build Shairport Sync for AirPlay 2, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A guide for building on FreeBSD is forthcoming. Note --- From 7c2b46f31ee0846f82fd73943bcc4e243406d15c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:20:31 +0100 Subject: [PATCH 185/878] Update README.md --- README.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1294e45c..d8eeb18c 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,16 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync can be built to support AirPlay 1 only or AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. AirPlay 2 also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort is on getting the best possible audio performance. Thus, many features that are outside that scope are missing or broken. So, for examples, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. +* Shairport Sync can be built to support AirPlay 1 only or it can be built to support Airplay 2, which includes AirPlay 1 support. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. +* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. * When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. FreeBSD implementation has not been extensively tested. -* OpenBSD and Cygwin are not supported. -* AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use NQPTP, which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. +* AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. -AirPlay 2 Issues +AirPlay 2 Matters ==== -Did we mention AirPlay 2 support is experimental? You should also note that it is entirely possible that changes could be made that would stop Shairport Sync AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there is little prospect of it having any kind of commercial significance. +Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there is little prospect of it having any kind of commercial significance. What Works --- @@ -23,22 +22,21 @@ What Works What Does Not Work --- * Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. -* There is no integration with the Home app or HomeKit, so nothing to do with the Home app works. -* There is no integration with Siri. -* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to NQPTP, and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) -* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` interface is working, but anything requiring sending commands to the player doesn't work. +* There is no integration with Siri or with the Home app or HomeKit, so nothing to do with the Home app works. +* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) +* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. * Artwork is not downloaded. More About What Works --- -* Two types of audio are received -- CD quality ALAC (like AirPlay 1) and AAC stereo at 44,100 frames per seconds. The selection of encoding is made by the player. +* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per seconds. The selection of stream type is made by the player. * Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. What You Need --- -* A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run on an original Pi or a Pi Zero. -* An extra program, NQPTP, is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for PTP clocks. If you are using PTP clock services for something else, you can't install NQPTP and so you can't use Shairport Sync. +* A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run successfully on an original Pi or a Pi Zero. +* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for PTP clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync. Guides --- From e4d0c2181dd34a1009b210165b307ff0672872ae Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:21:04 +0100 Subject: [PATCH 186/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8eeb18c..737335a0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Shairport Sync AirPlay 2 Matters ==== -Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there is little prospect of it having any kind of commercial significance. +Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. What Works --- From 861ef69302c05cfaf0fc2ae507cf4bfdca89aebb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:28:21 +0100 Subject: [PATCH 187/878] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 737335a0..54f9672a 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync can be built to support AirPlay 1 only or it can be built to support Airplay 2, which includes AirPlay 1 support. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. -* When built for AirPlay 1, Shairport Sync runs on Linux, FreeBSD and OpenBSD. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. -* AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. +* Shairport Sync can be built to support AirPlay 1 only or it can be built to support Airplay 2, which includes AirPlay 1 support. AirPlay 2 Matters ==== +* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. +* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. + Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. What Works @@ -29,7 +29,7 @@ What Does Not Work More About What Works --- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per seconds. The selection of stream type is made by the player. +* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. * Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. From 767bed6b9f3a809694c5c38738f35cd24e0442d3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:21:32 +0100 Subject: [PATCH 188/878] Sometimes the output_rtptime_mask seems to be calculated in 32-bit arithmetic (?), so do it all explicitly in 64-bit signed int as soon as the output_rtptime_sign_bit is calculated. --- player.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/player.c b/player.c index cd09bf8b..1d12e34c 100644 --- a/player.c +++ b/player.c @@ -1807,7 +1807,12 @@ void *player_thread_func(void *arg) { // debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); // debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); - conn->max_frame_size_change = + int64_t output_rtptime_mask = 1; + output_rtptime_mask = output_rtptime_mask << (output_rtptime_sign_bit + 1); + output_rtptime_mask = output_rtptime_mask - 1; + debug(3,"output_rtptime_mask is %" PRIx64 ".", output_rtptime_mask); + + conn->max_frame_size_change = 1 * conn->output_sample_ratio; // we add or subtract one frame at the nominal // rate, multiply it by the frame ratio. // but, on some occasions, more than one frame could be added @@ -2331,7 +2336,7 @@ void *player_thread_func(void *arg) { // current_delay is denominated in the frame rate of the outgoing stream int64_t will_be_frame = inframe->given_timestamp; will_be_frame = will_be_frame * conn->output_sample_ratio; - will_be_frame = (will_be_frame - current_delay) & ((1 << (output_rtptime_sign_bit + 1)) - 1); + will_be_frame = (will_be_frame - current_delay) & output_rtptime_mask; sync_error = SIGNEX(should_be_frame - will_be_frame, output_rtptime_sign_bit) ; // all int64_t From 861455f703a9d5ce4404c5e7a1cf3d9b1b6d865e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:18:04 +0100 Subject: [PATCH 189/878] Add some information about the length of time a new clock master has been monitored for ("history") and warn if the PTP information hasn't been updated for more than a second (not extensively tested). --- ptp-utilities.c | 9 +++++++-- ptp-utilities.h | 2 +- rtp.c | 23 +++++++++++++---------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index bda461ff..a9cf614f 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -65,7 +65,7 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { return response; } -int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 20000, 1); @@ -73,7 +73,10 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, *actual_clock_id = 0; if (raw_offset != NULL) *raw_offset = 0; - + if (time_of_sample != NULL) + *time_of_sample = 0; + if (mastership_start_time != NULL) + *mastership_start_time = 0; if (ptp_shm_interface_open() == 0) { struct shm_structure nqptp_data; if (get_nqptp_data(&nqptp_data) == 0) { @@ -82,6 +85,8 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, if (nqptp_data.master_clock_id != 0) { if (actual_clock_id != NULL) *actual_clock_id = nqptp_data.master_clock_id; + if (time_of_sample != NULL) + *time_of_sample = nqptp_data.local_time; if (raw_offset != NULL) *raw_offset = nqptp_data.local_to_master_time_offset; if (mastership_start_time != NULL) diff --git a/ptp-utilities.h b/ptp-utilities.h index a0ce1604..f18169d9 100644 --- a/ptp-utilities.h +++ b/ptp-utilities.h @@ -29,7 +29,7 @@ #include "config.h" #include -int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *raw_offset, +int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, uint64_t *mastership_start_time); void ptp_send_control_message_string(const char *msg); diff --git a/rtp.c b/rtp.c index 5050ea7b..e5dc8de8 100644 --- a/rtp.c +++ b/rtp.c @@ -1224,7 +1224,7 @@ void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtpti ((clock_id != conn->anchor_clock) || (conn->anchor_rtptime != rtptime) || (conn->anchor_time != networktime))) { uint64_t master_clock_id = 0; - ptp_get_clock_info(&master_clock_id, NULL, NULL); + ptp_get_clock_info(&master_clock_id, NULL, NULL, NULL); debug(1, "Connection %d: Note: anchor parameters have changed. Old clock: %" PRIx64 ", rtptime: %u, networktime: %" PRIu64 ". New clock: %" PRIx64 @@ -1269,14 +1269,17 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { - uint64_t actual_clock_id, actual_offset, start_of_mastership; - int response = ptp_get_clock_info(&actual_clock_id, &actual_offset, &start_of_mastership); + uint64_t actual_clock_id, actual_time_of_sample, actual_offset, start_of_mastership; + int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, &start_of_mastership); if (response == clock_ok) { + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_sample = time_now - actual_time_of_sample; + if (time_since_sample > 1000000000) + debug(1,"A long time -- %f milliseconds -- has elapsed since the last timing sample was received.", 0.000001 * time_since_sample); if (conn->anchor_remote_info_is_valid != 0) { // i.e. if we have anchor clock ID and anchor time / rtptime // figure out how long the clock has been master - uint64_t time_now = get_absolute_time_in_ns(); int64_t duration_of_mastership = time_now - start_of_mastership; // if we have an alternative, i.e. if last anchor stuff is valid // then we can wait for a long time to let the new master settle @@ -1302,8 +1305,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) debug(1, - "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock.", - conn->connection_number, conn->anchor_clock); + "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock. History: %f milliseconds.", + conn->connection_number, conn->anchor_clock, 0.000001 * duration_of_mastership); conn->anchor_clock_is_new = 0; } } else { @@ -1316,15 +1319,15 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (conn->anchor_clock_is_new != 0) debug(1, "Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 - ".", - conn->connection_number, conn->anchor_clock, actual_clock_id); + ". History: %f milliseconds.", + conn->connection_number, conn->anchor_clock, actual_clock_id, 0.000001 * duration_of_mastership); if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { - debug(1, "Connection %d: Master clock has changed to %" PRIx64 ".", - conn->connection_number, actual_clock_id); + debug(1, "Connection %d: Master clock has changed to %" PRIx64 ". History: %f milliseconds.", + conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to // calculate what time that must be on the new clock From 7b8e68650fb232f0734f9c29643be9ccda8b4d53 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:40:38 +0100 Subject: [PATCH 190/878] Fix the rtptime modulo calculations -- the SIGNEX idea doesn't seem to be portable, so it has to be done the hard way. --- player.c | 164 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/player.c b/player.c index 1d12e34c..a279f027 100644 --- a/player.c +++ b/player.c @@ -96,30 +96,7 @@ #include "activity_monitor.h" -// sign extending rtptime calculations to 64 bit is needed from time to time. - -// the standard rtptime is unsigned 32 bits, -// so you can do modulo 2^32 difference calculations -// and get a signed result simply by typing the result as a signed 32-bit number - -// So long as you can be sure the numbers are within 2^31 of each other, -// the sign of the result calculated in this way indicates which comes after which -// so if you subtract a from b and the result is positive, -// b is the same as or comes after a in module 2^32 order. - -// We want to do the same with the rtptime calculations for multiples of -// the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit -// so we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 but unsigned -// numbers on the same basis - -// See http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend -// and https://stackoverflow.com/a/31655073/2942008 -// of which this is a slight variation - -#define SIGNEX(v, sb) (((v) & ((1 << (sb + 1)) - 1)) | (((v) & (1 << (sb))) ? ~((1 << (sb))-1) : 0)) - - -// make the first audio packet deliberately early to bias the sync error of +// mframes_inward_measurement_time = time_now; conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; abuf = conn->audio_buffer + BUFIDX(seqno); - conn->ab_write = seqno + 1; // move the write pointer to the next free space + conn->ab_write = seqno + 1; // move the write pointer to the next free space } else if (is_after(conn->ab_write, seqno)) { // newer than expected int32_t gap = seqno - conn->ab_write; if (gap <= 0) @@ -535,7 +512,7 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta (uint64_t)1000000000); uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)1000000000); latency_time = latency_time / (uint64_t)conn->input_rate; - + // find the first frame that is missing, if known int x = conn->ab_read; if (first_possibly_missing_frame >= 0) { @@ -543,11 +520,11 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta int16_t buffer_size = conn->ab_write - conn->ab_read; // must be positive if (buffer_size >= 0) { int16_t position_in_buffer = first_possibly_missing_frame - conn->ab_read; - if ((position_in_buffer >=0) && (position_in_buffer < buffer_size)) + if ((position_in_buffer >= 0) && (position_in_buffer < buffer_size)) x = first_possibly_missing_frame; } } - + first_possibly_missing_frame = -1; // has not been set int missing_frame_run_count = 0; @@ -1783,36 +1760,62 @@ void *player_thread_func(void *arg) { // successive rtptimes, at worst conn->output_sample_ratio = config.output_rate / conn->input_rate; + + +// Sign extending rtptime calculations to 64 bit is needed from time to time. + +// The standard rtptime is unsigned 32 bits, +// so you can do modulo 2^32 difference calculations +// and get a signed result simply by typing the result as a signed 32-bit number. + +// So long as you can be sure the numbers are within 2^31 of each other, +// the sign of the result calculated in this way indicates the order of the operands. +// For example, if you subtract a from b and the result is positive, you can conclude +// b is the same as or comes after a in module 2^32 order. + +// We want to do the same with the rtptime calculations for multiples of +// the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit/ +// Therefore we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 bit unsigned +// numbers on the same basis. + +// That is what the output_rtptime_sign_bit, output_rtptime_mask, output_rtptime_mask_not and +// output_rtptime_sign_mask are for -- see later, calculating the sync error. + int output_rtptime_sign_bit; switch (conn->output_sample_ratio) { - case 1: - output_rtptime_sign_bit = 31; - break; - case 2: - output_rtptime_sign_bit = 32; - break; - case 4: - output_rtptime_sign_bit = 33; - break; - case 8: - output_rtptime_sign_bit = 34; - break; - default: - debug(1, "error with output ratio -- can't calculate sign bit number"); - output_rtptime_sign_bit = 31; - break; + case 1: + output_rtptime_sign_bit = 31; + break; + case 2: + output_rtptime_sign_bit = 32; + break; + case 4: + output_rtptime_sign_bit = 33; + break; + case 8: + output_rtptime_sign_bit = 34; + break; + default: + debug(1, "error with output ratio -- can't calculate sign bit number"); + output_rtptime_sign_bit = 31; + break; } -// debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); -// debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); + // debug(1, "Output sample ratio is %d.", conn->output_sample_ratio); + // debug(1, "Output output_rtptime_sign_bit: %d.", output_rtptime_sign_bit); int64_t output_rtptime_mask = 1; output_rtptime_mask = output_rtptime_mask << (output_rtptime_sign_bit + 1); output_rtptime_mask = output_rtptime_mask - 1; - debug(3,"output_rtptime_mask is %" PRIx64 ".", output_rtptime_mask); - - conn->max_frame_size_change = + + int64_t output_rtptime_mask_not = output_rtptime_mask; + output_rtptime_mask_not = ~output_rtptime_mask; + + int64_t output_rtptime_sign_mask = 1; + output_rtptime_sign_mask = output_rtptime_sign_mask << output_rtptime_sign_bit; + + conn->max_frame_size_change = 1 * conn->output_sample_ratio; // we add or subtract one frame at the nominal // rate, multiply it by the frame ratio. // but, on some occasions, more than one frame could be added @@ -2270,7 +2273,7 @@ void *player_thread_func(void *arg) { } int16_t bo = conn->ab_write - conn->ab_read; // do this in 16 bits - conn->buffer_occupancy = bo; // 32 bits + conn->buffer_occupancy = bo; // 32 bits if (conn->buffer_occupancy < minimum_buffer_occupancy) minimum_buffer_occupancy = conn->buffer_occupancy; @@ -2336,10 +2339,28 @@ void *player_thread_func(void *arg) { // current_delay is denominated in the frame rate of the outgoing stream int64_t will_be_frame = inframe->given_timestamp; will_be_frame = will_be_frame * conn->output_sample_ratio; - will_be_frame = (will_be_frame - current_delay) & output_rtptime_mask; + will_be_frame = (will_be_frame - current_delay) & + output_rtptime_mask; // this is to make sure it's unsigned modulo 2^bits + // for the rtptime - sync_error = - SIGNEX(should_be_frame - will_be_frame, output_rtptime_sign_bit) ; // all int64_t + // Now we have a tricky piece of calculation to perform. + // We know the rtptimes are unsigned in 32 or more bits -- call it r bits. We have to + // calculate the difference between them. on the basis that they should be within + // 2^(r-1) of one another, so that the unsigned subtraction result, modulo 2^r, if + // interpreted as a signed number, should yield the difference _and_ the ordering. + + sync_error = should_be_frame - will_be_frame; // this is done in int64_t form + + // sign-extend the r-bit unsigned int calculation by treating it as an r-bit signed + // integer + if ((sync_error & output_rtptime_sign_mask) != + 0) { // check what would be the sign bit in "r" bit unsigned arithmetic + // result is negative + sync_error = sync_error | output_rtptime_mask_not; + } else { + // result is positive + sync_error = sync_error & output_rtptime_mask; + } if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; @@ -2810,17 +2831,16 @@ void *player_thread_func(void *arg) { statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); - statistics_item("nominal fps", "%*.2f", 11, - conn->remote_frame_rate); - statistics_item(" actual fps", "%*.2f", 11, - conn->input_frame_rate); + statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); + statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); statistics_item("source drift ppm", "%*.2f", 16, (conn->local_to_remote_time_gradient - 1.0) * 1000000); statistics_item("drift samples", "%*d", 13, conn->local_to_remote_time_gradient_sample_count); statistics_item( - "estimated (unused) correction ppm", "%*.2f", strlen("estimated (unused) correction ppm"), + "estimated (unused) correction ppm", "%*.2f", + strlen("estimated (unused) correction ppm"), (conn->frame_rate > 0.0) ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * conn->local_to_remote_time_gradient) * @@ -3154,7 +3174,7 @@ int player_prepare_to_play(rtsp_conn_info *conn) { if (config.buffer_start_fill > BUFFER_FRAMES) die("specified buffer starting fill %d > buffer size %d", config.buffer_start_fill, BUFFER_FRAMES); - // active, and should be before play's command hook, command_start() + // active, and should be before play's command hook, command_start() command_start(); conn->input_bytes_per_frame = 4; // default -- may be changed later // call on the output device to prepare itself From ce49f3a9fdb0b6d88891f51e051fd09b7bc4515a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:54:51 +0100 Subject: [PATCH 191/878] Quieten the PTP informatin age checking code. --- rtp.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index e5dc8de8..2542621e 100644 --- a/rtp.c +++ b/rtp.c @@ -1267,6 +1267,7 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { conn->anchor_remote_info_is_valid = 0; } +int long_time_notifcation_done = 0; int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { uint64_t actual_clock_id, actual_time_of_sample, actual_offset, start_of_mastership; @@ -1275,8 +1276,16 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (response == clock_ok) { uint64_t time_now = get_absolute_time_in_ns(); int64_t time_since_sample = time_now - actual_time_of_sample; - if (time_since_sample > 1000000000) - debug(1,"A long time -- %f milliseconds -- has elapsed since the last timing sample was received.", 0.000001 * time_since_sample); + if (time_since_sample > 10000000000) { + if (long_time_notifcation_done == 0) { + debug(1,"The last PTP timing sample is pretty old: %f seconds.", 0.000000001 * time_since_sample); + long_time_notifcation_done = 1; + } + } else if ((time_since_sample < 2000000000) && (long_time_notifcation_done != 0)) { + debug(1,"The last PTP timing sample is no longer too old: %f seconds.", 0.000000001 * time_since_sample); + long_time_notifcation_done = 0; + } + if (conn->anchor_remote_info_is_valid != 0) { // i.e. if we have anchor clock ID and anchor time / rtptime // figure out how long the clock has been master From 136bf13a3615988413f2600bb6a03a651c70893e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 20:01:03 +0100 Subject: [PATCH 192/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54f9672a..d1031647 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ More About What Works What You Need --- * A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run successfully on an original Pi or a Pi Zero. -* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for PTP clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync. +* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync. Guides --- From a6ac3f63e936a8314d6d3521b27d99468bc7a577 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 20:06:58 +0100 Subject: [PATCH 193/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index cd97f62b..a33f8e7c 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -65,10 +65,7 @@ Download, install and start/enable `nqptp` from [here](https://github.com/mikebr ***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. The `nqptp` service monitors PTP clocks. It provides a POSIX Shared Memory Interface (SMI) at `/nqptp`. A shared -pthread mutex is contained within the interface, and to use it you need write access. This is available -to the `root` user and also to members of the unix group `nqptp`, which is created by the `nqptp` installer. - -You should include the relevant users in the `nqptp` group -- including the `shairport-sync` user if you are using the standard Shairport Sync installer. +pthread mutex is contained within the interface, and to use it you need write access. Next, download Shairport Sync, configure it, compile and install it: ``` @@ -118,11 +115,11 @@ You may wish to run Shairport Sync from the command line (but remember to ensure ``` $ shairport-sync -vu --statistics ``` -The user doesn't need to be privileged, but should be a member of the `nqptp` group for access to the `nqptp` service, and it should also be a member of the `audio` group for access to the `alsa` subsystem. +The user doesn't need to be privileged, but should be a member of the `audio` group for access to the `alsa` subsystem. ### Using Shairport Sync ### -The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ububntu`. You can change the service name in the configuration file. +The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ubuntu`. You can change the service name in the configuration file. Connect and enjoy... From c3faddde4700005a32e05896c65dd971d333fec4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 23 Jun 2021 20:09:39 +0100 Subject: [PATCH 194/878] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1031647..204b1b75 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ AirPlay 2 Matters * The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. * **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. * For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are used by Mac OS X to support its implementation of AirPlay 2. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. @@ -36,7 +36,7 @@ More About What Works What You Need --- * A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run successfully on an original Pi or a Pi Zero. -* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync. +* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) Guides --- From 8a72c11835489faf2e8bf940acf7071f32bf1b87 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 24 Jun 2021 01:09:46 +0100 Subject: [PATCH 195/878] Docker README fix. --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 8172ec08..a460af13 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,7 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh github=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . +docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file From a565f9f0ca285e2d93a9ca2f0b6a3d813a5f85e4 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 24 Jun 2021 01:31:27 +0100 Subject: [PATCH 196/878] README update to include HomePod issues. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 204b1b75..76492318 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ What Does Not Work * You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) * Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. * Artwork is not downloaded. +* Playing from certain Apple devices to Shairport Sync does not work as expected. For example: + * When using a service with the ability to play directly on HomePod (e.g. Apple Music). If the music starts on the HomePod, or is the first device selected from the AirPlay list on iOS, when Shairport Sync is then selected iOS shows it as not connected (even though it is). Therefore the expected volume controls are not displayed. A workaround for this is to force an AirPlay stream to the HomePod (rather than just acting as a remote controller), this can be done by selected Shairport Sync _before_ the HomePod. Note, this issue is not present for other services such as SoundCloud which do not integrate into the HomePod like Apple Music does. + * The above example is true also when trying to select Shairport Sync from the AirPlay menu on the HomePod from the Home app. More About What Works --- From 33dd3037da4315ff04a78f1a34b888d798d17ac1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 24 Jun 2021 10:45:32 +0100 Subject: [PATCH 197/878] Wait a little longer to allow the master clock to stabilise. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index 2542621e..1b2cffad 100644 --- a/rtp.c +++ b/rtp.c @@ -1299,7 +1299,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock // note that mastershipo may be backdated - if (duration_of_mastership < 1100000000) { + if (duration_of_mastership < 1500000000) { debug(3,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || From f9eef5bd7c6c4b0a98eadfc8da8aec3aa41b7624 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sat, 26 Jun 2021 15:46:02 +0100 Subject: [PATCH 198/878] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76492318..d670abf9 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,15 @@ More About What Works What You Need --- -* A Raspberry Pi 3 or better is needed. At present, Shairport Sync will not run successfully on an original Pi or a Pi Zero. +* A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! * [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) +| Tested | Native Hardware Status | Docker Status | +|---------------------|------------------------|---------------| +| Raspberry Pi 4 | Working | Working | +| Raspberry Pi 3 | Working | Working. | +| Raspberry Pi Zero W | Untested | Working but since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs. Total CPU usage sits at around 75% when using Shairport Sync.| + Guides --- * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). From 7258944be8f87e69d4b9c8c93336e944c68aa876 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sat, 26 Jun 2021 15:46:48 +0100 Subject: [PATCH 199/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d670abf9..7e6ebd83 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ What You Need | Tested | Native Hardware Status | Docker Status | |---------------------|------------------------|---------------| | Raspberry Pi 4 | Working | Working | -| Raspberry Pi 3 | Working | Working. | +| Raspberry Pi 3 | Working | Working | | Raspberry Pi Zero W | Untested | Working but since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs. Total CPU usage sits at around 75% when using Shairport Sync.| Guides From 530e078a5fcc6e96ae785a0f063d860c63fd2bba Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sun, 27 Jun 2021 22:13:14 +0100 Subject: [PATCH 200/878] Documentation updates (WIP). --- README.md | 10 +++++----- documentation/Example Setups.md | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 documentation/Example Setups.md diff --git a/README.md b/README.md index 7e6ebd83..6966cce6 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ What You Need * A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! * [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) -| Tested | Native Hardware Status | Docker Status | -|---------------------|------------------------|---------------| -| Raspberry Pi 4 | Working | Working | -| Raspberry Pi 3 | Working | Working | -| Raspberry Pi Zero W | Untested | Working but since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs. Total CPU usage sits at around 75% when using Shairport Sync.| +| Device | Native | Docker | Notes | +|---------------------|----------|----------------|-------| +| Raspberry Pi 4 | Working | Working | | +| Raspberry Pi 3 | Working | Working | | +| Raspberry Pi Zero W | Untested | Mostly working | Since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs, this currently includes quickly changing the volume repeatedly. Total CPU usage sits at around 75% when using Shairport Sync. Updates to Shairport Sync could make this better or worse. | Guides --- diff --git a/documentation/Example Setups.md b/documentation/Example Setups.md new file mode 100644 index 00000000..f87ead00 --- /dev/null +++ b/documentation/Example Setups.md @@ -0,0 +1,20 @@ +Example Setups +=== + +Amazon Echo Devices +--- +Since some Amazon Echo devices have either a 3.5mm input or can be used as a bluetooth speaker, Shairport Sync can be connected allowing you to Airplay to these speakers. Currently using the 3.5mm jack is the most reliable method - bluetooth connectivity can have an unreliable delay. Check first to see if your Echo device supports either of these. Volume control of the Echo device can be added via Home Assistant and a MQTT broker. Below goes through some example setups. +### _Raspberry Pi --> Echo 3.5mm Input_ +* Set the Echo device to use the 3.5mm jack as an input. +* Via the Shairport Sync config, set the correct `output_device` and `mixer_control_name` for your configuration. Likely to be +`output_device = "hw:1";` and `mixer_control_name = "Headphone";`. +* At this point, you should be able to hear music via the Echo. Remember that so far, the volume of the Echo and Shairport Sync are _independent_. If you prefer, you can disable any volume control in Shairport Sync by setting `ignore_volume_control = "yes";`. +* Amazon Echo devices appear to add a bit of latency, currently this is thought to be 0.12 seconds. This can be countered by setting the following: `audio_backend_latency_offset_in_seconds = -0.12;`. This was measured _very_ crudely using Audacity so could be wrong. If anyone is able to determine the latency more accurately, please open an issue or pull request so these documents can be updated. +* You should now be able to use your Echo device in sync with other AirPlay devices! + +### _Raspberry Pi --> Echo as Bluetooth Speaker (Docker Version)_ +* + +### _Raspberry Pi --> Echo with Volume Control_ +* Before continuing with volume control via Home Assistant, please note that currently only one way volume control is tested - Shairport Sync to the Echo device. This means if the volume of the Echo is changed (e.g. by voice control or by the physical controls on the device), this will not be reflected in the volume of Shairport Sync and will be overwritten the next time Shairport Sync's volume is changed. +* This setup requires Home Assistant and a MQTT broker. The basic idea is that Shairport Sync sends a MQTT message containing the new volume amount to the MQTT broker when the volume is changed, this is then picked up by Home Assistant (which is monitoring the MQTT broker). Home Assistant will then send a command to the Amazon Echo device to change the volume. From 1a21a1aaae9dcdb02b072414c26f4568641f30b0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:23:56 +0100 Subject: [PATCH 201/878] Switch to libgcrypt for the pairing library for compatibility with Ubuntu on an M1. --- Makefile.am | 2 +- configure.ac | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index d0a025e1..2cb27d48 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,7 @@ SUBDIRS = man ARFLAGS = cr -lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_OPENSSL -pthread +lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread lib_tinyhttp_a_CFLAGS = -pthread lib_dbus_interface_a_CFLAGS = -pthread lib_mpris_interface_a_CFLAGS = -pthread diff --git a/configure.ac b/configure.ac index 4b107b33..a41319f1 100644 --- a/configure.ac +++ b/configure.ac @@ -390,6 +390,7 @@ if test "x$with_airplay_2" = "xyes" ; then 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-2.2.0 or later!)]) fi PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) + PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) # AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [AC_MSG_ERROR([AirPlay 2 support requires libavcodec -- libavcodec-dev suggested])]) From a86955c0cefccef1b15f8f801d633225e5b45185 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:41:59 +0100 Subject: [PATCH 202/878] Added libgcrypt --- BUILDFORAP2.md | 2 +- docker/Dockerfile | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index a33f8e7c..ab340075 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -55,7 +55,7 @@ First, install the packages needed by Shairport Sync: ``` # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ - libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt ``` (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9393f072..59121716 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -110,7 +110,8 @@ RUN apk add \ libuuid \ ffmpeg \ libsodium \ - libplist + libplist \ + libgcrypt RUN rm -rf /lib/apk/db/* From 5850696ac0f4ffe6d35e2a8f5803f2f32581b7ce Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:55:21 +0100 Subject: [PATCH 203/878] Switch over to AC_CHECK_LIB from PKG_CHECK_MODULES for checking for libgcrypt. --- configure.ac | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index a41319f1..541ec999 100644 --- a/configure.ac +++ b/configure.ac @@ -390,7 +390,8 @@ if test "x$with_airplay_2" = "xyes" ; then 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-2.2.0 or later!)]) fi PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) + AC_CHECK_LIB([gcrypt], [gcry_control], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) +# PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) # AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [AC_MSG_ERROR([AirPlay 2 support requires libavcodec -- libavcodec-dev suggested])]) From fb3fb4e7d3bd1c7fb0d4dd197d3d145b0cef6831 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:10:40 +0100 Subject: [PATCH 204/878] Test. --- BUILDFORAP2.md | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index ab340075..e044c865 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -55,7 +55,7 @@ First, install the packages needed by Shairport Sync: ``` # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ - libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) diff --git a/docker/Dockerfile b/docker/Dockerfile index 59121716..02136803 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -111,7 +111,7 @@ RUN apk add \ ffmpeg \ libsodium \ libplist \ - libgcrypt + libgcrypt-dev RUN rm -rf /lib/apk/db/* From dda067298d7935d33ee90932b82a8ef165a8b300 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:51:45 +0100 Subject: [PATCH 205/878] Test. --- BUILDFORAP2.md | 2 +- docker/Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index e044c865..ab340075 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -55,7 +55,7 @@ First, install the packages needed by Shairport Sync: ``` # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ - libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt ``` (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) diff --git a/docker/Dockerfile b/docker/Dockerfile index 02136803..d3997e54 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -93,7 +93,7 @@ RUN make -j RUN make install # Shairport Sync Runtime System: -FROM alpine:3.12 +FROM alpine:3.8 RUN apk add \ alsa-lib \ @@ -111,7 +111,7 @@ RUN apk add \ ffmpeg \ libsodium \ libplist \ - libgcrypt-dev + libgcrypt RUN rm -rf /lib/apk/db/* From 432e0876c5aaf40fac416e6eb9f735482a21b3be Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:55:29 +0100 Subject: [PATCH 206/878] Revert. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d3997e54..59121716 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -93,7 +93,7 @@ RUN make -j RUN make install # Shairport Sync Runtime System: -FROM alpine:3.8 +FROM alpine:3.12 RUN apk add \ alsa-lib \ From f91efd77d252e6665abcc44f246d7d1a4bf2f292 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 13:03:55 +0100 Subject: [PATCH 207/878] Test. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 541ec999..07c71c91 100644 --- a/configure.ac +++ b/configure.ac @@ -390,7 +390,7 @@ if test "x$with_airplay_2" = "xyes" ; then 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-2.2.0 or later!)]) fi PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - AC_CHECK_LIB([gcrypt], [gcry_control], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) + AC_CHECK_LIB([gcrypt], [main], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) # PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) # AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) From d4b453538ea2e0bdfb69d15ffa63ef7bee170fe9 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 13:13:58 +0100 Subject: [PATCH 208/878] Try this just in case. --- configure.ac | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 07c71c91..541ec999 100644 --- a/configure.ac +++ b/configure.ac @@ -390,7 +390,7 @@ if test "x$with_airplay_2" = "xyes" ; then 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-2.2.0 or later!)]) fi PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - AC_CHECK_LIB([gcrypt], [main], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) + AC_CHECK_LIB([gcrypt], [gcry_control], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) # PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) # AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) diff --git a/docker/Dockerfile b/docker/Dockerfile index 59121716..6228aff1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -95,7 +95,7 @@ RUN make install # Shairport Sync Runtime System: FROM alpine:3.12 -RUN apk add \ +RUN apk -U add \ alsa-lib \ dbus \ popt \ From 9152c4a6a61547a5eb756578573fc75347895700 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 13:14:52 +0100 Subject: [PATCH 209/878] Now try this. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6228aff1..98a624ec 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -111,7 +111,7 @@ RUN apk -U add \ ffmpeg \ libsodium \ libplist \ - libgcrypt + libgcrypt-static RUN rm -rf /lib/apk/db/* From 123dc2e451294cac36583d3371285286d37e2679 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:54:47 +0100 Subject: [PATCH 210/878] Please work. --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 98a624ec..91df6232 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,7 +27,8 @@ RUN apk -U add \ xxd \ libressl-dev \ openssl-dev \ - libplist-dev + libplist-dev \ + libgcrypt-dev # ALAC Build System: FROM builder-base AS builder-alac @@ -110,8 +111,7 @@ RUN apk -U add \ libuuid \ ffmpeg \ libsodium \ - libplist \ - libgcrypt-static + libplist RUN rm -rf /lib/apk/db/* From 91934188d7b20252db88807e72561515ae5106e0 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:58:29 +0100 Subject: [PATCH 211/878] Update readme. --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index ab340075..e044c865 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -55,7 +55,7 @@ First, install the packages needed by Shairport Sync: ``` # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ - libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) From 105f642952f0ea465755f8c92184bc62f4d5ed7b Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:59:16 +0100 Subject: [PATCH 212/878] Update readme. --- documentation/Example Setups.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/Example Setups.md b/documentation/Example Setups.md index f87ead00..7ce6f658 100644 --- a/documentation/Example Setups.md +++ b/documentation/Example Setups.md @@ -13,8 +13,9 @@ Since some Amazon Echo devices have either a 3.5mm input or can be used as a blu * You should now be able to use your Echo device in sync with other AirPlay devices! ### _Raspberry Pi --> Echo as Bluetooth Speaker (Docker Version)_ -* +* WIP ### _Raspberry Pi --> Echo with Volume Control_ * Before continuing with volume control via Home Assistant, please note that currently only one way volume control is tested - Shairport Sync to the Echo device. This means if the volume of the Echo is changed (e.g. by voice control or by the physical controls on the device), this will not be reflected in the volume of Shairport Sync and will be overwritten the next time Shairport Sync's volume is changed. * This setup requires Home Assistant and a MQTT broker. The basic idea is that Shairport Sync sends a MQTT message containing the new volume amount to the MQTT broker when the volume is changed, this is then picked up by Home Assistant (which is monitoring the MQTT broker). Home Assistant will then send a command to the Amazon Echo device to change the volume. +* WIP From db580c26dc082c986d085038b0969beba13b8525 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 29 Jun 2021 21:07:26 +0100 Subject: [PATCH 213/878] Added libgcrypt to runtime image too. --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 91df6232..40822505 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -111,7 +111,8 @@ RUN apk -U add \ libuuid \ ffmpeg \ libsodium \ - libplist + libplist \ + libgcrypt RUN rm -rf /lib/apk/db/* From 6e24897d4dac74eba06ca8efcf9bc8fdc54c799e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Jun 2021 09:46:41 +0100 Subject: [PATCH 214/878] Correct the information in a non-available frame rate error message from audio_alsa.c. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index aa1258f8..74eecc08 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -559,7 +559,7 @@ int actual_open_alsa_device(int do_auto_setup) { if (ret == 0) { speed_found = 1; if (actual_sample_rate != speeds[i]) - die("The frame rate of the output DAC can not be set to a multiple of 44100 fps. (The nearest speed available is: %d fps.)", speeds[i], actual_sample_rate); + die("The frame rate of the output DAC can not be set to a %d fps. (The nearest speed available is: %d fps.)", speeds[i], actual_sample_rate); } else { i++; } From 5c08ccd4d0f70b67a3aba5655c9e982609d71c4f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Jun 2021 09:49:16 +0100 Subject: [PATCH 215/878] Correct the information in a non-available frame rate error message from audio_alsa.c. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 74eecc08..14dfcec2 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -559,7 +559,7 @@ int actual_open_alsa_device(int do_auto_setup) { if (ret == 0) { speed_found = 1; if (actual_sample_rate != speeds[i]) - die("The frame rate of the output DAC can not be set to a %d fps. (The nearest speed available is: %d fps.)", speeds[i], actual_sample_rate); + die("The output DAC can not be set to %d frames per second (fps). The nearest speed available is %d fps.", speeds[i], actual_sample_rate); } else { i++; } From f923e99f98f86ba04bde0ab9c1bd942444c26dc4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Jun 2021 10:02:46 +0100 Subject: [PATCH 216/878] Terminate the program if the specified output device can not be found. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 14dfcec2..564570bb 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -434,7 +434,7 @@ int actual_open_alsa_device(int do_auto_setup) { ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0); if (ret < 0) { if (ret == -ENOENT) { - warn("the alsa output_device \"%s\" can not be found.", alsa_out_dev); + die("the alsa output_device \"%s\" can not be found.", alsa_out_dev); } else { char errorstring[1024]; strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); From 1ca4ebcf028de0a49fe23d5d893c58a04fae6041 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Jun 2021 10:13:13 +0100 Subject: [PATCH 217/878] Make some more error conditions terminate the program. --- audio_alsa.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 564570bb..ed4f3c88 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -475,14 +475,14 @@ int actual_open_alsa_device(int do_auto_setup) { ret = snd_pcm_hw_params_set_access(alsa_handle, alsa_params, access); if (ret < 0) { - warn("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev, + die("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); return ret; } ret = snd_pcm_hw_params_set_channels(alsa_handle, alsa_params, 2); if (ret < 0) { - warn("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev, + die("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); return ret; } @@ -528,7 +528,7 @@ int actual_open_alsa_device(int do_auto_setup) { debug(1, "alsa: output format chosen is \"%s\".", sps_format_description_string(config.output_format)); } else { - warn("audio_alsa: Could not automatically set the output format for device \"%s\": %s", + die("audio_alsa: Could not automatically set the output format for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); return ret; } @@ -539,7 +539,7 @@ int actual_open_alsa_device(int do_auto_setup) { config.output_rate; // this is the requested rate -- it'll be changed to the actual rate ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir); if (ret < 0) { - warn("audio_alsa: Rate %iHz not available for playback: %s", config.output_rate, + die("audio_alsa: The frame rate of %i frames per second is not available for playback: %s", config.output_rate, snd_strerror(ret)); return ret; } @@ -568,7 +568,7 @@ int actual_open_alsa_device(int do_auto_setup) { config.output_rate = actual_sample_rate; debug(1, "alsa: output speed chosen is %d.", config.output_rate); } else { - warn("audio_alsa: Could not automatically set the output rate for device \"%s\": %s", + die("audio_alsa: Could not automatically set the output rate for device \"%s\": %s", alsa_out_dev, snd_strerror(ret)); return ret; } @@ -613,7 +613,7 @@ int actual_open_alsa_device(int do_auto_setup) { ret = snd_pcm_hw_params(alsa_handle, alsa_params); if (ret < 0) { - warn("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev, + die("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev, snd_strerror(ret)); return ret; } From 5ab2b67a6164d0e3fe82ba84b9ea43bd40fd6ef2 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:00:09 +0100 Subject: [PATCH 218/878] First attempt at additional airplay 1 build and releases process. --- .github/workflows/docker-build-releases.yaml | 57 +++++++++++ .github/workflows/docker-build.yaml | 19 +++- docker/Dockerfile | 2 +- docker/README.md | 29 +++++- docker/airplay1/Dockerfile | 102 +++++++++++++++++++ docker/airplay1/README.md | 3 + docker/airplay1/start.sh | 13 +++ docker/docker-compose.yaml | 1 - 8 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/docker-build-releases.yaml create mode 100644 docker/airplay1/Dockerfile create mode 100644 docker/airplay1/README.md create mode 100644 docker/airplay1/start.sh diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml new file mode 100644 index 00000000..11716fe4 --- /dev/null +++ b/.github/workflows/docker-build-releases.yaml @@ -0,0 +1,57 @@ +# Builds & pushes a docker image when a release occurs. + +# The following tags are created: +# 'latest' & 'latest-airplay1' +# '[RELEASE TAG]' & '[RELEASE TAG]-airplay1' + +name: Build and push docker images on release. + +on: + release: + types: [ published ] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set SHAIRPORT_SYNC_BRANCH env. + run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Set up ssh agent + uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Registry + uses: docker/login-action@v1 + with: + registry: registry.nova.do + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: [ registry.nova.do/shairport-sync:latest, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }} ] + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + - name: Build and push (AirPlay 1) + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/airplay1/Dockerfile-AirPlay1 + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: [ registry.nova.do/shairport-sync:latest-airplay1, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 ] + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index fc670487..f3aa91e3 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,5 +1,6 @@ -# Pushes to tag 'unstable-BRANCH'. -# Releases are pushed to latest and tagged - see docker-build-releases.yaml. +# Builds & pushes a docker image when a commit is made to one of the branches specified below. +# Tag pattern: 'unstable-[BRANCH NAME]'. +# See docker-build-releases.yaml for release build process. name: Build and push docker image based on specified branches. @@ -38,7 +39,17 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: [registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}] build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file + ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + - name: Build and push (AirPlay 1) + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/airplay1/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: [registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1] + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 40822505..6da101a1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -67,7 +67,7 @@ WORKDIR / WORKDIR /shairport-sync COPY . . -RUN git checkout $SHAIRPORT_SYNC_BRANCH +RUN git checkout "$SHAIRPORT_SYNC_BRANCH" RUN git submodule init RUN git submodule update RUN autoreconf -fi diff --git a/docker/README.md b/docker/README.md index a460af13..ccb691a2 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,6 +2,30 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync +## Example Docker Compose File +See the `docker-compose.yaml` file in this folder for an example. + +## Basic Usage + +``` +$ docker run -d --restart unless-stopped --net host --device /dev/snd \ + mikebrady/shairport-sync +``` +The above command will run Shairport Sync as a daemon in a Docker container, accessing the computer's ALSA audio infrastructure. It will send audio to the default output device and make no use of any hardware mixers the default device might have. The AirPlay service name will be the host's `hostname` with the first letter capitalised, e.g. `Ubuntu`. + +## Options + +Any options you add to the command above will be passed to Shairport Sync. Here is an example: +``` +$ docker run -d --restart unless-stopped --net host --device /dev/snd \ + mikebrady/shairport-sync -a DenSystem -- -d hw:0 -c PCM +``` +This will sent audio to alsa hardware device `hw:0` and make use of the that device's mixer control called `PCM`. The service will be visible as `DenSystem` on the network. + +## Configuration File + +Edit the configuration file `/etc/shairport-sync.conf` in the container (or use the `-v` option to mirror an external copy of `shairport-sync.conf` in to `/etc/shairport-sync.conf`) to get access to the full range of configuration options. + ## Build Example (for arm7 devices) TODO: remove `--ssh` when going public as this is only for private repos. @@ -9,4 +33,7 @@ TODO: remove `--ssh` when going public as this is only for private repos. docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . ``` -`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file +`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. + +## AirPlay 1 Only +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. \ No newline at end of file diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile new file mode 100644 index 00000000..da564276 --- /dev/null +++ b/docker/airplay1/Dockerfile @@ -0,0 +1,102 @@ +FROM alpine:3.12 AS builder-base +# General Build System: +RUN apk -U add \ + git \ + build-base \ + autoconf \ + automake \ + libtool \ + dbus \ + su-exec \ + alsa-lib-dev \ + libdaemon-dev \ + popt-dev \ + mbedtls-dev \ + soxr-dev \ + avahi-dev \ + libconfig-dev \ + libsndfile-dev \ + mosquitto-dev \ + xmltoman + +# ALAC Build System: +FROM builder-base AS builder-alac + +RUN git clone https://github.com/mikebrady/alac +WORKDIR alac +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install + +# Shairport Sync Build System: +FROM builder-base AS builder-sps + +# This will be modified by the Github Action Workflow and is required +# to ensure the correct branch is being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc +COPY --from=builder-alac /usr/local/include /usr/local/include + +WORKDIR shairport-sync +COPY . . +RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +RUN autoreconf -fi +RUN ./configure \ + --with-alsa \ + --with-dummy \ + --with-pipe \ + --with-stdout \ + --with-avahi \ + --with-ssl=mbedtls \ + --with-soxr \ + --sysconfdir=/etc \ + --with-dbus-interface \ + --with-mpris-interface \ + --with-mqtt-client \ + --with-apple-alac \ + --with-convolution +RUN make -j $(nproc) +RUN make install + +# Shairport Sync Runtime System: +FROM alpine:3.12 + +RUN apk -U add \ + alsa-lib \ + dbus \ + popt \ + glib \ + mbedtls \ + soxr \ + avahi \ + libconfig \ + libsndfile \ + mosquitto-libs \ + su-exec \ + libgcc \ + libgc++ + +RUN rm -rf /lib/apk/db/* + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-sps /etc/shairport-sync* /etc/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync + +# Create non-root user for running the container -- running as the user 'shairport-sync' also allows +# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container + +RUN addgroup shairport-sync +RUN adduser -D shairport-sync -G shairport-sync + +# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff +RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio + +COPY start.sh / + +ENTRYPOINT [ "/start.sh" ] diff --git a/docker/airplay1/README.md b/docker/airplay1/README.md new file mode 100644 index 00000000..247754e6 --- /dev/null +++ b/docker/airplay1/README.md @@ -0,0 +1,3 @@ +# AirPlay 1 Only Docker Image + +See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. For the latest build, use `latest-airplay1`. \ No newline at end of file diff --git a/docker/airplay1/start.sh b/docker/airplay1/start.sh new file mode 100644 index 00000000..f68a122f --- /dev/null +++ b/docker/airplay1/start.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +rm -rf /var/run/dbus.pid +#mkdir -p /var/run/dbus + +dbus-uuidgen --ensure +dbus-daemon --system + +avahi-daemon --daemonize --no-chroot + +su-exec shairport-sync shairport-sync $@ \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 672639c9..abcce4bc 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,6 +1,5 @@ # Example docker compose config. -version: "3.8" services: shairport-sync: image: registry.nova.do/shairport-sync:unstable-development From 136d50f27aae4758a6cdb7f2491756523c8146d9 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:02:44 +0100 Subject: [PATCH 219/878] Second attempt at additional airplay 1 build and releases process. --- .github/workflows/docker-build-releases.yaml | 4 ++-- .github/workflows/docker-build.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 11716fe4..855d6a56 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -41,7 +41,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: [ registry.nova.do/shairport-sync:latest, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }} ] + tags: registry.nova.do/shairport-sync:latest registry.nova.do/shairport-sync:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. @@ -52,6 +52,6 @@ jobs: file: ./docker/airplay1/Dockerfile-AirPlay1 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: [ registry.nova.do/shairport-sync:latest-airplay1, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 ] + tags: registry.nova.do/shairport-sync:latest-airplay1 registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index f3aa91e3..0cd23b13 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -39,7 +39,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: [registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}] + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. @@ -50,6 +50,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: [registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1] + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 165f1577599761e5f82424e3c9827955ccb353d2 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:03:39 +0100 Subject: [PATCH 220/878] Renamed. --- .github/workflows/docker-build-releases.yaml | 2 +- .github/workflows/docker-build.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 855d6a56..fef35d10 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -4,7 +4,7 @@ # 'latest' & 'latest-airplay1' # '[RELEASE TAG]' & '[RELEASE TAG]-airplay1' -name: Build and push docker images on release. +name: Build and push docker images on releases. on: release: diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 0cd23b13..a6dacf47 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -2,7 +2,7 @@ # Tag pattern: 'unstable-[BRANCH NAME]'. # See docker-build-releases.yaml for release build process. -name: Build and push docker image based on specified branches. +name: Build and push docker image based on commit to specified branches. on: push: From be252e105238f0ecbc5305405484276249c1e6b2 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:32:33 +0100 Subject: [PATCH 221/878] AirPlay 1 dockerfile path update. --- docker/airplay1/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile index da564276..76b4d73b 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/airplay1/Dockerfile @@ -97,6 +97,6 @@ RUN adduser -D shairport-sync -G shairport-sync # Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio -COPY start.sh / +COPY ./docker/airplay1/start.sh / ENTRYPOINT [ "/start.sh" ] From cbd3051c9a50f272d1138cbccd0f411a1f360d76 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:10:05 +0100 Subject: [PATCH 222/878] Added git submodule for airplay 1 docker build. --- docker/airplay1/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile index 76b4d73b..3795352e 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/airplay1/Dockerfile @@ -44,6 +44,8 @@ COPY --from=builder-alac /usr/local/include /usr/local/include WORKDIR shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +RUN git submodule init +RUN git submodule update RUN autoreconf -fi RUN ./configure \ --with-alsa \ From 6e7e47e66bdadf11c94fe2ee37002ecea20ab16d Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:03:21 +0100 Subject: [PATCH 223/878] Test workflow. --- .github/workflows/docker-build-releases.yaml | 7 +++++++ .github/workflows/docker-build.yaml | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index fef35d10..d7c8f021 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -18,22 +18,28 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Set up ssh agent uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up QEMU uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 + - name: Login to Docker Registry uses: docker/login-action@v1 with: registry: registry.nova.do username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + - name: Build and push uses: docker/build-push-action@v2 with: @@ -45,6 +51,7 @@ jobs: build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 with: diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index a6dacf47..5422b24a 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -16,22 +16,28 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Set up ssh agent uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up QEMU uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 + - name: Login to Docker Registry uses: docker/login-action@v1 with: registry: registry.nova.do username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + - name: Build and push uses: docker/build-push-action@v2 with: @@ -43,6 +49,7 @@ jobs: build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 with: @@ -50,6 +57,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 7ac1a9768efbec66675fe8bdbaa182f12efc70c1 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:21:57 +0100 Subject: [PATCH 224/878] Attempt at multiple tags. --- .github/workflows/docker-build-releases.yaml | 3 ++- .github/workflows/docker-build.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index d7c8f021..3ab06367 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -59,6 +59,7 @@ jobs: file: ./docker/airplay1/Dockerfile-AirPlay1 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:latest-airplay1 registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: registry.nova.do/shairport-sync:latest-airplay1 + tags: registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 5422b24a..10842c2e 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -57,6 +57,7 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: registry.nova.do/shairport-sync:unstable-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 85b23228ea12b93b751d3a8edf16197116346d5c Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:22:54 +0100 Subject: [PATCH 225/878] Attempt at multiple tags. --- .github/workflows/docker-build-releases.yaml | 3 +-- .github/workflows/docker-build.yaml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 3ab06367..80e6d263 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -59,7 +59,6 @@ jobs: file: ./docker/airplay1/Dockerfile-AirPlay1 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:latest-airplay1 - tags: registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: registry.nova.do/shairport-sync:latest-airplay1, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 10842c2e..1edb4457 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -57,7 +57,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 - tags: registry.nova.do/shairport-sync:unstable-airplay1 + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1, registry.nova.do/shairport-sync:unstable-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From c0fd379a769cde5298dff4944407331be1a2de5e Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:47:33 +0100 Subject: [PATCH 226/878] Working. --- .github/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 1edb4457..a5a5f68c 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -57,6 +57,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1, registry.nova.do/shairport-sync:unstable-airplay1 + tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 6082fc82055683d0205fb7fcd218152552aedfc7 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 1 Jul 2021 10:10:05 +0100 Subject: [PATCH 227/878] Added chmod to dockerfile. --- docker/airplay1/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile index 3795352e..8b21dba3 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/airplay1/Dockerfile @@ -100,5 +100,6 @@ RUN adduser -D shairport-sync -G shairport-sync RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio COPY ./docker/airplay1/start.sh / +RUN chmod +x /start.sh ENTRYPOINT [ "/start.sh" ] From 70594bffd04c2e9e0a537fe8bf7f66acea722755 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 12:49:35 +0100 Subject: [PATCH 228/878] Update RELEASENOTES-DEVELOPMENT.md Beginning to enter release notes. --- RELEASENOTES-DEVELOPMENT.md | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index e490d184..e43dbaca 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,70 @@ +Version 4.0-dev +==== + +Big Update +---- +Version 4 brings limited AirPlay 2 functionality to Shairport Sync. + +A major feature of Shairport Sync's AirPlay 2 support is Audio Synchronisation. This is achieved using an purpose-built passive [PTP](https://standards.ieee.org/standard/1588-2008.html) client called [`nqptp`](https://github.com/mikebrady/nqptp), which provides precise timing information that Shairport Sync uses to maintain tight synchronisation. (BTW, `nqptp` is not a "PTP slave". Instead, it passively monitors PTP traffic and extracts timing information from it.) + +Acknowledgements +---- +Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. + +A number of individuals made direct and invaluable contributions to Shairport Sync, so a huge "thank you" to them: +1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. +2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. +3. [Charles Somer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). + +Warning +---- +1. Major changes have been made to the codebase to enable it to be used to build either a "traditonal" AirPlay 1 player or a new AirPlay 2 player. These changes may have had unexpected effects on the AirPlay 1 build. +3. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. + +AirPlay 2 Overview +---- + +#### What Works +- AirPlay 2 for iOS and Mac players. + +#### What Does Not Work +- No AirPlay 2 for Windows iTunes +- No integration with Siri or HomeKit +- No Lossless or High Definition Lossless +- No Remote Control +- No Artwork in the Metadata + +#### What Partly Works +- Incomplete functionality using AppleTV or HomePod as player. + A Shairport Sync player does work, but the selection button doesn't persist, and volume can't be set. + +#### Limitations +- You can only run one instance of Shairport Sync on a device. + +AirPlay 2 -- More About What Works +--- +* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. + +AirPlay 2 -- What You Need +--- +The requirements for AirPlay 2 support are generally the same as before, but you need a more powerful CPU for decoding and synchronising, and you need more memory to accommodate bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 used to a lesser extent. + +Here are some suggestions: +* A Raspberry Pi 3 or better. +* A fully up-to-date Linux. +* An audio infrastructure as before -- an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. + +Guides +--- +The instructions for building Shairport Sync are different for AirPlay 2 than for AirPlay 1 -- please follow them very carefully. + +* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A guide for building on FreeBSD is forthcoming. + + Version 3.3.8d7 ==== **Bug Fix** From e2f7f1ed0fd811879363a75b1d28b49006a1d726 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:38:11 +0100 Subject: [PATCH 229/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index e43dbaca..645048fc 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -14,7 +14,7 @@ Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at A number of individuals made direct and invaluable contributions to Shairport Sync, so a huge "thank you" to them: 1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. 2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Somer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). +3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). Warning ---- From d388cdc8cea86753c533153c99d0a63dad41ea78 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 15:02:51 +0100 Subject: [PATCH 230/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 645048fc..089c7caa 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -5,7 +5,7 @@ Big Update ---- Version 4 brings limited AirPlay 2 functionality to Shairport Sync. -A major feature of Shairport Sync's AirPlay 2 support is Audio Synchronisation. This is achieved using an purpose-built passive [PTP](https://standards.ieee.org/standard/1588-2008.html) client called [`nqptp`](https://github.com/mikebrady/nqptp), which provides precise timing information that Shairport Sync uses to maintain tight synchronisation. (BTW, `nqptp` is not a "PTP slave". Instead, it passively monitors PTP traffic and extracts timing information from it.) +A major feature of Shairport Sync's AirPlay 2 support is Audio Synchronisation. This is achieved using an purpose-built passive [PTP](https://standards.ieee.org/standard/1588-2008.html) client called [`nqptp`](https://github.com/mikebrady/nqptp), which provides precise timing information that Shairport Sync uses to maintain tight synchronisation. (BTW, `nqptp` is not a "PTP slave". Instead, it passively monitors PTP traffic for timing information.) Acknowledgements ---- @@ -18,8 +18,9 @@ A number of individuals made direct and invaluable contributions to Shairport Sy Warning ---- -1. Major changes have been made to the codebase to enable it to be used to build either a "traditonal" AirPlay 1 player or a new AirPlay 2 player. These changes may have had unexpected effects on the AirPlay 1 build. -3. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. +1. Big changes have been made to the codebase to enable it to be used to build either a "traditional" AirPlay 1 player or a new AirPlay 2 player. These changes may have introduced bugs into the AirPlay 1 build. +2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. +3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. If you are not adept with Linux or FreeBSD, please don't get involved. AirPlay 2 Overview ---- @@ -36,35 +37,35 @@ AirPlay 2 Overview #### What Partly Works - Incomplete functionality using AppleTV or HomePod as player. - A Shairport Sync player does work, but the selection button doesn't persist, and volume can't be set. + A Shairport Sync player will work, but the selection button won't persist and the volume can't be set. #### Limitations - You can only run one instance of Shairport Sync on a device. +- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. AirPlay 2 -- More About What Works --- * Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. AirPlay 2 -- What You Need --- -The requirements for AirPlay 2 support are generally the same as before, but you need a more powerful CPU for decoding and synchronising, and you need more memory to accommodate bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 used to a lesser extent. +The requirements for AirPlay 2 support are generally the same as before, except that you need a more powerful CPU for decoding and synchronisation, and you need more memory for bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. Here are some suggestions: -* A Raspberry Pi 3 or better. -* A fully up-to-date Linux. -* An audio infrastructure as before -- an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. +* Full access, including `root` priviliges, to a system at least as powerful as a Raspberry Pi 3. +* A fully up-to-date Linux. This is important, as some of the libraries must be the very laterst available. +* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. Guides --- -The instructions for building Shairport Sync are different for AirPlay 2 than for AirPlay 1 -- please follow them very carefully. +Instructions for building Shairport Sync are different for AirPlay 2 than for AirPlay 1 -- please follow them very carefully. * A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. - Version 3.3.8d7 ==== **Bug Fix** From cfd27f0a69bfab1c10ea06a44bb7bbf0eb7d8bc0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:24:19 +0100 Subject: [PATCH 231/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 089c7caa..f64308e8 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -11,7 +11,7 @@ Acknowledgements ---- Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. -A number of individuals made direct and invaluable contributions to Shairport Sync, so a huge "thank you" to them: +Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: 1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. 2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. 3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). @@ -20,7 +20,7 @@ Warning ---- 1. Big changes have been made to the codebase to enable it to be used to build either a "traditional" AirPlay 1 player or a new AirPlay 2 player. These changes may have introduced bugs into the AirPlay 1 build. 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. -3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. If you are not adept with Linux or FreeBSD, please don't get involved. +3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. AirPlay 2 Overview ---- @@ -40,22 +40,19 @@ AirPlay 2 Overview A Shairport Sync player will work, but the selection button won't persist and the volume can't be set. #### Limitations -- You can only run one instance of Shairport Sync on a device. +- You can only run a single instance of Shairport Sync on a device. - The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. -AirPlay 2 -- More About What Works ---- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. +#### Known Issues +- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, pleasse let us know. AirPlay 2 -- What You Need --- -The requirements for AirPlay 2 support are generally the same as before, except that you need a more powerful CPU for decoding and synchronisation, and you need more memory for bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. +AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. Here are some suggestions: -* Full access, including `root` priviliges, to a system at least as powerful as a Raspberry Pi 3. -* A fully up-to-date Linux. This is important, as some of the libraries must be the very laterst available. +* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. +* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. * An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. Guides @@ -66,6 +63,12 @@ Instructions for building Shairport Sync are different for AirPlay 2 than for Ai * To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. +AirPlay 2 -- More About What Works +--- +* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. + Version 3.3.8d7 ==== **Bug Fix** From 365cecd35dcc46719ec4178b9dc5f9b82a1fca23 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:19:10 +0100 Subject: [PATCH 232/878] Remove tinyhttp as a submodule --- .gitmodules | 4 ---- tinyhttp | 1 - 2 files changed, 5 deletions(-) delete mode 160000 tinyhttp diff --git a/.gitmodules b/.gitmodules index a2828361..0e9d4cac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "tinyhttp"] - path = tinyhttp - url = https://github.com/mendsley/tinyhttp.git - ignore = dirty [submodule "pair_ap"] path = pair_ap url = https://github.com/ejurgensen/pair_ap diff --git a/tinyhttp b/tinyhttp deleted file mode 160000 index a524562a..00000000 --- a/tinyhttp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a524562a5318457d63ec21488c4ccc54202c0436 From d4a625cf9c0be2452c213d891c673cd042d0db5c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:24:51 +0100 Subject: [PATCH 233/878] Check that a git archive actually exists as well as the git app being installed. --- configure.ac | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 541ec999..5e4cf244 100644 --- a/configure.ac +++ b/configure.ac @@ -9,11 +9,10 @@ AC_CONFIG_HEADERS([config.h]) AC_PROG_RANLIB AC_CHECK_PROGS([GIT], [git]) -if test -n "$GIT"; then +if test -n "$GIT" && test -e ".git/index" ; then AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.]) fi -AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT"]) - +AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e ".git/index" ]) # Derived from the Avahi configure.ac file # Specifying the OS type, defaulting to linux. From e3131c53944061991e24124dbefcb07eb7b43c43 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:41:24 +0100 Subject: [PATCH 234/878] Add tinyhttp as a subdirectory and not a submodule for ease of switching from master to development --- tinyhttp/LICENSE | 23 +++++ tinyhttp/README.md | 28 ++++++ tinyhttp/chunk.c | 79 +++++++++++++++ tinyhttp/chunk.h | 48 +++++++++ tinyhttp/example.cpp | 190 ++++++++++++++++++++++++++++++++++++ tinyhttp/header.c | 72 ++++++++++++++ tinyhttp/header.h | 61 ++++++++++++ tinyhttp/http.c | 227 +++++++++++++++++++++++++++++++++++++++++++ tinyhttp/http.h | 93 ++++++++++++++++++ 9 files changed, 821 insertions(+) create mode 100644 tinyhttp/LICENSE create mode 100644 tinyhttp/README.md create mode 100644 tinyhttp/chunk.c create mode 100644 tinyhttp/chunk.h create mode 100644 tinyhttp/example.cpp create mode 100644 tinyhttp/header.c create mode 100644 tinyhttp/header.h create mode 100644 tinyhttp/http.c create mode 100644 tinyhttp/http.h diff --git a/tinyhttp/LICENSE b/tinyhttp/LICENSE new file mode 100644 index 00000000..ba420590 --- /dev/null +++ b/tinyhttp/LICENSE @@ -0,0 +1,23 @@ +Copyright 2012 Matthew Endsley +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/tinyhttp/README.md b/tinyhttp/README.md new file mode 100644 index 00000000..367138f7 --- /dev/null +++ b/tinyhttp/README.md @@ -0,0 +1,28 @@ +tinyhttp +======== +Tiny (as in minimal) implementation of an HTTP response parser. The parser +itself is only dependent on: +* ` - tolower` +* ` - memcpy` + +For more information please see my blog post at: + +[![Build Status](https://api.travis-ci.org/mendsley/tinyhttp.png)](http://travis-ci.org/mendsley/tinyhttp) + +Contact +------- +[@MatthewEndsley](https://twitter.com/#!/MatthewEndsley) + + +License +------- +Copyright 2012 Matthew Endsley + +This project is governed by the BSD 2-clause license. For details see the file +titled LICENSE in the project root folder. + +Compiling +--------- +`gcc -c *.c && g++ -std=c++0x example.cpp *.o -o example` + +`./example` will fetch the root of diff --git a/tinyhttp/chunk.c b/tinyhttp/chunk.c new file mode 100644 index 00000000..3f7c889e --- /dev/null +++ b/tinyhttp/chunk.c @@ -0,0 +1,79 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +static const unsigned char http_chunk_state[] = { +/* * LF CR HEX */ + 0xC1, 0xC1, 0xC1, 1, /* s0: initial hex char */ + 0xC1, 0xC1, 2, 0x81, /* s1: additional hex chars, followed by CR */ + 0xC1, 0x83, 0xC1, 0xC1, /* s2: trailing LF */ + 0xC1, 0xC1, 4, 0xC1, /* s3: CR after chunk block */ + 0xC1, 0xC0, 0xC1, 0xC1, /* s4: LF after chunk block */ +}; + +int http_parse_chunked(int* state, int *size, char ch) +{ + int newstate, code = 0; + switch (ch) { + case '\n': code = 1; break; + case '\r': code = 2; break; + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + case '8': case '9': case 'a': case 'b': + case 'c': case 'd': case 'e': case 'f': + case 'A': case 'B': case 'C': case 'D': + case 'E': case 'F': code = 3; break; + } + + newstate = http_chunk_state[*state * 4 + code]; + *state = (newstate & 0xF); + + switch (newstate) { + case 0xC0: + return *size != 0; + + case 0xC1: /* error */ + *size = -1; + return 0; + + case 0x01: /* initial char */ + *size = 0; + /* fallthrough */ + case 0x81: /* size char */ + if (ch >= 'a') + *size = *size * 16 + (ch - 'a' + 10); + else if (ch >= 'A') + *size = *size * 16 + (ch - 'A' + 10); + else + *size = *size * 16 + (ch - '0'); + break; + + case 0x83: + return *size == 0; + } + + return 1; +} + diff --git a/tinyhttp/chunk.h b/tinyhttp/chunk.h new file mode 100644 index 00000000..e0df0600 --- /dev/null +++ b/tinyhttp/chunk.h @@ -0,0 +1,48 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef HTTP_CHUNK_H +#define HTTP_CHUNK_H + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * Parses the size out of a chunk-encoded HTTP response. Returns non-zero if it + * needs more data. Returns zero success or error. When error: size == -1 On + * success, size = size of following chunk data excluding trailing \r\n. User is + * expected to process or otherwise seek past chunk data up to the trailing + * \r\n. The state parameter is used for internal state and should be + * initialized to zero the first call. + */ +int http_parse_chunked(int* state, int *size, char ch); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/tinyhttp/example.cpp b/tinyhttp/example.cpp new file mode 100644 index 00000000..4054f9a7 --- /dev/null +++ b/tinyhttp/example.cpp @@ -0,0 +1,190 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +/* +Compiling example: +$ g++ -o example example.cpp +*/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "http.h" + +// directly embed the source here +extern "C" { + #include "http.c" + #include "header.c" + #include "chunk.c" +} + +// return a socket connected to a hostname, or -1 +int connectsocket(const char* host, int port) +{ + + addrinfo* result = NULL; + sockaddr_in addr = {0}; + int s; + + if (getaddrinfo(host, NULL, NULL, &result)) + goto error; + + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = INADDR_ANY; + for (addrinfo* ai = result; ai != NULL; ai = ai->ai_next) { + if (ai->ai_family != AF_INET) + continue; + + const sockaddr_in *ai_in = (const sockaddr_in*)ai->ai_addr; + addr.sin_addr = ai_in->sin_addr; + break; + } + + freeaddrinfo(result); + + if (addr.sin_addr.s_addr == INADDR_ANY) + goto error; + + s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == -1) + goto error; + + if (connect(s, (const sockaddr*)&addr, sizeof(addr))) + goto error; + + return s; + +error: + if (s != -1) + close(s); + if (result) + freeaddrinfo(result); + return -1; +} + +// Response data/funcs +struct HttpResponse { + std::vector body; + int code; +}; + +static void* response_realloc(void* opaque, void* ptr, int size) +{ + return realloc(ptr, size); +} + +static void response_body(void* opaque, const char* data, int size) +{ + HttpResponse* response = (HttpResponse*)opaque; + response->body.insert(response->body.end(), data, data + size); +} + +static void response_header(void* opaque, const char* ckey, int nkey, const char* cvalue, int nvalue) +{ /* example doesn't care about headers */ } + +static void response_code(void* opaque, int code) +{ + HttpResponse* response = (HttpResponse*)opaque; + response->code = code; +} + +static const http_funcs responseFuncs = { + response_realloc, + response_body, + response_header, + response_code, +}; + +int main() +{ + + int conn = connectsocket("nothings.org", 80); + if (conn < 0) { + fprintf(stderr, "Failed to connect socket\n"); + return -1; + } + + const char request[] = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n"; + int len = send(conn, request, sizeof(request) - 1, 0); + if (len != sizeof(request) - 1) { + fprintf(stderr, "Failed to send request\n"); + close(conn); + return -1; + } + + HttpResponse response; + response.code = 0; + + http_roundtripper rt; + http_init(&rt, responseFuncs, &response); + + bool needmore = true; + char buffer[1024]; + while (needmore) { + const char* data = buffer; + int ndata = recv(conn, buffer, sizeof(buffer), 0); + if (ndata <= 0) { + fprintf(stderr, "Error receiving data\n"); + http_free(&rt); + close(conn); + return -1; + } + + while (needmore && ndata) { + int read; + needmore = http_data(&rt, data, ndata, &read); + ndata -= read; + data += read; + } + } + + if (http_iserror(&rt)) { + fprintf(stderr, "Error parsing data\n"); + http_free(&rt); + close(conn); + return -1; + } + + http_free(&rt); + close(conn); + + printf("Response: %d\n", response.code); + if (!response.body.empty()) { + printf("%s\n", &response.body[0]); + } + + return 0; +} diff --git a/tinyhttp/header.c b/tinyhttp/header.c new file mode 100644 index 00000000..cda9eb54 --- /dev/null +++ b/tinyhttp/header.c @@ -0,0 +1,72 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "header.h" + +static unsigned char http_header_state[] = { +/* * \t \n \r ' ' , : PAD */ + 0x80, 1, 0xC1, 0xC1, 1, 0x80, 0x80, 0xC1, /* state 0: HTTP version */ + 0x81, 2, 0xC1, 0xC1, 2, 1, 1, 0xC1, /* state 1: Response code */ + 0x82, 0x82, 4, 3, 0x82, 0x82, 0x82, 0xC1, /* state 2: Response reason */ + 0xC1, 0xC1, 4, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 3: HTTP version newline */ + 0x84, 0xC1, 0xC0, 5, 0xC1, 0xC1, 6, 0xC1, /* state 4: Start of header field */ + 0xC1, 0xC1, 0xC0, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 5: Last CR before end of header */ + 0x87, 6, 0xC1, 0xC1, 6, 0x87, 0x87, 0xC1, /* state 6: leading whitespace before header value */ + 0x87, 0x87, 0xC4, 10, 0x87, 0x88, 0x87, 0xC1, /* state 7: header field value */ + 0x87, 0x88, 6, 9, 0x88, 0x88, 0x87, 0xC1, /* state 8: Split value field value */ + 0xC1, 0xC1, 6, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 9: CR after split value field */ + 0xC1, 0xC1, 0xC4, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, /* state 10:CR after header value */ +}; + +int http_parse_header_char(int* state, char ch) +{ + int newstate, code = 0; + switch (ch) { + case '\t': code = 1; break; + case '\n': code = 2; break; + case '\r': code = 3; break; + case ' ': code = 4; break; + case ',': code = 5; break; + case ':': code = 6; break; + } + + newstate = http_header_state[*state * 8 + code]; + *state = (newstate & 0xF); + + switch (newstate) { + case 0xC0: return http_header_status_done; + case 0xC1: return http_header_status_done; + case 0xC4: return http_header_status_store_keyvalue; + case 0x80: return http_header_status_version_character; + case 0x81: return http_header_status_code_character; + case 0x82: return http_header_status_status_character; + case 0x84: return http_header_status_key_character; + case 0x87: return http_header_status_value_character; + case 0x88: return http_header_status_value_character; + } + + return http_header_status_continue; +} diff --git a/tinyhttp/header.h b/tinyhttp/header.h new file mode 100644 index 00000000..7f8ee6ab --- /dev/null +++ b/tinyhttp/header.h @@ -0,0 +1,61 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef HTTP_HEADER_H +#define HTTP_HEADER_H + +#if defined(__cplusplus) +extern "C" { +#endif + +enum http_header_status +{ + http_header_status_done, + http_header_status_continue, + http_header_status_version_character, + http_header_status_code_character, + http_header_status_status_character, + http_header_status_key_character, + http_header_status_value_character, + http_header_status_store_keyvalue +}; + +/** + * Parses a single character of an HTTP header stream. The state parameter is + * used as internal state and should be initialized to zero for the first call. + * Return value is a value from the http_header_status enuemeration specifying + * the semantics of the character. If an error is encountered, + * http_header_status_done will be returned with a non-zero state parameter. On + * success http_header_status_done is returned with the state parameter set to + * zero. + */ +int http_parse_header_char(int* state, char ch); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/tinyhttp/http.c b/tinyhttp/http.c new file mode 100644 index 00000000..5ef0ae3f --- /dev/null +++ b/tinyhttp/http.c @@ -0,0 +1,227 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "http.h" + +#include +#include + +#include "header.h" +#include "chunk.h" + +static void append_body(struct http_roundtripper* rt, const char* data, int ndata) +{ + rt->funcs.body(rt->opaque, data, ndata); +} + +static void grow_scratch(struct http_roundtripper* rt, int size) +{ + if (rt->nscratch >= size) + return; + + if (size < 64) + size = 64; + int nsize = (rt->nscratch * 3) / 2; + if (nsize < size) + nsize = size; + + rt->scratch = (char*)rt->funcs.realloc_scratch(rt->opaque, rt->scratch, nsize); + rt->nscratch = nsize; +} + +static int min(int a, int b) +{ + return a > b ? b : a; +} + +enum http_roundtripper_state { + http_roundtripper_header, + http_roundtripper_chunk_header, + http_roundtripper_chunk_data, + http_roundtripper_raw_data, + http_roundtripper_unknown_data, + http_roundtripper_close, + http_roundtripper_error, +}; + +void http_init(struct http_roundtripper* rt, struct http_funcs funcs, void* opaque) +{ + rt->funcs = funcs; + rt->scratch = 0; + rt->opaque = opaque; + rt->code = 0; + rt->parsestate = 0; + rt->contentlength = -1; + rt->state = http_roundtripper_header; + rt->nscratch = 0; + rt->nkey = 0; + rt->nvalue = 0; + rt->chunked = 0; +} + +void http_free(struct http_roundtripper* rt) +{ + if (rt->scratch) { + rt->funcs.realloc_scratch(rt->opaque, rt->scratch, 0); + rt->scratch = 0; + } +} + +int http_data(struct http_roundtripper* rt, const char* data, int size, int* read) +{ + const int initial_size = size; + while (size) { + switch (rt->state) { + case http_roundtripper_header: + switch (http_parse_header_char(&rt->parsestate, *data)) { + case http_header_status_done: + rt->funcs.code(rt->opaque, rt->code); + if (rt->parsestate != 0) + rt->state = http_roundtripper_error; + else if (rt->chunked) { + rt->contentlength = 0; + rt->state = http_roundtripper_chunk_header; + } else if (rt->contentlength == 0) + rt->state = http_roundtripper_close; + else if (rt->contentlength > 0) + rt->state = http_roundtripper_raw_data; + else if (rt->contentlength == -1) + rt->state = http_roundtripper_unknown_data; + else + rt->state = http_roundtripper_error; + break; + + case http_header_status_code_character: + rt->code = rt->code * 10 + *data - '0'; + break; + + case http_header_status_key_character: + grow_scratch(rt, rt->nkey + 1); + rt->scratch[rt->nkey] = tolower(*data); + ++rt->nkey; + break; + + case http_header_status_value_character: + grow_scratch(rt, rt->nkey + rt->nvalue + 1); + rt->scratch[rt->nkey+rt->nvalue] = *data; + ++rt->nvalue; + break; + + case http_header_status_store_keyvalue: + if (rt->nkey == 17 && 0 == strncmp(rt->scratch, "transfer-encoding", rt->nkey)) + rt->chunked = (rt->nvalue == 7 && 0 == strncmp(rt->scratch + rt->nkey, "chunked", rt->nvalue)); + else if (rt->nkey == 14 && 0 == strncmp(rt->scratch, "content-length", rt->nkey)) { + int ii, end; + rt->contentlength = 0; + for (ii = rt->nkey, end = rt->nkey + rt->nvalue; ii != end; ++ii) + rt->contentlength = rt->contentlength * 10 + rt->scratch[ii] - '0'; + } + + rt->funcs.header(rt->opaque, rt->scratch, rt->nkey, rt->scratch + rt->nkey, rt->nvalue); + + rt->nkey = 0; + rt->nvalue = 0; + break; + } + + --size; + ++data; + break; + + case http_roundtripper_chunk_header: + if (!http_parse_chunked(&rt->parsestate, &rt->contentlength, *data)) { + if (rt->contentlength == -1) + rt->state = http_roundtripper_error; + else if (rt->contentlength == 0) + rt->state = http_roundtripper_close; + else + rt->state = http_roundtripper_chunk_data; + } + + --size; + ++data; + break; + + case http_roundtripper_chunk_data: { + const int chunksize = min(size, rt->contentlength); + append_body(rt, data, chunksize); + rt->contentlength -= chunksize; + size -= chunksize; + data += chunksize; + + if (rt->contentlength == 0) { + rt->contentlength = 1; + rt->state = http_roundtripper_chunk_header; + } + } + break; + + case http_roundtripper_raw_data: { + const int chunksize = min(size, rt->contentlength); + append_body(rt, data, chunksize); + rt->contentlength -= chunksize; + size -= chunksize; + data += chunksize; + + if (rt->contentlength == 0) + rt->state = http_roundtripper_close; + } + break; + + case http_roundtripper_unknown_data: { + if (size == 0) + rt->state = http_roundtripper_close; + else { + append_body(rt, data, size); + size -= size; + data += size; + } + } + break; + + case http_roundtripper_close: + case http_roundtripper_error: + break; + } + + if (rt->state == http_roundtripper_error || rt->state == http_roundtripper_close) { + if (rt->scratch) { + rt->funcs.realloc_scratch(rt->opaque, rt->scratch, 0); + rt->scratch = 0; + } + *read = initial_size - size; + return 0; + } + } + + *read = initial_size - size; + return 1; +} + +int http_iserror(struct http_roundtripper* rt) +{ + return rt->state == http_roundtripper_error; +} diff --git a/tinyhttp/http.h b/tinyhttp/http.h new file mode 100644 index 00000000..ba7e4cfb --- /dev/null +++ b/tinyhttp/http.h @@ -0,0 +1,93 @@ +/*- + * Copyright 2012 Matthew Endsley + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef HTTP_HTTP_H +#define HTTP_HTTP_H + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * Callbacks for handling response data. + * realloc_scratch - reallocate memory, cannot fail. There will only + * be one scratch buffer. Implemnentation may take + * advantage of this fact. + * body - handle HTTP response body data + * header - handle an HTTP header key/value pair + * code - handle the HTTP status code for the response + */ +struct http_funcs { + void* (*realloc_scratch)(void* opaque, void* ptr, int size); + void (*body)(void* opaque, const char* data, int size); + void (*header)(void* opaque, const char* key, int nkey, const char* value, int nvalue); + void (*code)(void* opqaue, int code); +}; + +struct http_roundtripper { + struct http_funcs funcs; + void *opaque; + char *scratch; + int code; + int parsestate; + int contentlength; + int state; + int nscratch; + int nkey; + int nvalue; + int chunked; +}; + +/** + * Initializes a rountripper with the specified response functions. This must + * be called before the rt object is used. + */ +void http_init(struct http_roundtripper* rt, struct http_funcs, void* opaque); + +/** + * Frees any scratch memory allocated during parsing. + */ +void http_free(struct http_roundtripper* rt); + +/** + * Parses a block of HTTP response data. Returns zero if the parser reached the + * end of the response, or an error was encountered. Use http_iserror to check + * for the presence of an error. Returns non-zero if more data is required for + * the response. + */ +int http_data(struct http_roundtripper* rt, const char* data, int size, int* read); + +/** + * Returns non-zero if a completed parser encounted an error. If http_data did + * not return non-zero, the results of this function are undefined. + */ +int http_iserror(struct http_roundtripper* rt); + +#if defined(__cplusplus) +} +#endif + +#endif From cafa751af4a449be7d4602a6f50e00e8bba1d0f0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 6 Jul 2021 22:28:15 +0100 Subject: [PATCH 235/878] Update MOREINFO.md --- MOREINFO.md | 336 ++-------------------------------------------------- 1 file changed, 9 insertions(+), 327 deletions(-) diff --git a/MOREINFO.md b/MOREINFO.md index 1b024340..7b957974 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -1,9 +1,8 @@ - More Information ---------- Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. +Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. @@ -11,13 +10,13 @@ For more about the motivation behind Shairport Sync, please see the wiki at http Synchronisation, Latency, "Stuffing" --------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. +The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. +Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using either the PTP or NTP synchronisation protocols. What else? -------------- @@ -27,9 +26,8 @@ What else? * Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. * Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. * Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`. -* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) * Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. -* Compiles on Linux, Cygwin, FreeBSD, OpenBSD. +* Compiles on Linux and FreeBSD. * Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io). * An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. * An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. @@ -39,268 +37,10 @@ Heritage ------- Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. -Status ------- -Shairport Sync works on a wide variety of Linux devices, FreeBSD and OpenBSD. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs. Shairport Sync runs well on the Raspberry Pi. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card. - -Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. - -Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system. - -Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora, FreeBSD and OpenBSD inside VMWare Fusion on a Mac, but synchronisation in inaccurate — possibly because the sound card is being emulated. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more: - -**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. - -**Debian:** shairport-sync is in the Debian archive. - -**OpenWrt:** There is a Shairport Sync package in OpenWrt trunk. Also, there's an OpenWrt package at https://github.com/mikebrady/shairport-sync-for-openwrt, including one that builds back to Barrier Breaker. - -**Arch Linux:** Shairport Sync is available for x86_64 and i686 platforms in the Arch Linux Community Repository -- search for `shairport-sync`. See also https://www.archlinux.org/packages/. - -To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda). - -**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: -``` -$brew install shairport-sync -``` -Note that the installation uses the libao library and so synchronisation is not available — playback glitches will occur occasionally, when the ao system's buffers overflow or underflow. - -**Fedora:** Please see the guide at [FEDORA.md](https://github.com/mikebrady/shairport-sync/blob/master/FEDORA.md). - -**Cygwin:** Please see the guide at [CYGWIN.md](https://github.com/mikebrady/shairport-sync/blob/master/CYGWIN.md). - -Sincere thanks to all package contributors! - -If you wish to build and install the latest version of Shairport Sync on Debian, Ubuntu, Fedora or Arch platforms, please continue to follow these instructions. When the program has been compiled and installed, refer to the section on Configuring Shairport Sync that follows. To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md). - -**Remove Old Versions of Shairport Sync and its Startup Scripts** - -You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. - -You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. - -If you removed any installations of Shairport Sync or any of its startup script files, you should reboot. - -**Determine The Configuration Needed** - -Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: unix:/run/user/1000/pulse/native -Library Protocol Version: 30 -Server Protocol Version: 30 -Is Local: yes -Client Index: 9 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu -Server Name: pulseaudio -Server Version: 8.0 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 96f9:3e8d -$ -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. - -If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. - -**Building** - -To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. - -The following libraries are required: -* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) -* Avahi -* ALSA and/or PulseAudio -* libdaemon -* autoconf -* automake -* libtool -* libpopt -* libconfig - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) - -Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt install build-essential git xmltoman` – these may already be installed. -- `# apt install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev` -- `# apt install libasound2-dev` for the ALSA libraries -- `# apt install libpulse-dev` for the PulseAudio libraries -- `# apt install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in recent distributions; if not, instructions for how to build it from source on Linux are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). -- `# apt install libsndfile1-dev` if you want to use the convolution filter. - -If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. - -**Download Shairport Sync:** -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -``` - -Next, `cd` into the shairport-sync directory and execute the following commands: -``` -$ autoreconf -i -f -``` -(Note that the `autoreconf...` step may take some time on less powerful machines.) - -**Choose the appropriate `--with-*` options:** - -(Don't worry -- there's a recommended set of configuration options further down.) - -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. -- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. -- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). -- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. -- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system. -- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. -- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. -- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently. -- `--with-soxr` for libsoxr-based resampling. -- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. -- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. -- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. -- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. -- `--with-apple-alac` to include the Apple ALAC Decoder. -- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. -- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. -- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. - -**Determine if it's a `systemd` or a "System V" installation:** - -If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) - -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. - -The easiest way is to look at the first few lines of the `init` manual. Enter the command: - -``` -$ man init -``` -In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): -``` -SYSTEMD(1) systemd SYSTEMD(1) - -NAME - systemd, init - systemd system and service manager -... -``` -Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): - -``` -init(8) System Manager's Manual init(8) - -NAME - init - Upstart process management daemon - -SYNOPSIS - init [OPTION]... -... -``` -In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): -``` -INIT(8) Linux System Administrator's Manual INIT(8) - -NAME - init, telinit - process control initialization - -SYNOPSIS - /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] - /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] - /sbin/telinit [ -e VAR[=VAL] ] - -... -``` -If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-libdaemon --with-systemv`. - -**Choose the location of the configuration file** - -A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. - -**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** - -Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Buster, Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: - -`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd` - -* Omit the `--with-soxr` if the libsoxr library is not available. -* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`. -* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`. - -**Build and Install the Application:** - -Enter: - -``` -$ make -``` - -to build the application. - -Assuming you have used the `./configure` settings suggested above, you can now install the application: - -``` -$ sudo make install -``` -The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. - - -**Complete installation into to a `systemd` system** - -If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: - -``` -$ sudo systemctl enable shairport-sync -``` -You should then reboot the system. - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -$ sudo update-rc.d shairport-sync enable -``` -You should then reboot the system. - -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html Configuring Shairport Sync -------- -There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. - -**(1) Starting and Stopping:** -Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. - -If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, if the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. - -**(2) Settings:** -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. +To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end, etc. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. @@ -355,21 +95,14 @@ Note: Shairport Sync can take configuration settings from command line options. **Raspberry Pi** -The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. +Many Raspberry Pi Models have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. -To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. - -Do the usual update and upgrade: -``` -# apt update -# apt upgrade -``` To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: ``` alsa = { output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. - mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. + mixer_control_name = "Headphone"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. // ... other alsa settings ``` (Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. @@ -378,20 +111,6 @@ A problem with the built-in DAC that it declares itself to have a very large mix From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. -*Command Line Arguments* - -As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html. - -Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. - -* The `-c` option allows you to specify the location of the configuration file. -* The `-V` option gives you version information about Shairport Sync and then quits. -* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. - -The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : -`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. - Examples -------- @@ -419,7 +138,7 @@ general = { alsa = { output_device = "hw:0"; - mixer_control_name = "PCM"; + mixer_control_name = "Headphone"; }; ``` @@ -447,41 +166,6 @@ alsa = { }; ``` -For a first generation Griffin iMic on a Raspberry Pi: -``` -general = { - name = "Attic"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: - -On an NSLU2, to drive a first generation Griffin iMic: -``` -general = { - name = "Den"; -}; - -alsa = { - mixer_control_name = "PCM"; -}; -``` - -On an NSLU2, to drive the "3D Sound" USB card: -``` -general = { - name = "TV Room"; -}; - -alsa = { - mixer_control_name = "Speaker"; -}; -``` Finally, here is an example of using the PulseAudio backend: ``` @@ -510,9 +194,7 @@ Latency ------- Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. -Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. +Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. Resynchronisation ------------- From c030e88c59d37c69bbfef77876385fe76b6df3e8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 07:45:22 +0100 Subject: [PATCH 236/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index f64308e8..870a2fea 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -18,7 +18,7 @@ Big thanks are due to number of individuals made direct and valuable contributio Warning ---- -1. Big changes have been made to the codebase to enable it to be used to build either a "traditional" AirPlay 1 player or a new AirPlay 2 player. These changes may have introduced bugs into the AirPlay 1 build. +1. Big changes have been made to the codebase to enable it to be used to build either as the "classic" AirPlay 1 player or as a new AirPlay 2 player. These changes have probably introduced bugs into the classic AirPlay 1 build. 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. 3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. @@ -44,7 +44,7 @@ AirPlay 2 Overview - The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. #### Known Issues -- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, pleasse let us know. +- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. AirPlay 2 -- What You Need --- From 7c82f13be3312c300cf36192b50597763dd0a3cc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 07:47:48 +0100 Subject: [PATCH 237/878] Create AIRPLAY2.md --- AIRPLAY2.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 AIRPLAY2.md diff --git a/AIRPLAY2.md b/AIRPLAY2.md new file mode 100644 index 00000000..ea84155c --- /dev/null +++ b/AIRPLAY2.md @@ -0,0 +1,42 @@ +AirPlay 2 Matters +==== +* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. +* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. +* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. + +Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. + +What Works +--- +* AirPlay 2 audio for players hosted on macOS and iOS. +* AirPlay 2 audio for System Sound Output from macOS. +* The `ALSA`, `stdout` and `pipe` backends definitely work, as does the `sndio` backend for FreeBSD. + +What Does Not Work +--- +* Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. +* There is no integration with Siri or with the Home app or HomeKit, so nothing to do with the Home app works. +* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) +* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. +* Artwork is not downloaded. +* Playing from certain Apple devices to Shairport Sync does not work as expected. For example: + * When using a service with the ability to play directly on HomePod (e.g. Apple Music). If the music starts on the HomePod, or is the first device selected from the AirPlay list on iOS, when Shairport Sync is then selected iOS shows it as not connected (even though it is). Therefore the expected volume controls are not displayed. A workaround for this is to force an AirPlay stream to the HomePod (rather than just acting as a remote controller), this can be done by selected Shairport Sync _before_ the HomePod. Note, this issue is not present for other services such as SoundCloud which do not integrate into the HomePod like Apple Music does. + * The above example is true also when trying to select Shairport Sync from the AirPlay menu on the HomePod from the Home app. + +More About What Works +--- +* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. + +What You Need +--- +* A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! +* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) + +| Device | Native | Docker | Notes | +|---------------------|----------|----------------|-------| +| Raspberry Pi 4 | Working | Working | | +| Raspberry Pi 3 | Working | Working | | +| Raspberry Pi Zero W | Untested | Mostly working | Since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs, this currently includes quickly changing the volume repeatedly. Total CPU usage sits at around 75% when using Shairport Sync. Updates to Shairport Sync could make this better or worse. | From f67bcc8af1827a143ced28eab8c9c6266b545dd6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 07:59:35 +0100 Subject: [PATCH 238/878] Update README.md --- README.md | 53 +++++++---------------------------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 6966cce6..c3720469 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,17 @@ Shairport Sync ============= -* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync can be built to support AirPlay 1 only or it can be built to support Airplay 2, which includes AirPlay 1 support. +* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). +* Shairport Sync can be built for "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much bigger and requires more libraries and RAM at build time and run time. -AirPlay 2 Matters -==== -* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. - -Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. - -What Works +AirPlay 2 --- -* AirPlay 2 audio for players hosted on macOS and iOS. -* AirPlay 2 audio for System Sound Output from macOS. -* The `ALSA`, `stdout` and `pipe` backends definitely work, as does the `sndio` backend for FreeBSD. - -What Does Not Work ---- -* Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. -* There is no integration with Siri or with the Home app or HomeKit, so nothing to do with the Home app works. -* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) -* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. -* Artwork is not downloaded. -* Playing from certain Apple devices to Shairport Sync does not work as expected. For example: - * When using a service with the ability to play directly on HomePod (e.g. Apple Music). If the music starts on the HomePod, or is the first device selected from the AirPlay list on iOS, when Shairport Sync is then selected iOS shows it as not connected (even though it is). Therefore the expected volume controls are not displayed. A workaround for this is to force an AirPlay stream to the HomePod (rather than just acting as a remote controller), this can be done by selected Shairport Sync _before_ the HomePod. Note, this issue is not present for other services such as SoundCloud which do not integrate into the HomePod like Apple Music does. - * The above example is true also when trying to select Shairport Sync from the AirPlay menu on the HomePod from the Home app. - -More About What Works ---- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. - -What You Need ---- -* A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! -* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) - -| Device | Native | Docker | Notes | -|---------------------|----------|----------------|-------| -| Raspberry Pi 4 | Working | Working | | -| Raspberry Pi 3 | Working | Working | | -| Raspberry Pi Zero W | Untested | Mostly working | Since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs, this currently includes quickly changing the volume repeatedly. Total CPU usage sits at around 75% when using Shairport Sync. Updates to Shairport Sync could make this better or worse. | +For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/aillwee/shairport-sync/blob/development/AIRPLAY2.md). Guides --- -* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A brief guide to building classic Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. Note @@ -68,6 +29,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. -* [Charles Omer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for Docker autoation (forthcoming), testing, encouragement, enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From f6c88faf5aaa0ae2750e05ab54e82b657fbd696b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:01:14 +0100 Subject: [PATCH 239/878] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3720469..59e63e42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). -* Shairport Sync can be built for "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much bigger and requires more libraries and RAM at build time and run time. +* Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much bigger and requires more libraries and RAM at build time and run time. AirPlay 2 --- @@ -10,7 +10,7 @@ For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/aillw Guides --- -* A brief guide to building classic Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. From c9da658fe11a72f6547aa4f546c8f9d47b9ecf1e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:23:06 +0100 Subject: [PATCH 240/878] Update AIRPLAY2.md --- AIRPLAY2.md | 82 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index ea84155c..0c3a7a1e 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,42 +1,62 @@ -AirPlay 2 Matters -==== -* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. +AirPlay 2 +=== + +## General * **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. +* For AirPlay 2, Shairport Sync requires the services of another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. +* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`nqptp`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. +* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. -Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. +### What Works +- AirPlay 2 for iOS and Mac players. -What Works +### What Does Not Work +- No AirPlay 2 for Windows iTunes +- No integration with Siri or HomeKit +- Lossless or High Definition Lossless material seems to be transcoded to AAC before transmission over AirPlay 2. +- No Remote Control +- No Artwork in the Metadata + +### What Partly Works +- Incomplete functionality using AppleTV or HomePod as player. + A Shairport Sync player will work -- i.e. it will play, but the selection button won't persist and the volume can't be set. + +### Limitations +- You can only run a single instance of Shairport Sync on a device. +- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. + +### Known Issues +- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. + +AirPlay 2 -- What You Need --- -* AirPlay 2 audio for players hosted on macOS and iOS. -* AirPlay 2 audio for System Sound Output from macOS. -* The `ALSA`, `stdout` and `pipe` backends definitely work, as does the `sndio` backend for FreeBSD. +AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. -What Does Not Work +Here are some suggestions: +* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. +* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. +* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. + +Guides --- -* Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. -* There is no integration with Siri or with the Home app or HomeKit, so nothing to do with the Home app works. -* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) -* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. -* Artwork is not downloaded. -* Playing from certain Apple devices to Shairport Sync does not work as expected. For example: - * When using a service with the ability to play directly on HomePod (e.g. Apple Music). If the music starts on the HomePod, or is the first device selected from the AirPlay list on iOS, when Shairport Sync is then selected iOS shows it as not connected (even though it is). Therefore the expected volume controls are not displayed. A workaround for this is to force an AirPlay stream to the HomePod (rather than just acting as a remote controller), this can be done by selected Shairport Sync _before_ the HomePod. Note, this issue is not present for other services such as SoundCloud which do not integrate into the HomePod like Apple Music does. - * The above example is true also when trying to select Shairport Sync from the AirPlay menu on the HomePod from the Home app. +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A guide for building on FreeBSD is forthcoming. -More About What Works +AirPlay 2 -- More About What Works --- * Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. -What You Need ---- -* A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! -* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) +Acknowledgements +---- +Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. -| Device | Native | Docker | Notes | -|---------------------|----------|----------------|-------| -| Raspberry Pi 4 | Working | Working | | -| Raspberry Pi 3 | Working | Working | | -| Raspberry Pi Zero W | Untested | Mostly working | Since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs, this currently includes quickly changing the volume repeatedly. Total CPU usage sits at around 75% when using Shairport Sync. Updates to Shairport Sync could make this better or worse. | +Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: +1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. +2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. +3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). + +Finally +---- +Did we mention AirPlay 2 support is experimental? From 1f8999522fe39c2b178198547565181fd3ad51d0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:45:51 +0100 Subject: [PATCH 241/878] Update AIRPLAY2.md --- AIRPLAY2.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 0c3a7a1e..8070a730 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -2,27 +2,34 @@ AirPlay 2 === ## General -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`nqptp`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. -* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the sources is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not work with the Home app or with Siri. + +The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. + +For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. + +AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. + +AirPlay 2 support definitely does not work on macOS. This is because Shairport Sync needs to use [`nqptp`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by macOS to support its implementation of AirPlay 2. + +The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. ### What Works - AirPlay 2 for iOS and Mac players. ### What Does Not Work -- No AirPlay 2 for Windows iTunes - No integration with Siri or HomeKit - Lossless or High Definition Lossless material seems to be transcoded to AAC before transmission over AirPlay 2. - No Remote Control - No Artwork in the Metadata +- No AirPlay 2 for Windows iTunes ### What Partly Works - Incomplete functionality using AppleTV or HomePod as player. A Shairport Sync player will work -- i.e. it will play, but the selection button won't persist and the volume can't be set. ### Limitations -- You can only run a single instance of Shairport Sync on a device. +- Only a single instance of Shairport Sync can run on a device. - The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. ### Known Issues @@ -32,7 +39,7 @@ AirPlay 2 -- What You Need --- AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. -Here are some suggestions: +So, here are some guideline requirements: * Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. * A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. * An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. From 5addb07b8e54e6b10667532a51de6b6e1b8d2bc8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:46:51 +0100 Subject: [PATCH 242/878] Update AIRPLAY2.md --- AIRPLAY2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 8070a730..afb39f81 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -2,7 +2,7 @@ AirPlay 2 === ## General -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the sources is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not work with the Home app or with Siri. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not work with the Home app or with Siri. The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. From d56ecb46ed099bc1983d5da62d0511d52ae8a15e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:50:02 +0100 Subject: [PATCH 243/878] Update AIRPLAY2.md --- AIRPLAY2.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index afb39f81..23025303 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -2,7 +2,7 @@ AirPlay 2 === ## General -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not work with the Home app or with Siri. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not integrate with the Home app or with Siri. The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. @@ -19,7 +19,7 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi ### What Does Not Work - No integration with Siri or HomeKit -- Lossless or High Definition Lossless material seems to be transcoded to AAC before transmission over AirPlay 2. +- Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. - No Remote Control - No Artwork in the Metadata - No AirPlay 2 for Windows iTunes @@ -51,7 +51,7 @@ Guides AirPlay 2 -- More About What Works --- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. +* Two types of audio are received by Shairport Sync -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. * Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. From e0a11fa627fbc9dbe01144d9fdb7d0585b674949 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:53:54 +0100 Subject: [PATCH 244/878] Update AIRPLAY2.md --- AIRPLAY2.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 23025303..a0d9f4dd 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,7 +1,6 @@ AirPlay 2 === -## General Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not integrate with the Home app or with Siri. The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. @@ -10,7 +9,7 @@ For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https:// AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. -AirPlay 2 support definitely does not work on macOS. This is because Shairport Sync needs to use [`nqptp`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by macOS to support its implementation of AirPlay 2. +AirPlay 2 support definitely does not work on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. From c0d80693a38a9451a5bac5cb9d06e6e4d4efd4c3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 08:55:20 +0100 Subject: [PATCH 245/878] Update AIRPLAY2.md --- AIRPLAY2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index a0d9f4dd..f9d1face 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -25,7 +25,7 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi ### What Partly Works - Incomplete functionality using AppleTV or HomePod as player. - A Shairport Sync player will work -- i.e. it will play, but the selection button won't persist and the volume can't be set. + A Shairport Sync player will work -- i.e. it will play -- but the selection button won't persist and the volume can't be set. ### Limitations - Only a single instance of Shairport Sync can run on a device. From 33a2da1c21a0cd6189b0ce7095c5647d15bf05e2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:00:02 +0100 Subject: [PATCH 246/878] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 59e63e42..cc375cd4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). -* Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much bigger and requires more libraries and RAM at build time and run time. +* Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much more demanding. It requires a faster system, more storage and more RAM both at build time and run time. AirPlay 2 --- @@ -12,7 +12,7 @@ Guides --- * A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). * A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide for building on FreeBSD is forthcoming. +* A guide to building for AirPlay 2 on FreeBSD is forthcoming. Note --- @@ -20,7 +20,7 @@ Shairport Sync does not support AirPlay video or photo streaming. More Information --- -For more information, and for a more complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). +For more information, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). Acknowledgements --- From ae0319f294783119d85168d1250656b0104340fe Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:01:13 +0100 Subject: [PATCH 247/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 062b6df5..0a82b68a 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -1,4 +1,4 @@ -# Shairport Sync for Cars +# Shairport Sync for Cars [Needs updating for Airplay 2] If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. Your passengers can enjoy movies with the soundtrack on the car speakers. You can even listen to Siri's traffic directions on your car audio. ## The Basic Idea From d112eb55ba28ad2da220b85f04bb9dd5fc5827e5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:02:15 +0100 Subject: [PATCH 248/878] Update CYGWIN.md --- CYGWIN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CYGWIN.md b/CYGWIN.md index 80cb3517..491ca7c1 100644 --- a/CYGWIN.md +++ b/CYGWIN.md @@ -1,4 +1,4 @@ -Installing Shairport Sync into Cygwin +Installing Shairport Sync into Cygwin [Airplay 2 Not Supported] ==== This guide is based on installing onto a fresh installation of Cygwin 2.895 (64-bit installation) running in Windows 10 From 6cb30de13feffd49f41de14168189e36fbb2ccb7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:02:41 +0100 Subject: [PATCH 249/878] Update FEDORA.md --- FEDORA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FEDORA.md b/FEDORA.md index bb57d58b..cf8cbc50 100644 --- a/FEDORA.md +++ b/FEDORA.md @@ -1,4 +1,4 @@ -Fedora Installation Guide +Fedora Installation Guide [Needs updating for Airplay 2] ----- Install the toolchain and pre-requisites, if necessary: From df2846b498ba50ef4f40497ce0e745d1c960318e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:03:20 +0100 Subject: [PATCH 250/878] Update FREEBSD.md --- FREEBSD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FREEBSD.md b/FREEBSD.md index fe3a2a5e..7ec87e1d 100644 --- a/FREEBSD.md +++ b/FREEBSD.md @@ -1,4 +1,4 @@ -Shairport Sync on FreeBSD using `sndio` +Shairport Sync on FreeBSD using `sndio` [Needs updating for Airplay 2] ---- Shairport Sync runs natively on FreeBSD using the `sndio` back end, thanks to the work of [Tobias Kortkamp (t6)](https://github.com/t6). From 2e2d4261bb7171aa5b23b67ef4f144699325ed9b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:03:57 +0100 Subject: [PATCH 251/878] Update OPENBSD.md --- OPENBSD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPENBSD.md b/OPENBSD.md index 219d7402..6b18f979 100644 --- a/OPENBSD.md +++ b/OPENBSD.md @@ -1,4 +1,4 @@ -Shairport Sync on OpenBSD +Shairport Sync on OpenBSD [AirPlay 2 Not Supported] ---- This is an initial note about installing Shairport Sync on OpenBSD. Shairport Sync compiles and runs natively on OpenBSD using the `sndio` back end. From cd774c6585d6d2c679c4bd30193366fdb3cb2bd5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:04:31 +0100 Subject: [PATCH 252/878] Update UPDATING.md --- UPDATING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPDATING.md b/UPDATING.md index 66916847..73e18fb0 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,5 +1,5 @@ -### Updating Shairport Sync +### Updating Shairport Sync [Needs updating for Airplay 2] This guide is for updating an installation of Shairport Sync. If you installed Shairport Sync from a package, most of these instructions don't apply – please go to the section at the end called "Post Update Tasks" To do an update, you basically have to go through the whole process of building Shairport Sync again, From b2a6296e2eb85b60a46df732969cd8cb24d190fc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:09:38 +0100 Subject: [PATCH 253/878] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index cc375cd4..70f0bf31 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Shairport Sync ============= * Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). * Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much more demanding. It requires a faster system, more storage and more RAM both at build time and run time. +* Shairport Sync does not support AirPlay video or photo streaming. AirPlay 2 --- @@ -14,10 +15,6 @@ Guides * A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). * A guide to building for AirPlay 2 on FreeBSD is forthcoming. -Note ---- -Shairport Sync does not support AirPlay video or photo streaming. - More Information --- For more information, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). From e849b12c18cd9e538e47b8b9fa9c50386c046e67 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:31:58 +0100 Subject: [PATCH 254/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index e044c865..8814664c 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -80,7 +80,21 @@ $ CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadat $ make -j # make install ``` -Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: +Now to configure Shairport Sync. Here, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. Thus, at the end of the output after the command `$ shairport-sync -h` on a Raspberry Pi, for example, the following appears: + +``` +... +Settings and options for the audio backend "alsa": + -d output-device set the output device, default is "default". + -c mixer-control set the mixer control name, default is to use no mixer. + -m mixer-device set the mixer device, default is the output device. + -i mixer-index set the mixer index, default is 0. + hardware output devices: + "hw:Headphones" +``` +Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. + +Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: ``` // Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC general = @@ -90,12 +104,12 @@ general = alsa = { - output_device = "hw:0"; + output_device = "hw:Headphones"; mixer_control_name = "Headphone"; }; ``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range. +The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. ### Automatic Start ### @@ -115,7 +129,7 @@ You may wish to run Shairport Sync from the command line (but remember to ensure ``` $ shairport-sync -vu --statistics ``` -The user doesn't need to be privileged, but should be a member of the `audio` group for access to the `alsa` subsystem. +The user doesn't need to be privileged, but must be a member of the `audio` group for access to the `alsa` subsystem. ### Using Shairport Sync ### @@ -123,3 +137,6 @@ The Shairport Sync AirPlay service should appear on the network with a service n Connect and enjoy... +### Restart Your Mac! ### + +At the time of writing, there seems to be a bug in the Mac Music app. It appears that, when the Mac has been awoken after sleeping, the Music app may not fully reawaken, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. From 52029718ac3a788bf7923b613a5d622feb94e11b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 09:34:22 +0100 Subject: [PATCH 255/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70f0bf31..fe9eec75 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. -* [Charles Omer](https://github.com/charlesomer) for Docker autoation (forthcoming), testing, encouragement, enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for Docker automation (forthcoming), testing, encouragement, enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From 52c24b467c80f4c37ead163a919c1d00504b4e8a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 10:06:01 +0100 Subject: [PATCH 256/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 59 ++----------------------------------- 1 file changed, 3 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 870a2fea..5f73973d 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -5,16 +5,10 @@ Big Update ---- Version 4 brings limited AirPlay 2 functionality to Shairport Sync. -A major feature of Shairport Sync's AirPlay 2 support is Audio Synchronisation. This is achieved using an purpose-built passive [PTP](https://standards.ieee.org/standard/1588-2008.html) client called [`nqptp`](https://github.com/mikebrady/nqptp), which provides precise timing information that Shairport Sync uses to maintain tight synchronisation. (BTW, `nqptp` is not a "PTP slave". Instead, it passively monitors PTP traffic for timing information.) +For more information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md]. -Acknowledgements ----- -Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. - -Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: -1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. -2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). +#### Known Issues +- In the AirPlay 2 build, a crash can occur occasionally when the player is being selected. If you can get this to happen reliably, please let us know. Warning ---- @@ -22,53 +16,6 @@ Warning 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. 3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. -AirPlay 2 Overview ----- - -#### What Works -- AirPlay 2 for iOS and Mac players. - -#### What Does Not Work -- No AirPlay 2 for Windows iTunes -- No integration with Siri or HomeKit -- No Lossless or High Definition Lossless -- No Remote Control -- No Artwork in the Metadata - -#### What Partly Works -- Incomplete functionality using AppleTV or HomePod as player. - A Shairport Sync player will work, but the selection button won't persist and the volume can't be set. - -#### Limitations -- You can only run a single instance of Shairport Sync on a device. -- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. - -#### Known Issues -- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. - -AirPlay 2 -- What You Need ---- -AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. - -Here are some suggestions: -* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. -* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. -* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. - -Guides ---- -Instructions for building Shairport Sync are different for AirPlay 2 than for AirPlay 1 -- please follow them very carefully. - -* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide for building on FreeBSD is forthcoming. - -AirPlay 2 -- More About What Works ---- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. - Version 3.3.8d7 ==== **Bug Fix** From 5915c91fc5eda6865fcc00c18fda0e406ab8c6f5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:45:11 +0000 Subject: [PATCH 257/878] Stop including pair_ap as a gi submodule. It will be included as a simple subdirectory instead. --- pair_ap | 1 - 1 file changed, 1 deletion(-) delete mode 160000 pair_ap diff --git a/pair_ap b/pair_ap deleted file mode 160000 index d955c447..00000000 --- a/pair_ap +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d955c447dac0545803dee9c90312c9d42310e9b1 From fdb919a7883daa929d1f698c95397e08138e2563 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:54:18 +0000 Subject: [PATCH 258/878] continue to remover pair_ap as a git submodule. --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0e9d4cac..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "pair_ap"] - path = pair_ap - url = https://github.com/ejurgensen/pair_ap From 94e47fdf9ef110851ee273e6396bd71bbe886d08 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 14:11:32 +0000 Subject: [PATCH 259/878] Include pair_ap as a simple directory. --- pair_ap/README.md | 72 + pair_ap/client-example.c | 578 +++ pair_ap/pair-internal.h | 381 ++ pair_ap/pair-tlv.c | 218 ++ pair_ap/pair-tlv.h | 77 + pair_ap/pair.c | 771 ++++ pair_ap/pair.h | 266 ++ pair_ap/pair_ap-master/.gitignore | 10 + pair_ap/pair_ap-master/evrtsp/evrtsp.h | 176 + pair_ap/pair_ap-master/evrtsp/log.h | 51 + pair_ap/pair_ap-master/evrtsp/rtsp-internal.h | 115 + pair_ap/pair_ap-master/evrtsp/rtsp.c | 1816 ++++++++++ pair_ap/pair_fruit.c | 1063 ++++++ pair_ap/pair_homekit.c | 3123 +++++++++++++++++ pair_ap/server-example.c | 685 ++++ 15 files changed, 9402 insertions(+) create mode 100644 pair_ap/README.md create mode 100644 pair_ap/client-example.c create mode 100644 pair_ap/pair-internal.h create mode 100644 pair_ap/pair-tlv.c create mode 100644 pair_ap/pair-tlv.h create mode 100644 pair_ap/pair.c create mode 100644 pair_ap/pair.h create mode 100644 pair_ap/pair_ap-master/.gitignore create mode 100644 pair_ap/pair_ap-master/evrtsp/evrtsp.h create mode 100644 pair_ap/pair_ap-master/evrtsp/log.h create mode 100644 pair_ap/pair_ap-master/evrtsp/rtsp-internal.h create mode 100644 pair_ap/pair_ap-master/evrtsp/rtsp.c create mode 100644 pair_ap/pair_fruit.c create mode 100644 pair_ap/pair_homekit.c create mode 100644 pair_ap/server-example.c diff --git a/pair_ap/README.md b/pair_ap/README.md new file mode 100644 index 00000000..1c855444 --- /dev/null +++ b/pair_ap/README.md @@ -0,0 +1,72 @@ +# pair_ap +C client implementation of pairing for: +* Apple TV device verification, which became mandatory with tvOS 10.2 (this is + called fruit mode in pair_ap) +* Homekit pairing (for AirPlay 2, not working for Home app) + +Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting. +## Requirements +- libsodium +- libgcrypt or libopenssl +- libplist (only for Apple TV device verification) + +To build the example client and server you also need libevent2. If the +dependencies are met you can build simply by running 'make'. + +## Homekit pairing +Since I haven't been able to find much information on the internet on how +Homekit pairing is designed, here is a write-up of my current understanding. If +you know better, please help improve this. + +With Homekit pairing, there may be a controller (e.g. the Home app), and a +number of devices/accessories (e.g. speakers). The controller acts as a client +and can make requests for pairing. After it is paired it can also add other +"third-party" pairings to the device, it can remove pairings and it can ask for +a list of pairings. + +Other parties, e.g. the Music app or just iOS as an Airplay sender, can also +pair with devices/accesssories in a similar manner, but they are not full- +fledged Homekit controllers and thus don't make requests for adding, removing +or listing pairings. + +The controller uses `/pair-add` to make sure that all devices on a network get +the ID and public key of all the other devices, so that the user only needs to +pair a device once. + +### Normal pairing +For a normal first-time pairing, the client needs a one-time code (the device +announces via mDNS whether a code is required). The client calls +`/pair-pin-start` and the device displays the code. There is also QR-based +pairing, which is (probably?) an encoded code. + +After obtaining the code, the client initiates a three step `/pair-setup` +sequence, which results in both peers registering each other's ID and public +key. Henceforth, a pairing is verified with the two step `/pair-verify`, where +the parties check each-others identify. Saving the peer's ID + public key isn't +strictly necessary if client or server doesn't care about verifying the peer, +i.e. that `/pair-setup` has actually been completed. + +The result of `/pair-verify` is a shared secret that is used for symmetric +encryption of the following communinacation between the parties. + +### Transient pairing +Some devices don't require a code from the user for pairing (e.g. an Airport +Express 2). If so, the client just needs to go through a two-step `/pair-setup` +sequence which results in a shared secret, which is then used for encrypted +communication. A fixed code of 3939 is used. + +Such devices don't appear to be fully Homekit compatible - they will not, for +instance - appear in the Home app. + +## "fruit" pairing +Like normal Homekit pairing, this consists of first requesting a code with +`/pair-pin-start`, then a three-step `/pair-setup` and finally a two-step +`/pair-verify`. After that the communication is encrypted with the resulting +shared secret. + + +## Acknowledgments +- [AirPlayAuth](https://github.com/funtax/AirPlayAuth) +- [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC) +- [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender) +- [csrp](https://github.com/cocagne/csrp) diff --git a/pair_ap/client-example.c b/pair_ap/client-example.c new file mode 100644 index 00000000..35e424ad --- /dev/null +++ b/pair_ap/client-example.c @@ -0,0 +1,578 @@ +#include +#include +#include +#include + +#include +#include + +#include "evrtsp/evrtsp.h" +#include "pair.h" + +#define DEVICE_ID "AABBCCDD11223344" +#define ACTIVE_REMOTE "3515324763" +#define DACP_ID "FF1DB45949E6CBD3" +#define USER_AGENT "AirPlay/381.13" + +#define ENDPOINT_SETUP_FRUIT "/pair-setup-pin" +#define CONTENTTYPE_SETUP_FRUIT "application/x-apple-binary-plist" + +#define ENDPOINT_SETUP_HOMEKIT "/pair-setup" +#define CONTENTTYPE_SETUP_HOMEKIT "application/octet-stream" + + +typedef void (*request_cb)(struct evrtsp_request *, void *); + +static struct event_base *evbase; +static struct evrtsp_connection *evcon; +static int cseq; + +static const char *endpoint_setup; +static const char *content_type_setup; + +static enum pair_type pair_type; + +static struct pair_cipher_context *cipher_ctx; +static struct pair_verify_context *verify_ctx; +static struct pair_setup_context *setup_ctx; + + +static char * +prompt_pin(void) +{ + char *pin = NULL; + size_t len; + + printf ("Enter pin: "); + fflush (stdout); + + len = getline(&pin, &len, stdin); + if (len != 5) // Includes EOL + { + printf ("Bad pin length %zu\n", len); + return NULL; + } + + return pin; +} + +static int +response_process(uint8_t **response, struct evrtsp_request *req) +{ + if (!req) + { + printf("failed, could not read response\n"); + return -1; + } + else if (req->response_code != 200) + { + printf("failed with error code %d: %s (body is %zu bytes)\n\n", req->response_code, req->response_code_line, evbuffer_get_length(req->input_buffer)); + return -1; + } + + printf("success\n\n"); + + *response = evbuffer_pullup(req->input_buffer, -1); + + return evbuffer_get_length(req->input_buffer); +} + +static int +make_request(const char *url, const void *data, size_t len, const char *content_type, request_cb cb) +{ + struct evrtsp_request *req; + char buffer[1024]; + + req = evrtsp_request_new(cb, NULL); + + if (data) + evbuffer_add(req->output_buffer, data, len); + + if (content_type) + evrtsp_add_header(req->output_headers, "Content-Type", content_type); + + cseq++; + snprintf(buffer, sizeof(buffer), "%d", cseq); + evrtsp_add_header(req->output_headers, "CSeq", buffer); + + evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT); +// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID); +// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE); + + if (pair_type == PAIR_CLIENT_HOMEKIT_NORMAL) + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3"); + else if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT) + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "4"); + + printf("Making request %d to '%s'... ", cseq, url); + + return evrtsp_make_request(evcon, req, EVRTSP_REQ_POST, url); +} + +static int +make_request_options(const char *url, const void *data, size_t len, const char *content_type, request_cb cb) +{ + struct evrtsp_request *req; + char buffer[1024]; + + req = evrtsp_request_new(cb, NULL); + + if (data) + evbuffer_add(req->output_buffer, data, len); + + if (content_type) + evrtsp_add_header(req->output_headers, "Content-Type", content_type); + + cseq++; + snprintf(buffer, sizeof(buffer), "%d", cseq); + evrtsp_add_header(req->output_headers, "CSeq", buffer); + + evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT); +// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID); +// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE); + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3"); + + printf("Making request %d to '%s'... ", cseq, url); + + return evrtsp_make_request(evcon, req, EVRTSP_REQ_OPTIONS, url); +} + + +static void +options_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret >= 0) + printf("OPTIONS complete\n"); + + printf("Done\n"); + + event_base_loopbreak(evbase); +} + +static int +options_request(void) +{ + return make_request_options("*", NULL, 0, NULL, options_response); +} + +static void +rtsp_cipher(struct evbuffer *evbuf, void *arg, int encrypt) +{ + uint8_t *out = NULL; + size_t out_len = 0; + ssize_t processed; + + uint8_t *in = evbuffer_pullup(evbuf, -1); + size_t in_len = evbuffer_get_length(evbuf); + + if (encrypt) + processed = pair_encrypt(&out, &out_len, in, in_len, cipher_ctx); + else + processed = pair_decrypt(&out, &out_len, in, in_len, cipher_ctx); + + evbuffer_drain(evbuf, in_len); + + if (processed < 0) + { + printf("Error while ciphering: %s\n", pair_cipher_errmsg(cipher_ctx)); + return; + } + else if (processed != in_len) + { + printf("Partial ciphering, only %zd of %zu input bytes were processed\n", processed, in_len); + } + + evbuffer_add(evbuf, out, out_len); +} + +static void +verify_step2_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret < 0) + goto error; + + ret = pair_verify_response2(verify_ctx, response, ret); + if (ret < 0) + goto error; + + printf("Verify complete!\n\n"); + + ret = pair_verify_result(&result, verify_ctx); + if (ret < 0) + goto error; + + cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len); + if (!cipher_ctx) + goto error; + + evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL); + + ret = options_request(); + if (ret < 0) + goto error; + + pair_verify_free(verify_ctx); + + return; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + pair_cipher_free(cipher_ctx); + event_base_loopbreak(evbase); +} + +static int +verify_step2_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_verify_request2(&len, verify_ctx); + if (!request) + return -1; + + ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step2_response); + + free(request); + + return ret; +} + +static void +verify_step1_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_verify_response1(verify_ctx, response, ret); + if (ret < 0) + goto error; + + ret = verify_step2_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + event_base_loopbreak(evbase); +} + +static int +verify_step1_request(const char *authorisation_key) +{ + uint8_t *request = NULL; + size_t len; + int ret; + + verify_ctx = pair_verify_new(pair_type, authorisation_key, NULL, NULL, DEVICE_ID); + if (!verify_ctx) + return -1; + + request = pair_verify_request1(&len, verify_ctx); + if (!request) + goto error; + + ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step1_response); + if (ret < 0) + goto error; + + free(request); + return ret; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + free(request); + return -1; +} + +static void +setup_step3_response(struct evrtsp_request *req, void *arg) +{ + const char *key; + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response3(setup_ctx, response, ret); + if (ret < 0) + goto error; + + ret = pair_setup_result(&key, &result, setup_ctx); + if (ret < 0) + goto error; + + printf("Setup of device ID %s complete, got key: %s\n", result->device_id, key); + + ret = verify_step1_request(key); + if (ret < 0) + goto error; + + pair_setup_free(setup_ctx); + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step3_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request3(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step3_response); + + free(request); + + return ret; +} + +static void +setup_step2_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response2(setup_ctx, response, ret); + if (ret < 0) + goto error; + + printf("Setup SRP stage complete\n"); + + if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT) + { + ret = pair_setup_result(NULL, &result, setup_ctx); + if (ret < 0) + goto error; + + cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len); + if (!cipher_ctx) + goto error; + + evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL); + + ret = options_request(); + if (ret < 0) + goto error; + + pair_setup_free(setup_ctx); + + return; + } + + ret = setup_step3_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step2_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request2(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step2_response); + + free(request); + + return ret; +} + +static void +setup_step1_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response1(setup_ctx, response, ret); + if (ret < 0) + goto error; + + ret = setup_step2_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step1_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request1(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step1_response); + + free(request); + return ret; +} + +static void +setup_start_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + char *pin = NULL; + int ret; + + if (req) + { + ret = response_process(&response, req); + if (ret < 0) + goto error; + } + + if (pair_type != PAIR_CLIENT_HOMEKIT_TRANSIENT) + { + pin = prompt_pin(); + if (!pin) + goto error; + } + + setup_ctx = pair_setup_new(pair_type, pin, NULL, NULL, DEVICE_ID); + if (!setup_ctx) + goto error; + + ret = setup_step1_request(); + if (ret < 0) + goto error; + + free(pin); + return; + + error: + if (setup_ctx) + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + free(pin); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_start_request(void) +{ + return make_request("/pair-pin-start", NULL, 0, "application/x-apple-binary-plist", setup_start_response); +} + + +int +main( int argc, char * argv[] ) +{ + int ret; + + if (argc < 4 || argc > 5) + { + printf("%s ip_address port homekit|fruit|transient [skip_pin]\n", argv[0]); + return -1; + } + + const char *address = argv[1]; + const char *port = argv[2]; + int skip_pin = (argc == 5); + + if (strcmp(argv[3], "fruit") == 0) + { + printf("Pair type is fruit\n"); + pair_type = PAIR_CLIENT_FRUIT; + endpoint_setup = ENDPOINT_SETUP_FRUIT; + content_type_setup = CONTENTTYPE_SETUP_FRUIT; + } + else if (strcmp(argv[3], "homekit") == 0) + { + printf("Pair type is homekit (normal)\n"); + pair_type = PAIR_CLIENT_HOMEKIT_NORMAL; + endpoint_setup = ENDPOINT_SETUP_HOMEKIT; + content_type_setup = CONTENTTYPE_SETUP_HOMEKIT; + } + else if (strcmp(argv[3], "transient") == 0) + { + printf("Pair type is homekit (transient)\n"); + pair_type = PAIR_CLIENT_HOMEKIT_TRANSIENT; + endpoint_setup = ENDPOINT_SETUP_HOMEKIT; + content_type_setup = CONTENTTYPE_SETUP_HOMEKIT; + } + + evbase = event_base_new(); + evcon = evrtsp_connection_new(address, atoi(port)); + evrtsp_connection_set_base(evcon, evbase); + + if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT || skip_pin) + { + setup_start_response(NULL, NULL); + } + else + { + ret = setup_start_request(); + if (ret < 0) + goto the_end; + } + + event_base_dispatch(evbase); + + the_end: + evrtsp_connection_free(evcon); + event_base_free(evbase); + + return 0; +} diff --git a/pair_ap/pair-internal.h b/pair_ap/pair-internal.h new file mode 100644 index 00000000..e7cd85e3 --- /dev/null +++ b/pair_ap/pair-internal.h @@ -0,0 +1,381 @@ +#include +#include +#include + +#include "pair.h" + +#define RETURN_ERROR(s, m) \ + do { handle->status = (s); handle->errmsg = (m); goto error; } while(0) + + +struct SRPUser; +struct SRPVerifier; + +struct pair_client_setup_context +{ + struct SRPUser *user; + + uint8_t pin[4]; + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + pair_cb add_cb; + void *add_cb_arg; + + uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + + const uint8_t *pkA; + int pkA_len; + + uint8_t *pkB; + uint64_t pkB_len; + + const uint8_t *M1; + int M1_len; + + uint8_t *M2; + uint64_t M2_len; + + uint8_t *salt; + uint64_t salt_len; + + // We don't actually use the server's epk and authtag for anything + uint8_t *epk; + uint64_t epk_len; + uint8_t *authtag; + uint64_t authtag_len; +}; + +struct pair_server_setup_context +{ + struct SRPVerifier *verifier; + + uint8_t pin[4]; + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + pair_cb add_cb; + void *add_cb_arg; + + uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + + bool is_transient; + + uint8_t *pkA; + uint64_t pkA_len; + + uint8_t *pkB; + int pkB_len; + + uint8_t *b; + int b_len; + + uint8_t *M1; + uint64_t M1_len; + + const uint8_t *M2; + int M2_len; + + uint8_t *v; + int v_len; + + uint8_t *salt; + int salt_len; +}; + +enum pair_status +{ + PAIR_STATUS_IN_PROGRESS, + PAIR_STATUS_COMPLETED, + PAIR_STATUS_AUTH_FAILED, + PAIR_STATUS_INVALID, +}; + +struct pair_setup_context +{ + struct pair_definition *type; + + enum pair_status status; + const char *errmsg; + + struct pair_result result; + char result_str[256]; // Holds the hex string version of the keys that pair_verify_new() needs + + // Hex-formatet concatenation of public + private, 0-terminated + char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1]; + + union pair_setup_union + { + struct pair_client_setup_context client; + struct pair_server_setup_context server; + } sctx; +}; + +struct pair_client_verify_context +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + // These are the keys that were registered with the server in pair-setup + uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + uint8_t client_private_key[crypto_sign_SECRETKEYBYTES]; // 64 + + bool verify_server_signature; + uint8_t server_fruit_public_key[64]; // Not sure why it has this length in fruit mode + uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + + // For establishing the shared secret for encrypted communication + uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + uint8_t client_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32 + + uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + + uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32 +}; + +struct pair_server_verify_context +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + // Same keys as used for pair-setup, derived from device_id + uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + uint8_t server_private_key[crypto_sign_SECRETKEYBYTES]; // 64 + + bool verify_client_signature; + pair_cb get_cb; + void *get_cb_arg; + + // For establishing the shared secret for encrypted communication + uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + uint8_t server_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32 + + uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + + uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32 +}; + +struct pair_verify_context +{ + struct pair_definition *type; + + enum pair_status status; + const char *errmsg; + + struct pair_result result; + + union pair_verify_union + { + struct pair_client_verify_context client; + struct pair_server_verify_context server; + } vctx; +}; + +struct pair_cipher_context +{ + struct pair_definition *type; + + uint8_t encryption_key[32]; + uint8_t decryption_key[32]; + + uint64_t encryption_counter; + uint64_t decryption_counter; + + // For rollback + uint64_t encryption_counter_prev; + uint64_t decryption_counter_prev; + + const char *errmsg; +}; + +struct pair_definition +{ + int (*pair_setup_new)(struct pair_setup_context *sctx, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id); + void (*pair_setup_free)(struct pair_setup_context *sctx); + int (*pair_setup_result)(struct pair_setup_context *sctx); + + uint8_t *(*pair_setup_request1)(size_t *len, struct pair_setup_context *sctx); + uint8_t *(*pair_setup_request2)(size_t *len, struct pair_setup_context *sctx); + uint8_t *(*pair_setup_request3)(size_t *len, struct pair_setup_context *sctx); + + int (*pair_setup_response1)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + int (*pair_setup_response2)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + int (*pair_setup_response3)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + + int (*pair_verify_new)(struct pair_verify_context *vctx, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id); + void (*pair_verify_free)(struct pair_verify_context *vctx); + int (*pair_verify_result)(struct pair_verify_context *vctx); + + uint8_t *(*pair_verify_request1)(size_t *len, struct pair_verify_context *vctx); + uint8_t *(*pair_verify_request2)(size_t *len, struct pair_verify_context *vctx); + + int (*pair_verify_response1)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + int (*pair_verify_response2)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + + int (*pair_add)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + int (*pair_remove)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + int (*pair_list)(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + + struct pair_cipher_context *(*pair_cipher_new)(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len); + void (*pair_cipher_free)(struct pair_cipher_context *cctx); + + ssize_t (*pair_encrypt)(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx); + ssize_t (*pair_decrypt)(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx); + + int (*pair_state_get)(const char **errmsg, const uint8_t *in, size_t in_len); + void (*pair_public_key_get)(uint8_t server_public_key[32], const char *device_id); +}; + + +/* -------------------- GCRYPT AND OPENSSL COMPABILITY --------------------- */ +/* partly borrowed from ffmpeg (rtmpdh.c) */ + +#if CONFIG_GCRYPT +#include +#define SHA512_DIGEST_LENGTH 64 +#define bnum_new(bn) \ + do { \ + if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \ + if (!gcry_check_version("1.5.4")) \ + abort(); \ + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); \ + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); \ + } \ + bn = gcry_mpi_new(1); \ + } while (0) +#define bnum_free(bn) gcry_mpi_release(bn) +#define bnum_num_bytes(bn) (gcry_mpi_get_nbits(bn) + 7) / 8 +#define bnum_is_zero(bn) (gcry_mpi_cmp_ui(bn, (unsigned long)0) == 0) +#define bnum_bn2bin(bn, buf, len) gcry_mpi_print(GCRYMPI_FMT_USG, buf, len, NULL, bn) +#define bnum_bin2bn(bn, buf, len) gcry_mpi_scan(&bn, GCRYMPI_FMT_USG, buf, len, NULL) +#define bnum_hex2bn(bn, buf) gcry_mpi_scan(&bn, GCRYMPI_FMT_HEX, buf, 0, 0) +#define bnum_random(bn, num_bits) gcry_mpi_randomize(bn, num_bits, GCRY_WEAK_RANDOM) +#define bnum_add(bn, a, b) gcry_mpi_add(bn, a, b) +#define bnum_sub(bn, a, b) gcry_mpi_sub(bn, a, b) +#define bnum_mul(bn, a, b) gcry_mpi_mul(bn, a, b) +#define bnum_mod(bn, a, b) gcry_mpi_mod(bn, a, b) +typedef gcry_mpi_t bnum; +__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) +{ + gcry_mpi_powm(bn, y, q, p); +} +__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m) +{ + gcry_mpi_addm(bn, a, b, m); +} +#elif CONFIG_OPENSSL +#include +#include +#include +#include +#include +#define bnum_new(bn) bn = BN_new() +#define bnum_free(bn) BN_free(bn) +#define bnum_num_bytes(bn) BN_num_bytes(bn) +#define bnum_is_zero(bn) BN_is_zero(bn) +#define bnum_bn2bin(bn, buf, len) BN_bn2bin(bn, buf) +#define bnum_bin2bn(bn, buf, len) bn = BN_bin2bn(buf, len, 0) +#define bnum_hex2bn(bn, buf) BN_hex2bn(&bn, buf) +#define bnum_random(bn, num_bits) BN_rand(bn, num_bits, 0, 0) +#define bnum_add(bn, a, b) BN_add(bn, a, b) +#define bnum_sub(bn, a, b) BN_sub(bn, a, b) +typedef BIGNUM* bnum; +__attribute__((unused)) static void bnum_mul(bnum bn, bnum a, bnum b) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mul(bn, a, b, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_mod(bnum bn, bnum a, bnum b) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod(bn, a, b, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod_exp(bn, y, q, p, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod_add(bn, a, b, m, ctx); + BN_CTX_free(ctx); +} +#endif + + +/* -------------------------- SHARED HASHING HELPERS ------------------------ */ + +#ifdef CONFIG_OPENSSL +enum hash_alg +{ + HASH_SHA1, + HASH_SHA224, + HASH_SHA256, + HASH_SHA384, + HASH_SHA512, +}; +#elif CONFIG_GCRYPT +enum hash_alg +{ + HASH_SHA1 = GCRY_MD_SHA1, + HASH_SHA224 = GCRY_MD_SHA224, + HASH_SHA256 = GCRY_MD_SHA256, + HASH_SHA384 = GCRY_MD_SHA384, + HASH_SHA512 = GCRY_MD_SHA512, +}; +#endif + +#if CONFIG_OPENSSL +typedef union +{ + SHA_CTX sha; + SHA256_CTX sha256; + SHA512_CTX sha512; +} HashCTX; +#elif CONFIG_GCRYPT +typedef gcry_md_hd_t HashCTX; +#endif + +int +hash_init(enum hash_alg alg, HashCTX *c); + +int +hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len); + +int +hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md); + +unsigned char * +hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md); + +int +hash_length(enum hash_alg alg); + +int +hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len); + +bnum +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2); + +bnum +H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes); + +void +update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n); + +void +hash_num(enum hash_alg alg, const bnum n, unsigned char *dest); + + +/* ----------------------------- OTHER HELPERS -------------------------------*/ + +#ifdef DEBUG_PAIR +void +hexdump(const char *msg, uint8_t *mem, size_t len); +#endif diff --git a/pair_ap/pair-tlv.c b/pair_ap/pair-tlv.c new file mode 100644 index 00000000..ea1a7614 --- /dev/null +++ b/pair_ap/pair-tlv.c @@ -0,0 +1,218 @@ +/* + * TLV helpers are adapted from ESP homekit: + * + * + * The MIT License (MIT) + * + * 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 + +#include "pair-tlv.h" +#include "pair-internal.h" + + +static int +tlv_add_value_(pair_tlv_values_t *values, uint8_t type, uint8_t *value, size_t size) { + pair_tlv_t *tlv = malloc(sizeof(pair_tlv_t)); + if (!tlv) { + return PAIR_TLV_ERROR_MEMORY; + } + tlv->type = type; + tlv->size = size; + tlv->value = value; + tlv->next = NULL; + + if (!values->head) { + values->head = tlv; + } else { + pair_tlv_t *t = values->head; + while (t->next) { + t = t->next; + } + t->next = tlv; + } + + return 0; +} + + +pair_tlv_values_t * +pair_tlv_new() { + pair_tlv_values_t *values = malloc(sizeof(pair_tlv_values_t)); + if (!values) + return NULL; + + values->head = NULL; + return values; +} + +void +pair_tlv_free(pair_tlv_values_t *values) { + pair_tlv_t *t = values->head; + while (t) { + pair_tlv_t *t2 = t; + t = t->next; + if (t2->value) + free(t2->value); + free(t2); + } + free(values); +} + +int +pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size) { + uint8_t *data = NULL; + int ret; + if (size) { + data = malloc(size); + if (!data) { + return PAIR_TLV_ERROR_MEMORY; + } + memcpy(data, value, size); + } + ret = tlv_add_value_(values, type, data, size); + if (ret < 0) + free(data); + return ret; +} + +pair_tlv_t * +pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type) { + pair_tlv_t *t = values->head; + while (t) { + if (t->type == type) + return t; + t = t->next; + } + return NULL; +} + +int +pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size) { + size_t required_size = 0; + pair_tlv_t *t = values->head; + while (t) { + required_size += t->size + 2 * ((t->size + 254) / 255); + t = t->next; + } + + if (*size < required_size) { + *size = required_size; + return PAIR_TLV_ERROR_INSUFFICIENT_SIZE; + } + + *size = required_size; + + t = values->head; + while (t) { + uint8_t *data = t->value; + if (!t->size) { + buffer[0] = t->type; + buffer[1] = 0; + buffer += 2; + t = t->next; + continue; + } + + size_t remaining = t->size; + + while (remaining) { + buffer[0] = t->type; + size_t chunk_size = (remaining > 255) ? 255 : remaining; + buffer[1] = chunk_size; + memcpy(&buffer[2], data, chunk_size); + remaining -= chunk_size; + buffer += chunk_size + 2; + data += chunk_size; + } + + t = t->next; + } + + return 0; +} + +int +pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values) { + size_t i = 0; + int ret; + while (i < length) { + uint8_t type = buffer[i]; + size_t size = 0; + uint8_t *data = NULL; + + // scan TLVs to accumulate total size of subsequent TLVs with same type (chunked data) + size_t j = i; + while (j < length && buffer[j] == type && buffer[j+1] == 255) { + size_t chunk_size = buffer[j+1]; + size += chunk_size; + j += chunk_size + 2; + } + if (j < length && buffer[j] == type) { + size_t chunk_size = buffer[j+1]; + size += chunk_size; + } + + // allocate memory to hold all pieces of chunked data and copy data there + if (size != 0) { + data = malloc(size); + if (!data) + return PAIR_TLV_ERROR_MEMORY; + + uint8_t *p = data; + + size_t remaining = size; + while (remaining) { + size_t chunk_size = buffer[i+1]; + memcpy(p, &buffer[i+2], chunk_size); + p += chunk_size; + i += chunk_size + 2; + remaining -= chunk_size; + } + } + + ret = tlv_add_value_(values, type, data, size); + if (ret < 0) { + free(data); + return ret; + } + } + + return 0; +} + +#ifdef DEBUG_PAIR +void +pair_tlv_debug(const pair_tlv_values_t *values) +{ + printf("Received TLV values\n"); + for (pair_tlv_t *t=values->head; t; t=t->next) + { + printf("Type %d value (%zu bytes): \n", t->type, t->size); + hexdump("", t->value, t->size); + } +} +#endif + diff --git a/pair_ap/pair-tlv.h b/pair_ap/pair-tlv.h new file mode 100644 index 00000000..6ef8db20 --- /dev/null +++ b/pair_ap/pair-tlv.h @@ -0,0 +1,77 @@ +#ifndef __PAIR_AP_TLV_H__ +#define __PAIR_AP_TLV_H__ + +#include + +#define PAIR_TLV_ERROR_MEMORY -1 +#define PAIR_TLV_ERROR_INSUFFICIENT_SIZE -2 + +typedef enum { + TLVType_Method = 0, // (integer) Method to use for pairing. See PairMethod + TLVType_Identifier = 1, // (UTF-8) Identifier for authentication + TLVType_Salt = 2, // (bytes) 16+ bytes of random salt + TLVType_PublicKey = 3, // (bytes) Curve25519, SRP public key or signed Ed25519 key + TLVType_Proof = 4, // (bytes) Ed25519 or SRP proof + TLVType_EncryptedData = 5, // (bytes) Encrypted data with auth tag at end + TLVType_State = 6, // (integer) State of the pairing process. 1=M1, 2=M2, etc. + TLVType_Error = 7, // (integer) Error code. Must only be present if error code is + // not 0. See TLVError + TLVType_RetryDelay = 8, // (integer) Seconds to delay until retrying a setup code + TLVType_Certificate = 9, // (bytes) X.509 Certificate + TLVType_Signature = 10, // (bytes) Ed25519 + TLVType_Permissions = 11, // (integer) Bit value describing permissions of the controller + // being added. + // None (0x00): Regular user + // Bit 1 (0x01): Admin that is able to add and remove + // pairings against the accessory + TLVType_FragmentData = 13, // (bytes) Non-last fragment of data. If length is 0, + // it's an ACK. + TLVType_FragmentLast = 14, // (bytes) Last fragment of data + TLVType_Flags = 19, // Added from airplay2_receiver + TLVType_Separator = 0xff, +} TLVType; + + +typedef enum { + TLVError_Unknown = 1, // Generic error to handle unexpected errors + TLVError_Authentication = 2, // Setup code or signature verification failed + TLVError_Backoff = 3, // Client must look at the retry delay TLV item and + // wait that many seconds before retrying + TLVError_MaxPeers = 4, // Server cannot accept any more pairings + TLVError_MaxTries = 5, // Server reached its maximum number of + // authentication attempts + TLVError_Unavailable = 6, // Server pairing method is unavailable + TLVError_Busy = 7, // Server is busy and cannot accept a pairing + // request at this time +} TLVError; + +typedef struct _tlv { + struct _tlv *next; + uint8_t type; + uint8_t *value; + size_t size; +} pair_tlv_t; + + +typedef struct { + pair_tlv_t *head; +} pair_tlv_values_t; + + +pair_tlv_values_t *pair_tlv_new(); + +void pair_tlv_free(pair_tlv_values_t *values); + +int pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size); + +pair_tlv_t *pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type); + +int pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size); + +int pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values); + +#ifdef DEBUG_PAIR +void pair_tlv_debug(const pair_tlv_values_t *values); +#endif + +#endif /* !__PAIR_AP_TLV_H__ */ diff --git a/pair_ap/pair.c b/pair_ap/pair.c new file mode 100644 index 00000000..6908e8bd --- /dev/null +++ b/pair_ap/pair.c @@ -0,0 +1,771 @@ +/* + * The MIT License (MIT) + * + * 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 // for isprint() + +#include + +#include "pair.h" +#include "pair-internal.h" + +extern struct pair_definition pair_client_fruit; +extern struct pair_definition pair_client_homekit_normal; +extern struct pair_definition pair_client_homekit_transient; +extern struct pair_definition pair_server_homekit; + +// Must be in sync with enum pair_type +static struct pair_definition *pair[] = { + &pair_client_fruit, + &pair_client_homekit_normal, + &pair_client_homekit_transient, + &pair_server_homekit, +}; + +/* -------------------------- SHARED HASHING HELPERS ------------------------ */ + +int +hash_init(enum hash_alg alg, HashCTX *c) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Init(&c->sha); + case HASH_SHA224: return SHA224_Init(&c->sha256); + case HASH_SHA256: return SHA256_Init(&c->sha256); + case HASH_SHA384: return SHA384_Init(&c->sha512); + case HASH_SHA512: return SHA512_Init(&c->sha512); + default: + return -1; + }; +#elif CONFIG_GCRYPT + gcry_error_t err; + + err = gcry_md_open(c, alg, 0); + + if (err) + return -1; + + return 0; +#endif +} + +int +hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Update(&c->sha, data, len); + case HASH_SHA224: return SHA224_Update(&c->sha256, data, len); + case HASH_SHA256: return SHA256_Update(&c->sha256, data, len); + case HASH_SHA384: return SHA384_Update(&c->sha512, data, len); + case HASH_SHA512: return SHA512_Update(&c->sha512, data, len); + default: + return -1; + }; +#elif CONFIG_GCRYPT + gcry_md_write(*c, data, len); + return 0; +#endif +} + +int +hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Final(md, &c->sha); + case HASH_SHA224: return SHA224_Final(md, &c->sha256); + case HASH_SHA256: return SHA256_Final(md, &c->sha256); + case HASH_SHA384: return SHA384_Final(md, &c->sha512); + case HASH_SHA512: return SHA512_Final(md, &c->sha512); + default: + return -1; + }; +#elif CONFIG_GCRYPT + unsigned char *buf = gcry_md_read(*c, alg); + if (!buf) + return -1; + + memcpy(md, buf, gcry_md_get_algo_dlen(alg)); + gcry_md_close(*c); + return 0; +#endif +} + +unsigned char * +hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1(d, n, md); + case HASH_SHA224: return SHA224(d, n, md); + case HASH_SHA256: return SHA256(d, n, md); + case HASH_SHA384: return SHA384(d, n, md); + case HASH_SHA512: return SHA512(d, n, md); + default: + return NULL; + }; +#elif CONFIG_GCRYPT + gcry_md_hash_buffer(alg, md, d, n); + return md; +#endif +} + +int +hash_length(enum hash_alg alg) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA_DIGEST_LENGTH; + case HASH_SHA224: return SHA224_DIGEST_LENGTH; + case HASH_SHA256: return SHA256_DIGEST_LENGTH; + case HASH_SHA384: return SHA384_DIGEST_LENGTH; + case HASH_SHA512: return SHA512_DIGEST_LENGTH; + default: + return -1; + }; +#elif CONFIG_GCRYPT + return gcry_md_get_algo_dlen(alg); +#endif +} + +int +hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len) +{ + HashCTX ctx; + + hash_init(alg, &ctx); + hash_update(alg, &ctx, m1, m1_len); + hash_update(alg, &ctx, m2, m2_len); + return hash_final(alg, &ctx, md); +} + +bnum +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2) +{ + bnum bn; + unsigned char *bin; + unsigned char buff[SHA512_DIGEST_LENGTH]; + int len_n1 = bnum_num_bytes(n1); + int len_n2 = bnum_num_bytes(n2); + int nbytes = 2 * len_n1; + + if ((len_n2 < 1) || (len_n2 > len_n1)) + return 0; + + bin = calloc( 1, nbytes ); + + bnum_bn2bin(n1, bin, len_n1); + bnum_bn2bin(n2, bin + nbytes - len_n2, len_n2); + hash( alg, bin, nbytes, buff ); + free(bin); + bnum_bin2bn(bn, buff, hash_length(alg)); + return bn; +} + +bnum +H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes) +{ + bnum bn; + unsigned char buff[SHA512_DIGEST_LENGTH]; + int len_n = bnum_num_bytes(n); + int nbytes = len_n + len_bytes; + unsigned char *bin = malloc(nbytes); + + bnum_bn2bin(n, bin, len_n); + memcpy( bin + len_n, bytes, len_bytes ); + hash( alg, bin, nbytes, buff ); + free(bin); + bnum_bin2bn(bn, buff, hash_length(alg)); + return bn; +} + +void +update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n) +{ + unsigned long len = bnum_num_bytes(n); + unsigned char *n_bytes = malloc(len); + + bnum_bn2bin(n, n_bytes, len); + hash_update(alg, ctx, n_bytes, len); + free(n_bytes); +} + +void +hash_num(enum hash_alg alg, const bnum n, unsigned char *dest) +{ + int nbytes = bnum_num_bytes(n); + unsigned char *bin = malloc(nbytes); + + bnum_bn2bin(n, bin, nbytes); + hash( alg, bin, nbytes, dest ); + free(bin); +} + + +/* ----------------------------- OTHER HELPERS -------------------------------*/ + +#ifdef DEBUG_PAIR +void +hexdump(const char *msg, uint8_t *mem, size_t len) +{ + int i, j; + int hexdump_cols = 16; + + if (msg) + printf("%s", msg); + + for (i = 0; i < len + ((len % hexdump_cols) ? (hexdump_cols - len % hexdump_cols) : 0); i++) + { + if(i % hexdump_cols == 0) + printf("0x%06x: ", i); + + if (i < len) + printf("%02x ", 0xFF & ((char*)mem)[i]); + else + printf(" "); + + if (i % hexdump_cols == (hexdump_cols - 1)) + { + for (j = i - (hexdump_cols - 1); j <= i; j++) + { + if (j >= len) + putchar(' '); + else if (isprint(((char*)mem)[j])) + putchar(0xFF & ((char*)mem)[j]); + else + putchar('.'); + } + + putchar('\n'); + } + } +} +#endif + + +/* ----------------------------------- API -----------------------------------*/ + +struct pair_setup_context * +pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_setup_context *sctx; + + if (!pair[type]->pair_setup_new) + return NULL; + + sctx = calloc(1, sizeof(struct pair_setup_context)); + if (!sctx) + return NULL; + + sctx->type = pair[type]; + + if (pair[type]->pair_setup_new(sctx, pin, add_cb, cb_arg, device_id) < 0) + { + free(sctx); + return NULL; + } + + return sctx; +} + +void +pair_setup_free(struct pair_setup_context *sctx) +{ + if (!sctx) + return; + + if (sctx->type->pair_setup_free) + sctx->type->pair_setup_free(sctx); + + free(sctx); +} + +const char * +pair_setup_errmsg(struct pair_setup_context *sctx) +{ + return sctx->errmsg; +} + +int +pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + int state; + int ret = -1; + + if (!sctx->type->pair_state_get) + { + sctx->errmsg = "Getting pair state unsupported"; + return -1; + } + + *out = NULL; + *out_len = 0; + + state = sctx->type->pair_state_get(&sctx->errmsg, in, in_len); + if (state < 0) + return -1; + + switch (state) + { + case 0: + *out = pair_setup_request1(out_len, sctx); + break; + case 1: + ret = pair_setup_response1(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request1(out_len, sctx); + break; + case 2: + ret = pair_setup_response1(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request2(out_len, sctx); + break; + case 3: + ret = pair_setup_response2(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request2(out_len, sctx); + break; + case 4: + ret = pair_setup_response2(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request3(out_len, sctx); + break; + case 5: + ret = pair_setup_response3(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request3(out_len, sctx); + break; + case 6: + ret = pair_setup_response3(sctx, in, in_len); + if (ret < 0) + break; + break; + default: + sctx->errmsg = "Setup: Unsupported state"; + ret = -1; + } + + if (ret < 0 || !(*out)) + return -1; + + return 0; +} + +uint8_t * +pair_setup_request1(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request1) + { + sctx->errmsg = "Setup request 1: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request1(len, sctx); +} + +uint8_t * +pair_setup_request2(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request2) + { + sctx->errmsg = "Setup request 2: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request2(len, sctx); +} + +uint8_t * +pair_setup_request3(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request3) + { + sctx->errmsg = "Setup request 3: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request3(len, sctx); +} + +int +pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response1) + { + sctx->errmsg = "Setup response 1: Unsupported"; + return -1; + } + + return sctx->type->pair_setup_response1(sctx, in, in_len); +} + +int +pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response2) + { + sctx->errmsg = "Setup response 2: Unsupported"; + return -1; + } + + return sctx->type->pair_setup_response2(sctx, in, in_len); +} + +int +pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response3) + { + sctx->errmsg = "Setup response 3: Unsupported"; + return -1; + } + + if (sctx->type->pair_setup_response3(sctx, in, in_len) != 0) + return -1; + + return 0; +} + +int +pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx) +{ + if (sctx->status != PAIR_STATUS_COMPLETED) + { + sctx->errmsg = "Setup result: Pair setup has not been completed"; + return -1; + } + + if (sctx->type->pair_setup_result) + { + if (sctx->type->pair_setup_result(sctx) != 0) + return -1; + } + + if (client_setup_keys) + *client_setup_keys = sctx->result_str; + if (result) + *result = &sctx->result; + return 0; +} + +struct pair_verify_context * +pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id) +{ + struct pair_verify_context *vctx; + + if (!pair[type]->pair_verify_new) + return NULL; + + vctx = calloc(1, sizeof(struct pair_verify_context)); + if (!vctx) + return NULL; + + vctx->type = pair[type]; + + if (pair[type]->pair_verify_new(vctx, client_setup_keys, get_cb, cb_arg, device_id) < 0) + { + free(vctx); + return NULL; + } + + return vctx; +} + +void +pair_verify_free(struct pair_verify_context *vctx) +{ + if (!vctx) + return; + + if (vctx->type->pair_verify_free) + vctx->type->pair_verify_free(vctx); + + free(vctx); +} + +const char * +pair_verify_errmsg(struct pair_verify_context *vctx) +{ + return vctx->errmsg; +} + +int +pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + int state; + int ret = -1; + + if (!vctx->type->pair_state_get) + { + vctx->errmsg = "Getting pair state unsupported"; + return -1; + } + + *out = NULL; + *out_len = 0; + + state = vctx->type->pair_state_get(&vctx->errmsg, in, in_len); + if (state < 0) + return -1; + + switch (state) + { + case 0: + *out = pair_verify_request1(out_len, vctx); + break; + case 1: + ret = pair_verify_response1(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request1(out_len, vctx); + break; + case 2: + ret = pair_verify_response1(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request2(out_len, vctx); + break; + case 3: + ret = pair_verify_response2(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request2(out_len, vctx); + break; + case 4: + ret = pair_verify_response2(vctx, in, in_len); + if (ret < 0) + break; + break; + default: + vctx->errmsg = "Verify: Unsupported state"; + ret = -1; + } + + if (ret < 0 || !(*out)) + return -1; + + return 0; +} + +uint8_t * +pair_verify_request1(size_t *len, struct pair_verify_context *vctx) +{ + if (!vctx->type->pair_verify_request1) + { + vctx->errmsg = "Verify request 1: Unsupported"; + return NULL; + } + + return vctx->type->pair_verify_request1(len, vctx); +} + +uint8_t * +pair_verify_request2(size_t *len, struct pair_verify_context *vctx) +{ + if (!vctx->type->pair_verify_request2) + { + vctx->errmsg = "Verify request 2: Unsupported"; + return NULL; + } + + return vctx->type->pair_verify_request2(len, vctx); +} + +int +pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + if (!vctx->type->pair_verify_response1) + { + vctx->errmsg = "Verify response 1: Unsupported"; + return -1; + } + + return vctx->type->pair_verify_response1(vctx, in, in_len); +} + +int +pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + if (!vctx->type->pair_verify_response2) + { + vctx->errmsg = "Verify response 2: Unsupported"; + return -1; + } + + if (vctx->type->pair_verify_response2(vctx, in, in_len) != 0) + return -1; + + return 0; +} + +int +pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx) +{ + if (vctx->status != PAIR_STATUS_COMPLETED) + { + vctx->errmsg = "Verify result: The pairing verification did not complete"; + return -1; + } + + if (vctx->type->pair_verify_result) + { + if (vctx->type->pair_verify_result(vctx) != 0) + return -1; + } + + if (result) + *result = &vctx->result; + return 0; +} + +struct pair_cipher_context * +pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len) +{ + if (!pair[type]->pair_cipher_new) + return NULL; + + return pair[type]->pair_cipher_new(pair[type], channel, shared_secret, shared_secret_len); +} + +void +pair_cipher_free(struct pair_cipher_context *cctx) +{ + if (!cctx) + return; + + if (!cctx->type->pair_cipher_free) + return; + + return cctx->type->pair_cipher_free(cctx); +} + +const char * +pair_cipher_errmsg(struct pair_cipher_context *cctx) +{ + return cctx->errmsg; +} + +ssize_t +pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx) +{ + if (!cctx->type->pair_encrypt) + { + cctx->errmsg = "Encryption unsupported"; + return -1; + } + + return cctx->type->pair_encrypt(ciphertext, ciphertext_len, plaintext, plaintext_len, cctx); +} + +ssize_t +pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx) +{ + if (!cctx->type->pair_decrypt) + { + cctx->errmsg = "Decryption unsupported"; + return -1; + } + + return cctx->type->pair_decrypt(plaintext, plaintext_len, ciphertext, ciphertext_len, cctx); +} + +void +pair_encrypt_rollback(struct pair_cipher_context *cctx) +{ + cctx->encryption_counter = cctx->encryption_counter_prev; +} + +void +pair_decrypt_rollback(struct pair_cipher_context *cctx) +{ + cctx->decryption_counter = cctx->decryption_counter_prev; +} + +int +pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_add) + { + return -1; + } + + return pair[type]->pair_add(out, out_len, add_cb, cb_arg, in, in_len); +} + +int +pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_remove) + { + return -1; + } + + return pair[type]->pair_remove(out, out_len, remove_cb, cb_arg, in, in_len); +} + +int +pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_list) + { + return -1; + } + + return pair[type]->pair_list(out, out_len, list_cb, cb_arg, in, in_len); +} + +int +pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_state_get) + { + *errmsg = "Getting pair state unsupported"; + return -1; + } + + return pair[type]->pair_state_get(errmsg, in, in_len); +} + +void +pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id) +{ + if (!pair[type]->pair_public_key_get) + { + return; + } + + pair[type]->pair_public_key_get(server_public_key, device_id); +} diff --git a/pair_ap/pair.h b/pair_ap/pair.h new file mode 100644 index 00000000..6324bc15 --- /dev/null +++ b/pair_ap/pair.h @@ -0,0 +1,266 @@ +#ifndef __PAIR_AP_H__ +#define __PAIR_AP_H__ + +#include + +#define PAIR_AP_VERSION_MAJOR 0 +#define PAIR_AP_VERSION_MINOR 5 + +#define PAIR_AP_DEVICE_ID_LEN_MAX 64 + +#define PAIR_AP_POST_PIN_START "POST /pair-pin-start" +#define PAIR_AP_POST_SETUP "POST /pair-setup" +#define PAIR_AP_POST_VERIFY "POST /pair-verify" +#define PAIR_AP_POST_ADD "POST /pair-add" +#define PAIR_AP_POST_LIST "POST /pair-list" +#define PAIR_AP_POST_REMOVE "POST /pair-remove" + +enum pair_type +{ + // This is the pairing type required for Apple TV device verification, which + // became mandatory with tvOS 10.2. + PAIR_CLIENT_FRUIT, + // This is the Homekit type required for AirPlay 2 with both PIN setup and + // verification + PAIR_CLIENT_HOMEKIT_NORMAL, + // Same as normal except PIN is fixed to 3939 and stops after setup step 2, + // when session key is established + PAIR_CLIENT_HOMEKIT_TRANSIENT, + // Server side implementation supporting both transient and normal mode, + // letting client choose mode. If a PIN is with pair_setup_new() then only + // normal mode will be possible. + PAIR_SERVER_HOMEKIT, +}; + +/* This struct stores the various forms of pairing results. The shared secret + * is used to initialise an encrypted session via pair_cipher_new(). For + * non-transient client pair setup, you also get a key string (client_setup_keys) from + * pair_setup_result() that you can store and use to later initialise + * pair_verify_new(). For non-transient server pair setup, you can either: + * - Register an "add pairing" callback (add_cb) with pair_setup_new(), and + * then save the client id and key in the callback (see server-example.c for + * this approach). + * - Check pairing result with pair_setup_result() and if successful read and + * store the client id and key from the result struct. + * - Decide not to authenticate clients during pair-verify (set get_cb to NULL) + * in which case you don't need to save client ids and keys from pair-setup. + * + * Table showing returned data (everything else will be zeroed): + * + * | pair-setup | pair-verify + * --------------------------------|-------------------------------|-------------- + * PAIR_CLIENT_FRUIT | client keys | shared secret + * PAIR_CLIENT_HOMEKIT_NORMAL | client keys, server public | shared secret + | key, server id | shared secret + * PAIR_CLIENT_HOMEKIT_TRANSIENT | shared secret | n/a + * PAIR_SERVER_HOMEKIT (normal) | client public key, client id | shared secret + * PAIR_SERVER_HOMEKIT (transient) | shared secret | n/a + */ +struct pair_result +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; // ID of the peer + uint8_t client_private_key[64]; + uint8_t client_public_key[32]; + uint8_t server_public_key[32]; + uint8_t shared_secret[64]; + size_t shared_secret_len; // Will be 32 (normal) or 64 (transient) +}; + +struct pair_setup_context; +struct pair_verify_context; +struct pair_cipher_context; + +typedef int (*pair_cb)(uint8_t public_key[32], const char *device_id, void *cb_arg); +typedef void (*pair_list_cb)(pair_cb list_cb, void *list_cb_arg, void *cb_arg); + + +/* ------------------------------- pair setup ------------------------------- */ + +/* Client + * When you have the pin-code (must be 4 chars), create a new context with this + * function and then call pair_setup() or pair_setup_request1(). device_id is + * only required for Homekit pairing. If the client previously paired + * (non-transient) and has saved credentials, it should instead skip setup and + * only do verification. The callback is only for Homekit, and you can leave it + * at NULL if you don't care about saving ID and key of the server for later + * verification (then you also set get_cb to NULL in pair_verify_new), or if you + * will read the id and key via pair_setup_result. + * + * Server + * The client will make a connection and then at some point make a /pair-setup + * or a /pair-verify. The server should: + * - new /pair-setup: create a setup context with a pin-code (or NULL to allow + * transient pairing), and then call pair_setup() to process request and + * construct reply (also for subsequent /pair-setup requests) + * - new /pair_verify: create a verify context and then call pair_verify() + * to process request and construct reply (also for subsequent /pair-verify + * requests) + */ +struct pair_setup_context * +pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id); +void +pair_setup_free(struct pair_setup_context *sctx); + +/* Returns last error message + */ +const char * +pair_setup_errmsg(struct pair_setup_context *sctx); + +/* Will create a request (if client) or response (if server) based on the setup + * context and last message from the peer. If this is the first client request + * then set *in to NULL. Returns negative on error. + */ +int +pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + +/* Returns the result of a pairing, or negative if pairing is not completed. See + * 'struct pair_result' for info about pairing results. The string is a + * representation of the result that is easy to persist and can be used to feed + * back into pair_verify_new. The result and string becomes invalid when you + * free sctx. + */ +int +pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx); + +/* These are for constructing specific message types and reading specific + * message types. Not needed for Homekit pairing if you use pair_setup(). + */ +uint8_t * +pair_setup_request1(size_t *len, struct pair_setup_context *sctx); +uint8_t * +pair_setup_request2(size_t *len, struct pair_setup_context *sctx); +uint8_t * +pair_setup_request3(size_t *len, struct pair_setup_context *sctx); + +int +pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); +int +pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); +int +pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + + +/* ------------------------------ pair verify ------------------------------- */ + +/* Client + * When you have completed pair setup you get a string containing some keys + * from pair_setup_result(). Give the string as input to this function to create + * a verification context. Set the callback to NULL. Then call pair_verify(). + * The device_id is required for Homekit pairing. + * + * Server + * When you get a pair verify request from a new peer, create a new context with + * client_setup_keys set to NULL, with a callback set and the server's device ID + * (same as for setup). Then call pair_verify(). The callback is used to get + * the persisted client public key (saved after pair setup), so the client can + * be verified. You can set the callback to NULL if you don't care about that. + * If set, the callback is made as part of pair_verify_response2. The job of the + * callback is to fill out the public_key with the public key from the setup + * stage (see 'struct pair_result'). If the client device id is not known (i.e. + * it has not completed pair-setup), return -1. + */ +struct pair_verify_context * +pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id); +void +pair_verify_free(struct pair_verify_context *vctx); + +/* Returns last error message + */ +const char * +pair_verify_errmsg(struct pair_verify_context *vctx); + +/* Will create a request (if client) or response (if server) based on the verify + * context and last message from the peer. If this is the first client request + * then set *in to NULL. Returns negative on error. + */ +int +pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *sctx, const uint8_t *in, size_t in_len); + +/* Returns a pointer to the result of the pairing. Only the shared secret will + * be filled out. Note that the result become invalid when you free vctx. + */ +int +pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx); + +/* These are for constructing specific message types and reading specific + * message types. Not needed for Homekit pairing where you can use pair_verify(). + */ +uint8_t * +pair_verify_request1(size_t *len, struct pair_verify_context *vctx); +uint8_t * +pair_verify_request2(size_t *len, struct pair_verify_context *vctx); + +int +pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); +int +pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + + +/* ------------------------------- ciphering -------------------------------- */ + +/* When you have completed the verification you can extract a shared secret with + * pair_verify_result() - or, in case of transient pairing, from + * pair_setup_result(). Give the shared secret as input to this function to + * create a ciphering context. + */ +struct pair_cipher_context * +pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len); +void +pair_cipher_free(struct pair_cipher_context *cctx); + +/* Returns last error message + */ +const char * +pair_cipher_errmsg(struct pair_cipher_context *cctx); + +/* The return value equals length of plaintext that was encrypted, so if the + * return value == plaintext_len then everything was encrypted. On error -1 is + * returned. + */ +ssize_t +pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx); + +/* The return value equals length of ciphertext that was decrypted, so if the + * return value == ciphertext_len then everything was decrypted. On error -1 is + * returned. + */ +ssize_t +pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx); + +/* Rolls back the nonce + */ +void +pair_encrypt_rollback(struct pair_cipher_context *cctx); +void +pair_decrypt_rollback(struct pair_cipher_context *cctx); + + +/* --------------------------------- other ---------------------------------- */ + +/* These are for Homekit pairing where they are called by the controller, e.g. + * the Home app + * + * TODO this part is currenly not working + */ +int +pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +int +pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +int +pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +/* For parsing an incoming message to see what type ("state") it is. Mostly + * useful for servers. Returns 1-6 for pair-setup and 1-4 for pair-verify. + */ +int +pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len); + +/* For servers, pair_ap calculates the public key using device_id as a seed. + * This function returns that public key. + */ +void +pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id); + +#endif /* !__PAIR_AP_H__ */ diff --git a/pair_ap/pair_ap-master/.gitignore b/pair_ap/pair_ap-master/.gitignore new file mode 100644 index 00000000..a7eeb67d --- /dev/null +++ b/pair_ap/pair_ap-master/.gitignore @@ -0,0 +1,10 @@ +*.o +*.lo +*.a +*.la +.dirstamp +.deps/ +.libs/ + +client-example +server-example diff --git a/pair_ap/pair_ap-master/evrtsp/evrtsp.h b/pair_ap/pair_ap-master/evrtsp/evrtsp.h new file mode 100644 index 00000000..5a03cd6d --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/evrtsp.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright (c) 2000-2004 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef _EVRTSP_H_ +#define _EVRTSP_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#undef WIN32_LEAN_AND_MEAN +#endif + +/* Response codes */ +#define RTSP_OK 200 +#define RTSP_UNAUTHORIZED 401 +#define RTSP_FORBIDDEN 403 + +struct evrtsp_connection; + +/* + * Interfaces for making requests + */ +enum evrtsp_cmd_type { + EVRTSP_REQ_ANNOUNCE, + EVRTSP_REQ_OPTIONS, + EVRTSP_REQ_SETUP, + EVRTSP_REQ_RECORD, + EVRTSP_REQ_PAUSE, + EVRTSP_REQ_GET_PARAMETER, // Careful using this, some devices do not support it or will hang up eg RAOP_DEV_APEX1_80211G + EVRTSP_REQ_SET_PARAMETER, + EVRTSP_REQ_FLUSH, + EVRTSP_REQ_TEARDOWN, + EVRTSP_REQ_POST, +}; + +enum evrtsp_request_kind { EVRTSP_REQUEST, EVRTSP_RESPONSE }; + +struct evrtsp_request { +#if defined(TAILQ_ENTRY) + TAILQ_ENTRY(evrtsp_request) next; +#else +struct { + struct evrtsp_request *tqe_next; + struct evrtsp_request **tqe_prev; +} next; +#endif + + /* the connection object that this request belongs to */ + struct evrtsp_connection *evcon; + int flags; +#define EVRTSP_REQ_OWN_CONNECTION 0x0001 + + struct evkeyvalq *input_headers; + struct evkeyvalq *output_headers; + + enum evrtsp_request_kind kind; + enum evrtsp_cmd_type type; + + char *uri; /* uri after RTSP request was parsed */ + + char major; /* RTSP Major number */ + char minor; /* RTSP Minor number */ + + int response_code; /* RTSP Response code */ + char *response_code_line; /* Readable response */ + + struct evbuffer *input_buffer; /* read data */ + ev_int64_t ntoread; + + struct evbuffer *output_buffer; /* outgoing post or data */ + + /* Callback */ + void (*cb)(struct evrtsp_request *, void *); + void *cb_arg; +}; + +/** + * Creates a new request object that needs to be filled in with the request + * parameters. The callback is executed when the request completed or an + * error occurred. + */ +struct evrtsp_request *evrtsp_request_new( + void (*cb)(struct evrtsp_request *, void *), void *arg); + +/** Frees the request object and removes associated events. */ +void evrtsp_request_free(struct evrtsp_request *req); + +/** + * A connection object that can be used to for making RTSP requests. The + * connection object tries to establish the connection when it is given an + * rtsp request object. + */ +struct evrtsp_connection *evrtsp_connection_new( + const char *address, unsigned short port); + +/** Frees an rtsp connection */ +void evrtsp_connection_free(struct evrtsp_connection *evcon); + +/** Set a callback for connection close. */ +void evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, + void (*)(struct evrtsp_connection *, void *), void *); + +/** Set a callback for encryption/decryption. */ +void evrtsp_connection_set_ciphercb(struct evrtsp_connection *evcon, + void (*)(struct evbuffer *, void *, int encrypt), void *); + +/** + * Associates an event base with the connection - can only be called + * on a freshly created connection object that has not been used yet. + */ +void evrtsp_connection_set_base(struct evrtsp_connection *evcon, + struct event_base *base); + +/** Get the remote address and port associated with this connection. */ +void evrtsp_connection_get_peer(struct evrtsp_connection *evcon, + char **address, u_short *port); + +/** Get the local address, port and family associated with this connection. */ +void +evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, + char **address, u_short *port, int *family); + +/** The connection gets ownership of the request */ +int evrtsp_make_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req, + enum evrtsp_cmd_type type, const char *uri); + +const char *evrtsp_request_uri(struct evrtsp_request *req); + +/* Interfaces for dealing with headers */ + +const char *evrtsp_find_header(const struct evkeyvalq *, const char *); +int evrtsp_remove_header(struct evkeyvalq *, const char *); +int evrtsp_add_header(struct evkeyvalq *, const char *, const char *); +void evrtsp_clear_headers(struct evkeyvalq *); + +const char *evrtsp_method(enum evrtsp_cmd_type type); + +#ifdef __cplusplus +} +#endif + +#endif /* !_EVRTSP_H_ */ diff --git a/pair_ap/pair_ap-master/evrtsp/log.h b/pair_ap/pair_ap-master/evrtsp/log.h new file mode 100644 index 00000000..7bc6632b --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/log.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2000-2004 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef _LOG_H_ +#define _LOG_H_ + +#ifdef __GNUC__ +#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b))) +#else +#define EV_CHECK_FMT(a,b) +#endif + +void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); +void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2); +void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); +void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2); +void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2); +void _event_debugx(const char *fmt, ...) EV_CHECK_FMT(1,2); + +#ifdef USE_DEBUG +#define event_debug(x) _event_debugx x +#else +#define event_debug(x) do {;} while (0) +#endif + +#undef EV_CHECK_FMT + +#endif diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h b/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h new file mode 100644 index 00000000..dcab48a1 --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright 2001 Niels Provos + * All rights reserved. + * + * This header file contains definitions for dealing with RTSP requests + * that are internal to libevent. As user of the library, you should not + * need to know about these. + */ + +#ifndef _RTSP_H_ +#define _RTSP_H_ + +#include +#include + +#define RTSP_CONNECT_TIMEOUT 5 +#define RTSP_WRITE_TIMEOUT 30 +#define RTSP_READ_TIMEOUT 30 + +#define RTSP_PREFIX "rtsp://" + +enum message_read_status { + ALL_DATA_READ = 1, + MORE_DATA_EXPECTED = 0, + DATA_CORRUPTED = -1, + REQUEST_CANCELED = -2 +}; + +enum evrtsp_connection_error { + EVCON_RTSP_TIMEOUT, + EVCON_RTSP_EOF, + EVCON_RTSP_INVALID_HEADER +}; + +struct evbuffer; +struct addrinfo; +struct evrtsp_request; + +/* A stupid connection object - maybe make this a bufferevent later */ + +enum evrtsp_connection_state { + EVCON_DISCONNECTED, /**< not currently connected not trying either*/ + EVCON_CONNECTING, /**< tries to currently connect */ + EVCON_IDLE, /**< connection is established */ + EVCON_READING_FIRSTLINE,/**< reading Request-Line (incoming conn) or + **< Status-Line (outgoing conn) */ + EVCON_READING_HEADERS, /**< reading request/response headers */ + EVCON_READING_BODY, /**< reading request/response body */ + EVCON_READING_TRAILER, /**< reading request/response chunked trailer */ + EVCON_WRITING /**< writing request/response headers/body */ +}; + +struct event_base; + +struct evrtsp_connection { + int fd; + struct event ev; + struct event close_ev; + struct evbuffer *input_buffer; + struct evbuffer *output_buffer; + + char *bind_address; /* address to use for binding the src */ + u_short bind_port; /* local port for binding the src */ + + char *address; /* address to connect to */ + int family; + u_short port; + + int flags; +#define EVRTSP_CON_CLOSEDETECT 0x0004 /* detecting if persistent close */ + + int timeout; /* timeout in seconds for events */ + + enum evrtsp_connection_state state; + int cseq; + + TAILQ_HEAD(evcon_requestq, evrtsp_request) requests; + + void (*cb)(struct evrtsp_connection *, void *); + void *cb_arg; + + void (*closecb)(struct evrtsp_connection *, void *); + void *closecb_arg; + + void (*ciphercb)(struct evbuffer *evbuf, void *, int encrypt); + void *ciphercb_arg; + + struct event_base *base; +}; + +/* resets the connection; can be reused for more requests */ +void evrtsp_connection_reset(struct evrtsp_connection *); + +/* connects if necessary */ +int evrtsp_connection_connect(struct evrtsp_connection *); + +/* notifies the current request that it failed; resets connection */ +void evrtsp_connection_fail(struct evrtsp_connection *, + enum evrtsp_connection_error error); + +int evrtsp_hostportfile(char *, char **, u_short *, char **); + +int evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*); +int evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*); + +void evrtsp_start_read(struct evrtsp_connection *); +void evrtsp_make_header(struct evrtsp_connection *, struct evrtsp_request *); + +void evrtsp_write_buffer(struct evrtsp_connection *, + void (*)(struct evrtsp_connection *, void *), void *); + +#endif /* _RTSP_H */ diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp.c b/pair_ap/pair_ap-master/evrtsp/rtsp.c new file mode 100644 index 00000000..82efc548 --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/rtsp.c @@ -0,0 +1,1816 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright (c) 2002-2006 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#undef timeout_pending +#undef timeout_initialized + +#include "evrtsp.h" +/* #define USE_DEBUG */ +#include "log.h" +#include "rtsp-internal.h" + +// For compability with libevent 2.0 (HAVE_LIBEVENT2_OLD) +#if defined(_EVENT_HAVE_GETNAMEINFO) +# define EVENT__HAVE_GETNAMEINFO 1 +#endif +#if defined(_EVENT_HAVE_GETADDRINFO) +# define EVENT__HAVE_GETADDRINFO 1 +#endif +#if defined(_EVENT_HAVE_STRSEP) +# define EVENT__HAVE_STRSEP 1 +#endif + +#ifndef EVENT__HAVE_GETNAMEINFO +#define NI_MAXSERV 32 +#define NI_MAXHOST 1025 + +#define NI_NUMERICHOST 1 +#define NI_NUMERICSERV 2 + +static int +fake_getnameinfo(const struct sockaddr *sa, size_t salen, char *host, + size_t hostlen, char *serv, size_t servlen, int flags) +{ + struct sockaddr_in *sin = (struct sockaddr_in *)sa; + int ret; + + if (serv != NULL) { + char tmpserv[16]; + evutil_snprintf(tmpserv, sizeof(tmpserv), + "%d", ntohs(sin->sin_port)); + ret = evutil_snprintf(serv, servlen, "%s", tmpserv); + if ((ret < 0) || (ret >= servlen)) + return (-1); + } + + if (host != NULL) { + if (flags & NI_NUMERICHOST) { + ret = evutil_snprintf(host, hostlen, "%s", inet_ntoa(sin->sin_addr)); + if ((ret < 0) || (ret >= hostlen)) + return (-1); + else + return (0); + } else { + struct hostent *hp; + hp = gethostbyaddr((char *)&sin->sin_addr, + sizeof(struct in_addr), AF_INET); + if (hp == NULL) + return (-2); + + ret = evutil_snprintf(host, hostlen, "%s", hp->h_name); + if ((ret < 0) || (ret >= hostlen)) + return (-1); + else + return (0); + } + } + return (0); +} + +#endif + +#ifndef EVENT__HAVE_GETADDRINFO +struct addrinfo { + int ai_family; + int ai_socktype; + int ai_protocol; + size_t ai_addrlen; + struct sockaddr *ai_addr; + struct addrinfo *ai_next; +}; +static int +fake_getaddrinfo(const char *hostname, struct addrinfo *ai) +{ + struct hostent *he = NULL; + struct sockaddr_in *sa; + if (hostname) { + he = gethostbyname(hostname); + if (!he) + return (-1); + } + ai->ai_family = he ? he->h_addrtype : AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_protocol = 0; + ai->ai_addrlen = sizeof(struct sockaddr_in); + if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen))) + return (-1); + sa = (struct sockaddr_in*)ai->ai_addr; + memset(sa, 0, ai->ai_addrlen); + if (he) { + sa->sin_family = he->h_addrtype; + memcpy(&sa->sin_addr, he->h_addr_list[0], he->h_length); + } else { + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = INADDR_ANY; + } + ai->ai_next = NULL; + return (0); +} +static void +fake_freeaddrinfo(struct addrinfo *ai) +{ + free(ai->ai_addr); +} +#endif + +#ifndef MIN +#define MIN(a,b) (((a)<(b))?(a):(b)) +#endif + +/* wrapper for setting the base from the rtsp server */ +#define EVRTSP_BASE_SET(x, y) do { \ + if ((x)->base != NULL) event_base_set((x)->base, y); \ +} while (0) + +extern int debug; + +static int socket_connect(int fd, const char *address, unsigned short port); +static int bind_socket_ai(int family, struct addrinfo *, int reuse); +static int bind_socket(int family, const char *, u_short, int reuse); +static void name_from_addr(struct sockaddr *, socklen_t, char **, char **); +static void evrtsp_connection_start_detectclose( + struct evrtsp_connection *evcon); +static void evrtsp_connection_stop_detectclose( + struct evrtsp_connection *evcon); +static void evrtsp_request_dispatch(struct evrtsp_connection* evcon); +static void evrtsp_read_firstline(struct evrtsp_connection *evcon, + struct evrtsp_request *req); +static void evrtsp_read_header(struct evrtsp_connection *evcon, + struct evrtsp_request *req); +static int evrtsp_add_header_internal(struct evkeyvalq *headers, + const char *key, const char *value); + +void evrtsp_read(int, short, void *); +void evrtsp_write(int, short, void *); + +#ifndef EVENT__HAVE_STRSEP +/* strsep replacement for platforms that lack it. Only works if + * del is one character long. */ +static char * +strsep(char **s, const char *del) +{ + char *d, *tok; + assert(strlen(del) == 1); + if (!s || !*s) + return NULL; + tok = *s; + d = strstr(tok, del); + if (d) { + *d = '\0'; + *s = d + 1; + } else + *s = NULL; + return tok; +} +#endif + +const char * +evrtsp_method(enum evrtsp_cmd_type type) +{ + const char *method; + + switch (type) { + case EVRTSP_REQ_ANNOUNCE: + method = "ANNOUNCE"; + break; + + case EVRTSP_REQ_OPTIONS: + method = "OPTIONS"; + break; + + case EVRTSP_REQ_SETUP: + method = "SETUP"; + break; + + case EVRTSP_REQ_RECORD: + method = "RECORD"; + break; + + case EVRTSP_REQ_PAUSE: + method = "PAUSE"; + break; + + case EVRTSP_REQ_GET_PARAMETER: + method = "GET_PARAMETER"; + break; + + case EVRTSP_REQ_SET_PARAMETER: + method = "SET_PARAMETER"; + break; + + case EVRTSP_REQ_FLUSH: + method = "FLUSH"; + break; + + case EVRTSP_REQ_TEARDOWN: + method = "TEARDOWN"; + break; + + case EVRTSP_REQ_POST: + method = "POST"; + break; + + default: + method = NULL; + break; + } + + return (method); +} + +static void +evrtsp_add_event(struct event *ev, int timeout, int default_timeout) +{ + if (timeout != 0) { + struct timeval tv; + + evutil_timerclear(&tv); + tv.tv_sec = timeout != -1 ? timeout : default_timeout; + event_add(ev, &tv); + } else { + event_add(ev, NULL); + } +} + +void +evrtsp_write_buffer(struct evrtsp_connection *evcon, + void (*cb)(struct evrtsp_connection *, void *), void *arg) +{ + event_debug(("%s: preparing to write buffer", __func__)); + + /* Set call back */ + evcon->cb = cb; + evcon->cb_arg = arg; + + /* check if the event is already pending */ + if (event_pending(&evcon->ev, EV_WRITE|EV_TIMEOUT, NULL)) + event_del(&evcon->ev); + + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_write, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_WRITE_TIMEOUT); +} + +static int +evrtsp_connected(struct evrtsp_connection *evcon) +{ + switch (evcon->state) { + case EVCON_DISCONNECTED: + case EVCON_CONNECTING: + return (0); + case EVCON_IDLE: + case EVCON_READING_FIRSTLINE: + case EVCON_READING_HEADERS: + case EVCON_READING_BODY: + case EVCON_READING_TRAILER: + case EVCON_WRITING: + default: + return (1); + } +} + +/* + * Create the headers needed for an RTSP request + */ +static void +evrtsp_make_header_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req) +{ + const char *method; + + /* Generate request line */ + method = evrtsp_method(req->type); + evbuffer_add_printf(evcon->output_buffer, "%s %s RTSP/%d.%d\r\n", + method, req->uri, req->major, req->minor); + + /* Content-Length is mandatory, absent means 0 */ + if ((evbuffer_get_length(req->output_buffer) > 0) + && (evrtsp_find_header(req->output_headers, "Content-Length") == NULL)) + { + char size[12]; + evutil_snprintf(size, sizeof(size), "%ld", + (long)evbuffer_get_length(req->output_buffer)); + evrtsp_add_header(req->output_headers, "Content-Length", size); + } +} + +void +evrtsp_make_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evkeyval *header; + + evrtsp_make_header_request(evcon, req); + + TAILQ_FOREACH(header, req->output_headers, next) { + evbuffer_add_printf(evcon->output_buffer, "%s: %s\r\n", + header->key, header->value); + } + evbuffer_add(evcon->output_buffer, "\r\n", 2); + + if (evbuffer_get_length(req->output_buffer) > 0) { + evbuffer_add_buffer(evcon->output_buffer, req->output_buffer); + } +} + +/* Separated host, port and file from URI */ + +int /* FIXME: needed? */ +evrtsp_hostportfile(char *url, char **phost, u_short *pport, char **pfile) +{ + /* XXX not threadsafe. */ + static char host[1024]; + static char file[1024]; + char *p; + const char *p2; + int len; + int ret; + u_short port; + + len = strlen(RTSP_PREFIX); + if (strncasecmp(url, RTSP_PREFIX, len)) + return (-1); + + url += len; + + /* We might overrun */ + ret = evutil_snprintf(host, sizeof(host), "%s", url); + if ((ret < 0) || (ret >= sizeof(host))) + return (-1); + + p = strchr(host, '/'); + if (p != NULL) { + *p = '\0'; + p2 = p + 1; + } else + p2 = NULL; + + if (pfile != NULL) { + /* Generate request file */ + if (p2 == NULL) + p2 = ""; + evutil_snprintf(file, sizeof(file), "/%s", p2); + } + + p = strchr(host, ':'); + if (p != NULL) { + *p = '\0'; + port = atoi(p + 1); + + if (port == 0) + return (-1); + } else + return -1; + + if (phost != NULL) + *phost = host; + if (pport != NULL) + *pport = port; + if (pfile != NULL) + *pfile = file; + + return (0); +} + +void +evrtsp_connection_fail(struct evrtsp_connection *evcon, + enum evrtsp_connection_error error) +{ + struct evrtsp_request* req = TAILQ_FIRST(&evcon->requests); + void (*cb)(struct evrtsp_request *, void *); + void *cb_arg; + assert(req != NULL); + + /* save the callback for later; the cb might free our object */ + cb = req->cb; + cb_arg = req->cb_arg; + + TAILQ_REMOVE(&evcon->requests, req, next); + evrtsp_request_free(req); + + /* xxx: maybe we should fail all requests??? */ + + /* reset the connection */ + evrtsp_connection_reset(evcon); + + /* We are trying the next request that was queued on us */ + if (TAILQ_FIRST(&evcon->requests) != NULL) + evrtsp_connection_connect(evcon); + + /* inform the user */ + if (cb != NULL) + (*cb)(NULL, cb_arg); +} + +void +evrtsp_write(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + int n; + + if (what == EV_TIMEOUT) { + event_warn("%s: write timeout", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); + return; + } + + n = evbuffer_write(evcon->output_buffer, fd); + if (n == -1) { + event_warn("%s: evbuffer_write", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + return; + } + + if (n == 0) { + event_warn("%s: write nothing", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + return; + } + + if (evbuffer_get_length(evcon->output_buffer) != 0) { + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_WRITE_TIMEOUT); + return; + } + + /* Activate our call back */ + if (evcon->cb != NULL) + (*evcon->cb)(evcon, evcon->cb_arg); +} + +/** + * Advance the connection state. + * - If this is an outgoing connection, we've just processed the response; + * idle or close the connection. + */ +static void +evrtsp_connection_done(struct evrtsp_connection *evcon) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + + /* idle or close the connection */ + TAILQ_REMOVE(&evcon->requests, req, next); + req->evcon = NULL; + + evcon->state = EVCON_IDLE; + + if (TAILQ_FIRST(&evcon->requests) != NULL) { + /* + * We have more requests; reset the connection + * and deal with the next request. + */ + if (!evrtsp_connected(evcon)) + evrtsp_connection_connect(evcon); + else + evrtsp_request_dispatch(evcon); + } else { + /* + * The connection is going to be persistent, but we + * need to detect if the other side closes it. + */ + evrtsp_connection_start_detectclose(evcon); + } + + /* notify the user of the request */ + (*req->cb)(req, req->cb_arg); + + evrtsp_request_free(req); +} + +static void /* FIXME: needed? */ +evrtsp_read_trailer(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evbuffer *buf = evcon->input_buffer; + + switch (evrtsp_parse_headers(req, buf)) { + case DATA_CORRUPTED: + event_warn("%s: invalid header", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + break; + case ALL_DATA_READ: + event_del(&evcon->ev); + evrtsp_connection_done(evcon); + break; + case MORE_DATA_EXPECTED: + default: + evrtsp_add_event(&evcon->ev, evcon->timeout, + RTSP_READ_TIMEOUT); + break; + } +} + +static void +evrtsp_read_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evbuffer *buf = evcon->input_buffer; + + if (req->ntoread < 0) { + /* Read until connection close. */ + evbuffer_add_buffer(req->input_buffer, buf); + } else if (evbuffer_get_length(buf) >= req->ntoread) { + /* Completed content length */ + evbuffer_add(req->input_buffer, evbuffer_pullup(buf,-1), + (size_t)req->ntoread); + evbuffer_drain(buf, (size_t)req->ntoread); + req->ntoread = 0; + evrtsp_connection_done(evcon); + return; + } + /* Read more! */ + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); +} + +/* + * Reads data into a buffer structure until no more data + * can be read on the file descriptor or we have read all + * the data that we wanted to read. + * Execute callback when done. + */ + +void +evrtsp_read(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + struct evbuffer *buf = evcon->input_buffer; + int n; + + if (what == EV_TIMEOUT) { + event_warn("%s: read timeout", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); + return; + } + n = evbuffer_read(buf, fd, -1); + event_debug(("%s: got %d on %d", __func__, n, fd)); + + if (n == -1) { + if (errno != EINTR && errno != EAGAIN) { + event_warn("%s: evbuffer_read", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + } else { + evrtsp_add_event(&evcon->ev, evcon->timeout, + RTSP_READ_TIMEOUT); + } + return; + } else if (n == 0) { + /* Connection closed */ + evcon->state = EVCON_DISCONNECTED; + evrtsp_connection_done(evcon); + return; + } + + if (evcon->ciphercb) + evcon->ciphercb(evcon->input_buffer, evcon->ciphercb_arg, 0); + + switch (evcon->state) { + case EVCON_READING_FIRSTLINE: + evrtsp_read_firstline(evcon, req); + break; + case EVCON_READING_HEADERS: + evrtsp_read_header(evcon, req); + break; + case EVCON_READING_BODY: + evrtsp_read_body(evcon, req); + break; + case EVCON_READING_TRAILER: + evrtsp_read_trailer(evcon, req); + break; + case EVCON_DISCONNECTED: + case EVCON_CONNECTING: + case EVCON_IDLE: + case EVCON_WRITING: + default: + event_errx(1, "%s: illegal connection state %d", + __func__, evcon->state); + } +} + +static void +evrtsp_write_connectioncb(struct evrtsp_connection *evcon, void *arg) +{ + /* This is after writing the request to the server */ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + assert(req != NULL); + + assert(evcon->state == EVCON_WRITING); + + /* We are done writing our header and are now expecting the response */ + req->kind = EVRTSP_RESPONSE; + + evrtsp_start_read(evcon); +} + + + +/* + * Clean up a connection object + */ + +void +evrtsp_connection_free(struct evrtsp_connection *evcon) +{ + struct evrtsp_request *req; + + /* notify interested parties that this connection is going down */ + if (evcon->fd != -1) { + if (evrtsp_connected(evcon) && evcon->closecb != NULL) + (*evcon->closecb)(evcon, evcon->closecb_arg); + } + + /* remove all requests that might be queued on this connection */ + while ((req = TAILQ_FIRST(&evcon->requests)) != NULL) { + TAILQ_REMOVE(&evcon->requests, req, next); + evrtsp_request_free(req); + } + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); + + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + + if (evcon->fd != -1) + EVUTIL_CLOSESOCKET(evcon->fd); + + if (evcon->bind_address != NULL) + free(evcon->bind_address); + + if (evcon->address != NULL) + free(evcon->address); + + if (evcon->input_buffer != NULL) + evbuffer_free(evcon->input_buffer); + + if (evcon->output_buffer != NULL) + evbuffer_free(evcon->output_buffer); + + free(evcon); +} + +static void +evrtsp_request_dispatch(struct evrtsp_connection* evcon) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + + /* this should not usually happy but it's possible */ + if (req == NULL) + return; + + /* delete possible close detection events */ + evrtsp_connection_stop_detectclose(evcon); + + /* we assume that the connection is connected already */ + assert(evcon->state == EVCON_IDLE); + + evcon->state = EVCON_WRITING; + + /* Create the header from the store arguments */ + evrtsp_make_header(evcon, req); + + /* forked-daapd customisation for encryption */ + if (evcon->ciphercb) + evcon->ciphercb(evcon->output_buffer, evcon->ciphercb_arg, 1); + + evrtsp_write_buffer(evcon, evrtsp_write_connectioncb, NULL); +} + +/* Reset our connection state */ +void +evrtsp_connection_reset(struct evrtsp_connection *evcon) +{ + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + + if (evcon->fd != -1) { + /* inform interested parties about connection close */ + if (evrtsp_connected(evcon) && evcon->closecb != NULL) + (*evcon->closecb)(evcon, evcon->closecb_arg); + + EVUTIL_CLOSESOCKET(evcon->fd); + evcon->fd = -1; + } + evcon->state = EVCON_DISCONNECTED; + + evbuffer_drain(evcon->input_buffer, + evbuffer_get_length(evcon->input_buffer)); + evbuffer_drain(evcon->output_buffer, + evbuffer_get_length(evcon->output_buffer)); +} + +static void +evrtsp_detect_close_cb(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + + evrtsp_connection_reset(evcon); +} + +static void +evrtsp_connection_start_detectclose(struct evrtsp_connection *evcon) +{ + evcon->flags |= EVRTSP_CON_CLOSEDETECT; + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); + event_assign(&evcon->close_ev, evcon->base, evcon->fd, EV_READ, + evrtsp_detect_close_cb, evcon); + event_add(&evcon->close_ev, NULL); +} + +static void +evrtsp_connection_stop_detectclose(struct evrtsp_connection *evcon) +{ + evcon->flags &= ~EVRTSP_CON_CLOSEDETECT; + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); +} + +/* + * Call back for asynchronous connection attempt. + */ + +static void +evrtsp_connectioncb(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + int error; + socklen_t errsz = sizeof(error); + + if (what == EV_TIMEOUT) { + event_warnx("%s: connection timeout for \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd); + goto cleanup; + } + + /* Check if the connection completed */ + if (getsockopt(evcon->fd, SOL_SOCKET, SO_ERROR, (void*)&error, + &errsz) == -1) { + event_warnx("%s: getsockopt for \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd); + goto cleanup; + } + + if (error) { + event_warnx("%s: connect failed for \"%s:%d\" on %d: %s", + __func__, evcon->address, evcon->port, evcon->fd, + strerror(error)); + goto cleanup; + } + + /* We are connected to the server now */ + event_debug(("%s: connected to \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd)); + + evcon->state = EVCON_IDLE; + + /* try to start requests that have queued up on this connection */ + evrtsp_request_dispatch(evcon); + return; + + cleanup: + evrtsp_connection_reset(evcon); + + /* for now, we just signal all requests by executing their callbacks */ + while (TAILQ_FIRST(&evcon->requests) != NULL) { + struct evrtsp_request *request = TAILQ_FIRST(&evcon->requests); + TAILQ_REMOVE(&evcon->requests, request, next); + request->evcon = NULL; + + /* we might want to set an error here */ + request->cb(request, request->cb_arg); + evrtsp_request_free(request); + } +} + +/* + * Check if we got a valid response code. + */ + +static int +evrtsp_valid_response_code(int code) +{ + if (code == 0) + return (0); + + return (1); +} + +/* Parses the status line of an RTSP server */ + +static int +evrtsp_parse_response_line(struct evrtsp_request *req, char *line) +{ + char *protocol; + char *number; + const char *readable = ""; + + protocol = strsep(&line, " "); + if (line == NULL) + return (-1); + number = strsep(&line, " "); + if (line != NULL) + readable = line; + + if (strcmp(protocol, "RTSP/1.0") == 0) { + req->major = 1; + req->minor = 0; + } else if (strcmp(protocol, "RTSP/1.1") == 0) { + req->major = 1; + req->minor = 1; + } else { + event_warnx("%s: bad protocol \"%s\"", + __func__, protocol); + return (-1); + } + + req->response_code = atoi(number); + if (!evrtsp_valid_response_code(req->response_code)) { + event_warnx("%s: bad response code \"%s\"", + __func__, number); + return (-1); + } + + if ((req->response_code_line = strdup(readable)) == NULL) + event_err(1, "%s: strdup", __func__); + + return (0); +} + +const char * +evrtsp_find_header(const struct evkeyvalq *headers, const char *key) +{ + struct evkeyval *header; + + TAILQ_FOREACH(header, headers, next) { + if (strcasecmp(header->key, key) == 0) + return (header->value); + } + + return (NULL); +} + +void +evrtsp_clear_headers(struct evkeyvalq *headers) +{ + struct evkeyval *header; + + for (header = TAILQ_FIRST(headers); + header != NULL; + header = TAILQ_FIRST(headers)) { + TAILQ_REMOVE(headers, header, next); + free(header->key); + free(header->value); + free(header); + } +} + +/* + * Returns 0, if the header was successfully removed. + * Returns -1, if the header could not be found. + */ + +int +evrtsp_remove_header(struct evkeyvalq *headers, const char *key) +{ + struct evkeyval *header; + + TAILQ_FOREACH(header, headers, next) { + if (strcasecmp(header->key, key) == 0) + break; + } + + if (header == NULL) + return (-1); + + /* Free and remove the header that we found */ + TAILQ_REMOVE(headers, header, next); + free(header->key); + free(header->value); + free(header); + + return (0); +} + +static int +evrtsp_header_is_valid_value(const char *value) +{ + const char *p = value; + + while ((p = strpbrk(p, "\r\n")) != NULL) { + /* we really expect only one new line */ + p += strspn(p, "\r\n"); + /* we expect a space or tab for continuation */ + if (*p != ' ' && *p != '\t') + return (0); + } + return (1); +} + +int +evrtsp_add_header(struct evkeyvalq *headers, + const char *key, const char *value) +{ + event_debug(("%s: key: %s val: %s", __func__, key, value)); + + if (strchr(key, '\r') != NULL || strchr(key, '\n') != NULL) { + /* drop illegal headers */ + event_warn("%s: dropping illegal header key", __func__); + return (-1); + } + + if (!evrtsp_header_is_valid_value(value)) { + event_warn("%s: dropping illegal header value", __func__); + return (-1); + } + + return (evrtsp_add_header_internal(headers, key, value)); +} + +static int +evrtsp_add_header_internal(struct evkeyvalq *headers, + const char *key, const char *value) +{ + struct evkeyval *header = calloc(1, sizeof(struct evkeyval)); + + if (header == NULL) { + event_warn("%s: calloc", __func__); + return (-1); + } + if ((header->key = strdup(key)) == NULL) { + free(header); + event_warn("%s: strdup", __func__); + return (-1); + } + if ((header->value = strdup(value)) == NULL) { + free(header->key); + free(header); + event_warn("%s: strdup", __func__); + return (-1); + } + + TAILQ_INSERT_TAIL(headers, header, next); + + return (0); +} + +/* + * Parses header lines from a request or a response into the specified + * request object given an event buffer. + * + * Returns + * DATA_CORRUPTED on error + * MORE_DATA_EXPECTED when we need to read more headers + * ALL_DATA_READ when all headers have been read. + */ + +enum message_read_status +evrtsp_parse_firstline(struct evrtsp_request *req, struct evbuffer *buffer) +{ + char *line; + enum message_read_status status = ALL_DATA_READ; + + line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_ANY); + if (line == NULL) + return (MORE_DATA_EXPECTED); + + switch (req->kind) { + case EVRTSP_RESPONSE: + if (evrtsp_parse_response_line(req, line) == -1) + status = DATA_CORRUPTED; + break; + default: + status = DATA_CORRUPTED; + } + + free(line); + return (status); +} + +static int +evrtsp_append_to_last_header(struct evkeyvalq *headers, const char *line) +{ + struct evkeyval *header = TAILQ_LAST(headers, evkeyvalq); + char *newval; + size_t old_len, line_len; + + if (header == NULL) + return (-1); + + old_len = strlen(header->value); + line_len = strlen(line); + + newval = realloc(header->value, old_len + line_len + 1); + if (newval == NULL) + return (-1); + + memcpy(newval + old_len, line, line_len + 1); + header->value = newval; + + return (0); +} + +enum message_read_status +evrtsp_parse_headers(struct evrtsp_request *req, struct evbuffer *buffer) +{ + char *line; + enum message_read_status status = MORE_DATA_EXPECTED; + + struct evkeyvalq *headers = req->input_headers; + while ((line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_CRLF)) + != NULL) { + char *skey, *svalue; + + if (*line == '\0') { /* Last header - Done */ + status = ALL_DATA_READ; + free(line); + break; + } + + /* Check if this is a continuation line */ + if (*line == ' ' || *line == '\t') { + if (evrtsp_append_to_last_header(headers, line) == -1) + goto error; + free(line); + continue; + } + + /* Processing of header lines */ + svalue = line; + skey = strsep(&svalue, ":"); + if (svalue == NULL) + goto error; + + svalue += strspn(svalue, " "); + + if (evrtsp_add_header(headers, skey, svalue) == -1) + goto error; + + free(line); + } + + return (status); + + error: + free(line); + return (DATA_CORRUPTED); +} + +static int +evrtsp_get_body_length(struct evrtsp_request *req) +{ + struct evkeyvalq *headers = req->input_headers; + const char *content_length; + + content_length = evrtsp_find_header(headers, "Content-Length"); + + if (content_length == NULL) { + /* If there is no Content-Length: header, a value of 0 is assumed, per spec. */ + req->ntoread = 0; + } else { + ev_int64_t ntoread = evutil_strtoll(content_length, NULL, 10); + if (*content_length == '\0' || ntoread < 0) { + event_warnx("%s: illegal content length: %s", + __func__, content_length); + return (-1); + } + req->ntoread = ntoread; + } + + event_debug(("%s: bytes to read: %lld (in buffer %zu)", + __func__, req->ntoread, + evbuffer_get_length(req->evcon->input_buffer))); + + return (0); +} + +static void +evrtsp_get_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + evcon->state = EVCON_READING_BODY; + if (evrtsp_get_body_length(req) == -1) { + event_warn("%s: invalid body", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } + + evrtsp_read_body(evcon, req); +} + +static void +evrtsp_read_firstline(struct evrtsp_connection *evcon, + struct evrtsp_request *req) +{ + enum message_read_status res; + + res = evrtsp_parse_firstline(req, evcon->input_buffer); + if (res == DATA_CORRUPTED) { + /* Error while reading, terminate */ + event_warnx("%s: bad header lines on %d", + __func__, evcon->fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } else if (res == MORE_DATA_EXPECTED) { + /* Need more header lines */ + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_READ_TIMEOUT); + return; + } + + evcon->state = EVCON_READING_HEADERS; + evrtsp_read_header(evcon, req); +} + +static void +evrtsp_read_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + enum message_read_status res; + int fd = evcon->fd; + + res = evrtsp_parse_headers(req, evcon->input_buffer); + if (res == DATA_CORRUPTED) { + /* Error while reading, terminate */ + event_warnx("%s: bad header lines on %d", __func__, fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } else if (res == MORE_DATA_EXPECTED) { + /* Need more header lines */ + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_READ_TIMEOUT); + return; + } + + /* Done reading headers, do the real work */ + switch (req->kind) { + case EVRTSP_RESPONSE: + event_debug(("%s: start of read body on %d", + __func__, fd)); + evrtsp_get_body(evcon, req); + break; + + default: + event_warnx("%s: bad header on %d", __func__, fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + break; + } +} + +/* + * Creates a TCP connection to the specified port and executes a callback + * when finished. Failure or sucess is indicate by the passed connection + * object. + * + * Although this interface accepts a hostname, it is intended to take + * only numeric hostnames so that non-blocking DNS resolution can + * happen elsewhere. + */ + +struct evrtsp_connection * +evrtsp_connection_new(const char *address, unsigned short port) +{ + struct evrtsp_connection *evcon = NULL; + char *intf; + char *addr; + unsigned char scratch[16]; + int family; + + if ((addr = strdup(address)) == NULL) { + event_warn("%s: strdup failed", __func__); + goto error; + } + + intf = strchr(addr, '%'); + if (intf) + *intf = '\0'; + + if (inet_pton(AF_INET6, addr, scratch) == 1) + family = AF_INET6; + else if (inet_pton(AF_INET, addr, scratch) == 1) + family = AF_INET; + else { + free(addr); + event_warn("%s: address is neither IPv6 nor IPv4", __func__); + return NULL; + } + + if (intf) + *intf = '%'; + + event_debug(("Attempting connection to %s:%d", address, port)); + + if ((evcon = calloc(1, sizeof(struct evrtsp_connection))) == NULL) { + free(addr); + event_warn("%s: calloc failed", __func__); + goto error; + } + + evcon->fd = -1; + evcon->port = port; + + evcon->timeout = -1; + + evcon->cseq = 1; + + evcon->family = family; + evcon->address = addr; + + if ((evcon->input_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new failed", __func__); + goto error; + } + + if ((evcon->output_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new failed", __func__); + goto error; + } + + evcon->state = EVCON_DISCONNECTED; + TAILQ_INIT(&evcon->requests); + + return (evcon); + + error: + if (evcon != NULL) + evrtsp_connection_free(evcon); + return (NULL); +} + +void evrtsp_connection_set_base(struct evrtsp_connection *evcon, + struct event_base *base) +{ + assert(evcon->base == NULL); + assert(evcon->state == EVCON_DISCONNECTED); + evcon->base = base; +} + +void +evrtsp_connection_set_timeout(struct evrtsp_connection *evcon, + int timeout_in_secs) +{ + evcon->timeout = timeout_in_secs; +} + +void +evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, + void (*cb)(struct evrtsp_connection *, void *), void *cbarg) +{ + evcon->closecb = cb; + evcon->closecb_arg = cbarg; +} + +void +evrtsp_connection_set_ciphercb(struct evrtsp_connection *evcon, + void (*cb)(struct evbuffer *, void *, int encrypt), void *cbarg) +{ + evcon->ciphercb = cb; + evcon->ciphercb_arg = cbarg; +} + +void +evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, + char **address, u_short *port, int *family) +{ + union { + struct sockaddr_storage ss; + struct sockaddr sa; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + } addr; + socklen_t slen; + int ret; + + *address = NULL; + *port = 0; + memset(&addr, 0, sizeof(addr)); + + if (!evrtsp_connected(evcon)) + return; + + slen = sizeof(struct sockaddr_storage); + ret = getsockname(evcon->fd, &addr.sa, &slen); + if (ret < 0) + return; + + name_from_addr(&addr.sa, slen, address, NULL); + + if (!*address) + return; + + *family = addr.sa.sa_family; + + switch (*family) + { + case AF_INET: + *port = ntohs(addr.sin.sin_port); + break; + +#ifdef AF_INET6 + case AF_INET6: + *port = ntohs(addr.sin6.sin6_port); + break; +#endif + + default: + free(*address); + *address = NULL; + + event_err(1, "%s: unhandled address family\n", __func__); + return; + } +} + +void +evrtsp_connection_get_peer(struct evrtsp_connection *evcon, + char **address, u_short *port) +{ + *address = evcon->address; + *port = evcon->port; +} + +int +evrtsp_connection_connect(struct evrtsp_connection *evcon) +{ + if (evcon->state == EVCON_CONNECTING) + return (0); + + evrtsp_connection_reset(evcon); + + evcon->fd = bind_socket(evcon->family, + evcon->bind_address, evcon->bind_port, 0 /*reuse*/); + if (evcon->fd == -1) { + event_warnx("%s: failed to bind to \"%s\"", + __func__, evcon->bind_address); + return (-1); + } + + if (socket_connect(evcon->fd, evcon->address, evcon->port) == -1) { + EVUTIL_CLOSESOCKET(evcon->fd); evcon->fd = -1; + return (-1); + } + + /* Set up a callback for successful connection setup */ + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_connectioncb, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_CONNECT_TIMEOUT); + + evcon->state = EVCON_CONNECTING; + + return (0); +} + +/* + * Starts an RTSP request on the provided evrtsp_connection object. + * If the connection object is not connected to the server already, + * this will start the connection. + */ + +int +evrtsp_make_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req, + enum evrtsp_cmd_type type, const char *uri) +{ +event_debug(("%s: TEST", __func__)); + + /* We are making a request */ + req->kind = EVRTSP_REQUEST; + req->type = type; + if (req->uri != NULL) + free(req->uri); + if ((req->uri = strdup(uri)) == NULL) + event_err(1, "%s: strdup", __func__); + + /* Set the protocol version if it is not supplied */ + if (!req->major && !req->minor) { + req->major = 1; + req->minor = 0; + } + + assert(req->evcon == NULL); + req->evcon = evcon; + assert(!(req->flags & EVRTSP_REQ_OWN_CONNECTION)); + + TAILQ_INSERT_TAIL(&evcon->requests, req, next); + + /* If the connection object is not connected; make it so */ + if (!evrtsp_connected(evcon)) + return (evrtsp_connection_connect(evcon)); + + /* + * If it's connected already and we are the first in the queue, + * then we can dispatch this request immediately. Otherwise, it + * will be dispatched once the pending requests are completed. + */ + if (TAILQ_FIRST(&evcon->requests) == req) + evrtsp_request_dispatch(evcon); + + return (0); +} + +/* + * Reads data from file descriptor into request structure + * Request structure needs to be set up correctly. + */ + +void +evrtsp_start_read(struct evrtsp_connection *evcon) +{ + /* Set up an event to read the headers */ + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); + evcon->state = EVCON_READING_FIRSTLINE; +} + +static void +evrtsp_send_done(struct evrtsp_connection *evcon, void *arg) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + TAILQ_REMOVE(&evcon->requests, req, next); + + /* delete possible close detection events */ + evrtsp_connection_stop_detectclose(evcon); + + assert(req->flags & EVRTSP_REQ_OWN_CONNECTION); + evrtsp_request_free(req); +} + +/* Requires that headers and response code are already set up */ + +static inline void +evrtsp_send(struct evrtsp_request *req, struct evbuffer *databuf) +{ + struct evrtsp_connection *evcon = req->evcon; + + if (evcon == NULL) { + evrtsp_request_free(req); + return; + } + + assert(TAILQ_FIRST(&evcon->requests) == req); + + /* xxx: not sure if we really should expose the data buffer this way */ + if (databuf != NULL) + evbuffer_add_buffer(req->output_buffer, databuf); + + /* Adds headers to the response */ + evrtsp_make_header(evcon, req); + + evrtsp_write_buffer(evcon, evrtsp_send_done, NULL); +} + +/* + * Request related functions + */ + +struct evrtsp_request * +evrtsp_request_new(void (*cb)(struct evrtsp_request *, void *), void *arg) +{ + struct evrtsp_request *req = NULL; + + /* Allocate request structure */ + if ((req = calloc(1, sizeof(struct evrtsp_request))) == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + + req->kind = EVRTSP_RESPONSE; + req->input_headers = calloc(1, sizeof(struct evkeyvalq)); + if (req->input_headers == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + TAILQ_INIT(req->input_headers); + + req->output_headers = calloc(1, sizeof(struct evkeyvalq)); + if (req->output_headers == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + TAILQ_INIT(req->output_headers); + + if ((req->input_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new", __func__); + goto error; + } + + if ((req->output_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new", __func__); + goto error; + } + + req->cb = cb; + req->cb_arg = arg; + + return (req); + + error: + if (req != NULL) + evrtsp_request_free(req); + return (NULL); +} + +void +evrtsp_request_free(struct evrtsp_request *req) +{ + if (req->uri != NULL) + free(req->uri); + if (req->response_code_line != NULL) + free(req->response_code_line); + + evrtsp_clear_headers(req->input_headers); + free(req->input_headers); + + evrtsp_clear_headers(req->output_headers); + free(req->output_headers); + + if (req->input_buffer != NULL) + evbuffer_free(req->input_buffer); + + if (req->output_buffer != NULL) + evbuffer_free(req->output_buffer); + + free(req); +} + +/* + * Allows for inspection of the request URI + */ + +const char * +evrtsp_request_uri(struct evrtsp_request *req) { + if (req->uri == NULL) + event_warn("%s: request has no uri", __func__); + return (req->uri); +} + +/* + * Network helper functions that we do not want to export to the rest of + * the world. + */ +#if 0 /* Unused */ +static struct addrinfo * +addr_from_name(char *address) +{ +#ifdef EVENT__HAVE_GETADDRINFO + struct addrinfo ai, *aitop; + int ai_result; + + memset(&ai, 0, sizeof(ai)); + ai.ai_family = AF_INET; + ai.ai_socktype = SOCK_RAW; + ai.ai_flags = 0; + if ((ai_result = getaddrinfo(address, NULL, &ai, &aitop)) != 0) { + if ( ai_result == EAI_SYSTEM ) + event_warn("getaddrinfo"); + else + event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); + } + + return (aitop); +#else + assert(0); + return NULL; /* XXXXX Use gethostbyname, if this function is ever used. */ +#endif +} +#endif + +static void +name_from_addr(struct sockaddr *sa, socklen_t salen, + char **phost, char **pport) +{ + char ntop[NI_MAXHOST]; + char strport[NI_MAXSERV]; + int ni_result; + +#ifdef EVENT__HAVE_GETNAMEINFO + ni_result = getnameinfo(sa, salen, + ntop, sizeof(ntop), strport, sizeof(strport), + NI_NUMERICHOST|NI_NUMERICSERV); + + if (ni_result != 0) { + if (ni_result == EAI_SYSTEM) + event_err(1, "getnameinfo failed"); + else + event_errx(1, "getnameinfo failed: %s", gai_strerror(ni_result)); + return; + } +#else + ni_result = fake_getnameinfo(sa, salen, + ntop, sizeof(ntop), strport, sizeof(strport), + NI_NUMERICHOST|NI_NUMERICSERV); + if (ni_result != 0) + return; +#endif + if (phost) + *phost = strdup(ntop); + if (pport) + *pport = strdup(strport); +} + +/* Create a non-blocking socket and bind it */ +/* todo: rename this function */ +static int +bind_socket_ai(int family, struct addrinfo *ai, int reuse) +{ + int fd, on = 1, r; + int serrno; + + if (ai) + family = ai->ai_family; + + /* Create listen socket */ + fd = socket(family, SOCK_STREAM, 0); + if (fd == -1) { + event_warn("socket"); + return (-1); + } + + if (evutil_make_socket_nonblocking(fd) < 0) + goto out; + +#ifndef WIN32 + if (fcntl(fd, F_SETFD, 1) == -1) { + event_warn("fcntl(F_SETFD)"); + goto out; + } +#endif + + if (family == AF_INET6) + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); + + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)); + if (reuse) { + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, + (void *)&on, sizeof(on)); + } + + if (ai != NULL) { + r = bind(fd, ai->ai_addr, ai->ai_addrlen); + if (r == -1) + goto out; + } + + return (fd); + + out: + serrno = EVUTIL_SOCKET_ERROR(); + EVUTIL_CLOSESOCKET(fd); + EVUTIL_SET_SOCKET_ERROR(serrno); + return (-1); +} + +static struct addrinfo * +make_addrinfo(const char *address, u_short port) +{ + struct addrinfo *aitop = NULL; + +#ifdef EVENT__HAVE_GETADDRINFO + struct addrinfo ai; + char strport[NI_MAXSERV]; + int ai_result; + + memset(&ai, 0, sizeof(ai)); + ai.ai_family = AF_UNSPEC; + ai.ai_socktype = SOCK_STREAM; + ai.ai_flags = AI_PASSIVE; /* turn NULL host name into INADDR_ANY */ + evutil_snprintf(strport, sizeof(strport), "%d", port); + if ((ai_result = getaddrinfo(address, strport, &ai, &aitop)) != 0) { + if ( ai_result == EAI_SYSTEM ) + event_warn("getaddrinfo"); + else + event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); + return (NULL); + } +#else + static int cur; + static struct addrinfo ai[2]; /* We will be returning the address of some of this memory so it has to last even after this call. */ + if (++cur == 2) cur = 0; /* allow calling this function twice */ + + if (fake_getaddrinfo(address, &ai[cur]) < 0) { + event_warn("fake_getaddrinfo"); + return (NULL); + } + aitop = &ai[cur]; + ((struct sockaddr_in *) aitop->ai_addr)->sin_port = htons(port); +#endif + + return (aitop); +} + +static int +bind_socket(int family, const char *address, u_short port, int reuse) +{ + int fd; + struct addrinfo *aitop = NULL; + + /* just create an unbound socket */ + if (address == NULL && port == 0) + return bind_socket_ai(family, NULL, 0); + + aitop = make_addrinfo(address, port); + + if (aitop == NULL) + return (-1); + + fd = bind_socket_ai(family, aitop, reuse); + +#ifdef EVENT__HAVE_GETADDRINFO + freeaddrinfo(aitop); +#else + fake_freeaddrinfo(aitop); +#endif + + return (fd); +} + +static int +socket_connect(int fd, const char *address, unsigned short port) +{ + struct addrinfo *ai = make_addrinfo(address, port); + int res = -1; + + if (ai == NULL) { + event_warnx("%s: make_addrinfo: \"%s:%d\"", + __func__, address, port); + return (-1); + } + + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == -1) { +#ifdef WIN32 + int tmp_error = WSAGetLastError(); + if (tmp_error != WSAEWOULDBLOCK && tmp_error != WSAEINVAL && + tmp_error != WSAEINPROGRESS) { + goto out; + } +#else + if (errno != EINPROGRESS) { + goto out; + } +#endif + } + + /* everything is fine */ + res = 0; + +out: +#ifdef EVENT__HAVE_GETADDRINFO + freeaddrinfo(ai); +#else + fake_freeaddrinfo(ai); +#endif + + return (res); +} diff --git a/pair_ap/pair_fruit.c b/pair_ap/pair_fruit.c new file mode 100644 index 00000000..42e0b066 --- /dev/null +++ b/pair_ap/pair_fruit.c @@ -0,0 +1,1063 @@ +/* + * + * The Secure Remote Password 6a implementation is adapted from: + * - Tom Cocagne + * + * + * The MIT License (MIT) + * + * 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 +#include + +#include + +#include "pair-internal.h" + +/* ----------------------------- DEFINES ETC ------------------------------- */ + +#define USERNAME "12:34:56:78:90:AB" +#define EPK_LENGTH 32 +#define AUTHTAG_LENGTH 16 +#define AES_SETUP_KEY "Pair-Setup-AES-Key" +#define AES_SETUP_IV "Pair-Setup-AES-IV" +#define AES_VERIFY_KEY "Pair-Verify-AES-Key" +#define AES_VERIFY_IV "Pair-Verify-AES-IV" + + +/* ---------------------------------- SRP ---------------------------------- */ + +typedef enum +{ + SRP_NG_2048, + SRP_NG_CUSTOM +} SRP_NGType; + +typedef struct +{ + bnum N; + bnum g; +} NGConstant; + +struct SRPUser +{ + enum hash_alg alg; + NGConstant *ng; + + bnum a; + bnum A; + bnum S; + + const unsigned char *bytes_A; + int authenticated; + + char *username; + unsigned char *password; + int password_len; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [2 * SHA512_DIGEST_LENGTH]; // See hash_session_key() + int session_key_len; +}; + +struct NGHex +{ + const char *n_hex; + const char *g_hex; +}; + +// We only need 2048 right now, but keep the array in case we want to add others later +// All constants here were pulled from Appendix A of RFC 5054 +static struct NGHex global_Ng_constants[] = +{ + { /* 2048 */ + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + {0,0} /* null sentinel */ +}; + + +static NGConstant * +new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) +{ + NGConstant *ng = calloc(1, sizeof(NGConstant)); + + if ( ng_type != SRP_NG_CUSTOM ) + { + n_hex = global_Ng_constants[ ng_type ].n_hex; + g_hex = global_Ng_constants[ ng_type ].g_hex; + } + + bnum_hex2bn(ng->N, n_hex); + bnum_hex2bn(ng->g, g_hex); + + return ng; +} + +static void +free_ng(NGConstant * ng) +{ + if (!ng) + return; + + bnum_free(ng->N); + bnum_free(ng->g); + free(ng); +} + +static bnum +calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) +{ + unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; + HashCTX ctx; + + hash_init( alg, &ctx ); + hash_update( alg, &ctx, username, strlen(username) ); + hash_update( alg, &ctx, ":", 1 ); + hash_update( alg, &ctx, password, password_len ); + hash_final( alg, &ctx, ucp_hash ); + + return H_ns( alg, salt, ucp_hash, hash_length(alg) ); +} + +static int +hash_session_key(enum hash_alg alg, const bnum n, unsigned char *dest) +{ + int nbytes = bnum_num_bytes(n); + unsigned char *bin = malloc(nbytes); + unsigned char fourbytes[4] = { 0 }; // Only God knows the reason for this, and perhaps some poor soul at Apple + + bnum_bn2bin(n, bin, nbytes); + + hash_ab(alg, dest, bin, nbytes, fourbytes, sizeof(fourbytes)); + + fourbytes[3] = 1; // Again, only ... + + hash_ab(alg, dest + hash_length(alg), bin, nbytes, fourbytes, sizeof(fourbytes)); + + free(bin); + + return (2 * hash_length(alg)); +} + +static void +calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char *I, const bnum s, + const bnum A, const bnum B, const unsigned char *K, int K_len) +{ + unsigned char H_N[ SHA512_DIGEST_LENGTH ]; + unsigned char H_g[ SHA512_DIGEST_LENGTH ]; + unsigned char H_I[ SHA512_DIGEST_LENGTH ]; + unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; + HashCTX ctx; + int i = 0; + int hash_len = hash_length(alg); + + hash_num( alg, ng->N, H_N ); + hash_num( alg, ng->g, H_g ); + + hash(alg, (const unsigned char *)I, strlen(I), H_I); + + for (i=0; i < hash_len; i++ ) + H_xor[i] = H_N[i] ^ H_g[i]; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, H_xor, hash_len ); + hash_update( alg, &ctx, H_I, hash_len ); + update_hash_n( alg, &ctx, s ); + update_hash_n( alg, &ctx, A ); + update_hash_n( alg, &ctx, B ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static void +calculate_H_AMK(enum hash_alg alg, unsigned char *dest, const bnum A, const unsigned char * M, const unsigned char * K, int K_len) +{ + HashCTX ctx; + + hash_init( alg, &ctx ); + + update_hash_n( alg, &ctx, A ); + hash_update( alg, &ctx, M, hash_length(alg) ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static struct SRPUser * +srp_user_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, + const char *n_hex, const char *g_hex) +{ + struct SRPUser *usr = calloc(1, sizeof(struct SRPUser)); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + usr->alg = alg; + usr->ng = new_ng( ng_type, n_hex, g_hex ); + + bnum_new(usr->a); + bnum_new(usr->A); + bnum_new(usr->S); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = malloc(ulen); + usr->password = malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy(usr->username, username, ulen); + memcpy(usr->password, bytes_password, len_password); + + usr->authenticated = 0; + usr->bytes_A = 0; + + return usr; + + err_exit: + if (!usr) + return NULL; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free(usr->username); + if (usr->password) + { + memset(usr->password, 0, usr->password_len); + free(usr->password); + } + free(usr); + + return NULL; +} + +static void +srp_user_free(struct SRPUser *usr) +{ + if(!usr) + return; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free_ng(usr->ng); + + memset(usr->password, 0, usr->password_len); + + free(usr->username); + free(usr->password); + free((char *)usr->bytes_A); + + memset(usr, 0, sizeof(*usr)); + free(usr); +} + +static int +srp_user_is_authenticated(struct SRPUser *usr) +{ + return usr->authenticated; +} + +static const unsigned char * +srp_user_get_session_key(struct SRPUser *usr, int *key_length) +{ + if (key_length) + *key_length = usr->session_key_len; + return usr->session_key; +} + +/* Output: username, bytes_A, len_A */ +static void +srp_user_start_authentication(struct SRPUser *usr, const char **username, + const unsigned char **bytes_A, int *len_A) +{ + bnum_random(usr->a, 256); + bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); + + *len_A = bnum_num_bytes(usr->A); + *bytes_A = malloc(*len_A); + + if (!*bytes_A) + { + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + bnum_bn2bin(usr->A, (unsigned char *) *bytes_A, *len_A); + + usr->bytes_A = *bytes_A; + *username = usr->username; +} + +/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ +static void +srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, + const unsigned char **bytes_M, int *len_M ) +{ + bnum s, B, k, v; + bnum tmp1, tmp2, tmp3; + bnum u, x; + + *len_M = 0; + *bytes_M = 0; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(B, bytes_B, len_B); + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + bnum_new(v); + bnum_new(tmp1); + bnum_new(tmp2); + bnum_new(tmp3); + + if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) + goto cleanup1; + + u = H_nn_pad(usr->alg, usr->A, B); + x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); + if (!u || !x) + goto cleanup2; + + // SRP-6a safety check + if (!bnum_is_zero(B) && !bnum_is_zero(u)) + { + bnum_modexp(v, usr->ng->g, x, usr->ng->N); + + // S = (B - k*(g^x)) ^ (a + ux) + bnum_mul(tmp1, u, x); + bnum_add(tmp2, usr->a, tmp1); // tmp2 = (a + ux) + bnum_modexp(tmp1, usr->ng->g, x, usr->ng->N); + bnum_mul(tmp3, k, tmp1); // tmp3 = k*(g^x) + bnum_sub(tmp1, B, tmp3); // tmp1 = (B - K*(g^x)) + bnum_modexp(usr->S, tmp1, tmp2, usr->ng->N); + + usr->session_key_len = hash_session_key(usr->alg, usr->S, usr->session_key); + + calculate_M(usr->alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key, usr->session_key_len); + calculate_H_AMK(usr->alg, usr->H_AMK, usr->A, usr->M, usr->session_key, usr->session_key_len); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length(usr->alg); + } + else + { + *bytes_M = NULL; + if (len_M) + *len_M = 0; + } + + cleanup2: + bnum_free(x); + bnum_free(u); + cleanup1: + bnum_free(tmp3); + bnum_free(tmp2); + bnum_free(tmp1); + bnum_free(v); + bnum_free(k); + bnum_free(B); + bnum_free(s); +} + +static void +srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) +{ + if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->alg)) == 0) + usr->authenticated = 1; +} + + +/* -------------------------------- HELPERS -------------------------------- */ + +static int +encrypt_gcm(unsigned char *ciphertext, int ciphertext_len, unsigned char *tag, unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, const char **errmsg) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + *errmsg = NULL; + + if ( !(ctx = EVP_CIPHER_CTX_new()) || + (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) || + (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) || + (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1) ) + { + *errmsg = "Error initialising AES 128 GCM encryption"; + goto error; + } + + if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len) != 1) + { + *errmsg = "Error GCM encrypting"; + goto error; + } + + if (len > ciphertext_len) + { + *errmsg = "Bug! Buffer overflow"; + goto error; + } + + if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) + { + *errmsg = "Error finalising GCM encryption"; + goto error; + } + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AUTHTAG_LENGTH, tag) != 1) + { + *errmsg = "Error getting authtag"; + goto error; + } + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + gcry_error_t err; + + err = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, 0); + if (err) + { + *errmsg = "Error initialising AES 128 GCM encryption"; + return -1; + } + + err = gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set key for AES 128 GCM"; + goto error; + } + + err = gcry_cipher_setiv(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set iv for AES 128 GCM"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext, plaintext_len); + if (err) + { + *errmsg = "Error GCM encrypting"; + goto error; + } + + err = gcry_cipher_gettag(hd, tag, AUTHTAG_LENGTH); + if (err) + { + *errmsg = "Error getting authtag"; + goto error; + } + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#endif +} + +static int +encrypt_ctr(unsigned char *ciphertext, int ciphertext_len, + unsigned char *plaintext1, int plaintext1_len, unsigned char *plaintext2, int plaintext2_len, + unsigned char *key, unsigned char *iv, const char **errmsg) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + *errmsg = NULL; + + if ( !(ctx = EVP_CIPHER_CTX_new()) || (EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), NULL, key, iv) != 1) ) + { + *errmsg = "Error initialising AES 128 CTR encryption"; + goto error; + } + + if ( (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext1, plaintext1_len) != 1) || + (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext2, plaintext2_len) != 1) ) + { + *errmsg = "Error CTR encrypting"; + goto error; + } + + if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) + { + *errmsg = "Error finalising encryption"; + goto error; + } + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + gcry_error_t err; + + err = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0); + if (err) + { + *errmsg = "Error initialising AES 128 CTR encryption"; + return -1; + } + err = gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set key for AES 128 CTR"; + goto error; + } + + err = gcry_cipher_setctr(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set iv for AES 128 CTR"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext1, plaintext1_len); + if (err) + { + *errmsg = "Error CTR encrypting plaintext 1"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext2, plaintext2_len); + if (err) + { + *errmsg = "Error CTR encrypting plaintext 2"; + goto error; + } + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#endif +} + + +/* -------------------------- IMPLEMENTATION -------------------------------- */ + +static int +client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + if (sodium_init() == -1) + return -1; + + if (!pin || strlen(pin) < 4) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + return 0; +} + +static void +client_setup_free(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + srp_user_free(sctx->user); + + free(sctx->pkB); + free(sctx->M2); + free(sctx->salt); + free(sctx->epk); + free(sctx->authtag); +} + +static uint8_t * +client_setup_request1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t method; + plist_t user; + uint32_t uint32; + char *data = NULL; // Necessary to initialize because plist_to_bin() uses value + + sctx->user = srp_user_new(HASH_SHA1, SRP_NG_2048, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), 0, 0); + + dict = plist_new_dict(); + + method = plist_new_string("pin"); + user = plist_new_string(USERNAME); + + plist_dict_set_item(dict, "method", method); + plist_dict_set_item(dict, "user", user); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static uint8_t * +client_setup_request2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t pk; + plist_t proof; + const char *auth_username = NULL; + uint32_t uint32; + char *data = NULL; + + // Calculate A + srp_user_start_authentication(sctx->user, &auth_username, &sctx->pkA, &sctx->pkA_len); + + // Calculate M1 (client proof) + srp_user_process_challenge(sctx->user, (const unsigned char *)sctx->salt, sctx->salt_len, (const unsigned char *)sctx->pkB, sctx->pkB_len, &sctx->M1, &sctx->M1_len); + + pk = plist_new_data((char *)sctx->pkA, sctx->pkA_len); + proof = plist_new_data((char *)sctx->M1, sctx->M1_len); + + dict = plist_new_dict(); + plist_dict_set_item(dict, "pk", pk); + plist_dict_set_item(dict, "proof", proof); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static uint8_t * +client_setup_request3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t epk; + plist_t authtag; + uint32_t uint32; + char *data = NULL; + const unsigned char *session_key; + int session_key_len; + unsigned char key[SHA512_DIGEST_LENGTH]; + unsigned char iv[SHA512_DIGEST_LENGTH]; + unsigned char encrypted[128]; // Alloc a bit extra - should only need 2*16 + unsigned char tag[16]; + const char *errmsg; + int ret; + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup request 3: No valid session key"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_SETUP_KEY, strlen(AES_SETUP_KEY), session_key, session_key_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Hashing of key string and shared secret failed"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_SETUP_IV, strlen(AES_SETUP_IV), session_key, session_key_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Hashing of iv string and shared secret failed"; + return NULL; + } + + iv[15]++; // Nonce? +/* + if (iv[15] == 0x00 || iv[15] == 0xff) + printf("- note that value of last byte is %d!\n", iv[15]); +*/ + crypto_sign_keypair(sctx->public_key, sctx->private_key); + + ret = encrypt_gcm(encrypted, sizeof(encrypted), tag, sctx->public_key, sizeof(sctx->public_key), key, iv, &errmsg); + if (ret < 0) + { + handle->errmsg = errmsg; + return NULL; + } + + epk = plist_new_data((char *)encrypted, EPK_LENGTH); + authtag = plist_new_data((char *)tag, AUTHTAG_LENGTH); + + dict = plist_new_dict(); + plist_dict_set_item(dict, "epk", epk); + plist_dict_set_item(dict, "authTag", authtag); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static int +client_setup_response1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t pk; + plist_t salt; + + plist_from_bin((const char *)data, data_len, &dict); + + pk = plist_dict_get_item(dict, "pk"); + salt = plist_dict_get_item(dict, "salt"); + if (!pk || !salt) + { + handle->errmsg = "Setup response 1: Missing pk or salt"; + plist_free(dict); + return -1; + } + + plist_get_data_val(pk, (char **)&sctx->pkB, &sctx->pkB_len); // B + plist_get_data_val(salt, (char **)&sctx->salt, &sctx->salt_len); + + plist_free(dict); + + return 0; +} + +static int +client_setup_response2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t proof; + + plist_from_bin((const char *)data, data_len, &dict); + + proof = plist_dict_get_item(dict, "proof"); + if (!proof) + { + handle->errmsg = "Setup response 2: Missing proof"; + plist_free(dict); + return -1; + } + + plist_get_data_val(proof, (char **)&sctx->M2, &sctx->M2_len); // M2 + + plist_free(dict); + + // Check M2 + srp_user_verify_session(sctx->user, (const unsigned char *)sctx->M2); + if (!srp_user_is_authenticated(sctx->user)) + { + handle->errmsg = "Setup response 2: Server authentication failed"; + return -1; + } + + return 0; +} + +static int +client_setup_response3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t epk; + plist_t authtag; + + plist_from_bin((const char *)data, data_len, &dict); + + epk = plist_dict_get_item(dict, "epk"); + if (!epk) + { + handle->errmsg = "Setup response 3: Missing epk"; + plist_free(dict); + return -1; + } + + plist_get_data_val(epk, (char **)&sctx->epk, &sctx->epk_len); + + authtag = plist_dict_get_item(dict, "authTag"); + if (!authtag) + { + handle->errmsg = "Setup response 3: Missing authTag"; + plist_free(dict); + return -1; + } + + plist_get_data_val(authtag, (char **)&sctx->authtag, &sctx->authtag_len); + + plist_free(dict); + + assert(sizeof(handle->result.client_private_key) == sizeof(sctx->private_key)); + assert(sizeof(handle->result.client_public_key) == sizeof(sctx->public_key)); + + memcpy(handle->result.client_private_key, sctx->private_key, sizeof(sctx->private_key)); + memcpy(handle->result.client_public_key, sctx->public_key, sizeof(sctx->public_key)); + + handle->status = PAIR_STATUS_COMPLETED; + return 0; +} + +static int +client_setup_result(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + char *ptr; + int i; + + // Last 32 bytes of the private key is the public key, so we don't need to + // explicitly export that + ptr = handle->result_str; + for (i = 0; i < sizeof(sctx->private_key); i++) + ptr += sprintf(ptr, "%02x", sctx->private_key[i]); // 2 x 64 bytes + *ptr = '\0'; + + return 0; +} + + +static int +client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + char hex[] = { 0, 0, 0 }; + size_t hexkey_len; + const char *ptr; + int i; + + if (sodium_init() == -1) + return -1; + + if (!client_setup_keys) + return -1; + + hexkey_len = strlen(client_setup_keys); + + if (hexkey_len != 2 * sizeof(vctx->client_private_key)) + return -1; + + if (device_id && strlen(device_id) != 16) + return -1; + + if (device_id) + memcpy(vctx->device_id, device_id, strlen(device_id)); + + ptr = client_setup_keys; + for (i = 0; i < sizeof(vctx->client_private_key); i++, ptr+=2) + { + hex[0] = ptr[0]; + hex[1] = ptr[1]; + vctx->client_private_key[i] = strtol(hex, NULL, 16); + } + + crypto_sign_ed25519_sk_to_pk(vctx->client_public_key, vctx->client_private_key); + + return 0; +} + +static uint8_t * +client_verify_request1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + const uint8_t basepoint[32] = {9}; + uint8_t *data; + int ret; + + ret = crypto_scalarmult(vctx->client_eph_public_key, vctx->client_eph_private_key, basepoint); + if (ret < 0) + { + handle->errmsg = "Verify request 1: Curve 25519 returned an error"; + return NULL; + } + + *len = 4 + sizeof(vctx->client_eph_public_key) + sizeof(vctx->client_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 1: Out of memory"; + return NULL; + } + + data[0] = 1; // Magic + memcpy(data + 4, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + memcpy(data + 4 + sizeof(vctx->client_eph_public_key), vctx->client_public_key, sizeof(vctx->client_public_key)); + + return data; +} + +static uint8_t * +client_verify_request2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + uint8_t shared_secret[crypto_scalarmult_BYTES]; + uint8_t key[SHA512_DIGEST_LENGTH]; + uint8_t iv[SHA512_DIGEST_LENGTH]; + uint8_t encrypted[128]; // Alloc a bit extra, should only really need size of public key len + uint8_t signature[crypto_sign_BYTES]; + uint8_t *data; + int ret; + const char *errmsg; + + *len = sizeof(vctx->client_eph_public_key) + sizeof(vctx->server_eph_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 2: Out of memory"; + return NULL; + } + + memcpy(data, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + memcpy(data + sizeof(vctx->client_eph_public_key), vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + + crypto_sign_detached(signature, NULL, data, *len, vctx->client_private_key); + + free(data); + + ret = crypto_scalarmult(shared_secret, vctx->client_eph_private_key, vctx->server_eph_public_key); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Curve 25519 returned an error"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_VERIFY_KEY, strlen(AES_VERIFY_KEY), shared_secret, sizeof(shared_secret)); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Hashing of key string and shared secret failed"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_VERIFY_IV, strlen(AES_VERIFY_IV), shared_secret, sizeof(shared_secret)); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Hashing of iv string and shared secret failed"; + return NULL; + } + + ret = encrypt_ctr(encrypted, sizeof(encrypted), vctx->server_fruit_public_key, sizeof(vctx->server_fruit_public_key), signature, sizeof(signature), key, iv, &errmsg); + if (ret < 0) + { + handle->errmsg = errmsg; + return NULL; + } + + *len = 4 + sizeof(vctx->server_fruit_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 2: Out of memory"; + return NULL; + } + + memcpy(data + 4, encrypted, sizeof(vctx->server_fruit_public_key)); + + return data; +} + +static int +client_verify_response1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + size_t wanted; + + wanted = sizeof(vctx->server_eph_public_key) + sizeof(vctx->server_fruit_public_key); + if (data_len < wanted) + { + handle->errmsg = "Verify response 2: Unexpected response (too short)"; + return -1; + } + + memcpy(vctx->server_eph_public_key, data, sizeof(vctx->server_eph_public_key)); + memcpy(vctx->server_fruit_public_key, data + sizeof(vctx->server_eph_public_key), sizeof(vctx->server_fruit_public_key)); + + return 0; +} + +static int +client_verify_response2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + // TODO actually check response + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + return 0; +} + + +struct pair_definition pair_client_fruit = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + .pair_setup_result = client_setup_result, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, +}; diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c new file mode 100644 index 00000000..076ea14a --- /dev/null +++ b/pair_ap/pair_homekit.c @@ -0,0 +1,3123 @@ +/* + * Adaption of ap2-sender by ViktoriiaKh: + * + * + * To test, it is useful to try with this receiver: + * + * + * The Secure Remote Password 6a implementation is adapted from: + * - Tom Cocagne + * + * + * TLV helpers are adapted from ESP homekit: + * + * + * The MIT License (MIT) + * + * 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 + +#include + +#include + +#include "pair-internal.h" +#include "pair-tlv.h" + + +/* ----------------------------- DEFINES ETC ------------------------------- */ + +#define USERNAME "Pair-Setup" +#define AUTHTAG_LENGTH 16 +#define NONCE_LENGTH 12 // 96 bits according to chacha poly1305 +#define REQUEST_BUFSIZE 4096 +#define ENCRYPTED_LEN_MAX 0x400 + +enum pair_keys +{ + PAIR_SETUP_MSG01 = 0, + PAIR_SETUP_MSG02, + PAIR_SETUP_MSG03, + PAIR_SETUP_MSG04, + PAIR_SETUP_MSG05, + PAIR_SETUP_MSG06, + PAIR_SETUP_CONTROLLER_SIGN, + PAIR_SETUP_ACCESSORY_SIGN, + PAIR_VERIFY_MSG01, + PAIR_VERIFY_MSG02, + PAIR_VERIFY_MSG03, + PAIR_VERIFY_MSG04, + PAIR_CONTROL_WRITE, + PAIR_CONTROL_READ, + PAIR_EVENTS_WRITE, + PAIR_EVENTS_READ, +}; + +struct pair_keys_map +{ + uint8_t state; + const char *salt; + const char *info; + const char nonce[8]; +}; + +static struct pair_keys_map pair_keys_map[] = +{ + // Used for /pair-setup + { 0x01, NULL, NULL, "" }, + { 0x02, NULL, NULL, "" }, + { 0x03, NULL, NULL, "" }, + { 0x04, NULL, NULL, "" }, + { 0x05, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg05" }, + { 0x06, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg06" }, + { 0, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", "" }, + { 0, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", "" }, + + // Used for /pair-verify + { 0x01, NULL, NULL, "" }, + { 0x02, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg02" }, + { 0x03, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg03" }, + { 0x04, NULL, NULL, "" }, + + // Encryption/decryption of control channel + { 0, "Control-Salt", "Control-Write-Encryption-Key", "" }, + { 0, "Control-Salt", "Control-Read-Encryption-Key", "" }, + + // Encryption/decryption of event channel + { 0, "Events-Salt", "Events-Write-Encryption-Key", "" }, + { 0, "Events-Salt", "Events-Read-Encryption-Key", "" }, +}; + +enum pair_method { + PairingMethodPairSetup = 0x00, + PairingMethodPairSetupWithAuth = 0x01, + PairingMethodPairVerify = 0x02, + PairingMethodAddPairing = 0x03, + PairingMethodRemovePairing = 0x04, + PairingMethodListPairings = 0x05 +}; + +enum pair_flags { + PairingFlagsTransient = 0x10, +}; + +// Forwards +const struct pair_definition pair_client_homekit_normal; +const struct pair_definition pair_client_homekit_transient; +const struct pair_definition pair_server_homekit; + + +/* ---------------------------------- SRP ----------------------------------- */ + +typedef enum +{ + SRP_NG_2048, + SRP_NG_3072, + SRP_NG_CUSTOM +} SRP_NGType; + +typedef struct +{ + bnum N; + bnum g; +} NGConstant; + +struct SRPUser +{ + enum hash_alg alg; + NGConstant *ng; + + bnum a; + bnum A; + bnum S; + + const unsigned char *bytes_A; + int authenticated; + + char *username; + unsigned char *password; + int password_len; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; + int session_key_len; +}; + +struct SRPVerifier +{ + enum hash_alg alg; + NGConstant *ng; + + const unsigned char *bytes_B; + int authenticated; + + char *username; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; + int session_key_len; +}; + +struct NGHex +{ + const char *n_hex; + const char *g_hex; +}; + +// These constants here were pulled from Appendix A of RFC 5054 +static struct NGHex global_Ng_constants[] = +{ + { /* 2048 */ + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + { /* 3072 */ + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" + "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" + "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" + "E649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23" + "DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32" + "905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558" + "17183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521" + "ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D7" + "1E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B1817" + "7B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82" + "D120A93AD2CAFFFFFFFFFFFFFFFF", + "5" + }, + {0,0} /* null sentinel */ +}; + + +static NGConstant * +new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) +{ + NGConstant *ng = calloc(1, sizeof(NGConstant)); + + if ( ng_type != SRP_NG_CUSTOM ) + { + n_hex = global_Ng_constants[ ng_type ].n_hex; + g_hex = global_Ng_constants[ ng_type ].g_hex; + } + + bnum_hex2bn(ng->N, n_hex); + bnum_hex2bn(ng->g, g_hex); + + return ng; +} + +static void +free_ng(NGConstant * ng) +{ + if (!ng) + return; + + bnum_free(ng->N); + bnum_free(ng->g); + free(ng); +} + +static bnum +calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) +{ + unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; + HashCTX ctx; + + hash_init( alg, &ctx ); + hash_update( alg, &ctx, username, strlen(username) ); + hash_update( alg, &ctx, ":", 1 ); + hash_update( alg, &ctx, password, password_len ); + hash_final( alg, &ctx, ucp_hash ); + + return H_ns( alg, salt, ucp_hash, hash_length(alg) ); +} + +static void +calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char *I, const bnum s, + const bnum A, const bnum B, const unsigned char *K, int K_len) +{ + unsigned char H_N[ SHA512_DIGEST_LENGTH ]; + unsigned char H_g[ SHA512_DIGEST_LENGTH ]; + unsigned char H_I[ SHA512_DIGEST_LENGTH ]; + unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; + HashCTX ctx; + int i = 0; + int hash_len = hash_length(alg); + + hash_num( alg, ng->N, H_N ); + hash_num( alg, ng->g, H_g ); + + hash(alg, (const unsigned char *)I, strlen(I), H_I); + + for (i=0; i < hash_len; i++ ) + H_xor[i] = H_N[i] ^ H_g[i]; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, H_xor, hash_len ); + hash_update( alg, &ctx, H_I, hash_len ); + update_hash_n( alg, &ctx, s ); + update_hash_n( alg, &ctx, A ); + update_hash_n( alg, &ctx, B ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static void +calculate_H_AMK(enum hash_alg alg, unsigned char *dest, const bnum A, const unsigned char * M, const unsigned char * K, int K_len) +{ + HashCTX ctx; + + hash_init( alg, &ctx ); + + update_hash_n( alg, &ctx, A ); + hash_update( alg, &ctx, M, hash_length(alg) ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +/* ----------------------- SRP for the client side -------------------------- */ + +static struct SRPUser * +srp_user_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, + const char *n_hex, const char *g_hex) +{ + struct SRPUser *usr = calloc(1, sizeof(struct SRPUser)); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + usr->alg = alg; + usr->ng = new_ng( ng_type, n_hex, g_hex ); + + bnum_new(usr->a); + bnum_new(usr->A); + bnum_new(usr->S); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = malloc(ulen); + usr->password = malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy(usr->username, username, ulen); + memcpy(usr->password, bytes_password, len_password); + + usr->authenticated = 0; + usr->bytes_A = 0; + + return usr; + + err_exit: + if (!usr) + return NULL; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free(usr->username); + if (usr->password) + { + memset(usr->password, 0, usr->password_len); + free(usr->password); + } + free(usr); + + return NULL; +} + +static void +srp_user_free(struct SRPUser *usr) +{ + if(!usr) + return; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free_ng(usr->ng); + + memset(usr->password, 0, usr->password_len); + + free(usr->username); + free(usr->password); + free((char *)usr->bytes_A); + + memset(usr, 0, sizeof(*usr)); + free(usr); +} + +static int +srp_user_is_authenticated(struct SRPUser *usr) +{ + return usr->authenticated; +} + +static const unsigned char * +srp_user_get_session_key(struct SRPUser *usr, int *key_length) +{ + if (key_length) + *key_length = usr->session_key_len; + return usr->session_key; +} + +/* Output: username, bytes_A, len_A */ +static void +srp_user_start_authentication(struct SRPUser *usr, const char **username, + const unsigned char **bytes_A, int *len_A) +{ + bnum_random(usr->a, 256); +// BN_hex2bn(&(usr->a), "D929DFB605687233C9E9030C2280156D03BDB9FDCF3CCE3BC27D9CCFCB5FF6A1"); + + bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); + + *len_A = bnum_num_bytes(usr->A); + *bytes_A = malloc(*len_A); + + if (!*bytes_A) + { + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + bnum_bn2bin(usr->A, (unsigned char *) *bytes_A, *len_A); + + usr->bytes_A = *bytes_A; + *username = usr->username; +} + +/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ +static void +srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, + const unsigned char **bytes_M, int *len_M ) +{ + bnum s, B, k, v; + bnum tmp1, tmp2, tmp3; + bnum u, x; + + *len_M = 0; + *bytes_M = 0; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(B, bytes_B, len_B); + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + + bnum_new(v); + bnum_new(tmp1); + bnum_new(tmp2); + bnum_new(tmp3); + + if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) + goto cleanup1; + + u = H_nn_pad(usr->alg, usr->A, B); + x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); + if (!u || !x) + goto cleanup2; + + // SRP-6a safety check + if (!bnum_is_zero(B) && !bnum_is_zero(u)) + { + bnum_modexp(v, usr->ng->g, x, usr->ng->N); + + // S = (B - k*(g^x)) ^ (a + ux) + bnum_mul(tmp1, u, x); + bnum_add(tmp2, usr->a, tmp1); // tmp2 = (a + ux) + bnum_modexp(tmp1, usr->ng->g, x, usr->ng->N); + bnum_mul(tmp3, k, tmp1); // tmp3 = k*(g^x) + bnum_sub(tmp1, B, tmp3); // tmp1 = (B - K*(g^x)) + bnum_modexp(usr->S, tmp1, tmp2, usr->ng->N); + + hash_num(usr->alg, usr->S, usr->session_key); + usr->session_key_len = hash_length(usr->alg); + + calculate_M(usr->alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key, usr->session_key_len); + calculate_H_AMK(usr->alg, usr->H_AMK, usr->A, usr->M, usr->session_key, usr->session_key_len); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length(usr->alg); + } + else + { + *bytes_M = NULL; + if (len_M) + *len_M = 0; + } + + cleanup2: + bnum_free(x); + bnum_free(u); + cleanup1: + bnum_free(tmp3); + bnum_free(tmp2); + bnum_free(tmp1); + bnum_free(v); + bnum_free(k); + bnum_free(B); + bnum_free(s); +} + +static void +srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) +{ + if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->alg)) == 0) + usr->authenticated = 1; +} + + +/* ----------------------- SRP for the server side -------------------------- */ + +static int +srp_create_salted_verification_key(enum hash_alg alg, + SRP_NGType ng_type, const char *username, + const unsigned char *password, int len_password, + unsigned char **bytes_s, int *len_s, + unsigned char **bytes_v, int *len_v, + const char *n_hex, const char *g_hex ) +{ + bnum s, v, x; + NGConstant *ng; + + bnum_new(s); + bnum_new(v); + x = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + *bytes_s = NULL; + *bytes_v = NULL; + + if (!s || !v || !ng) + goto error; + + bnum_random(s, 128); // MODIFIED from csrp's BN_rand(s, 32, -1, 0) + + x = calculate_x(alg, s, username, password, len_password); + if (!x) + goto error; + + bnum_modexp(v, ng->g, x, ng->N); + + *len_s = bnum_num_bytes(s); + *len_v = bnum_num_bytes(v); + + *bytes_s = malloc(*len_s); + *bytes_v = malloc(*len_v); + if (!(*bytes_s) || !(*bytes_v)) + goto error; + + bnum_bn2bin(s, (unsigned char *) *bytes_s, *len_s); + bnum_bn2bin(v, (unsigned char *) *bytes_v, *len_v); + + free_ng(ng); + bnum_free(s); + bnum_free(v); + bnum_free(x); + return 0; + + error: + free(*bytes_s); + free(*bytes_v); + free_ng(ng); + bnum_free(s); + bnum_free(v); + bnum_free(x); + return -1; +} + +static int +srp_verifier_start_authentication(enum hash_alg alg, SRP_NGType ng_type, + const unsigned char *bytes_v, int len_v, + unsigned char **bytes_b, int *len_b, + unsigned char **bytes_B, int *len_B, + const char *n_hex, const char *g_hex) +{ + bnum v, k, b, B, tmp1, tmp2; + NGConstant *ng; + + v = NULL; + k = NULL; + bnum_new(b); + bnum_new(B); + bnum_new(tmp1); + bnum_new(tmp2); + + *len_b = 0; + *bytes_b = NULL; + *len_B = 0; + *bytes_B = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + if (!b || !B || !tmp1 || !tmp2 || !ng) + goto error; + + bnum_bin2bn(v, bytes_v, len_v); + + bnum_random(b, 256); // MODIFIED from BN_rand(b, 256, -1, 0) + + k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) + if (!k) + goto error; + + // B = kv + g^b + bnum_mul(tmp1, k, v); + bnum_modexp(tmp2, ng->g, b, ng->N); + bnum_modadd(B, tmp1, tmp2, ng->N); + + *len_B = bnum_num_bytes(B); + *len_b = bnum_num_bytes(b); + + *bytes_B = malloc(*len_B); + *bytes_b = malloc(*len_b); + if (!(*bytes_B) || !(*bytes_b)) + goto error; + + bnum_bn2bin(B, (unsigned char *) *bytes_B, *len_B); + bnum_bn2bin(b, (unsigned char *) *bytes_b, *len_b); + + bnum_free(b); + bnum_free(B); + bnum_free(v); + bnum_free(k); + bnum_free(tmp1); + bnum_free(tmp2); + free_ng(ng); + return 0; + + error: + free(*bytes_B); + free(*bytes_b); + bnum_free(b); + bnum_free(B); + bnum_free(v); + bnum_free(k); + bnum_free(tmp1); + bnum_free(tmp2); + free_ng(ng); + return -1; +} + +static void +srp_verifier_free(struct SRPVerifier *ver) +{ + if (!ver) + return; + + free_ng(ver->ng); + + free(ver->username); + + memset(ver, 0, sizeof(struct SRPVerifier)); + free(ver); +} + +static struct SRPVerifier * +srp_verifier_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_v, int len_v, + const unsigned char *bytes_A, int len_A, + const unsigned char *bytes_b, int len_b, + const unsigned char *bytes_B, int len_B, + const char *n_hex, const char *g_hex ) +{ + struct SRPVerifier *ver = NULL; + bnum s, v, A, b, B, S, tmp1, tmp2, u, k; + NGConstant *ng; + size_t ulen; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(v, bytes_v, len_v); + bnum_bin2bn(A, bytes_A, len_A); + bnum_bin2bn(b, bytes_b, len_b); + bnum_bin2bn(B, bytes_B, len_B); + bnum_new(S); + bnum_new(tmp1); + bnum_new(tmp2); + u = NULL; + k = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + if (!s || !v || !A || !B || !S || !b || !tmp1 || !tmp2 || !ng) + goto error; + + ver = calloc(1, sizeof(struct SRPVerifier)); + if (!ver) + goto error; + + ulen = strlen(username) + 1; + + ver->alg = alg; + ver->ng = ng; + + ver->username = malloc(ulen); + if (!ver->username) + goto error; + + memcpy(ver->username, username, ulen); + + ver->authenticated = 0; + + // SRP-6a safety check + bnum_mod(tmp1, A, ng->N); + if (bnum_is_zero(tmp1)) + goto error; + + k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) + u = H_nn_pad(alg, A, B); // MODIFIED from H_nn(alg, A, B) + + // S = (A *(v^u)) ^ b + bnum_modexp(tmp1, v, u, ng->N); + bnum_mul(tmp2, A, tmp1); + bnum_modexp(S, tmp2, b, ng->N); + + hash_num(alg, S, ver->session_key); + ver->session_key_len = hash_length(ver->alg); + + calculate_M(alg, ng, ver->M, username, s, A, B, ver->session_key, ver->session_key_len); + calculate_H_AMK(alg, ver->H_AMK, A, ver->M, ver->session_key, ver->session_key_len); + + ver->bytes_B = bytes_B; + + bnum_free(s); + bnum_free(v); + bnum_free(A); + bnum_free(u); + bnum_free(k); + bnum_free(B); + bnum_free(S); + bnum_free(b); + bnum_free(tmp1); + bnum_free(tmp2); + return ver; + + error: + srp_verifier_free(ver); + bnum_free(s); + bnum_free(v); + bnum_free(A); + bnum_free(u); + bnum_free(k); + bnum_free(B); + bnum_free(S); + bnum_free(b); + bnum_free(tmp1); + bnum_free(tmp2); + return NULL; +} + +// user_M must be exactly SHA512_DIGEST_LENGTH bytes in size +static void +srp_verifier_verify_session(struct SRPVerifier *ver, const unsigned char *user_M, const unsigned char **bytes_HAMK) +{ + if (memcmp(ver->M, user_M, hash_length(ver->alg)) == 0) + { + ver->authenticated = 1; + *bytes_HAMK = ver->H_AMK; + } + else + *bytes_HAMK = NULL; +} + +static const unsigned char * +srp_verifier_get_session_key(struct SRPVerifier *ver, int *key_length) +{ + if (key_length) + *key_length = hash_length(ver->alg); + + return ver->session_key; +} + +/* -------------------------------- HELPERS --------------------------------- */ + +static void +hexread(uint8_t *out, size_t out_len, const char *in) +{ + char hex[] = { 0, 0, 0 }; + int i; + + for (i = 0; i < out_len; i++, in+=2) + { + hex[0] = in[0]; + hex[1] = in[1]; + out[i] = strtol(hex, NULL, 16); + } +} + +static pair_tlv_values_t * +message_process(const uint8_t *data, size_t data_len, const char **errmsg) +{ + pair_tlv_values_t *response; + pair_tlv_t *error; + int ret; + + response = pair_tlv_new(); + if (!response) + { + *errmsg = "Out of memory\n"; + return NULL; + } + + ret = pair_tlv_parse(data, data_len, response); + if (ret < 0) + { + *errmsg = "Could not parse TLV\n"; + goto error; + } + +#ifdef DEBUG_PAIR + pair_tlv_debug(response); +#endif + + error = pair_tlv_get_value(response, TLVType_Error); + if (error) + { + if (error->value[0] == TLVError_Authentication) + *errmsg = "Device returned an authentication failure"; + else if (error->value[0] == TLVError_Backoff) + *errmsg = "Device told us to back off pairing attempts\n"; + else if (error->value[0] == TLVError_MaxPeers) + *errmsg = "Max peers trying to connect to device\n"; + else if (error->value[0] == TLVError_MaxTries) + *errmsg = "Max pairing attemps reached\n"; + else if (error->value[0] == TLVError_Unavailable) + *errmsg = "Device is unuavailble at this time\n"; + else + *errmsg = "Device is busy/returned unknown error\n"; + + goto error; + } + + return response; + + error: + pair_tlv_free(response); + return NULL; +} + +/* Executes SHA512 RFC 5869 extract + expand, writing a derived key to okm + + hkdfExtract(SHA512, salt, salt_len, ikm, ikm_len, prk); + hkdfExpand(SHA512, prk, SHA512_LEN, info, info_len, okm, okm_len); +*/ +static int +hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, enum pair_keys pair_key) +{ +#ifdef CONFIG_OPENSSL +#include + EVP_PKEY_CTX *pctx; + + if (okm_len > SHA512_DIGEST_LENGTH) + return -1; + if (! (pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL))) + return -1; + if (EVP_PKEY_derive_init(pctx) <= 0) + goto error; + if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha512()) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_key(pctx, ikm, ikm_len) <= 0) + goto error; + if (EVP_PKEY_CTX_add1_hkdf_info(pctx, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info)) <= 0) + goto error; + if (EVP_PKEY_derive(pctx, okm, &okm_len) <= 0) + goto error; + + EVP_PKEY_CTX_free(pctx); + return 0; + + error: + EVP_PKEY_CTX_free(pctx); + return -1; +#elif CONFIG_GCRYPT + uint8_t prk[SHA512_DIGEST_LENGTH]; + gcry_md_hd_t hmac_handle; + + if (okm_len > SHA512_DIGEST_LENGTH) + return -1; // Below calculation not valid if output is larger than hash size + if (gcry_md_open(&hmac_handle, GCRY_MD_SHA512, GCRY_MD_FLAG_HMAC) != GPG_ERR_NO_ERROR) + return -1; + if (gcry_md_setkey(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) != GPG_ERR_NO_ERROR) + goto error; + gcry_md_write(hmac_handle, ikm, ikm_len); + memcpy(prk, gcry_md_read(hmac_handle, 0), sizeof(prk)); + + gcry_md_reset(hmac_handle); + + if (gcry_md_setkey(hmac_handle, prk, sizeof(prk)) != GPG_ERR_NO_ERROR) + goto error; + gcry_md_write(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info)); + gcry_md_putc(hmac_handle, 1); + + memcpy(okm, gcry_md_read(hmac_handle, 0), okm_len); + + gcry_md_close(hmac_handle); + return 0; + + error: + gcry_md_close(hmac_handle); + return -1; +#else + return -1; +#endif +} + +static int +encrypt_chacha(uint8_t *cipher, const uint8_t *plain, size_t plain_len, const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, uint8_t *tag, size_t tag_len, const uint8_t nonce[NONCE_LENGTH]) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + if (! (ctx = EVP_CIPHER_CTX_new())) + return -1; + + if (EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + + if (ad_len > 0 && EVP_EncryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + + if (EVP_EncryptUpdate(ctx, cipher, &len, plain, plain_len) != 1) + goto error; + + assert(len == plain_len); + + if (EVP_EncryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, tag_len, tag) != 1) + goto error; + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + + if (gcry_cipher_open(&hd, GCRY_CIPHER_CHACHA20, GCRY_CIPHER_MODE_POLY1305, 0) != GPG_ERR_NO_ERROR) + return -1; + + if (gcry_cipher_setkey(hd, key, key_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_setiv(hd, nonce, NONCE_LENGTH) != GPG_ERR_NO_ERROR) + goto error; + + if (ad_len > 0 && gcry_cipher_authenticate(hd, ad, ad_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_encrypt(hd, cipher, plain_len, plain, plain_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_gettag(hd, tag, tag_len) != GPG_ERR_NO_ERROR) + goto error; + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#else + return -1; +#endif +} + +static int +decrypt_chacha(uint8_t *plain, const uint8_t *cipher, size_t cipher_len, const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, uint8_t *tag, size_t tag_len, const uint8_t nonce[NONCE_LENGTH]) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + if (! (ctx = EVP_CIPHER_CTX_new())) + return -1; + + if (EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag) != 1) + goto error; + + if (ad_len > 0 && EVP_DecryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + + if (EVP_DecryptUpdate(ctx, plain, &len, cipher, cipher_len) != 1) + goto error; + + if (EVP_DecryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + + if (gcry_cipher_open(&hd, GCRY_CIPHER_CHACHA20, GCRY_CIPHER_MODE_POLY1305, 0) != GPG_ERR_NO_ERROR) + return -1; + + if (gcry_cipher_setkey(hd, key, key_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_setiv(hd, nonce, NONCE_LENGTH) != GPG_ERR_NO_ERROR) + goto error; + + if (ad_len > 0 && gcry_cipher_authenticate(hd, ad, ad_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_decrypt(hd, plain, cipher_len, cipher, cipher_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_checktag(hd, tag, tag_len) != GPG_ERR_NO_ERROR) + goto error; + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#else + return -1; +#endif +} + +static int +create_info(uint8_t *info, size_t *info_len, uint8_t *a, size_t a_len, uint8_t *b, size_t b_len, uint8_t *c, size_t c_len) +{ + if (a_len + b_len + c_len > *info_len) + return -1; + + *info_len = a_len + b_len + c_len; + memcpy(info, a, a_len); + memcpy(info + a_len, b, b_len); + memcpy(info + a_len + b_len, c, c_len); + + return 0; +} + +static int +create_and_sign_device_info(uint8_t *msg, size_t *msg_len, const char *device_id, uint8_t *device_x, size_t device_x_len, uint8_t *pk, size_t pk_len, uint8_t *sk) +{ + pair_tlv_values_t *tlv; + uint8_t device_info[256]; + size_t device_info_len; + size_t device_id_len; + uint8_t signature[crypto_sign_BYTES]; + int ret; + + device_id_len = strlen(device_id); + device_info_len = sizeof(device_info); + + ret = create_info(device_info, &device_info_len, device_x, device_x_len, (uint8_t *)device_id, device_id_len, pk, pk_len); + if (ret < 0) + return -1; + + crypto_sign_detached(signature, NULL, device_info, device_info_len, sk); + + tlv = pair_tlv_new(); + pair_tlv_add_value(tlv, TLVType_Identifier, (unsigned char *)device_id, device_id_len); + pair_tlv_add_value(tlv, TLVType_Signature, signature, sizeof(signature)); + + ret = pair_tlv_format(tlv, msg, msg_len); + + pair_tlv_free(tlv); + return ret; +} + +static int +create_and_sign_accessory_info(uint8_t *msg, size_t *msg_len, uint8_t *server_pk, size_t server_pk_len, const char *accessory_id, uint8_t *client_pk, size_t client_pk_len, uint8_t *sk) +{ + pair_tlv_values_t *tlv; + uint8_t accessory_info[256]; + size_t accessory_info_len; + size_t accessory_id_len; + uint8_t signature[crypto_sign_BYTES]; + int ret; + + accessory_id_len = strlen(accessory_id); + accessory_info_len = sizeof(accessory_info); + + ret = create_info(accessory_info, &accessory_info_len, server_pk, server_pk_len, (uint8_t *)accessory_id, accessory_id_len, client_pk, client_pk_len); + if (ret < 0) + return -1; + + crypto_sign_detached(signature, NULL, accessory_info, accessory_info_len, sk); + + tlv = pair_tlv_new(); + pair_tlv_add_value(tlv, TLVType_Identifier, (unsigned char *)accessory_id, accessory_id_len); + pair_tlv_add_value(tlv, TLVType_Signature, signature, sizeof(signature)); + + ret = pair_tlv_format(tlv, msg, msg_len); + + pair_tlv_free(tlv); + return ret; +} + +static int +verify_info(uint8_t *signature, uint8_t *pk, uint8_t *a, size_t a_len, uint8_t *b, size_t b_len, uint8_t *c, size_t c_len) +{ + uint8_t info[256]; + size_t info_len; + int ret; + + info_len = sizeof(info); + ret = create_info(info, &info_len, a, a_len, b, b_len, c, c_len); + if (ret < 0) + return -1; + + return crypto_sign_verify_detached(signature, info, info_len, pk); +} + + +/* ------------------------- CLIENT IMPLEMENTATION -------------------------- */ + +static int +client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + if (sodium_init() == -1) + return -1; + + if (handle->type == &pair_client_homekit_normal) + { + if (!pin || strlen(pin) < 4) + return -1; + } + else if (handle->type == &pair_client_homekit_transient && !pin) + { + pin = "3939"; + } + + if (device_id && strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + sctx->add_cb = add_cb; + sctx->add_cb_arg = cb_arg; + + if (device_id) + memcpy(sctx->device_id, device_id, strlen(device_id)); + + crypto_sign_keypair(sctx->public_key, sctx->private_key); + + return 0; +} + +static void +client_setup_free(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + srp_user_free(sctx->user); + + free(sctx->pkB); + free(sctx->M2); + free(sctx->salt); + free(sctx->epk); + free(sctx->authtag); +} + +static uint8_t * +client_setup_request1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + uint8_t method; + uint8_t flags; + int endian_test = 1; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + // Test here instead of setup_new() so we can give an error message + if(*(char *)&endian_test != 1) + { + handle->errmsg = "Setup request 1: No support for big endian architechture"; + goto error; + } + + sctx->user = srp_user_new(HASH_SHA512, SRP_NG_3072, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), 0, 0); + if (!sctx->user) + { + handle->errmsg = "Setup request 1: Create SRP user failed"; + goto error; + } + + method = PairingMethodPairSetup; + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG01].state, sizeof(pair_keys_map[PAIR_SETUP_MSG01].state)); + pair_tlv_add_value(request, TLVType_Method, &method, sizeof(method)); + + if (handle->type == &pair_client_homekit_transient) + { + flags = PairingFlagsTransient; + pair_tlv_add_value(request, TLVType_Flags, &flags, sizeof(flags)); + } + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 1: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_setup_request2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + const char *auth_username = NULL; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + // Calculate A + srp_user_start_authentication(sctx->user, &auth_username, &sctx->pkA, &sctx->pkA_len); + + // Calculate M1 (client proof) + srp_user_process_challenge(sctx->user, (const unsigned char *)sctx->salt, sctx->salt_len, (const unsigned char *)sctx->pkB, sctx->pkB_len, &sctx->M1, &sctx->M1_len); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG03].state, sizeof(pair_keys_map[PAIR_SETUP_MSG03].state)); + pair_tlv_add_value(request, TLVType_PublicKey, sctx->pkA, sctx->pkA_len); + pair_tlv_add_value(request, TLVType_Proof, sctx->M1, sctx->M1_len); + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 2: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_setup_request3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + const unsigned char *session_key; + int session_key_len; + uint8_t device_x[32]; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + pair_tlv_values_t *append; + size_t append_len; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup request 3: No valid session key"; + goto error; + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_CONTROLLER_SIGN); + if (ret < 0) + { + handle->errmsg = "Setup request 3: hkdf error getting device_x"; + goto error; + } + + ret = create_and_sign_device_info(data, &data_len, sctx->device_id, device_x, sizeof(device_x), sctx->public_key, sizeof(sctx->public_key), sctx->private_key); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error creating signed device info"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, PAIR_SETUP_MSG05); + if (ret < 0) + { + handle->errmsg = "Setup request 3: hkdf error getting derived_key"; + goto error; + } + + // Append TLV-encoded public key to *data, which already has identifier and signature + append = pair_tlv_new(); + append_len = REQUEST_BUFSIZE - data_len; + pair_tlv_add_value(append, TLVType_PublicKey, sctx->public_key, sizeof(sctx->public_key)); + ret = pair_tlv_format(append, data + data_len, &append_len); + pair_tlv_free(append); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error appending public key to TLV"; + goto error; + } + data_len += append_len; + + memcpy(nonce + 4, pair_keys_map[PAIR_SETUP_MSG05].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Could not encrypt"; + goto error; + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG05].state, sizeof(pair_keys_map[PAIR_SETUP_MSG05].state)); + pair_tlv_add_value(request, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error appending public key to TLV"; + goto error; + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(request); + return data; + + error: + free(encrypted_data); + pair_tlv_free(request); + free(data); + return NULL; +} + +static int +client_setup_response1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *pk; + pair_tlv_t *salt; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + pk = pair_tlv_get_value(response, TLVType_PublicKey); + salt = pair_tlv_get_value(response, TLVType_Salt); + if (!pk || !salt) + { + handle->errmsg = "Setup response 1: Missing or invalid pk/salt"; + goto error; + } + + sctx->pkB_len = pk->size; // 384 + sctx->pkB = malloc(sctx->pkB_len); + memcpy(sctx->pkB, pk->value, sctx->pkB_len); + + sctx->salt_len = salt->size; // 16 + sctx->salt = malloc(sctx->salt_len); + memcpy(sctx->salt, salt->value, sctx->salt_len); + + pair_tlv_free(response); + return 0; + + error: + pair_tlv_free(response); + return -1; +} + +static int +client_setup_response2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *proof; + const uint8_t *session_key; + int session_key_len; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + proof = pair_tlv_get_value(response, TLVType_Proof); + if (!proof) + { + handle->errmsg = "Setup response 2: Missing proof"; + goto error; + } + + sctx->M2_len = proof->size; // 64 + sctx->M2 = malloc(sctx->M2_len); + memcpy(sctx->M2, proof->value, sctx->M2_len); + + // Check M2 + srp_user_verify_session(sctx->user, (const unsigned char *)sctx->M2); + if (!srp_user_is_authenticated(sctx->user)) + { + handle->errmsg = "Setup response 2: Server authentication failed"; + goto error; + } + + if (handle->type == &pair_client_homekit_transient) + { + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup response 2: Could not compute session key"; + goto error; + } + + assert(sizeof(handle->result.shared_secret) >= session_key_len); + + memcpy(handle->result.shared_secret, session_key, session_key_len); + handle->result.shared_secret_len = session_key_len; + + handle->status = PAIR_STATUS_COMPLETED; + } + + pair_tlv_free(response); + return 0; + + error: + pair_tlv_free(response); + return -1; +} + +static int +client_setup_response3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *pk; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + const uint8_t *session_key; + int session_key_len; + uint8_t device_x[32]; + int ret; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + encrypted_data = pair_tlv_get_value(response, TLVType_EncryptedData); + if (!encrypted_data) + { + handle->errmsg = "Setup response 3: Missing encrypted_data"; + goto error; + } + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup response 3: No valid session key"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, PAIR_SETUP_MSG06); + if (ret < 0) + { + handle->errmsg = "Setup response 3: hkdf error getting derived_key"; + goto error; + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + handle->errmsg = "Setup response 3: Invalid encrypted data"; + goto error; + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[PAIR_SETUP_MSG06].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Setup response 3: Decryption error"; + goto error; + } + + pair_tlv_free(response); + response = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!response) + { + goto error; + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_ACCESSORY_SIGN); + if (ret < 0) + { + handle->errmsg = "Setup response 3: hkdf error getting device_x"; + goto error; + } + + device_id = pair_tlv_get_value(response, TLVType_Identifier); + pk = pair_tlv_get_value(response, TLVType_PublicKey); + signature = pair_tlv_get_value(response, TLVType_Signature); + if (!device_id || device_id->size >= sizeof(handle->result.device_id) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES || !signature || signature->size != crypto_sign_BYTES) + { + handle->errmsg = "Setup response 3: Missing/invalid device ID, public key or signature"; + goto error; + } + + ret = verify_info(signature->value, pk->value, device_x, sizeof(device_x), device_id->value, device_id->size, pk->value, pk->size); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Invalid signature"; + goto error; + } + + assert(sizeof(handle->result.client_private_key) == sizeof(sctx->private_key)); + assert(sizeof(handle->result.client_public_key) == sizeof(sctx->public_key)); + assert(sizeof(handle->result.server_public_key) == pk->size); + + memcpy(handle->result.client_private_key, sctx->private_key, sizeof(sctx->private_key)); + memcpy(handle->result.client_public_key, sctx->public_key, sizeof(sctx->public_key)); + memcpy(handle->result.server_public_key, pk->value, pk->size); + memcpy(handle->result.device_id, device_id->value, device_id->size); + + if (sctx->add_cb) + sctx->add_cb(handle->result.server_public_key, handle->result.device_id, sctx->add_cb_arg); + + handle->status = PAIR_STATUS_COMPLETED; + + free(decrypted_data); + pair_tlv_free(response); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(response); + return -1; +} + +static int +client_setup_result(struct pair_setup_context *handle) +{ + char *ptr; + int i; + + assert(sizeof(handle->result_str) >= 2 * sizeof(handle->result.client_private_key) + 2 * sizeof(handle->result.server_public_key) + 1); + + // It is enough to export the private key, since the public key can be + // extracted from that with crypto_sign_ed25519_sk_to_pk (it is the last 32 + // bytes) + ptr = handle->result_str; + for (i = 0; i < sizeof(handle->result.client_private_key); i++) + ptr += sprintf(ptr, "%02x", handle->result.client_private_key[i]); // 2 x 64 bytes + for (i = 0; i < sizeof(handle->result.server_public_key); i++) + ptr += sprintf(ptr, "%02x", handle->result.server_public_key[i]); // 2 x 32 bytes + *ptr = '\0'; + + return 0; +} + +static int +client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + size_t hexkey_len; + + if (sodium_init() == -1) + return -1; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + if (!client_setup_keys) + return -1; + + hexkey_len = strlen(client_setup_keys); + if (hexkey_len == 2 * sizeof(vctx->client_private_key) + 2 * sizeof(vctx->server_public_key)) + { + hexread(vctx->client_private_key, sizeof(vctx->client_private_key), client_setup_keys); + hexread(vctx->server_public_key, sizeof(vctx->server_public_key), client_setup_keys + 2 * sizeof(vctx->client_private_key)); + vctx->verify_server_signature = true; + } + else if (hexkey_len == 2 * sizeof(vctx->client_private_key)) // No server public key known, so signature validation will be skipped + { + hexread(vctx->client_private_key, sizeof(vctx->client_private_key), client_setup_keys); + } + else + return -1; + + crypto_sign_ed25519_sk_to_pk(vctx->client_public_key, vctx->client_private_key); + + snprintf(vctx->device_id, sizeof(vctx->device_id), "%s", device_id); + + return 0; +} + +static uint8_t * +client_verify_request1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; +// const uint8_t basepoint[crypto_scalarmult_BYTES] = {9}; // 32 bytes + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + crypto_box_keypair(vctx->client_eph_public_key, vctx->client_eph_private_key); + +/* + // TODO keep around in case box_keypair doesn't work + ret = crypto_scalarmult(vctx->client_eph_public_key, vctx->client_eph_private_key, basepoint); + if (ret < 0) + { + handle->errmsg = "Verify request 1: Curve 25519 returned an error"; + goto error; + } +*/ + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_VERIFY_MSG01].state, sizeof(pair_keys_map[PAIR_VERIFY_MSG01].state)); + pair_tlv_add_value(request, TLVType_PublicKey, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Verify request 1: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_verify_request2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + ret = create_and_sign_device_info(data, &data_len, vctx->device_id, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), + vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), vctx->client_private_key); + if (ret < 0) + { + handle->errmsg = "Verify request 2: error creating signed device info"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), PAIR_VERIFY_MSG03); + if (ret < 0) + { + handle->errmsg = "Verify request 2: hkdf error getting derived_key"; + goto error; + } + + memcpy(nonce + 4, pair_keys_map[PAIR_VERIFY_MSG03].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Could not encrypt"; + goto error; + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_VERIFY_MSG03].state, sizeof(pair_keys_map[PAIR_VERIFY_MSG03].state)); + pair_tlv_add_value(request, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Verify request 2: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(request); + return data; + + error: + free(encrypted_data); + pair_tlv_free(request); + free(data); + return NULL; +} + +static int +client_verify_response1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *response; + pair_tlv_t *encrypted_data; + pair_tlv_t *public_key; + pair_tlv_t *device_id; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + int ret; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + encrypted_data = pair_tlv_get_value(response, TLVType_EncryptedData); + if (!encrypted_data) + { + handle->errmsg = "Verify response 1: Missing encrypted_data"; + goto error; + } + + public_key = pair_tlv_get_value(response, TLVType_PublicKey); + if (!public_key || public_key->size != sizeof(vctx->server_eph_public_key)) + { + handle->errmsg = "Verify response 1: Missing or invalid public_key"; + goto error; + } + + memcpy(vctx->server_eph_public_key, public_key->value, sizeof(vctx->server_eph_public_key)); + ret = crypto_scalarmult(vctx->shared_secret, vctx->client_eph_private_key, vctx->server_eph_public_key); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Curve 25519 returned an error"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), PAIR_VERIFY_MSG02); + if (ret < 0) + { + handle->errmsg = "Verify response 1: hkdf error getting derived_key"; + goto error; + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + handle->errmsg = "Verify response 1: Invalid encrypted data"; + goto error; + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[PAIR_VERIFY_MSG02].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Decryption error"; + goto error; + } + + pair_tlv_free(response); + response = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!response) + { + goto error; + } + + device_id = pair_tlv_get_value(response, TLVType_Identifier); + signature = pair_tlv_get_value(response, TLVType_Signature); + if (!device_id || !signature || signature->size != crypto_sign_BYTES) + { + handle->errmsg = "Verify response 1: Missing device ID or signature"; + goto error; + } + + if (vctx->verify_server_signature) + { + ret = verify_info(signature->value, vctx->server_public_key, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), + device_id->value, device_id->size, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Invalid signature"; + goto error; + } + } + + free(decrypted_data); + pair_tlv_free(response); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(response); + return -1; +} + +static int +client_verify_response2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *response; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + return 0; +} + + +/* ------------------------- SERVER IMPLEMENTATION -------------------------- */ + +// Use (unsecure) keys seeded from device_id. We need the keys to always be the +// same during pair setup and pair verify, since the client saves them after +// pair-setup 3, so that the signature in pair-verify 1 can be checked. +static void +server_keypair(uint8_t *public_key, uint8_t *private_key, const char *device_id) +{ + uint8_t seed[crypto_sign_SEEDBYTES] = { 0 }; + size_t len; + + len = strlen(device_id); + if (len > sizeof(seed)) + len = sizeof(seed); + + memcpy(seed, device_id, len); + + crypto_sign_seed_keypair(public_key, private_key, seed); +} + +static uint8_t * +server_auth_failed_response(size_t *len, enum pair_keys msg_state) +{ + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + uint8_t error = TLVError_Authentication; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Error, &error, sizeof(error)); + + pair_tlv_format(response, data, &data_len); + pair_tlv_free(response); + + *len = data_len; + + return data; +} + +static int +server_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + + if (sodium_init() == -1) + return -1; + + if (!pin) + pin = "3939"; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + sctx->add_cb = add_cb; + sctx->add_cb_arg = cb_arg; + + snprintf(sctx->device_id, sizeof(sctx->device_id), "%s", device_id); + + server_keypair(sctx->public_key, sctx->private_key, sctx->device_id); + + return 0; +} + +static void +server_setup_free(struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + + srp_verifier_free(sctx->verifier); + + free(sctx->pkA); + free(sctx->pkB); + free(sctx->b); + free(sctx->M1); + free(sctx->v); + free(sctx->salt); +} + +static int +server_setup_request1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; +// enum pair_keys msg_state = PAIR_SETUP_MSG01; + pair_tlv_values_t *request; + pair_tlv_t *method; + pair_tlv_t *type; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + method = pair_tlv_get_value(request, TLVType_Method); + if (!method || method->size != 1 || method->value[0] != 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Missing or unexpected pairing method in TLV"); + } + + type = pair_tlv_get_value(request, TLVType_Flags); + sctx->is_transient = (type && type->size == 1 && type->value[0] == PairingFlagsTransient); + + // Note this is modified to return a 16 byte salt + ret = srp_create_salted_verification_key(HASH_SHA512, SRP_NG_3072, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), + &sctx->salt, &sctx->salt_len, &sctx->v, &sctx->v_len, NULL, NULL); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Could not create verification key"); + } + + ret = srp_verifier_start_authentication(HASH_SHA512, SRP_NG_3072, sctx->v, sctx->v_len, + &sctx->b, &sctx->b_len, &sctx->pkB, &sctx->pkB_len, NULL, NULL); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Could not compute B"); + } + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_setup_request2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; +// enum pair_keys msg_state = PAIR_SETUP_MSG03; + pair_tlv_values_t *request; + pair_tlv_t *pk; + pair_tlv_t *proof; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + pk = pair_tlv_get_value(request, TLVType_PublicKey); + proof = pair_tlv_get_value(request, TLVType_Proof); + if (!pk || !proof) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Missing pkA or proof"); + } + + sctx->pkA_len = pk->size; // 384 + sctx->pkA = malloc(sctx->pkA_len); + memcpy(sctx->pkA, pk->value, sctx->pkA_len); + + sctx->M1_len = proof->size; // 64 + sctx->M1 = malloc(sctx->M1_len); + memcpy(sctx->M1, proof->value, sctx->M1_len); + + sctx->verifier = srp_verifier_new(HASH_SHA512, SRP_NG_3072, USERNAME, sctx->salt, sctx->salt_len, sctx->v, sctx->v_len, + sctx->pkA, sctx->pkA_len, sctx->b, sctx->b_len, sctx->pkB, sctx->pkB_len, NULL, NULL); + if (!sctx->verifier) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + sctx->M2_len = 64; // 512 bit hash + srp_verifier_verify_session(sctx->verifier, sctx->M1, &sctx->M2); + if (!sctx->M2) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; // Not an error, server should give proper TLV-formatet reply + } + + out: + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_setup_request3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG05; + pair_tlv_values_t *request; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *pk; + pair_tlv_t *signature; + const uint8_t *session_key; + int session_key_len; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + uint8_t device_x[32]; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: No valid session key"); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: hkdf error getting derived_key"); + } + + encrypted_data = pair_tlv_get_value(request, TLVType_EncryptedData); + if (!encrypted_data) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Missing encrypted_data"); + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Invalid encrypted data"); + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Decryption error"); + } + + pair_tlv_free(request); + request = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_CONTROLLER_SIGN); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: hkdf error getting device_x"); + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + pk = pair_tlv_get_value(request, TLVType_PublicKey); + signature = pair_tlv_get_value(request, TLVType_Signature); + if (!device_id || device_id->size >= sizeof(handle->result.device_id) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES || !signature || signature->size != crypto_sign_BYTES) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Missing/invalid device ID, public key or signature"); + } + + ret = verify_info(signature->value, pk->value, device_x, sizeof(device_x), device_id->value, device_id->size, pk->value, pk->size); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + memcpy(handle->result.device_id, device_id->value, device_id->size); + memcpy(handle->result.client_public_key, pk->value, pk->size); + + out: + free(decrypted_data); + pair_tlv_free(request); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_setup_response1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG02; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Salt, sctx->salt, sctx->salt_len); // 16 + pair_tlv_add_value(response, TLVType_PublicKey, sctx->pkB, sctx->pkB_len); // 384 + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 1: pair_tlv_format returned an error"); + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_setup_response2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG04; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + const uint8_t *session_key; + int session_key_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Proof, sctx->M2, sctx->M2_len); // 384 + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 2: pair_tlv_format returned an error"); + } + + if (sctx->is_transient) + { + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Could not compute session key"); + } + + assert(sizeof(handle->result.shared_secret) >= session_key_len); + + memcpy(handle->result.shared_secret, session_key, session_key_len); + handle->result.shared_secret_len = session_key_len; + + handle->status = PAIR_STATUS_COMPLETED; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_setup_response3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG06; + const uint8_t *session_key; + int session_key_len; + pair_tlv_values_t *response; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + pair_tlv_values_t *append; + size_t append_len; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + uint8_t *data; + size_t data_len; + uint8_t device_x[32]; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: No valid session key"); + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_ACCESSORY_SIGN); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: hkdf error getting device_x"); + } + + ret = create_and_sign_device_info(data, &data_len, sctx->device_id, device_x, sizeof(device_x), sctx->public_key, sizeof(sctx->public_key), sctx->private_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: create device info returned an error"); + } + + // Append TLV-encoded public key to *data, which already has identifier and signature + append = pair_tlv_new(); + append_len = REQUEST_BUFSIZE - data_len; + pair_tlv_add_value(append, TLVType_PublicKey, sctx->public_key, sizeof(sctx->public_key)); + ret = pair_tlv_format(append, data + data_len, &append_len); + pair_tlv_free(append); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: error appending public key to TLV"); + } + data_len += append_len; + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: hkdf error getting derived_key"); + } + + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: Could not encrypt"); + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: error appending public key to TLV"); + } + + if (sctx->add_cb) + sctx->add_cb(handle->result.client_public_key, handle->result.device_id, sctx->add_cb_arg); + + handle->status = PAIR_STATUS_COMPLETED; + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(response); + return data; + + error: + free(encrypted_data); + free(data); + pair_tlv_free(response); + return NULL; +} + + +static int +server_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + + if (sodium_init() == -1) + return -1; + + if (client_setup_keys) + return -1; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + snprintf(vctx->device_id, sizeof(vctx->device_id), "%s", device_id); + + vctx->get_cb = cb; + vctx->get_cb_arg = cb_arg; + vctx->verify_client_signature = cb; + + server_keypair(vctx->server_public_key, vctx->server_private_key, vctx->device_id); + + return 0; +} + +static int +server_verify_request1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; +// enum pair_keys msg_state = PAIR_VERIFY_MSG01; + pair_tlv_values_t *request; + pair_tlv_t *pk; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (!pk || pk->size != sizeof(vctx->client_eph_public_key)) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 1: Missing or invalid public_key"); + } + + memcpy(vctx->client_eph_public_key, pk->value, sizeof(vctx->client_eph_public_key)); + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_verify_request2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG03; + pair_tlv_values_t *request; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: hkdf error getting derived_key"); + } + + encrypted_data = pair_tlv_get_value(request, TLVType_EncryptedData); + if (!encrypted_data) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Missing encrypted_data"); + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Invalid encrypted data"); + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Decryption error"); + } + + pair_tlv_free(request); + request = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + signature = pair_tlv_get_value(request, TLVType_Signature); + if (!device_id || !signature || signature->size != crypto_sign_BYTES) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Missing identifier or signature"); + } + + if (vctx->verify_client_signature) + { + if (device_id->size >= sizeof(id_str)) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Device ID from peer is too long"); + } + + memcpy(id_str, device_id->value, device_id->size); + + ret = vctx->get_cb(client_public_key, id_str, vctx->get_cb_arg); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + ret = verify_info(signature->value, client_public_key, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), + device_id->value, device_id->size, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + } + + out: + free(decrypted_data); + pair_tlv_free(request); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_verify_response1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG02; + pair_tlv_values_t *response; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + crypto_box_keypair(vctx->server_eph_public_key, vctx->server_eph_private_key); + + ret = crypto_scalarmult(vctx->shared_secret, vctx->server_eph_private_key, vctx->client_eph_public_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Error generating shared secret"); + } + + ret = create_and_sign_accessory_info(data, &data_len, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), vctx->device_id, + vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), vctx->server_private_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Error creating device info"); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: hkdf error getting derived_key"); + } + + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Could not encrypt"); + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_PublicKey, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + pair_tlv_add_value(response, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: pair_tlv_format returned an error"); + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(response); + return data; + + error: + free(encrypted_data); + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_verify_response2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG04; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 2: pair_tlv_format returned an error"); + } + + *len = data_len; + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + const char *errmsg; + pair_tlv_values_t *request; + pair_tlv_t *device_id; + pair_tlv_t *pk; + char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + + request = message_process(in, in_len, &errmsg); + if (!request) + { + goto error; + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (!device_id || device_id->size >= sizeof(id_str) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES) + { + goto error; + } + + memcpy(id_str, device_id->value, device_id->size); + + cb(pk->value, id_str, cb_arg); + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_add_remove_response(size_t *len) +{ + pair_tlv_values_t *response; + uint8_t *data; + uint8_t state = 2; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &state, sizeof(state)); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + goto error; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_add_remove(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + int ret; + + ret = server_add_remove_request(cb, cb_arg, in, in_len); + if (ret < 0) + return -1; + + *out = server_add_remove_response(out_len); + if (!*out) + return -1; + + return 0; +} + +static int +server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg) +{ + pair_tlv_values_t *response = cb_arg; + + pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id)); + pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES); + return 0; +} + +static uint8_t * +server_list_response(size_t *len, pair_list_cb cb, void *cb_arg) +{ + pair_tlv_values_t *response; + uint8_t *data; + uint8_t state = 2; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &state, sizeof(state)); + + cb(server_list_cb, response, cb_arg); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + goto error; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_list(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + // Skip reading the request, it just has state = 1 and pair method = + // PairingMethodListPairings + + *out = server_list_response(out_len, cb, cb_arg); + if (!*out) + return -1; + + return 0; +} + + +/* ----------------------- CIPHERING IMPLEMENTATION ------------------------- */ + +static void +cipher_free(struct pair_cipher_context *cctx) +{ + if (!cctx) + return; + + free(cctx); +} + +static struct pair_cipher_context * +cipher_new(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len) +{ + struct pair_cipher_context *cctx; + enum pair_keys write_key; + enum pair_keys read_key; + int ret; + + // Note that events is opposite, probably because it is a reverse connection + switch (channel) + { + case 0: + write_key = PAIR_CONTROL_WRITE; + read_key = PAIR_CONTROL_READ; + break; + case 1: + write_key = PAIR_EVENTS_READ; + read_key = PAIR_EVENTS_WRITE; + break; + case 2: + write_key = PAIR_CONTROL_READ; + read_key = PAIR_CONTROL_WRITE; + break; + case 3: + write_key = PAIR_EVENTS_WRITE; + read_key = PAIR_EVENTS_READ; + break; + default: + return NULL; + } + + cctx = calloc(1, sizeof(struct pair_cipher_context)); + if (!cctx) + goto error; + + cctx->type = type; + + ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, write_key); + if (ret < 0) + goto error; + + ret = hkdf_extract_expand(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, read_key); + if (ret < 0) + goto error; + + return cctx; + + error: + pair_cipher_free(cctx); + return NULL; +} + +static ssize_t +encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx) +{ + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + const uint8_t *plain_block; + uint8_t *cipher_block; + uint16_t block_len; + int nblocks; + int ret; + int i; + + if (plaintext_len == 0 || !plaintext) + return -1; + + // Encryption is done in blocks, where each block consists of a short, the + // encrypted data and an auth tag. The short is the size of the encrypted + // data. The encrypted data in the block cannot exceed ENCRYPTED_LEN_MAX. + nblocks = 1 + ((plaintext_len - 1) / ENCRYPTED_LEN_MAX); // Ceiling of division + + *ciphertext_len = nblocks * (sizeof(block_len) + AUTHTAG_LENGTH) + plaintext_len; + *ciphertext = malloc(*ciphertext_len); + + cctx->encryption_counter_prev = cctx->encryption_counter; + + for (i = 0, plain_block = plaintext, cipher_block = *ciphertext; i < nblocks; i++) + { + // If it is the last block we will encrypt only the remaining data + block_len = (i + 1 == nblocks) ? (plaintext + plaintext_len - plain_block) : ENCRYPTED_LEN_MAX; + + memcpy(nonce + 4, &(cctx->encryption_counter), sizeof(cctx->encryption_counter));// TODO BE or LE? + + // Write the ciphered block + memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE? + ret = encrypt_chacha(cipher_block + sizeof(block_len), plain_block, block_len, cctx->encryption_key, sizeof(cctx->encryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) + { + cctx->errmsg = "Encryption with chacha poly1305 failed"; + cctx->encryption_counter = cctx->encryption_counter_prev; + free(*ciphertext); + return -1; + } + memcpy(cipher_block + sizeof(block_len) + block_len, tag, AUTHTAG_LENGTH); + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; + cctx->encryption_counter++; + } + +#ifdef DEBUG_PAIR + hexdump("Encrypted:\n", *ciphertext, *ciphertext_len); +#endif + + return plain_block - plaintext; +} + +static ssize_t +decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx) +{ + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t *plain_block; + const uint8_t *cipher_block; + uint16_t block_len; + int ret; + + if (ciphertext_len < sizeof(block_len) || !ciphertext) + return -1; + + // This will allocate more than we need. Since we don't know the number of + // blocks in the ciphertext yet we can't calculate the exact required length. + *plaintext = malloc(ciphertext_len); + + cctx->decryption_counter_prev = cctx->decryption_counter; + + for (plain_block = *plaintext, cipher_block = ciphertext; cipher_block < ciphertext + ciphertext_len; ) + { + memcpy(&block_len, cipher_block, sizeof(block_len)); // TODO BE or LE? + if (cipher_block + block_len + sizeof(block_len) + AUTHTAG_LENGTH > ciphertext + ciphertext_len) + { + // The remaining ciphertext doesn't contain an entire block, so stop + break; + } + + memcpy(tag, cipher_block + sizeof(block_len) + block_len, sizeof(tag)); + memcpy(nonce + 4, &(cctx->decryption_counter), sizeof(cctx->decryption_counter));// TODO BE or LE? + + ret = decrypt_chacha(plain_block, cipher_block + sizeof(block_len), block_len, cctx->decryption_key, sizeof(cctx->decryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) + { + cctx->errmsg = "Decryption with chacha poly1305 failed"; + cctx->decryption_counter = cctx->decryption_counter_prev; + free(*plaintext); + return -1; + } + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; + cctx->decryption_counter++; + } + + *plaintext_len = plain_block - *plaintext; + +#ifdef DEBUG_PAIR + hexdump("Decrypted:\n", *plaintext, *plaintext_len); +#endif + + return cipher_block - ciphertext; +} + +static int +state_get(const char **errmsg, const uint8_t *data, size_t data_len) +{ + pair_tlv_values_t *message; + pair_tlv_t *state; + int ret; + + if (!data || data_len == 0) + { + return 0; // state 0 = no incoming data yet -> first request + } + + message = message_process(data, data_len, errmsg); + if (!message) + { + goto error; + } + + state = pair_tlv_get_value(message, TLVType_State); + if (!state || state->size != 1) + { + *errmsg = "Could not get message state"; + goto error; + } + + ret = state->value[0]; + + pair_tlv_free(message); + return ret; + + error: + pair_tlv_free(message); + return -1; +} + +static void +public_key_get(uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id) +{ + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + server_keypair(server_public_key, private_key, device_id); +} + +const struct pair_definition pair_client_homekit_normal = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + .pair_setup_result = client_setup_result, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, +}; + +const struct pair_definition pair_client_homekit_transient = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, +}; + +const struct pair_definition pair_server_homekit = +{ + .pair_setup_new = server_setup_new, + .pair_setup_free = server_setup_free, + + .pair_setup_request1 = server_setup_response1, + .pair_setup_request2 = server_setup_response2, + .pair_setup_request3 = server_setup_response3, + + .pair_setup_response1 = server_setup_request1, + .pair_setup_response2 = server_setup_request2, + .pair_setup_response3 = server_setup_request3, + + .pair_verify_new = server_verify_new, + + .pair_verify_request1 = server_verify_response1, + .pair_verify_request2 = server_verify_response2, + + .pair_verify_response1 = server_verify_request1, + .pair_verify_response2 = server_verify_request2, + + .pair_add = server_add_remove, + .pair_remove = server_add_remove, + .pair_list = server_list, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, + .pair_public_key_get = public_key_get, +}; diff --git a/pair_ap/server-example.c b/pair_ap/server-example.c new file mode 100644 index 00000000..73fe4bda --- /dev/null +++ b/pair_ap/server-example.c @@ -0,0 +1,685 @@ +/* + * + * The MIT License (MIT) + * + * 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 +#include + +#include + +#include +#include +#include +#include + +#include "pair.h" + +#define DEVICE_ID "FFEEDDCCBBAA9988" +#define LISTEN_PORT 7000 +#define CONTENT_TYPE_OCTET "application/octet-stream" +#define RTSP_VERSION "RTSP/1.0" +#define OPTIONS "OPTIONS *" + +struct connection_ctx +{ + struct evbuffer *pending; + struct pair_setup_context *setup_ctx; + struct pair_verify_context *verify_ctx; + struct pair_cipher_context *cipher_ctx; + + int pair_completed; +}; + +struct rtsp_msg +{ + int content_length; + char *content_type; + char *first_line; + int cseq; + + const uint8_t *body; + size_t bodylen; + + const uint8_t *data; + size_t datalen; +}; + +struct pairings +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + uint8_t public_key[32]; + + struct pairings *next; +} *pairings; + + +static void +connection_free(struct connection_ctx *conn_ctx) +{ + if (!conn_ctx) + return; + + evbuffer_free(conn_ctx->pending); + pair_setup_free(conn_ctx->setup_ctx); + pair_cipher_free(conn_ctx->cipher_ctx); + + free(conn_ctx); +} + +static void +response_headers_add(struct evbuffer *response, int cseq, size_t content_length, const char *content_type) +{ + evbuffer_add_printf(response, "%s 200 OK\r\n", RTSP_VERSION); + evbuffer_add_printf(response, "Server: MyServer/1.0\r\n"); + if (content_length) + evbuffer_add_printf(response, "Content-Length: %zu\r\n", content_length); + if (content_type) + evbuffer_add_printf(response, "Content-Type: %s\r\n", content_type); + evbuffer_add_printf(response, "CSeq: %d\r\n", cseq); + evbuffer_add_printf(response, "\r\n"); +} + +static void +response_create_from_raw(struct evbuffer *response, uint8_t *body, size_t body_len, int cseq, const char *content_type) +{ + response_headers_add(response, cseq, body_len, content_type); + + if (body) + evbuffer_add(response, body, body_len); +} + +static int +encryption_enable(struct connection_ctx *conn_ctx, const uint8_t *shared_secret, size_t shared_secret_len) +{ + conn_ctx->cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, shared_secret, shared_secret_len); + if (!conn_ctx->cipher_ctx) + { + printf("Error setting up ciphering\n"); + return -1; + } + + return 0; +} + +static int +buffer_encrypt(struct evbuffer *output, uint8_t *in, size_t in_len, struct connection_ctx *conn_ctx) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_encrypt(&out, &out_len, in, in_len, conn_ctx->cipher_ctx); + if (ret < 0) + { + printf("Error encrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx)); + return -1; + } + + evbuffer_add(output, out, out_len); + free(out); + return 0; +} + +static int +buffer_decrypt(struct evbuffer *output, struct evbuffer *input, struct connection_ctx *conn_ctx) +{ + uint8_t *in; + size_t in_len; + ssize_t bytes_decrypted; + uint8_t *plain; + size_t plain_len; + + in = evbuffer_pullup(input, -1); + in_len = evbuffer_get_length(input); + + // Note that bytes_decrypted is not necessarily equal to plain_len + bytes_decrypted = pair_decrypt(&plain, &plain_len, in, in_len, conn_ctx->cipher_ctx); + if (bytes_decrypted < 0) + { + printf("Error decrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx)); + return -1; + } + + evbuffer_add(output, plain, plain_len); + evbuffer_drain(input, bytes_decrypted); + free(plain); + return 0; +} + + +/* ---------------------------- Pairing callbacks --------------------------- */ +/* Note that none of these callbacks are required if you don't care about */ +/* securely verifying the client + don't require support for the pair-add, */ +/* pair-remove and pair-list methods. */ + +static struct pairings * +pairing_find(const char *device_id) +{ + struct pairings *pairing; + + for (pairing = pairings; pairing; pairing = pairing->next) + { + if (strcmp(device_id, pairing->device_id) == 0) + break; + } + + return pairing; +} + +static int +pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + + printf("Adding paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (pairing) + { + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + return 0; + } + + pairing = calloc(1, sizeof(struct pairings)); + snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + + pairing->next = pairings; + pairings = pairing; + + return 0; +} + +static int +pairing_remove_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + struct pairings *iter; + + printf("Removing paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (!pairing) + { + printf("Remove callback for unknown device\n"); + return -1; + } + + if (pairing == pairings) + pairings = pairing->next; + else + { + for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) + ; /* EMPTY */ + + if (iter) + iter->next = pairing->next; + } + + free(pairing); + return 0; +} + +static void +pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg) +{ + struct pairings *pairing; + + printf("Listing paired devices\n"); + + for (pairing = pairings; pairing; pairing = pairing->next) + { + enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); + } +} + +static int +pairing_get_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + + printf("Returning public key for paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (!pairing) + return -1; + + memcpy(public_key, pairing->public_key, sizeof(pairing->public_key)); + + return 0; +} + + +/* -------------------------- Pair request handlers ------------------------- */ + +static int +handle_pin_start(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + printf("Please pair with code 3939\n"); + + response_create_from_raw(output, NULL, 0, msg->cseq, NULL); + + return 0; +} + +static int +handle_pair_setup(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + struct pair_result *result; + int ret; + + if (!conn_ctx->setup_ctx) + { + conn_ctx->setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, NULL, pairing_add_cb, NULL, DEVICE_ID); + if (!conn_ctx->setup_ctx) + { + printf("Error creating setup context\n"); + return -1; + } + } + + ret = pair_setup(&out, &out_len, conn_ctx->setup_ctx, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Pair setup error: %s\n", pair_setup_errmsg(conn_ctx->setup_ctx)); + return -1; + } + + ret = pair_setup_result(NULL, &result, conn_ctx->setup_ctx); + if (ret == 0 && result->shared_secret_len > 0) // Transient pairing completed (step 2) + { + encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len); + conn_ctx->pair_completed = 1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_verify(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + struct pair_result *result; + int ret; + + if (!conn_ctx->verify_ctx) + { + conn_ctx->verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, pairing_get_cb, NULL, DEVICE_ID); + if (!conn_ctx->verify_ctx) + { + printf("Error creating verify context\n"); + return -1; + } + } + + ret = pair_verify(&out, &out_len, conn_ctx->verify_ctx, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Pair verify error: %s\n", pair_verify_errmsg(conn_ctx->verify_ctx)); + return -1; + } + + ret = pair_verify_result(&result, conn_ctx->verify_ctx); + if (ret == 0) + { + encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len); + conn_ctx->pair_completed = 1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_add(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_add(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_add_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error adding device to list\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_remove(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_remove(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_remove_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error removing device from list\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_list(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_list(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_list_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error creating list of paired devices\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_options(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + struct evbuffer *response; + uint8_t *plain; + size_t plain_len; + int ret; + + response = evbuffer_new(); + + response_create_from_raw(response, NULL, 0, msg->cseq, NULL); + + if (!conn_ctx->cipher_ctx) + { + evbuffer_add_buffer(output, response); + evbuffer_free(response); + return 0; + } + + plain = evbuffer_pullup(response, -1); + plain_len = evbuffer_get_length(response); + + ret = buffer_encrypt(output, plain, plain_len, conn_ctx); + + evbuffer_free(response); + return ret; +} + +static int +response_send(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + if (!msg->first_line) + return -1; + + if (strncmp(msg->first_line, PAIR_AP_POST_PIN_START, strlen(PAIR_AP_POST_PIN_START)) == 0) + return handle_pin_start(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_SETUP, strlen(PAIR_AP_POST_SETUP)) == 0) + return handle_pair_setup(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_VERIFY, strlen(PAIR_AP_POST_VERIFY)) == 0) + return handle_pair_verify(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_ADD, strlen(PAIR_AP_POST_ADD)) == 0) + return handle_pair_add(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_LIST, strlen(PAIR_AP_POST_LIST)) == 0) + return handle_pair_list(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_REMOVE, strlen(PAIR_AP_POST_REMOVE)) == 0) + return handle_pair_remove(output, conn_ctx, msg); + else if (strncmp(msg->first_line, OPTIONS, strlen(OPTIONS)) == 0) + return handle_options(output, conn_ctx, msg); + + printf("Unknown method: %s\n", msg->first_line); + return -1; +} + + +/* --------------------- A basic RTSP server implementation ----------------- */ + +static void +rtsp_clear(struct rtsp_msg *msg) +{ + free(msg->first_line); + free(msg->content_type); +} + +// Very primitive RTSP message parser, hope you have a better one +static int +rtsp_parse(struct rtsp_msg *msg, uint8_t *in, size_t in_len) +{ + char *line; + int i; + + line = (char *)in; + for (i = 0; i < in_len; i++) + { + if (in[i] != '\n' && in[i - 1] != '\r') + continue; + + if (in[i - 2] == '\n' && in[i - 3] == '\r') + { + msg->bodylen = in_len - (i + 1); + if (msg->bodylen != msg->content_length) + { + printf("Incomplete read (have %zu, content-length %d), waiting for more data\n\n", msg->bodylen, msg->content_length); + rtsp_clear(msg); + return 1; + } + else if (msg->bodylen > 0) + msg->body = in + i + 1; + + break; + } + + in[i - 1] = '\0'; + + if (!msg->first_line) + msg->first_line = strdup(line); + + if (strncmp(line, "CSeq: ", strlen("CSeq: ")) == 0) + msg->cseq = atoi(line + strlen("CSeq: ")); + + if (strncmp(line, "Content-Length: ", strlen("Content-Length: ")) == 0) + msg->content_length = atoi(line + strlen("Content-Length: ")); + + if (strncmp(line, "Content-Type: ", strlen("Content-Type: ")) == 0 && !msg->content_type) + msg->content_type = strdup(line + strlen("Content-Type: ")); + + in[i - 1] = '\r'; + + line = (char *)in + i + 1; + } + + msg->data = in; + msg->datalen = in_len; + + return 0; +} + +static void +in_read_cb(struct bufferevent *bev, void *arg) +{ + struct connection_ctx *conn_ctx = arg; + struct evbuffer *input; + struct evbuffer *output; + uint8_t *plain; + size_t plain_len; + struct rtsp_msg msg = { 0 }; + int ret; + + input = bufferevent_get_input(bev); + output = bufferevent_get_output(bev); + + printf("\n--------------------------------------------------------------------------\n"); + + if (conn_ctx->pair_completed) + { + buffer_decrypt(conn_ctx->pending, input, conn_ctx); + } + else + { + evbuffer_add_buffer(conn_ctx->pending, input); + } + + // Pending holds all the message we have received so far, incl. what parts we + // might have received in previous callbacks + plain = evbuffer_pullup(conn_ctx->pending, -1); + plain_len = evbuffer_get_length(conn_ctx->pending); + + ret = rtsp_parse(&msg, plain, plain_len); + if (ret < 0) + { + printf("Could not parse RTSP message\n"); + goto error; + } + else if (ret == 1) + return; // Message incomplete, wait for more data + + ret = response_send(output, conn_ctx, &msg); + if (ret < 0) + { + goto error; + } + + error: + rtsp_clear(&msg); + evbuffer_drain(conn_ctx->pending, evbuffer_get_length(conn_ctx->pending)); + return; +} + +static void +in_event_cb(struct bufferevent *bev, short events, void *arg) +{ + struct connection_ctx *conn_ctx = arg; + + if (events & BEV_EVENT_ERROR) + printf("Error from bufferevent: %s\n", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR())); + + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) + bufferevent_free(bev); + + connection_free(conn_ctx); +} + + +/*------------------------- General server stuff ----------------------------*/ + +static void +in_accept_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *address, int socklen, void *ctx) +{ + struct event_base *base = evconnlistener_get_base(listener); + struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE); + struct connection_ctx *conn_ctx; + + conn_ctx = calloc(1, sizeof(struct connection_ctx)); + conn_ctx->pending = evbuffer_new(); + + bufferevent_setcb(bev, in_read_cb, NULL, in_event_cb, conn_ctx); + bufferevent_enable(bev, EV_READ | EV_WRITE); + + printf("New connection accepted\n"); +} + +static void +in_error_cb(struct evconnlistener *listener, void *ctx) +{ + int err = EVUTIL_SOCKET_ERROR(); + printf("Error occured %d (%s) on the listener\n", err, evutil_socket_error_to_string(err)); +} + +static struct evconnlistener * +listen_add(struct event_base *evbase, evconnlistener_cb req_cb, evconnlistener_errorcb err_cb, unsigned short port) +{ + struct evconnlistener *listener; + struct addrinfo hints = { 0 }; + struct addrinfo *servinfo; + char strport[8]; + int ret; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_PASSIVE; + + snprintf(strport, sizeof(strport), "%hu", port); + ret = getaddrinfo(NULL, strport, &hints, &servinfo); + if (ret < 0) + { + printf("getaddrinf() failed: %s\n", gai_strerror(ret)); + return NULL; + } + + listener = evconnlistener_new_bind(evbase, req_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, servinfo->ai_addr, servinfo->ai_addrlen); + freeaddrinfo(servinfo); + if (!listener) + { + printf("Could not create listener for port %hu\n", port); + return NULL; + } + + evconnlistener_set_error_cb(listener, err_cb); + + return listener; +} + +int +main(int argc, char * argv[]) +{ + struct event_base *evbase; + struct evconnlistener *listener; + + evbase = event_base_new(); + + listener = listen_add(evbase, in_accept_cb, in_error_cb, LISTEN_PORT); + if (!listener) + return -1; + + printf("Listening for pairing requests on port %d\n", LISTEN_PORT); + + event_base_dispatch(evbase); + + evconnlistener_free(listener); + event_base_free(evbase); + + return 0; +} From b3f32feccbcf1ceb868d045d0b28a662ea015a7f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 14:21:34 +0000 Subject: [PATCH 260/878] Remove the ADVANCED folder -- not used anymore. --- ADVANCED/CONFIGURATION_OPTIONS.md | 76 ------------------------------- ADVANCED/README.md | 10 ---- 2 files changed, 86 deletions(-) delete mode 100644 ADVANCED/CONFIGURATION_OPTIONS.md delete mode 100644 ADVANCED/README.md diff --git a/ADVANCED/CONFIGURATION_OPTIONS.md b/ADVANCED/CONFIGURATION_OPTIONS.md deleted file mode 100644 index 4553a48b..00000000 --- a/ADVANCED/CONFIGURATION_OPTIONS.md +++ /dev/null @@ -1,76 +0,0 @@ -Under Construction -===== - -Configuration Flags -=== -Configuration flags determine the features of Shairport Sync that are included. The idea is to have a compact core set of features, so that Shairport Sync can be installed in low-capacity devices. Extra features that take up extra space can be added using configuration flags. - -Back Ends ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-alsa` | Choose ALSA API support (GNU/Linux only) | - | `--with-pa` | Choose PulseAudio support. | - | `--with-sndio` | Choose SNDIO API support | - | `--with-jack` | Include a Jack Audio Connection Kit (jack) backend | - | `--with-stdout` | Include the stdout audio back end | - | `--with-pipe` | Include the pipe audio back end | - | `--with-dummy` | Include the dummy audio back end | - | `--with-ao` | Choose AO (Audio Output?) API support. | - | `--with-soundio` | Choose soundio API support. | - -Operating System Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-os=OSType` | Specify the distribution to target: One of `linux`, `freebsd` or `darwin`. | - -Installation Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-systemv` | Install a System V startup script during a make install | - | `--with-systemd` | Install a systemd startup script during a make install | - | `--with-freebsd-service` | Install a FreeBSD startup script during a make install | - | `--with-cygwin-service` | Install a CYGWIN config script during a make install | - | `--with-configfiles` | Install configuration files during a make install | - -Zeroconf/Bonjour Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-external-mdns` | Support the use of `avahi-publish-service` or `mDNSPublish` to advertise the serviceon Bonjour/ZeroConf | - | `--with-avahi` | Choose Avahi-based mDNS support | - | `--with-tinysvcmdns` | Choose tinysvcmdns-based mDNS support | - | `--with-dns_sd` | Choose `dns_sd` mDNS support | - -Interprocess Communication ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-dbus-interface` | Include support for the native Shairport Sync D-Bus interface | - | `--with-dbus-test-client` | Compile dbus test client | - | `--with-mpris-interface` | Include support for a D-Bus interface conforming to the MPRIS standard | - | `--with-mpris-test-client` | Compile an MPRIS test client | - | `--with-mqtt-client` | Include a client for the Message Queuing Telemetry Transport (MQTT) protocol | - -Audio Features ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-apple-alac` | Include support for the Apple ALAC decoder | - | `--with-soxr` | Choose `libsoxr` for high-quality interpolation | - | `--with-convolution` | Choose audio DSP convolution support | - - -General ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-pkg-config` | Use pkg-config to find libraries | - | `--with-libdaemon` | Include support for daemonising in older non-systemd systems | - | `--with-piddir=` | Specify a pathname to a directory in which to write the PID file. - | `--with-ssl=` | Choose `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption services. | - | `--with-metadata` | Include support for a metadata feed | - | `--with-systemdsystemunitdir=DIR` | Specify the directory for `systemd` service files | - diff --git a/ADVANCED/README.md b/ADVANCED/README.md deleted file mode 100644 index 055b176a..00000000 --- a/ADVANCED/README.md +++ /dev/null @@ -1,10 +0,0 @@ -Advanced Topics [Under Construction] -====== - -Shairport Sync has many advanced features. -Some may enhance the audio experience and some may be of interest if you want to integrate Shairport Sync into an existing system -Some advanced features allow you to get metadata about the material being played, including cover art, and some remote control of the audio source may be available. - -Configuration Options ------- -Please take a look at [CONFIGURATION_OPTIONS.md](CONFIGURATION_OPTIONS.md) for a discussion of the options you can use when configuring the building of Shairport Sync. From dca7b9c6e30415a61d1bc8ad15252cfb3c847e26 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:33:17 +0100 Subject: [PATCH 261/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 8814664c..540a9516 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -67,20 +67,20 @@ Download, install and start/enable `nqptp` from [here](https://github.com/mikebr The `nqptp` service monitors PTP clocks. It provides a POSIX Shared Memory Interface (SMI) at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. -Next, download Shairport Sync, configure it, compile and install it: +Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` $ git clone git@github.com:aillwee/shairport-sync.git $ cd shairport-sync $ git checkout development -$ git submodule init -$ git submodule update $ autoreconf -fi -$ CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata \ - --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 +$ ./configure --sysconfdir=/etc --with-alsa \ + --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 $ make -j # make install ``` -Now to configure Shairport Sync. Here, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. Thus, at the end of the output after the command `$ shairport-sync -h` on a Raspberry Pi, for example, the following appears: +By the way, the `autoreconf` step may take quite a while -- be patient! + +Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: ``` ... From 91cbcb96dbe02fff83fee12a2474c4d4af949baa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:33:29 +0100 Subject: [PATCH 262/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 5d4e26db..07dac81d 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -46,36 +46,48 @@ First, install the packages needed by Shairport Sync: # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev ``` -Next, download Shairport Sync, configure it, compile and install it: +Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` $ git clone https://github.com/aillwee/shairport-sync.git $ cd shairport-sync $ git checkout development -$ git submodule init -$ git submodule update $ autoreconf -fi $ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd $ make $ sudo make install ``` -By the way, the `autoreconf` step may take quite a while on a Raspberry Pi -- be patient! +By the way, the `autoreconf` step may take quite a while -- be patient! + +Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: -Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: ``` -// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC: +... +Settings and options for the audio backend "alsa": + -d output-device set the output device, default is "default". + -c mixer-control set the mixer control name, default is to use no mixer. + -m mixer-device set the mixer device, default is the output device. + -i mixer-index set the mixer index, default is 0. + hardware output devices: + "hw:Headphones" +``` +Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. + +Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: +``` +// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC general = { - volume_range_db = 60; + volume_range_db = 60; }; alsa = { - output_device = "hw:0"; + output_device = "hw:Headphones"; mixer_control_name = "Headphone"; }; ``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range. +The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. The next step is to enable Shairport Sync to start automatically on boot up: ``` From 101cc0273df570423bc4c9524e61cdfa5525f51e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:36:56 +0100 Subject: [PATCH 263/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 540a9516..9d630183 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -4,7 +4,6 @@ Experimental Build Instructions for AirPlay 2 * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). * AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) @@ -13,6 +12,8 @@ Experimental Build Instructions for AirPlay 2 Build and Install Instructions == +Overall, you'll be installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. + In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. ### Turn Off WiFi Power Management From 4fedf2f32f625f036e2a06634adc78a1b2aadecc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:38:27 +0100 Subject: [PATCH 264/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 9d630183..fa372960 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -21,7 +21,7 @@ If you are using WiFi, you should turn off WiFi Power Management: ``` # iwconfig wlan0 power off ``` -WiFi Power Management will put the WiFi system in low-power mode when the WiFi system considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) +WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) ### Configure and Update Do the usual update and upgrade: From 4552172c2cf9e8732ff96924c0b1c80f4ee7d6e7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:44:24 +0100 Subject: [PATCH 265/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index fa372960..c0ab0cd4 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -61,12 +61,11 @@ First, install the packages needed by Shairport Sync: (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) ### nqptp ### -Download, install and start/enable `nqptp` from [here](https://github.com/mikebrady/nqptp). +Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). ***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. -The `nqptp` service monitors PTP clocks. It provides a POSIX Shared Memory Interface (SMI) at `/nqptp`. A shared -pthread mutex is contained within the interface, and to use it you need write access. +The `nqptp` service monitors PTP clocks. It provides a [POSIX Shared Memory](https://man7.org/linux/man-pages/man7/shm_overview.7.html) Interface at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` From abe7eecc28a8caa73608111252a7288a3d226e0c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:45:40 +0100 Subject: [PATCH 266/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index c0ab0cd4..df956758 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,10 +1,10 @@ Experimental Build Instructions for AirPlay 2 == -* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected! +* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected. * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. +* AirPlay 2 uses a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. From dcd3d3ab549e3f2bbec9a72b132e00b2283f18de Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:50:19 +0100 Subject: [PATCH 267/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index df956758..a1000d49 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -6,13 +6,14 @@ Experimental Build Instructions for AirPlay 2 * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * AirPlay 2 uses a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. - (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) -* When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. + (FYI, most computers do not have a PTP clock running. They often use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) +* Shairport Sync has a build dependency on `nqptp`, so you should check and update `nqptp` *before* building or updating Shairport Sync. +* The SMI Interface Version Numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. Build and Install Instructions == -Overall, you'll be installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. +Overall, you'll be installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. From e8e420f2ed4357e23a1a9fa181a558181a889340 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 15:54:30 +0100 Subject: [PATCH 268/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index a1000d49..65bc3599 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -13,7 +13,7 @@ Experimental Build Instructions for AirPlay 2 Build and Install Instructions == -Overall, you'll be installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. +Overall, you'll be building and installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. @@ -24,7 +24,7 @@ If you are using WiFi, you should turn off WiFi Power Management: ``` WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) -### Configure and Update +### Update Your System Do the usual update and upgrade: ``` # apt-get update @@ -64,11 +64,11 @@ First, install the packages needed by Shairport Sync: ### nqptp ### Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). -***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. +***Note:*** Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. The `nqptp` service monitors PTP clocks. It provides a [POSIX Shared Memory](https://man7.org/linux/man-pages/man7/shm_overview.7.html) Interface at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. -Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: +Next, download Shairport Sync, check out the `development` branch and configure, compile and install it: ``` $ git clone git@github.com:aillwee/shairport-sync.git $ cd shairport-sync @@ -81,7 +81,9 @@ $ make -j ``` By the way, the `autoreconf` step may take quite a while -- be patient! -Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: +Now to configure Shairport Sync. In this walkthrough, it is configured for an `alsa` output device. + +A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: ``` ... From 8e231c7ed327317bba5f251c4704f7dab63f5e4e Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 7 Jul 2021 16:50:13 +0100 Subject: [PATCH 269/878] Remove submodule docker lines. --- docker/Dockerfile | 2 -- docker/airplay1/Dockerfile | 2 -- 2 files changed, 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6da101a1..1203b1c6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,8 +68,6 @@ WORKDIR / WORKDIR /shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" -RUN git submodule init -RUN git submodule update RUN autoreconf -fi # RUN ./configure \ # --with-alsa \ diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile index 8b21dba3..e60fd0b5 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/airplay1/Dockerfile @@ -44,8 +44,6 @@ COPY --from=builder-alac /usr/local/include /usr/local/include WORKDIR shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" -RUN git submodule init -RUN git submodule update RUN autoreconf -fi RUN ./configure \ --with-alsa \ From 27895723b96a8972a361822048e185096573f8e2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Jul 2021 17:00:52 +0000 Subject: [PATCH 270/878] Prevent a crash when statistics are requested over the D-Bus interface by preparing for them even when not initially requested. --- player.c | 58 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/player.c b/player.c index a279f027..70ba36ec 100644 --- a/player.c +++ b/player.c @@ -1984,44 +1984,44 @@ void *player_thread_func(void *arg) { conn->correctionsRequestedInThisEpoch = 0; statistics_row = 0; // statistics_line 0 means print the headings; anything else 1 means print the // values. Set to 0 the first time out. - if (config.statistics_requested) { - // decide on what statistics profile to use + + // decide on what statistics profile to use, if requested #ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_type == ap_2) { - if (conn->airplay_stream_type == realtime_stream) { - if (config.output->delay) { - if (config.no_sync == 0) - statistics_print_profile = ap2_realtime_synced_stream_statistics_print_profile; - else - statistics_print_profile = ap2_realtime_nosync_stream_statistics_print_profile; - } else { - statistics_print_profile = ap2_realtime_nodelay_stream_statistics_print_profile; - } - } else { - if (config.output->delay) { - if (config.no_sync == 0) - statistics_print_profile = ap2_buffered_synced_stream_statistics_print_profile; - else - statistics_print_profile = ap2_buffered_nosync_stream_statistics_print_profile; - } else { - statistics_print_profile = ap2_buffered_nodelay_stream_statistics_print_profile; - } - } - } else { -#endif + if (conn->airplay_type == ap_2) { + if (conn->airplay_stream_type == realtime_stream) { if (config.output->delay) { if (config.no_sync == 0) - statistics_print_profile = ap1_synced_statistics_print_profile; + statistics_print_profile = ap2_realtime_synced_stream_statistics_print_profile; else - statistics_print_profile = ap1_nosync_statistics_print_profile; + statistics_print_profile = ap2_realtime_nosync_stream_statistics_print_profile; } else { - statistics_print_profile = ap1_nodelay_statistics_print_profile; + statistics_print_profile = ap2_realtime_nodelay_stream_statistics_print_profile; } + } else { + if (config.output->delay) { + if (config.no_sync == 0) + statistics_print_profile = ap2_buffered_synced_stream_statistics_print_profile; + else + statistics_print_profile = ap2_buffered_nosync_stream_statistics_print_profile; + } else { + statistics_print_profile = ap2_buffered_nodelay_stream_statistics_print_profile; + } + } + } else { +#endif + if (config.output->delay) { + if (config.no_sync == 0) + statistics_print_profile = ap1_synced_statistics_print_profile; + else + statistics_print_profile = ap1_nosync_statistics_print_profile; + } else { + statistics_print_profile = ap1_nodelay_statistics_print_profile; + } // airplay 1 stuff here #ifdef CONFIG_AIRPLAY_2 - } -#endif } +#endif + #ifdef CONFIG_AIRPLAY_2 if (conn->timing_type == ts_ntp) { From 62dbf22278f6d4424e931d8f7a2e587cd2aa26f1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:33:47 +0100 Subject: [PATCH 271/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 65bc3599..f5554897 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,14 +1,15 @@ -Experimental Build Instructions for AirPlay 2 +Build Instructions for AirPlay 2 == +* Build instructions are different from previous versions of Shairport Sync. Please read carefully. * This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected. * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! -* Initially, at least, leave the settings in the configuration file at default except as noted below. -* At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* AirPlay 2 uses a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - In addition, `nqptp` must run with `root` privileges. - (FYI, most computers do not have a PTP clock running. They often use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) -* Shairport Sync has a build dependency on `nqptp`, so you should check and update `nqptp` *before* building or updating Shairport Sync. -* The SMI Interface Version Numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. +* for the moment, leave the settings in the configuration file at default except as noted below. +* At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. +* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. + In addition, `nqptp` must run with `root` privileges. + (FYI, most computers do not have a PTP clock running. They often use a totally different [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) +* Shairport Sync has a build dependency on `nqptp`, so you should build or update `nqptp` *before* building or updating Shairport Sync. +* The POSIX Shared Memory Interface (SMI) Version Numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. Build and Install Instructions @@ -68,7 +69,20 @@ Download, install, enable and start `nqptp` from [here](https://github.com/mikeb The `nqptp` service monitors PTP clocks. It provides a [POSIX Shared Memory](https://man7.org/linux/man-pages/man7/shm_overview.7.html) Interface at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. -Next, download Shairport Sync, check out the `development` branch and configure, compile and install it: +### Shairport Sync + +#### Please use `git` to clone the repository! +As you probably know, you can download the repository in two ways -- (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` commit hash in the version information. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: +``` +Version with git hash: +4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc + +Version without git hash: +4.0-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc +``` + +#### Build and Install +Download Shairport Sync, check out the `development` branch and configure, compile and install it: ``` $ git clone git@github.com:aillwee/shairport-sync.git $ cd shairport-sync @@ -81,6 +95,7 @@ $ make -j ``` By the way, the `autoreconf` step may take quite a while -- be patient! +#### Configure Now to configure Shairport Sync. In this walkthrough, it is configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: @@ -114,7 +129,7 @@ alsa = ``` The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. -### Automatic Start ### +#### Automatic Start To enable Shairport Sync to start automatically on boot up: ``` @@ -125,7 +140,7 @@ Now, either reboot or start the `shairport-sync` service: # systemctl start shairport-sync ``` -### Running From the Command Line ### +#### Running From the Command Line You may wish to run Shairport Sync from the command line (but remember to ensure it is not already running as a daemon). To enable debug messages and statistics, use the following: @@ -134,12 +149,12 @@ $ shairport-sync -vu --statistics ``` The user doesn't need to be privileged, but must be a member of the `audio` group for access to the `alsa` subsystem. -### Using Shairport Sync ### +### Using Shairport Sync The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ubuntu`. You can change the service name in the configuration file. Connect and enjoy... -### Restart Your Mac! ### +### Restart Your Mac! -At the time of writing, there seems to be a bug in the Mac Music app. It appears that, when the Mac has been awoken after sleeping, the Music app may not fully reawaken, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. +At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. From 3738aec4e8bec66a97bd0124d5b0072917e5972c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:43:58 +0100 Subject: [PATCH 272/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index f5554897..2e567724 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -67,17 +67,15 @@ Download, install, enable and start `nqptp` from [here](https://github.com/mikeb ***Note:*** Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. -The `nqptp` service monitors PTP clocks. It provides a [POSIX Shared Memory](https://man7.org/linux/man-pages/man7/shm_overview.7.html) Interface at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. - ### Shairport Sync #### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways -- (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` commit hash in the version information. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: +As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version information. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: ``` -Version with git hash: +Version with git information: 4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc -Version without git hash: +Version without git information: 4.0-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc ``` From ad6de95b49c18a4e700dfc70fb7ca37914289316 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:45:34 +0100 Subject: [PATCH 273/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 2e567724..5749fa15 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -7,9 +7,9 @@ Build Instructions for AirPlay 2 * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. - (FYI, most computers do not have a PTP clock running. They often use a totally different [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) + (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * Shairport Sync has a build dependency on `nqptp`, so you should build or update `nqptp` *before* building or updating Shairport Sync. -* The POSIX Shared Memory Interface (SMI) Version Numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. +* The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. Build and Install Instructions From 06274c2f7b876d744bd231cd8fbbe03235426914 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:46:45 +0100 Subject: [PATCH 274/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 5749fa15..99af3b72 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -3,7 +3,7 @@ Build Instructions for AirPlay 2 * Build instructions are different from previous versions of Shairport Sync. Please read carefully. * This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected. * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! -* for the moment, leave the settings in the configuration file at default except as noted below. +* For now, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. * Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. From cc7b5e7f724803b43bbeb17217384788bd4c4bc9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:48:22 +0100 Subject: [PATCH 275/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 99af3b72..71e789d8 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -16,7 +16,7 @@ Build and Install Instructions == Overall, you'll be building and installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. -In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. +In the commands below, for a Debian/Ubuntu-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. ### Turn Off WiFi Power Management If you are using WiFi, you should turn off WiFi Power Management: From 4cfa1416eef854b2891bbd4f3e465eeb87b484d3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:59:07 +0100 Subject: [PATCH 276/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 71e789d8..d9e019e3 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,8 +5,7 @@ Build Instructions for AirPlay 2 * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * For now, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - In addition, `nqptp` must run with `root` privileges. +* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * Shairport Sync has a build dependency on `nqptp`, so you should build or update `nqptp` *before* building or updating Shairport Sync. * The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. From a7047e41d7dfeeb8bd2b1e583271678750b57a11 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:03:31 +0100 Subject: [PATCH 277/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index d9e019e3..8bbc2a09 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,4 +1,4 @@ -Build Instructions for AirPlay 2 +Building Shairport Sync for AirPlay 2 == * Build instructions are different from previous versions of Shairport Sync. Please read carefully. * This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected. @@ -11,7 +11,7 @@ Build Instructions for AirPlay 2 * The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. -Build and Install Instructions +Instructions == Overall, you'll be building and installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. @@ -69,7 +69,7 @@ Download, install, enable and start `nqptp` from [here](https://github.com/mikeb ### Shairport Sync #### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version information. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: +As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: ``` Version with git information: 4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc From cdc249ba51bc7ac8cc14da05bb6a86a43669e6f2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:02:40 +0000 Subject: [PATCH 278/878] Temporarily remove prior to making public. --- .github/workflows/docker-build.yaml | 44 --------- docker/Dockerfile | 139 ---------------------------- docker/README.md | 12 --- docker/docker-compose.yaml | 12 --- docker/start.sh | 18 ---- documentation/Example Setups.md | 21 ----- 6 files changed, 246 deletions(-) delete mode 100644 .github/workflows/docker-build.yaml delete mode 100644 docker/Dockerfile delete mode 100644 docker/README.md delete mode 100644 docker/docker-compose.yaml delete mode 100644 docker/start.sh delete mode 100644 documentation/Example Setups.md diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml deleted file mode 100644 index fc670487..00000000 --- a/.github/workflows/docker-build.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Pushes to tag 'unstable-BRANCH'. -# Releases are pushed to latest and tagged - see docker-build-releases.yaml. - -name: Build and push docker image based on specified branches. - -on: - push: - branches: [ master, development, charles-dev ] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Set SHAIRPORT_SYNC_BRANCH env. - run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Set up ssh agent - uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Login to Docker Registry - uses: docker/login-action@v1 - with: - registry: registry.nova.do - username: ${{ secrets.DOCKER_REGISTRY_USER }} - password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./docker/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 - push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} - build-args: | - SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 40822505..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,139 +0,0 @@ -FROM alpine:3.12 AS builder-base -# Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard -# in debian/ubuntu. - -# General Build System: -RUN apk -U add \ - git \ - build-base \ - autoconf \ - automake \ - libtool \ - dbus \ - su-exec \ - alsa-lib-dev \ - libdaemon-dev \ - popt-dev \ - mbedtls-dev \ - soxr-dev \ - avahi-dev \ - libconfig-dev \ - libsndfile-dev \ - mosquitto-dev \ - xmltoman \ - openssh-client \ - libsodium-dev \ - ffmpeg-dev \ - xxd \ - libressl-dev \ - openssl-dev \ - libplist-dev \ - libgcrypt-dev - -# ALAC Build System: -FROM builder-base AS builder-alac - -RUN git clone https://github.com/mikebrady/alac -WORKDIR /alac -RUN autoreconf -fi -RUN ./configure -RUN make -RUN make install - -# Shairport Sync Build System: -FROM builder-base AS builder-sps - -# This will be modified by the Github Action Workflow and is required -# to ensure the correct branch is being used. -ARG SHAIRPORT_SYNC_BRANCH -RUN test -n "$SHAIRPORT_SYNC_BRANCH" - -COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ -COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc -COPY --from=builder-alac /usr/local/include /usr/local/include - -# TODO: Remove when no longer pulling from private repos. -RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - -RUN --mount=type=ssh git clone git@github.com:mikebrady/nqptp.git -WORKDIR /nqptp -# Use development branch for now. -RUN git checkout development -RUN autoreconf -fi -RUN ./configure -RUN make -RUN make install -WORKDIR / - -WORKDIR /shairport-sync -COPY . . -RUN git checkout $SHAIRPORT_SYNC_BRANCH -RUN git submodule init -RUN git submodule update -RUN autoreconf -fi -# RUN ./configure \ -# --with-alsa \ -# --with-dummy \ -# --with-pipe \ -# --with-stdout \ -# --with-avahi \ -# --with-ssl=mbedtls \ -# --with-soxr \ -# --sysconfdir=/etc \ -# --with-dbus-interface \ -# --with-mpris-interface \ -# --with-mqtt-client \ -# --with-apple-alac \ -# --with-convolution \ -# --with-airplay-2 -# RUN make -j $(nproc) -RUN CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata --with-dummy --with-pipe \ - --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-dbus-interface --with-stdout \ - --with-mpris-interface --with-mqtt-client --with-apple-alac --with-convolution --with-airplay-2 -RUN make -j -RUN make install - -# Shairport Sync Runtime System: -FROM alpine:3.12 - -RUN apk -U add \ - alsa-lib \ - dbus \ - popt \ - glib \ - mbedtls \ - soxr \ - avahi \ - libconfig \ - libsndfile \ - mosquitto \ - su-exec \ - libuuid \ - ffmpeg \ - libsodium \ - libplist \ - libgcrypt - -RUN rm -rf /lib/apk/db/* - -COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ -COPY --from=builder-sps /etc/shairport-sync* /etc/ -COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ -COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ -COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync -COPY --from=builder-sps /usr/local/bin/nqptp /usr/local/bin/nqptp - -# Create non-root user for running the container -- running as the user 'shairport-sync' also allows -# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container - -RUN addgroup shairport-sync -RUN adduser -D shairport-sync -G shairport-sync - -# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff -RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio - -COPY ./docker/start.sh / - -RUN chmod +x ./start.sh - -ENTRYPOINT [ "/start.sh" ] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index a460af13..00000000 --- a/docker/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Shairport Sync Docker Image - -Available at: https://hub.docker.com/r/mikebrady/shairport-sync - -## Build Example (for arm7 devices) - -TODO: remove `--ssh` when going public as this is only for private repos. -``` -docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . -``` - -`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index 672639c9..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Example docker compose config. - -version: "3.8" -services: - shairport-sync: - image: registry.nova.do/shairport-sync:unstable-development - network_mode: host - restart: unless-stopped - devices: - - "/dev/snd" - # volumes: - # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100644 index bb790283..00000000 --- a/docker/start.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -set -e - -rm -rf /var/run/dbus.pid -#mkdir -p /var/run/dbus - -dbus-uuidgen --ensure -dbus-daemon --system - -avahi-daemon --daemonize --no-chroot - -# TODO: This should probably be implemented in a better way and using the correct nqptp group. -nqptp & - -# su-exec shairport-sync shairport-sync $@ - -shairport-sync -vu --statistics \ No newline at end of file diff --git a/documentation/Example Setups.md b/documentation/Example Setups.md deleted file mode 100644 index 7ce6f658..00000000 --- a/documentation/Example Setups.md +++ /dev/null @@ -1,21 +0,0 @@ -Example Setups -=== - -Amazon Echo Devices ---- -Since some Amazon Echo devices have either a 3.5mm input or can be used as a bluetooth speaker, Shairport Sync can be connected allowing you to Airplay to these speakers. Currently using the 3.5mm jack is the most reliable method - bluetooth connectivity can have an unreliable delay. Check first to see if your Echo device supports either of these. Volume control of the Echo device can be added via Home Assistant and a MQTT broker. Below goes through some example setups. -### _Raspberry Pi --> Echo 3.5mm Input_ -* Set the Echo device to use the 3.5mm jack as an input. -* Via the Shairport Sync config, set the correct `output_device` and `mixer_control_name` for your configuration. Likely to be -`output_device = "hw:1";` and `mixer_control_name = "Headphone";`. -* At this point, you should be able to hear music via the Echo. Remember that so far, the volume of the Echo and Shairport Sync are _independent_. If you prefer, you can disable any volume control in Shairport Sync by setting `ignore_volume_control = "yes";`. -* Amazon Echo devices appear to add a bit of latency, currently this is thought to be 0.12 seconds. This can be countered by setting the following: `audio_backend_latency_offset_in_seconds = -0.12;`. This was measured _very_ crudely using Audacity so could be wrong. If anyone is able to determine the latency more accurately, please open an issue or pull request so these documents can be updated. -* You should now be able to use your Echo device in sync with other AirPlay devices! - -### _Raspberry Pi --> Echo as Bluetooth Speaker (Docker Version)_ -* WIP - -### _Raspberry Pi --> Echo with Volume Control_ -* Before continuing with volume control via Home Assistant, please note that currently only one way volume control is tested - Shairport Sync to the Echo device. This means if the volume of the Echo is changed (e.g. by voice control or by the physical controls on the device), this will not be reflected in the volume of Shairport Sync and will be overwritten the next time Shairport Sync's volume is changed. -* This setup requires Home Assistant and a MQTT broker. The basic idea is that Shairport Sync sends a MQTT message containing the new volume amount to the MQTT broker when the volume is changed, this is then picked up by Home Assistant (which is monitoring the MQTT broker). Home Assistant will then send a command to the Amazon Echo device to change the volume. -* WIP From e8076187a7e0f7020220158df791d037d0f03f48 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:15:53 +0000 Subject: [PATCH 279/878] Remove superceded document. --- README-DEVELOPMENT.md | 589 ------------------------------------------ 1 file changed, 589 deletions(-) delete mode 100644 README-DEVELOPMENT.md diff --git a/README-DEVELOPMENT.md b/README-DEVELOPMENT.md deleted file mode 100644 index 8193f84e..00000000 --- a/README-DEVELOPMENT.md +++ /dev/null @@ -1,589 +0,0 @@ -Shairport Sync -============= -Shairport Sync is an AirPlay audio player – it plays audio streamed from iTunes, iOS, Apple TV and macOS devices and AirPlay sources such as Quicktime Player and [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/), among others. - -Audio played by a Shairport Sync-powered device stays synchronised with the source and hence with similar devices playing the same source. In this way, synchronised multi-room audio is possible for players that support it, such as iTunes. - -Shairport Sync runs on Linux and FreeBSD. It does not support AirPlay video or photo streaming. - -This is the unstable "development" branch. Changes and updates are incorporated into this branch quickly. To access the stable version, where changes are made after due time, please switch to the "master" branch. - -More Information ----------- -Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). - -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. - -For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. - -For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. - -Synchronisation, Latency, "Stuffing" ---------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. - -As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. - -Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. - -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. - -What else? --------------- -* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. -* Hardware Mute — Shairport Sync can mute properly if the hardware supports it. -* Support for the Apple ALAC decoder. -* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. -* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.2 seconds with `alsa`, 0.35 seconds with `sndio`. -* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) -* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in iOS. -* Support for the [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) protocol with some support for remote control of the source. -* A native D-Bus IPC interface with access to metadata and with some support for remote control of the source. -* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. -* Raw Audio — Shairport Sync can deliver raw PCM audio to standard output or to a pipe. This output is delivered synchronously with the source after the appropriate latency and is not interpolated or "stuffed" on its way through Shairport Sync. -* Autotools and Libtool Support — the Shairport Sync build process uses GNU `autotools` and `libtool` to examine and configure the build environment — important for portability and for cross compilation. Previous versions of Shairport looked at the current system to determine which packages were available, instead of looking at the target system for what packages were available. - -Heritage -------- -Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/development/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. - -Status ------- -Shairport Sync works on a wide variety of Linux distributions including Debian, Ubuntu, Fedora asnd OpenWrt and works on FreeBSD and on OpenBSD. It works with built-in audio and with a variety of USB-connected sound cards, audio amplifiers and DACs, and works with I2S sound cards. - -Shairport Sync runs well on the Raspberry Pi -- including the original Raspberry Pi B -- using the built-in sound card or on USB and I2S cards. - -Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. - -Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system. - -Shairport Sync runs on Linuxes and the BSDs inside VMWare Fusion on a Mac, but synchronisation is inaccurate — possibly because the sound card is being emulated. - -In addition, Shairport Sync can output to standard output, pipes, `soundio` and `ao` devices using appropriate backends. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing the Development Version ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/development/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -The following procedures will build and install the `shairport-sync` application into your system. - -**Remove Old Versions of Shairport Sync and its Startup Scripts** - -You should check to see if `shairport-sync` is already installed – you can use the command `$ which -a shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. - -You should also remove the initialisation script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. - -**FreeBSD, OpenBSD, CYGWIN** - -Please refer to the relevant pages for building for the above systems. - -**Determine The Configuration Needed** - -Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: unix:/run/user/1000/pulse/native -Library Protocol Version: 30 -Server Protocol Version: 30 -Is Local: yes -Client Index: 9 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu -Server Name: pulseaudio -Server Version: 8.0 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 96f9:3e8d -$ -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. - -If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. - -**Building** - -To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. - -The following libraries are required: -* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) -* Avahi -* ALSA and/or PulseAudio -* autoconf -* automake -* libtool -* libpopt -* libconfig - -If you are building for a system that needs to be able to daemonise Shairport Sync with the `-d` option and to kill the daemon with the `-k` option, you need to include the `libdaemon` library: -* libdaemon - -Most current releases of Linuxes use the `systemd` startup and system manager, and this daemonises applications differently, so the `libdaemon` library can be omitted. - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) -* libmosquitto (This is to enable the mqtt-metadata-interface.) - -Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power — chances are an embedded processor won't be able to keep up. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt-get install build-essential git xmltoman` – these may already be installed. -- `# apt-get install autoconf automake libtool libpopt-dev libconfig-dev` -- `# apt-get install libdaemon-dev` – if you need libdaemon support. -- `# apt-get install libasound2-dev` for the ALSA libraries -- `# apt-get install libpulse-dev` for the PulseAudio libraries -- `# apt-get install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt-get install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt-get install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt-get install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt-get install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in many recent distributions; if not, instructions for how to build it from source for Rasbpian/Debian Wheezy are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). -- `# apt-get install libmosquitto-dev` if you want to use the mqtt client - -If you just want to have the default configuration options (systemd): -- `# apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libpulse-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev` - -If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. - -**Download Shairport Sync:** -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -``` - -Next, `cd` into the shairport-sync directory and execute the following commands: -``` -$ git checkout development -$ autoreconf -i -f -``` -(Note that the `autoreconf...` step may take some time on less powerful machines.) - -**Choose the appropriate `--with-*` options:** - -(Don't worry -- there's a recommended set of configuration options further down.) - -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. -- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. -- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). -- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. -- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system. -- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. -- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. -- `--with-soxr` for libsoxr-based resampling. -- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. -- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. -- `--with-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT). -- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. -- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. -- `--with-apple-alac` to include the Apple ALAC Decoder. -- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. -- `--with-libdaemon` to allow Shairport Sync to daemonise itself. -- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. -- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. - -**Determine if it's a `systemd` or a "System V" installation:** - -If you wish to have Shairport Sync start automatically when your system boots or to daemonise itself and run in the background, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) - -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. - -The easiest way to look at the first few lines of the `init` manual. Enter the command: - -``` -$ man init -``` -In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): -``` -SYSTEMD(1) systemd SYSTEMD(1) - -NAME - systemd, init - systemd system and service manager -... -``` -Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): - -``` -init(8) System Manager's Manual init(8) - -NAME - init - Upstart process management daemon - -SYNOPSIS - init [OPTION]... -... -``` -In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): -``` -INIT(8) Linux System Administrator's Manual INIT(8) - -NAME - init, telinit - process control initialization - -SYNOPSIS - /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] - /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] - /sbin/telinit [ -e VAR[=VAL] ] - -... -``` -If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-systemv`. - -**Choose the location of the configuration file** - -A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. - -**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** - -Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: - -`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd` - -* Omit the `--with-soxr` if the libsoxr library is not available. -* For installation into a System V system, add `--with-libdaemon` and replace the `--with-systemd` with `--with-systemv`. -* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`. If you want to be able to daemonise Shairport Sync without installing it as a service, include the `--with-libdaemon` flag. - -**Build and Install the Application:** - -Enter: - -``` -$ make -``` - -to build the application. - -Assuming you have used the `./configure` settings suggested above, you can now install the application: - -``` -$ sudo make install -``` -The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. - - -**Complete installation into to a `systemd` system** - -If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: - -``` -$ sudo systemctl enable shairport-sync -``` - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -``` - -**Identifying the configuration of an installed Shairport Sync - -To find out what configuration Shairport Sync was built with, enter: -``` -$ shairport-sync -V -``` -This will return something like the following: -``` -3.3-libdaemon-OpenSSL-Avahi-ALSA-sndio-stdout-pipe-metadata-dbus-sysconfdir:/etc -``` -This information may be useful when [troubleshooting](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) or [reporting issues](https://github.com/mikebrady/shairport-sync/blob/master/CONTRIBUTING.md). -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html - -Configuring Shairport Sync --------- -There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. - -**(1) Starting and Stopping:** -Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. - -If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, it the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. - -**(2) Settings:** -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. - -For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control, if any, to use to control the output level. To get values for (b) and (c) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. - -Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card. - -Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room`, give the service the password `secret` and use `libsoxr` interpolation, then you should do the following: - -``` -general = -{ - name = "Front Room"; - password = "secret"; - interpolation = "soxr"; - // ... other general settings -}; -``` -(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) - -**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable. - -No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: - -``` - output_backend = "pa"; -``` -to the `general` stanza. - -The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. - -The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. -* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. -* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: - -``` -alsa = -{ - output_device = "hw:0,5"; - mixer_device = "hw:0"; - mixer_control_name = "PCM"; - // ... other alsa settings -}; -``` - -The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. You can also set the sink name that audio will be pushed to. Here's an example `pa` group configuration: - -``` -pa = -{ - application_name = "Shairport Sync - Bedroom"; - sink = "server_bedroom"; // Sink name or ID. If not specified, the default sink will be used. -}; -``` - -Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. - -Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) - -Shairport Sync can run a program whenever the volume is set or changed. You specify them using the `general` group settings `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. - -Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. - -**Raspberry Pi** - -The Raspberry Pi has a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is quite adequate for casual listening. A problem is that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 50 dB of it is in any way usable. To help get the most from the DAC, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 50, the top 50 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -46dB, below which muting will occur. - -From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. - -*Command Line Arguments* - -As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html. - -Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. - -* The `-c` option allows you to specify the location of the configuration file. -* The `-V` option gives you version information about Shairport Sync and then quits. -* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. This is available if Shairport Sync is built with the `--with-libdaemon` configuration flag. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. This is available if Shairport Sync is built with the `--with-libdaemon` configuration flag. - -The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : -`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. - -Examples --------- - -Here are some examples of complete configuration files. - -``` -general = { - name = "Joe's Stereo"; -}; - -alsa = { - output_device = "hw:0"; -}; -``` - -This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used. - -For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. - -Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: -``` -general = { - name = "Mike's Boombox"; -}; - -alsa = { - output_device = "hw:0"; - mixer_control_name = "PCM"; -}; -``` - -Here is an example of using soxr-based resampling and driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: -``` -general = { - name = "Kitchen"; - interpolation = "soxr"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: -``` -general = { - name = "Front Room"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "Speaker"; -}; -``` - -For a first generation Griffin iMic on a Raspberry Pi: -``` -general = { - name = "Attic"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For an NSLU2, to drive a first generation Griffin iMic, a USB sound card: -``` -general = { - name = "Den"; -}; - -alsa = { - mixer_control_name = "PCM"; -}; -``` - -On an NSLU2, to drive the "3D Sound" USB card: -``` -general = { - name = "TV Room"; -}; - -alsa = { - mixer_control_name = "Speaker"; -}; -``` - -Finally, here is an example of using the PulseAudio backend: -``` -general = { - name = "Zoe's Computer"; - output_backend = "pa"; -}; - -``` - - -Metadata --------- - -Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in iOS. - -**Metadata broadcasting over UDP** - -As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. - -The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. - -The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. - -Latency -------- -Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. - -Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. - -Resynchronisation -------------- -Shairport Sync actively maintains synchronisation with the source. -If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. -* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. - -Tolerance ---------- -Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. -* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. - -Some Statistics ---------------- -If you turn on the `general` `statistics` setting, a heading like this will be output to the console or log file: -``` -sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm -``` -This will be followed by the statistics themselves at regular intervals, for example: -``` -1.04, 0.0, 0.0, 5015, 0, 2, 0, 2, 7847, 202, 226, 44099.99, 44103.19, 44100.30, 0.00, 0, 7.16 -1.00, 0.0, 0.0, 6018, 0, 2, 0, 2, 8375, 215, 226, 44099.99, 44106.36, 44100.32, 0.00, 0, 7.43 -0.84, 0.0, 0.0, 7021, 0, 2, 0, 2, 8453, 217, 226, 44099.99, 44102.16, 44100.23, 0.00, 0, 5.51 -0.79, 0.0, 0.0, 8024, 0, 2, 0, 2, 8215, 216, 226, 44100.00, 44098.16, 44100.31, 0.00, 0, 7.22 -0.83, 2.8, 2.8, 9027, 0, 2, 0, 2, 8078, 217, 226, 44099.99, 44105.64, 44100.31, 0.00, 0, 7.37 -1.09, 2.8, 2.8, 10030, 0, 2, 0, 2, 8117, 215, 226, 44099.99, 44100.59, 44100.31, 0.00, 0, 7.25 -1.37, 0.0, 0.0, 11033, 0, 2, 0, 2, 8429, 217, 226, 44099.99, 44081.51, 44100.32, 19.27, 10, -11.87 -1.49, 0.0, 0.0, 12036, 0, 2, 0, 2, 8423, 215, 226, 44099.99, 44095.55, 44100.31, 25.43, 12, -18.13 -1.46, 2.8, 2.8, 13039, 0, 2, 0, 2, 8064, 217, 226, 44099.99, 44097.07, 44100.31, 22.39, 14, -15.16 -1.77, 2.8, 2.8, 14042, 0, 2, 0, 2, 8195, 216, 226, 44100.00, 44095.31, 44100.32, 22.96, 16, -15.76 -1.88, 0.0, 5.7, 15045, 0, 2, 0, 2, 8054, 215, 226, 44100.00, 44100.36, 44100.32, 24.36, 18, -17.18 -2.01, -39.7, 45.3, 16048, 0, 2, 0, 2, 8138, 217, 226, 44099.99, 44098.67, 44100.32, 25.23, 19, -17.71 -1.85, -5.7, 17.0, 17051, 0, 2, 0, 2, 8063, 218, 226, 44100.00, 44100.22, 44100.32, 25.43, 22, -18.15 -1.92, 0.0, 0.0, 18054, 0, 2, 0, 2, 8381, 217, 226, 44100.00, 44101.46, 44100.31, 24.62, 23, -17.42 -1.96, -17.0, 17.0, 19057, 0, 2, 0, 2, 8479, 216, 226, 44099.99, 44098.06, 44100.32, 24.19, 24, -16.82 -2.02, -90.6, 90.6, 20060, 0, 2, 0, 2, 8438, 217, 226, 44100.00, 44097.24, 44100.33, 26.16, 26, -18.73 -2.01, -25.5, 25.5, 21063, 0, 2, 0, 2, 8417, 216, 226, 44100.00, 44103.05, 44100.31, 26.16, 26, -18.97 -2.00, -19.8, 25.5, 22066, 0, 5, 0, 5, 8121, 217, 226, 44100.00, 44101.97, 44100.32, 26.12, 27, -18.83 -1.44, 0.0, 0.0, 23069, 0, 5, 0, 5, 8451, 216, 226, 44100.00, 44102.68, 44100.31, 25.49, 30, -18.37 -1.47, 0.0, 0.0, 24072, 0, 5, 0, 5, 8439, 217, 226, 44100.00, 44102.03, 44100.31, 24.92, 33, -17.75 -1.50, 0.0, 0.0, 25075, 0, 5, 0, 5, 8401, 214, 226, 44100.00, 44102.00, 44100.32, 24.92, 33, -17.66 -1.86, 2.8, 8.5, 26078, 0, 5, 0, 5, 7778, 215, 227, 44100.00, 44103.63, 44100.31, 24.66, 35, -17.62 -1.87, -17.0, 17.0, 27081, 0, 5, 0, 5, 8489, 217, 226, 44100.00, 44100.92, 44100.32, 24.15, 37, -16.98 -1.92, -5.7, 17.0, 28084, 0, 5, 0, 5, 8172, 217, 226, 44100.00, 44100.05, 44100.31, 24.10, 39, -16.95 -2.00, -36.8, 36.8, 29087, 0, 5, 0, 5, 8468, 216, 226, 44100.00, 44100.87, 44100.32, 23.94, 41, -16.64 -1.62, -11.3, 11.3, 30090, 0, 5, 0, 5, 8433, 218, 226, 44100.00, 44097.75, 44100.28, 23.34, 43, -16.91 -1.75, 2.8, 2.8, 31093, 0, 5, 0, 5, 8224, 215, 226, 44100.00, 44101.39, 44100.31, 23.21, 44, -16.05 -1.95, -2.8, 2.8, 32096, 0, 5, 0, 5, 8472, 216, 226, 44100.00, 44101.39, 44100.31, 22.79, 46, -15.71 -1.83, -102.0, 102.0, 33099, 0, 5, 0, 5, 8411, 214, 226, 44100.00, 44100.58, 44100.31, 23.00, 49, -15.95 -1.48, 0.0, 0.0, 34102, 0, 5, 0, 5, 8419, 209, 226, 44100.00, 44101.87, 44100.31, 23.00, 49, -15.87 -1.62, 2.8, 2.8, 35105, 0, 11, 0, 11, 8074, 216, 226, 44100.00, 44102.71, 44100.31, 23.07, 50, -15.92 -1.71, -2.8, 8.5, 36108, 0, 11, 0, 11, 7835, 216, 227, 44100.00, 44102.47, 44100.32, 23.27, 51, -15.92 -1.94, -5.7, 5.7, 37111, 0, 11, 0, 11, 8470, 217, 226, 44100.00, 44102.54, 44100.31, 23.73, 53, -16.54 -1.95, -2.8, 2.8, 38114, 0, 11, 0, 11, 8461, 217, 226, 44100.00, 44102.93, 44100.32, 23.78, 54, -16.56 -``` - -"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output drifts to 1.95 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made. - -"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. The first line of the example above indicates that the output device is consuming frames 25.9 ppm faster than the source is generating them. The subsequent lines indicate that the stable actual difference in the clocks is around 95 ppm. - -"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections that are being made. Third party programs tend to have much larger levels of corrections. - -For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an iPhone XS Max to a Raspberry Pi Zero W using a Pimoroni pHAT DAC. - -It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway... - -"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.20 seconds = 8,820 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up. - -WiFi Issues ---------- -If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) for more details. - -Troubleshooting ---------------- -Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) for a few hints, contributed by users. - From 5abb4ac0d5aff6c23f9107f96edc6e44d007121f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:21:32 +0000 Subject: [PATCH 280/878] Update URLs to point to the public repository. --- AIRPLAY2.md | 2 +- BUILDFORAP1.md | 2 +- BUILDFORAP2.md | 2 +- README.md | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index f9d1face..65229145 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -45,7 +45,7 @@ So, here are some guideline requirements: Guides --- -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). * A guide for building on FreeBSD is forthcoming. AirPlay 2 -- More About What Works diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 07dac81d..d3834240 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -48,7 +48,7 @@ First, install the packages needed by Shairport Sync: ``` Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` -$ git clone https://github.com/aillwee/shairport-sync.git +$ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync $ git checkout development $ autoreconf -fi diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 8bbc2a09..eac2baf4 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -81,7 +81,7 @@ Version without git information: #### Build and Install Download Shairport Sync, check out the `development` branch and configure, compile and install it: ``` -$ git clone git@github.com:aillwee/shairport-sync.git +$ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync $ git checkout development $ autoreconf -fi diff --git a/README.md b/README.md index fe9eec75..234dc391 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Shairport Sync AirPlay 2 --- -For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/aillwee/shairport-sync/blob/development/AIRPLAY2.md). +For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). Guides --- -* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP1.md). +* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). * A guide to building for AirPlay 2 on FreeBSD is forthcoming. More Information --- -For more information, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). +For more information, please visit [MOREINFO.md](https://github.com/mikebrady/shairport-sync/blob/development/MOREINFO.md). Acknowledgements --- From fae983cd60e18b73b1ec230e171aeab16e040800 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:28:19 +0000 Subject: [PATCH 281/878] Highlight the AirPlay 2 information page. --- RELEASENOTES-DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 5f73973d..ac4023e3 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -3,9 +3,9 @@ Version 4.0-dev Big Update ---- -Version 4 brings limited AirPlay 2 functionality to Shairport Sync. +Version 4 brings [limited AirPlay 2 functionality](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md) to Shairport Sync. -For more information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md]. +For information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). #### Known Issues - In the AirPlay 2 build, a crash can occur occasionally when the player is being selected. If you can get this to happen reliably, please let us know. From b4a8249d0e88e5059b5551e905a1598bbba787f2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:55:33 +0000 Subject: [PATCH 282/878] Minor format changes. --- RELEASENOTES-DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index ac4023e3..b1010052 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -10,8 +10,8 @@ For information about AirPlay 2, including acknowledgements, please visit [AIRPL #### Known Issues - In the AirPlay 2 build, a crash can occur occasionally when the player is being selected. If you can get this to happen reliably, please let us know. -Warning ----- +#### Warning + 1. Big changes have been made to the codebase to enable it to be used to build either as the "classic" AirPlay 1 player or as a new AirPlay 2 player. These changes have probably introduced bugs into the classic AirPlay 1 build. 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. 3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. From a787d95bdb1841de88afcddddba435d9fa9b11e3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:20:44 +0000 Subject: [PATCH 283/878] Chage some ambiguous phrasing. --- AIRPLAY2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 65229145..6dbd97a8 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -9,7 +9,7 @@ For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https:// AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. -AirPlay 2 support definitely does not work on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. +The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. @@ -29,7 +29,7 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi ### Limitations - Only a single instance of Shairport Sync can run on a device. -- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. +- The AirPlay 2 version of Shairport Sync will not work if installed on macOS, since `nqptp` can not be installed on macOS. ### Known Issues - A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. From d0e00fa381bb843eb32620baecd3bddb5d81271d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:26:45 +0000 Subject: [PATCH 284/878] Slight rephrasing. --- AIRPLAY2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 6dbd97a8..42a20730 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -56,12 +56,12 @@ AirPlay 2 -- More About What Works Acknowledgements ---- -Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. - -Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: +Huge thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: 1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. 2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). +3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes, documentation and Docker automation (forthcoming). + +Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. It is a pleasure to acknowledge the work of the contributors there. Finally ---- From 9ccde50c47934a4879cf9a199387d223dee4ed00 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Jul 2021 15:27:23 +0100 Subject: [PATCH 285/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index eac2baf4..780a3c81 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -154,4 +154,4 @@ Connect and enjoy... ### Restart Your Mac! -At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. +At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only known solution is to reboot. From 25de2bd3cefbd1273ca72121d8b69af9675bcf17 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:29:58 +0100 Subject: [PATCH 286/878] Added docker. --- .github/workflows/docker-build-releases.yaml | 64 +++++++++ .github/workflows/docker-build.yaml | 62 +++++++++ docker/Dockerfile | 137 +++++++++++++++++++ docker/README.md | 39 ++++++ docker/airplay1/Dockerfile | 103 ++++++++++++++ docker/airplay1/README.md | 3 + docker/airplay1/start.sh | 13 ++ docker/docker-compose.yaml | 11 ++ docker/start.sh | 18 +++ 9 files changed, 450 insertions(+) create mode 100644 .github/workflows/docker-build-releases.yaml create mode 100644 .github/workflows/docker-build.yaml create mode 100644 docker/Dockerfile create mode 100644 docker/README.md create mode 100644 docker/airplay1/Dockerfile create mode 100644 docker/airplay1/README.md create mode 100644 docker/airplay1/start.sh create mode 100644 docker/docker-compose.yaml create mode 100644 docker/start.sh diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml new file mode 100644 index 00000000..745d4ffd --- /dev/null +++ b/.github/workflows/docker-build-releases.yaml @@ -0,0 +1,64 @@ +# Builds & pushes a docker image when a release occurs. + +# The following tags are created: +# 'latest' & 'latest-airplay1' +# '[RELEASE TAG]' & '[RELEASE TAG]-airplay1' + +name: Build and push docker images on releases. + +on: + release: + types: [ published ] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set SHAIRPORT_SYNC_BRANCH env. + run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: Set up ssh agent + uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Registry + uses: docker/login-action@v1 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:latest ${{ env.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + + - name: Build and push (AirPlay 1) + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/airplay1/Dockerfile-AirPlay1 + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:latest-airplay1, ${{ env.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 00000000..3a3d8392 --- /dev/null +++ b/.github/workflows/docker-build.yaml @@ -0,0 +1,62 @@ +# Builds & pushes a docker image when a commit is made to one of the branches specified below. +# Tag pattern: 'unstable-[BRANCH NAME]'. +# See docker-build-releases.yaml for release build process. + +name: Build and push docker image based on commit to specified branches. + +on: + push: + branches: [ master, development, charles-dev ] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set SHAIRPORT_SYNC_BRANCH env. + run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: Set up ssh agent + uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Registry + uses: docker/login-action@v1 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + + - name: Build and push (AirPlay 1) + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/airplay1/Dockerfile + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + push: true + tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..1203b1c6 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,137 @@ +FROM alpine:3.12 AS builder-base +# Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard +# in debian/ubuntu. + +# General Build System: +RUN apk -U add \ + git \ + build-base \ + autoconf \ + automake \ + libtool \ + dbus \ + su-exec \ + alsa-lib-dev \ + libdaemon-dev \ + popt-dev \ + mbedtls-dev \ + soxr-dev \ + avahi-dev \ + libconfig-dev \ + libsndfile-dev \ + mosquitto-dev \ + xmltoman \ + openssh-client \ + libsodium-dev \ + ffmpeg-dev \ + xxd \ + libressl-dev \ + openssl-dev \ + libplist-dev \ + libgcrypt-dev + +# ALAC Build System: +FROM builder-base AS builder-alac + +RUN git clone https://github.com/mikebrady/alac +WORKDIR /alac +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install + +# Shairport Sync Build System: +FROM builder-base AS builder-sps + +# This will be modified by the Github Action Workflow and is required +# to ensure the correct branch is being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc +COPY --from=builder-alac /usr/local/include /usr/local/include + +# TODO: Remove when no longer pulling from private repos. +RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts + +RUN --mount=type=ssh git clone git@github.com:mikebrady/nqptp.git +WORKDIR /nqptp +# Use development branch for now. +RUN git checkout development +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install +WORKDIR / + +WORKDIR /shairport-sync +COPY . . +RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +RUN autoreconf -fi +# RUN ./configure \ +# --with-alsa \ +# --with-dummy \ +# --with-pipe \ +# --with-stdout \ +# --with-avahi \ +# --with-ssl=mbedtls \ +# --with-soxr \ +# --sysconfdir=/etc \ +# --with-dbus-interface \ +# --with-mpris-interface \ +# --with-mqtt-client \ +# --with-apple-alac \ +# --with-convolution \ +# --with-airplay-2 +# RUN make -j $(nproc) +RUN CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata --with-dummy --with-pipe \ + --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-dbus-interface --with-stdout \ + --with-mpris-interface --with-mqtt-client --with-apple-alac --with-convolution --with-airplay-2 +RUN make -j +RUN make install + +# Shairport Sync Runtime System: +FROM alpine:3.12 + +RUN apk -U add \ + alsa-lib \ + dbus \ + popt \ + glib \ + mbedtls \ + soxr \ + avahi \ + libconfig \ + libsndfile \ + mosquitto \ + su-exec \ + libuuid \ + ffmpeg \ + libsodium \ + libplist \ + libgcrypt + +RUN rm -rf /lib/apk/db/* + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-sps /etc/shairport-sync* /etc/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync +COPY --from=builder-sps /usr/local/bin/nqptp /usr/local/bin/nqptp + +# Create non-root user for running the container -- running as the user 'shairport-sync' also allows +# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container + +RUN addgroup shairport-sync +RUN adduser -D shairport-sync -G shairport-sync + +# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff +RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio + +COPY ./docker/start.sh / + +RUN chmod +x ./start.sh + +ENTRYPOINT [ "/start.sh" ] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..fa2b1b53 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,39 @@ +# Shairport Sync Docker Image + +Available at: https://hub.docker.com/r/mikebrady/shairport-sync + +## Example Docker Compose File +See the `docker-compose.yaml` file in this folder for an example. + +## Basic Usage + +``` +$ docker run -d --restart unless-stopped --net host --device /dev/snd \ + mikebrady/shairport-sync +``` +The above command will run Shairport Sync as a daemon in a Docker container, accessing the computer's ALSA audio infrastructure. It will send audio to the default output device and make no use of any hardware mixers the default device might have. The AirPlay service name will be the host's `hostname` with the first letter capitalised, e.g. `Ubuntu`. + +## Options + +Any options you add to the command above will be passed to Shairport Sync. Here is an example: +``` +$ docker run -d --restart unless-stopped --net host --device /dev/snd \ + mikebrady/shairport-sync -a DenSystem -- -d hw:0 -c PCM +``` +This will sent audio to alsa hardware device `hw:0` and make use of the that device's mixer control called `PCM`. The service will be visible as `DenSystem` on the network. + +## Configuration File + +Edit the configuration file `/etc/shairport-sync.conf` in the container (or use the `-v` option to mirror an external copy of `shairport-sync.conf` in to `/etc/shairport-sync.conf`) to get access to the full range of configuration options. + +## Build Example (for arm7 devices) + +TODO: remove `--ssh` when going public as this is only for private repos. +``` +docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t shairport-sync:unstable-development . +``` + +`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. + +## AirPlay 1 Only +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. \ No newline at end of file diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile new file mode 100644 index 00000000..e60fd0b5 --- /dev/null +++ b/docker/airplay1/Dockerfile @@ -0,0 +1,103 @@ +FROM alpine:3.12 AS builder-base +# General Build System: +RUN apk -U add \ + git \ + build-base \ + autoconf \ + automake \ + libtool \ + dbus \ + su-exec \ + alsa-lib-dev \ + libdaemon-dev \ + popt-dev \ + mbedtls-dev \ + soxr-dev \ + avahi-dev \ + libconfig-dev \ + libsndfile-dev \ + mosquitto-dev \ + xmltoman + +# ALAC Build System: +FROM builder-base AS builder-alac + +RUN git clone https://github.com/mikebrady/alac +WORKDIR alac +RUN autoreconf -fi +RUN ./configure +RUN make +RUN make install + +# Shairport Sync Build System: +FROM builder-base AS builder-sps + +# This will be modified by the Github Action Workflow and is required +# to ensure the correct branch is being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc +COPY --from=builder-alac /usr/local/include /usr/local/include + +WORKDIR shairport-sync +COPY . . +RUN git checkout "$SHAIRPORT_SYNC_BRANCH" +RUN autoreconf -fi +RUN ./configure \ + --with-alsa \ + --with-dummy \ + --with-pipe \ + --with-stdout \ + --with-avahi \ + --with-ssl=mbedtls \ + --with-soxr \ + --sysconfdir=/etc \ + --with-dbus-interface \ + --with-mpris-interface \ + --with-mqtt-client \ + --with-apple-alac \ + --with-convolution +RUN make -j $(nproc) +RUN make install + +# Shairport Sync Runtime System: +FROM alpine:3.12 + +RUN apk -U add \ + alsa-lib \ + dbus \ + popt \ + glib \ + mbedtls \ + soxr \ + avahi \ + libconfig \ + libsndfile \ + mosquitto-libs \ + su-exec \ + libgcc \ + libgc++ + +RUN rm -rf /lib/apk/db/* + +COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder-sps /etc/shairport-sync* /etc/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ +COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync + +# Create non-root user for running the container -- running as the user 'shairport-sync' also allows +# Shairport Sync to provide the D-Bus and MPRIS interfaces within the container + +RUN addgroup shairport-sync +RUN adduser -D shairport-sync -G shairport-sync + +# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff +RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio + +COPY ./docker/airplay1/start.sh / +RUN chmod +x /start.sh + +ENTRYPOINT [ "/start.sh" ] diff --git a/docker/airplay1/README.md b/docker/airplay1/README.md new file mode 100644 index 00000000..247754e6 --- /dev/null +++ b/docker/airplay1/README.md @@ -0,0 +1,3 @@ +# AirPlay 1 Only Docker Image + +See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. For the latest build, use `latest-airplay1`. \ No newline at end of file diff --git a/docker/airplay1/start.sh b/docker/airplay1/start.sh new file mode 100644 index 00000000..f68a122f --- /dev/null +++ b/docker/airplay1/start.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +rm -rf /var/run/dbus.pid +#mkdir -p /var/run/dbus + +dbus-uuidgen --ensure +dbus-daemon --system + +avahi-daemon --daemonize --no-chroot + +su-exec shairport-sync shairport-sync $@ \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..74ed09ac --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,11 @@ +# Example docker compose config. + +services: + shairport-sync: + image: mikebrady/shairport-sync:unstable-development + network_mode: host + restart: unless-stopped + devices: + - "/dev/snd" + # volumes: + # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 00000000..bb790283 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +rm -rf /var/run/dbus.pid +#mkdir -p /var/run/dbus + +dbus-uuidgen --ensure +dbus-daemon --system + +avahi-daemon --daemonize --no-chroot + +# TODO: This should probably be implemented in a better way and using the correct nqptp group. +nqptp & + +# su-exec shairport-sync shairport-sync $@ + +shairport-sync -vu --statistics \ No newline at end of file From bb26b1bc8d228d72bd8d65bcb2bebc5da557c6ae Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:32:30 +0100 Subject: [PATCH 287/878] Remove ssh key stuff. --- .github/workflows/docker-build-releases.yaml | 8 +------- .github/workflows/docker-build.yaml | 8 +------- docker/Dockerfile | 5 +---- docker/README.md | 3 +-- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 745d4ffd..437c87e3 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -8,7 +8,7 @@ name: Build and push docker images on releases. on: release: - types: [ published ] + types: [ ] jobs: main: @@ -22,11 +22,6 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Set up ssh agent - uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -50,7 +45,6 @@ jobs: tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:latest ${{ env.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 3a3d8392..d3eab23d 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -6,7 +6,7 @@ name: Build and push docker image based on commit to specified branches. on: push: - branches: [ master, development, charles-dev ] + branches: [ development ] jobs: main: @@ -20,11 +20,6 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Set up ssh agent - uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -48,7 +43,6 @@ jobs: tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 diff --git a/docker/Dockerfile b/docker/Dockerfile index 1203b1c6..210d4868 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -52,10 +52,7 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -# TODO: Remove when no longer pulling from private repos. -RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - -RUN --mount=type=ssh git clone git@github.com:mikebrady/nqptp.git +RUN git clone git@github.com:mikebrady/nqptp.git WORKDIR /nqptp # Use development branch for now. RUN git checkout development diff --git a/docker/README.md b/docker/README.md index fa2b1b53..87c26f99 100644 --- a/docker/README.md +++ b/docker/README.md @@ -28,9 +28,8 @@ Edit the configuration file `/etc/shairport-sync.conf` in the container (or use ## Build Example (for arm7 devices) -TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t shairport-sync:unstable-development . +docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. From 95a9f68bc82a4a9782993b62fdb23854e5bc803b Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:37:02 +0100 Subject: [PATCH 288/878] Update docs. --- docker/airplay1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/airplay1/README.md b/docker/airplay1/README.md index 247754e6..1d68a959 100644 --- a/docker/airplay1/README.md +++ b/docker/airplay1/README.md @@ -1,3 +1,3 @@ # AirPlay 1 Only Docker Image -See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. For the latest build, use `latest-airplay1`. \ No newline at end of file +See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. \ No newline at end of file From 49a292759d2f9e95b4e358efe2a8daac4e0d42ce Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:39:40 +0100 Subject: [PATCH 289/878] Update docker registry location. --- .github/workflows/docker-build-releases.yaml | 6 +++--- .github/workflows/docker-build.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 437c87e3..a9da88e4 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -31,7 +31,7 @@ jobs: - name: Login to Docker Registry uses: docker/login-action@v1 with: - registry: ${{ env.DOCKER_REGISTRY }} + registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} @@ -42,7 +42,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:latest ${{ env.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} + tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:latest ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} @@ -53,6 +53,6 @@ jobs: file: ./docker/airplay1/Dockerfile-AirPlay1 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:latest-airplay1, ${{ env.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:latest-airplay1, ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index d3eab23d..fe14f93c 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -29,7 +29,7 @@ jobs: - name: Login to Docker Registry uses: docker/login-action@v1 with: - registry: ${{ env.DOCKER_REGISTRY }} + registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} @@ -40,7 +40,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} @@ -51,6 +51,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ env.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 782af5e39549945164b2e2671b1a83d43dc93783 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:43:53 +0100 Subject: [PATCH 290/878] Trigger test run. --- docker/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/README.md b/docker/README.md index 87c26f99..883c74af 100644 --- a/docker/README.md +++ b/docker/README.md @@ -35,4 +35,5 @@ docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg S `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. ## AirPlay 1 Only + The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. \ No newline at end of file From 99c74482f8d13fffebc9bf65e102fb58e7f06e13 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:52:21 +0100 Subject: [PATCH 291/878] Comments. --- .github/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index fe14f93c..dc27674f 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,5 +1,5 @@ # Builds & pushes a docker image when a commit is made to one of the branches specified below. -# Tag pattern: 'unstable-[BRANCH NAME]'. +# Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-airplay1' # See docker-build-releases.yaml for release build process. name: Build and push docker image based on commit to specified branches. From 932713b243bc9c3c7e1c9802c1a6039828842b6b Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:53:27 +0100 Subject: [PATCH 292/878] Updated nqptp clone. --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 210d4868..8dc5fe1e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -52,7 +52,7 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -RUN git clone git@github.com:mikebrady/nqptp.git +RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp # Use development branch for now. RUN git checkout development From bd82293c4b54bccd919a07164c7a9d07c526058e Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:06:10 +0100 Subject: [PATCH 293/878] Added nqptp branch switching. --- .github/workflows/docker-build-releases.yaml | 8 +++++++- .github/workflows/docker-build.yaml | 8 +++++++- docker/Dockerfile | 8 +++++--- docker/README.md | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index a9da88e4..6fe5c7e3 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -19,9 +19,15 @@ jobs: with: fetch-depth: 0 - - name: Set SHAIRPORT_SYNC_BRANCH env. + - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + run: echo "NQPTP_BRANCH=development" + + - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + run: echo "NQPTP_BRANCH=master" + - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index dc27674f..d40636a0 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -17,9 +17,15 @@ jobs: with: fetch-depth: 0 - - name: Set SHAIRPORT_SYNC_BRANCH env. + - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + run: echo "NQPTP_BRANCH=development" + + - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + run: echo "NQPTP_BRANCH=master" + - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/docker/Dockerfile b/docker/Dockerfile index 8dc5fe1e..98a3900c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,10 +43,12 @@ RUN make install # Shairport Sync Build System: FROM builder-base AS builder-sps -# This will be modified by the Github Action Workflow and is required -# to ensure the correct branch is being used. +# These will be modified by the Github Action Workflow and are required +# to ensure the correct branches are being used. ARG SHAIRPORT_SYNC_BRANCH RUN test -n "$SHAIRPORT_SYNC_BRANCH" +ARG NQPTP_BRANCH +RUN test -n "$NQPTP_BRANCH" COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc @@ -55,7 +57,7 @@ COPY --from=builder-alac /usr/local/include /usr/local/include RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp # Use development branch for now. -RUN git checkout development +RUN git checkout "$NQPTP_BRANCH" RUN autoreconf -fi RUN ./configure RUN make diff --git a/docker/README.md b/docker/README.md index 883c74af..25df20ec 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,7 @@ Edit the configuration file `/etc/shairport-sync.conf` in the container (or use ## Build Example (for arm7 devices) ``` -docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t shairport-sync:unstable-development . +docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development -t shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. From 60afdc09849da0396a48423b21367da26f227dd6 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:09:45 +0100 Subject: [PATCH 294/878] Use github ref. --- .github/workflows/docker-build-releases.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 6fe5c7e3..43bd7927 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -22,10 +22,10 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + - if: github.ref == 'development' run: echo "NQPTP_BRANCH=development" - - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + - if: github.ref != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU From 15f0bb34dc81d127b0f77df95e37bde6f55908b1 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:11:42 +0100 Subject: [PATCH 295/878] Use github ref 2. --- .github/workflows/docker-build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index d40636a0..f6c2481e 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -20,10 +20,10 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + - if: github.ref == 'development' run: echo "NQPTP_BRANCH=development" - - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + - if: github.ref != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU From 5bcf0d563f17ef72b73eb7fc916282a7506c956a Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:15:37 +0100 Subject: [PATCH 296/878] Use github ref 3. --- .github/workflows/docker-build-releases.yaml | 4 ++-- .github/workflows/docker-build.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 43bd7927..6fe5c7e3 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -22,10 +22,10 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: github.ref == 'development' + - if: env.SHAIRPORT_SYNC_BRANCH == 'development' run: echo "NQPTP_BRANCH=development" - - if: github.ref != 'development' + - if: env.SHAIRPORT_SYNC_BRANCH != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index f6c2481e..d40636a0 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -20,10 +20,10 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: github.ref == 'development' + - if: env.SHAIRPORT_SYNC_BRANCH == 'development' run: echo "NQPTP_BRANCH=development" - - if: github.ref != 'development' + - if: env.SHAIRPORT_SYNC_BRANCH != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU From 799d2e54d8592c92c1b6c55c9aeefc43898e3860 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:17:32 +0100 Subject: [PATCH 297/878] Test. --- .github/workflows/docker-build-releases.yaml | 8 +++++--- .github/workflows/docker-build.yaml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 6fe5c7e3..b39f4b0e 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -19,13 +19,15 @@ jobs: with: fetch-depth: 0 - - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. + - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + - name: Determine NQPTP env (part 1). + if: env.SHAIRPORT_SYNC_BRANCH == 'development' run: echo "NQPTP_BRANCH=development" - - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + - name: Determine NQPTP env (part 2). + if: env.SHAIRPORT_SYNC_BRANCH != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index d40636a0..9e95cd0d 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -17,13 +17,15 @@ jobs: with: fetch-depth: 0 - - name: Set SHAIRPORT_SYNC_BRANCH & NQPTP env. + - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + - name: Determine NQPTP env (part 1). + if: env.SHAIRPORT_SYNC_BRANCH == 'development' run: echo "NQPTP_BRANCH=development" - - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + - name: Determine NQPTP env (part 2). + if: env.SHAIRPORT_SYNC_BRANCH != 'development' run: echo "NQPTP_BRANCH=master" - name: Set up QEMU From e4a762579ca5710095182d19387fd6741d970745 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:19:24 +0100 Subject: [PATCH 298/878] Test. --- .github/workflows/docker-build-releases.yaml | 6 +++--- .github/workflows/docker-build.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index b39f4b0e..de44e7ea 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -23,13 +23,13 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} run: echo "NQPTP_BRANCH=development" - name: Determine NQPTP env (part 2). - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} run: echo "NQPTP_BRANCH=master" - + - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 9e95cd0d..0fd3b7ef 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -21,11 +21,11 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} run: echo "NQPTP_BRANCH=development" - name: Determine NQPTP env (part 2). - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} run: echo "NQPTP_BRANCH=master" - name: Set up QEMU From e973c782ad27d70dcc8a738f1dca46f194f75e22 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:23:00 +0100 Subject: [PATCH 299/878] Test. --- .github/workflows/docker-build-releases.yaml | 8 ++++---- .github/workflows/docker-build.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index de44e7ea..8ad75497 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -23,12 +23,12 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - run: echo "NQPTP_BRANCH=development" + if: env.SHAIRPORT_SYNC_BRANCH == 'development' + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). - if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" + if: env.SHAIRPORT_SYNC_BRANCH != 'development' + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 0fd3b7ef..fad6f5a3 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -21,12 +21,12 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - run: echo "NQPTP_BRANCH=development" + if: env.SHAIRPORT_SYNC_BRANCH == 'development' + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). - if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" + if: env.SHAIRPORT_SYNC_BRANCH != 'development' + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 From 8eaa80f4fb2e290259913a0d2f33b56c626b5a3b Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:25:01 +0100 Subject: [PATCH 300/878] Test. --- .github/workflows/docker-build-releases.yaml | 4 ++-- .github/workflows/docker-build.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 8ad75497..e20ecc47 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -23,11 +23,11 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - name: Set up QEMU diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index fad6f5a3..b02d0ce1 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -21,13 +21,13 @@ jobs: run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). - if: env.SHAIRPORT_SYNC_BRANCH == 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). - if: env.SHAIRPORT_SYNC_BRANCH != 'development' + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - + - name: Set up QEMU uses: docker/setup-qemu-action@v1 From 19608dbb3b20e3b15bee06e45012cc4fbb9d015a Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:29:20 +0100 Subject: [PATCH 301/878] Test. --- .github/workflows/docker-build-releases.yaml | 7 +++++-- .github/workflows/docker-build.yaml | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index e20ecc47..9d695d99 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -24,11 +24,14 @@ jobs: - name: Determine NQPTP env (part 1). if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV + env: + NQPTP_BRANCH: development - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + env: + NQPTP_BRANCH: master + - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index b02d0ce1..01435a89 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -22,11 +22,13 @@ jobs: - name: Determine NQPTP env (part 1). if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV + env: + NQPTP_BRANCH: development - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + env: + NQPTP_BRANCH: master - name: Set up QEMU uses: docker/setup-qemu-action@v1 From f80a5abc15da40605d2056a3f9496b74af222ece Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:37:09 +0100 Subject: [PATCH 302/878] Test. --- .github/workflows/docker-build-releases.yaml | 8 +++----- .github/workflows/docker-build.yaml | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 9d695d99..e664d7a9 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -24,14 +24,11 @@ jobs: - name: Determine NQPTP env (part 1). if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - env: - NQPTP_BRANCH: development + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - env: - NQPTP_BRANCH: master - + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -56,6 +53,7 @@ jobs: tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:latest ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 01435a89..b3d8eebb 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -22,13 +22,11 @@ jobs: - name: Determine NQPTP env (part 1). if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} - env: - NQPTP_BRANCH: development + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - env: - NQPTP_BRANCH: master + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -53,6 +51,7 @@ jobs: tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 From 2b313027ba94d4cbb3255bf57aaa009b60f11066 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:09:40 +0100 Subject: [PATCH 303/878] README update. --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 25df20ec..99bd85f2 100644 --- a/docker/README.md +++ b/docker/README.md @@ -36,4 +36,4 @@ docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg S ## AirPlay 1 Only -The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. \ No newline at end of file +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image build via the AirPlay 2 branch does not work with multiple instances on the same hardware. \ No newline at end of file From 7d43349f748c6df1b0bf4b1f2867a54cd4e760fb Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:18:42 +0100 Subject: [PATCH 304/878] Test. --- docker/airplay1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/airplay1/README.md b/docker/airplay1/README.md index 1d68a959..f8335d76 100644 --- a/docker/airplay1/README.md +++ b/docker/airplay1/README.md @@ -1,3 +1,3 @@ # AirPlay 1 Only Docker Image -See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. \ No newline at end of file +See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. From f634cc816cc7b9cf2eee39d1c092fba0743b8eca Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:22:13 +0100 Subject: [PATCH 305/878] Compatibility with docker hub. --- .github/workflows/docker-build-releases.yaml | 4 ++-- .github/workflows/docker-build.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index e664d7a9..59ef477e 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -50,7 +50,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:latest ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }} + tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:latest ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -62,6 +62,6 @@ jobs: file: ./docker/airplay1/Dockerfile-AirPlay1 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:latest-airplay1, ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:latest-airplay1, ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index b3d8eebb..c2d33331 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -48,7 +48,7 @@ jobs: file: ./docker/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -60,6 +60,6 @@ jobs: file: ./docker/airplay1/Dockerfile platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 58f0c34930a2a254a4b34ac488a592ef1ee14e40 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:38:49 +0100 Subject: [PATCH 306/878] Centralised docker image name. --- .github/workflows/docker-build-releases.yaml | 11 +++++++---- .github/workflows/docker-build.yaml | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 59ef477e..ef355fa8 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -10,6 +10,9 @@ on: release: types: [ ] +env: + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + jobs: main: runs-on: ubuntu-latest @@ -48,9 +51,9 @@ jobs: with: context: ./ file: ./docker/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:latest ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:${{ github.event.release.tag_name }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -60,8 +63,8 @@ jobs: with: context: ./ file: ./docker/airplay1/Dockerfile-AirPlay1 - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:latest-airplay1, ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest-airplay1, ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index c2d33331..af394e1c 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -7,6 +7,9 @@ name: Build and push docker image based on commit to specified branches. on: push: branches: [ development ] + +env: + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 jobs: main: @@ -46,9 +49,9 @@ jobs: with: context: ./ file: ./docker/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -58,8 +61,8 @@ jobs: with: context: ./ file: ./docker/airplay1/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_REGISTRY_USER }}/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file From 4b2518bbdd7d7b9757560163f6210db0316a591a Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:51:44 +0100 Subject: [PATCH 307/878] Updated README. --- docker/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 99bd85f2..6043cc44 100644 --- a/docker/README.md +++ b/docker/README.md @@ -36,4 +36,12 @@ docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg S ## AirPlay 1 Only -The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image build via the AirPlay 2 branch does not work with multiple instances on the same hardware. \ No newline at end of file +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image build via the AirPlay 2 branch does not work with multiple instances on the same hardware. + +## GitHub Action Builds + +Requires the following secrets to be set in the repo: +- `DOCKER_REGISTRY` - docker.io if using Docker Hub, else set to your registry URL. +- `DOCKER_REGISTRY_TOKEN` - Access token for your registry. +- `DOCKER_REGISTRY_USER` - Login user for your registry. +- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub. \ No newline at end of file From 87c588819ea68a2903a75f9580c84eaa3d50f8c6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 9 Jul 2021 09:03:14 +0100 Subject: [PATCH 308/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b1010052..c14ba33e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,4 @@ -Version 4.0-dev +Version 4.0-dev-153-g9ccde50 ==== Big Update From 0947bf3cc039be03a605b286520b3b4d9f8e8363 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 11 Jul 2021 18:38:45 +0100 Subject: [PATCH 309/878] Update CAR INSTALL.md --- CAR INSTALL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 0a82b68a..f2a97c26 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -92,6 +92,7 @@ A number of packages to enable the Pi to work as a WiFi base station are needed: ``` Disable both of these services from starting at boot time (this is because we will launch them sequentially later on): ``` +# systemctl unmask hostapd # systemctl disable hostapd # systemctl disable isc-dhcp-server ``` From 2184f1b744cbd9acf854808ac7584b88773742ba Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 11 Jul 2021 18:39:38 +0100 Subject: [PATCH 310/878] Update CAR INSTALL.md --- CAR INSTALL.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index f2a97c26..8fb2294b 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -42,7 +42,6 @@ The first thing to do on a Pi would be to use the `raspi-config` tool to expand ``` # apt-get update # apt-get upgrade -# rpi-update ``` ### Shairport Sync From d7fbdd2ef37c30491db6f13db8a80cdbe9ec62e0 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 12 Jul 2021 22:11:42 +0100 Subject: [PATCH 311/878] Added automated issue and PR management. --- .github/workflows/actions-stale.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/actions-stale.yaml diff --git a/.github/workflows/actions-stale.yaml b/.github/workflows/actions-stale.yaml new file mode 100644 index 00000000..3b0616f6 --- /dev/null +++ b/.github/workflows/actions-stale.yaml @@ -0,0 +1,16 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '* 8 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' + stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' + # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues + # days-before-stale: 60 + # days-before-close: 7 From 1959d2cf0c30d0b5bf491070ac509d91cf0eba9c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 13 Jul 2021 22:28:34 +0200 Subject: [PATCH 312/878] Update pair_ap to 0.7 (fixes incorrect pair-list response) --- pair_ap/README.md | 13 +++++++------ pair_ap/pair.h | 2 +- pair_ap/pair_homekit.c | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pair_ap/README.md b/pair_ap/README.md index 1c855444..d6b00d65 100644 --- a/pair_ap/README.md +++ b/pair_ap/README.md @@ -2,9 +2,10 @@ C client implementation of pairing for: * Apple TV device verification, which became mandatory with tvOS 10.2 (this is called fruit mode in pair_ap) -* Homekit pairing (for AirPlay 2, not working for Home app) +* Homekit pairing (also for AirPlay 2) Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting. + ## Requirements - libsodium - libgcrypt or libopenssl @@ -33,7 +34,7 @@ The controller uses `/pair-add` to make sure that all devices on a network get the ID and public key of all the other devices, so that the user only needs to pair a device once. -### Normal pairing +### Normal pairing with one-time code For a normal first-time pairing, the client needs a one-time code (the device announces via mDNS whether a code is required). The client calls `/pair-pin-start` and the device displays the code. There is also QR-based @@ -42,12 +43,12 @@ pairing, which is (probably?) an encoded code. After obtaining the code, the client initiates a three step `/pair-setup` sequence, which results in both peers registering each other's ID and public key. Henceforth, a pairing is verified with the two step `/pair-verify`, where -the parties check each-others identify. Saving the peer's ID + public key isn't +the parties check eachothers identify. Saving the peer's ID + public key isn't strictly necessary if client or server doesn't care about verifying the peer, i.e. that `/pair-setup` has actually been completed. The result of `/pair-verify` is a shared secret that is used for symmetric -encryption of the following communinacation between the parties. +encryption of the following communication between the parties. ### Transient pairing Some devices don't require a code from the user for pairing (e.g. an Airport @@ -55,8 +56,7 @@ Express 2). If so, the client just needs to go through a two-step `/pair-setup` sequence which results in a shared secret, which is then used for encrypted communication. A fixed code of 3939 is used. -Such devices don't appear to be fully Homekit compatible - they will not, for -instance - appear in the Home app. +The controller can still use `/pair-add` etc. towards such devices. ## "fruit" pairing Like normal Homekit pairing, this consists of first requesting a code with @@ -69,4 +69,5 @@ shared secret. - [AirPlayAuth](https://github.com/funtax/AirPlayAuth) - [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC) - [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender) +- [airplay2-receiver](https://github.com/ckdo/airplay2-receiver) - [csrp](https://github.com/cocagne/csrp) diff --git a/pair_ap/pair.h b/pair_ap/pair.h index 6324bc15..37f85b4e 100644 --- a/pair_ap/pair.h +++ b/pair_ap/pair.h @@ -4,7 +4,7 @@ #include #define PAIR_AP_VERSION_MAJOR 0 -#define PAIR_AP_VERSION_MINOR 5 +#define PAIR_AP_VERSION_MINOR 7 #define PAIR_AP_DEVICE_ID_LEN_MAX 64 diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c index 076ea14a..f875d237 100644 --- a/pair_ap/pair_homekit.c +++ b/pair_ap/pair_homekit.c @@ -2679,6 +2679,7 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in pair_tlv_t *device_id; pair_tlv_t *pk; char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + uint8_t *public_key = NULL; request = message_process(in, in_len, &errmsg); if (!request) @@ -2687,15 +2688,21 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in } device_id = pair_tlv_get_value(request, TLVType_Identifier); - pk = pair_tlv_get_value(request, TLVType_PublicKey); - if (!device_id || device_id->size >= sizeof(id_str) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES) + if (!device_id || device_id->size >= sizeof(id_str)) { goto error; } + // Only present when adding + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (pk && pk->size == crypto_sign_PUBLICKEYBYTES) + { + public_key = pk->value; + } + memcpy(id_str, device_id->value, device_id->size); - cb(pk->value, id_str, cb_arg); + cb(public_key, id_str, cb_arg); pair_tlv_free(request); return 0; @@ -2757,9 +2764,12 @@ static int server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg) { pair_tlv_values_t *response = cb_arg; + uint8_t permissions = 1; // Means admin (TODO don't hardcode - let caller set) pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id)); pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES); + pair_tlv_add_value(response, TLVType_Permissions, &permissions, sizeof(permissions)); + return 0; } From ae9e91f16016e5b19ace23283650f2c1bfb9e892 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 13 Jul 2021 21:43:31 +0100 Subject: [PATCH 313/878] Create actions-stale.yaml --- .github/workflows/actions-stale.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/actions-stale.yaml diff --git a/.github/workflows/actions-stale.yaml b/.github/workflows/actions-stale.yaml new file mode 100644 index 00000000..3b0616f6 --- /dev/null +++ b/.github/workflows/actions-stale.yaml @@ -0,0 +1,16 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '* 8 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' + stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' + # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues + # days-before-stale: 60 + # days-before-close: 7 From e4901d6be23caded39e05d78eeadaba04e11b871 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 13 Jul 2021 23:21:01 +0200 Subject: [PATCH 314/878] Add Homekit pairing methods (pair-add/list/remove + configure) Also change the features flag to match airplay2-receiver --- rtsp.c | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++ shairport.c | 13 ++--- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/rtsp.c b/rtsp.c index d0cc762c..ba09ce15 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1555,6 +1555,125 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) #endif #ifdef CONFIG_AIRPLAY_2 +struct pairings { + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + uint8_t public_key[32]; + + struct pairings *next; +} *pairings; + +static struct pairings * pairing_find(const char *device_id) +{ + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + if (strcmp(device_id, pairing->device_id) == 0) + return pairing; + } + return NULL; +} + +static void pairing_add(uint8_t public_key[32], const char *device_id) { + struct pairings *pairing = calloc(1, sizeof(struct pairings)); + snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + + pairing->next = pairings; + pairings = pairing; +} + +static void pairing_remove(struct pairings *pairing) { + if (pairing == pairings) { + pairings = pairing->next; + } else { + struct pairings *iter; + for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) + ; /* EMPTY */ + + if (iter) + iter->next = pairing->next; + } + + free(pairing); +} + +static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) { + debug(1, "pair-add cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (pairing) { + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + return 0; + } + + pairing_add(public_key, device_id); + return 0; +} + +static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) { + debug(1, "pair-remove cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (!pairing) { + debug(1, "pair-remove callback for unknown device"); + return -1; + } + + pairing_remove(pairing); + return 0; +} + +static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) { + debug(1, "pair-list cb"); + + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); + } +} + +void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-add returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-add response", resp); +} + +void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-list returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-list response", resp); +} + +void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-remove returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-remove response", resp); +} + void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; uint8_t *body = NULL; @@ -1747,14 +1866,56 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message msg_add_header(resp, "Content-Type", "application/octet-stream"); } +/* + Identifier + 21cc689d-d5de-4814-872c-71d1426b57e0 + Enable_HK_Access_Control + + PublicKey + + qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= + + Device_Name + MyDevice + Access_Control_Level + 0 +*/ +void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req __attribute__((unused)), rtsp_message *resp) { + uint8_t public_key[32]; + + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); + + plist_t response_plist = plist_new_dict(); + + plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); + plist_dict_set_item(response_plist, "PublicKey", plist_new_data((const char *)public_key, sizeof(public_key))); + plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); + plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "POST /configure response:", resp); +} + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { handle_pair_setup(conn, req, resp); } else if (strcmp(req->path, "/pair-verify") == 0) { handle_pair_verify(conn, req, resp); + } else if (strcmp(req->path, "/pair-add") == 0) { + handle_pair_add(conn, req, resp); + } else if (strcmp(req->path, "/pair-remove") == 0) { + handle_pair_remove(conn, req, resp); + } else if (strcmp(req->path, "/pair-list") == 0) { + handle_pair_list(conn, req, resp); } else if (strcmp(req->path, "/fp-setup") == 0) { handle_fp_setup(conn, req, resp); + } else if (strcmp(req->path, "/configure") == 0) { + handle_configure(conn, req, resp); } else { debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); diff --git a/shairport.c b/shairport.c index 45a08c7f..0f7f141f 100644 --- a/shairport.c +++ b/shairport.c @@ -1587,15 +1587,12 @@ int main(int argc, char **argv) { // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: // 496155702020608 this setting here is the source of both the plist features response and the // mDNS string. - config.airplay_features = 0x1C340405F4A00; + // note: 0x300401F4A00 works but with weird delays and stuff + config.airplay_features = 0x1C340405FCA00; // Advertised with mDNS and returned with GET /info, see // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN - // required (transient pairing), no Homekit access control 0x204: Audio cable attached, - // OneTimePairingRequired 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit - // access control - - // note: 0x300401F4A00 works but with weird delays and stuff - + // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio + // cable attached, OneTimePairingRequired, device was setup for Homekit access control config.airplay_statusflags = 0x4; // Set to NULL to work with transient pairing config.airplay_pin = NULL; @@ -1622,7 +1619,7 @@ int main(int argc, char **argv) { uuid_generate_random(binuuid); char *uuid = malloc(UUID_STR_LEN); - /* Produces a UUID string at uuid consisting of lower-case letters. */ + // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); config.airplay_pi = strdup(uuid); config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi From 58b8513ccd4a1cfadfd33556aceaea3bc0e8df97 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Jul 2021 08:58:43 +0100 Subject: [PATCH 315/878] Spell out "Shairport Sync" explicitly so that it appears in the info in the Home app. Quieten a debug message. --- rtsp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index ba09ce15..e8055fed 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2369,7 +2369,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "Can't find timingPeerInfo addresses"); } } else { - debug_log_rtsp_message(1, "Unrecognised SETUP incoming message", req); + debug_log_rtsp_message(2, "Unrecognised SETUP incoming message", req); } // make up the timing peer info list part of the response... @@ -4414,7 +4414,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = ap1_featuresString; *p++ = firmware_version; *p++ = "md=2"; - *p++ = "am=SPS"; + *p++ = "am=Shairport Sync"; *p++ = "sf=0x4"; *p++ = "tp=UDP"; *p++ = "vn=65537"; @@ -4475,7 +4475,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = "acl=0"; *p++ = "rsf=0x0"; *p++ = firmware_version; - *p++ = "model=SPS"; + *p++ = "model=Shairport Sync"; char piString[64]; snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); *p++ = piString; From a07ece496ef4664d0e26dc0418d29be740b17310 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Jul 2021 09:23:50 +0100 Subject: [PATCH 316/878] Quieten a few more debug messages. --- rtsp.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index e8055fed..7f9f7e61 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1596,7 +1596,7 @@ static void pairing_remove(struct pairings *pairing) { } static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) { - debug(1, "pair-add cb for %s", device_id); + debug(2, "pair-add cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); if (pairing) { @@ -1609,11 +1609,11 @@ static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *c } static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) { - debug(1, "pair-remove cb for %s", device_id); + debug(2, "pair-remove cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); if (!pairing) { - debug(1, "pair-remove callback for unknown device"); + debug(2, "pair-remove callback for unknown device"); return -1; } @@ -1622,7 +1622,7 @@ static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), con } static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) { - debug(1, "pair-list cb"); + debug(2, "pair-list cb"); for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); From 0ae70e9d842e9193934f37adaa8e8f5ae6f26296 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:56:39 +0100 Subject: [PATCH 317/878] Recognise the output_max_db setting even when ignore_volume_control is true, duh. --- player.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/player.c b/player.c index 70ba36ec..e2c7e801 100644 --- a/player.c +++ b/player.c @@ -2940,7 +2940,6 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c // if it's both, we haven't decided whether hw or sw should be on top // we have to consider the settings ignore_volume_control and mute. - if (config.ignore_volume_control == 0) { if (airplay_volume == -144.0) { if ((config.output->mute) && (config.output->mute(1) == 0)) @@ -2978,14 +2977,16 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c debug(1, "player_volume_without_notification: error: not in a volume mode"); break; } - double scaled_attenuation = 0.0; - if (config.volume_control_profile == VCP_standard) - scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points - else if (config.volume_control_profile == VCP_flat) - scaled_attenuation = - flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points - else - debug(1, "player_volume_without_notification: unrecognised volume control profile"); + double scaled_attenuation = max_db; + if (config.ignore_volume_control == 0) { + if (config.volume_control_profile == VCP_standard) + scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points + else if (config.volume_control_profile == VCP_flat) + scaled_attenuation = + flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points + else + debug(1, "player_volume_without_notification: unrecognised volume control profile"); + } // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward. double hardware_attenuation = 0.0; @@ -3043,8 +3044,10 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) { double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000); - // debug(1,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of - // %f",software_attenuation,temp_fix_volume,airplay_volume); + if (config.ignore_volume_control == 0) + debug(2,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of %f",software_attenuation,temp_fix_volume,airplay_volume); + else + debug(2,"Software attenuation set to %f, i.e %f out of 65,536. Volume control is ignored.",software_attenuation,temp_fix_volume); conn->fix_volume = temp_fix_volume; @@ -3083,6 +3086,8 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c "is disabled.", volume_mode, airplay_volume, software_attenuation, hardware_attenuation); } + + /* } #ifdef CONFIG_METADATA @@ -3095,6 +3100,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c send_ssnc_metadata('pvol', dv, strlen(dv), 1); } #endif +*/ // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; From f5d43c4b5c550385309a6a59b9dd15763e5eec14 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 22:50:54 +0100 Subject: [PATCH 318/878] Rename file. --- .github/workflows/{actions-stale.yaml => stale.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{actions-stale.yaml => stale.yaml} (100%) diff --git a/.github/workflows/actions-stale.yaml b/.github/workflows/stale.yaml similarity index 100% rename from .github/workflows/actions-stale.yaml rename to .github/workflows/stale.yaml From ef23f92e45ffc0b7b0d6901b08ed7780ef563068 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 22:52:43 +0100 Subject: [PATCH 319/878] Rename actions-stale.yaml to stale.yaml --- .github/workflows/{actions-stale.yaml => stale.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{actions-stale.yaml => stale.yaml} (100%) diff --git a/.github/workflows/actions-stale.yaml b/.github/workflows/stale.yaml similarity index 100% rename from .github/workflows/actions-stale.yaml rename to .github/workflows/stale.yaml From 5ba110058c2538aa36669deff81cc25ed2108d8f Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 22:57:25 +0100 Subject: [PATCH 320/878] Added permissions. --- .github/workflows/stale.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 3b0616f6..9a755de2 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -3,6 +3,10 @@ on: schedule: - cron: '* 8 * * *' +permissions: + issues: write + pull-requests: write + jobs: stale: runs-on: ubuntu-latest From 61109e9bd597c8851f4abe35b0199ea329749cc4 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 22:58:38 +0100 Subject: [PATCH 321/878] Added permissions. --- .github/workflows/stale.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 3b0616f6..9a755de2 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -3,6 +3,10 @@ on: schedule: - cron: '* 8 * * *' +permissions: + issues: write + pull-requests: write + jobs: stale: runs-on: ubuntu-latest From f4a0f4838a7ebef09ffb330af36f7f39f2e150cc Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 22:59:50 +0100 Subject: [PATCH 322/878] Increase cron for stale action - test. --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 9a755de2..96fdbe26 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '* 8 * * *' + - cron: '*/5 * * * *' permissions: issues: write From e6bd20f851f10922f851cfb15fe96c170a04ad92 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:32:40 +0100 Subject: [PATCH 323/878] Update version. --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 96fdbe26..88a26e63 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' From 46284bb115ae056a4567a1d175b67a48bf23f5d9 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:33:22 +0100 Subject: [PATCH 324/878] Update version. --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 9a755de2..8ca23def 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' From 91fdd7c3c2df891ce0ca4690c879e6e118f3a513 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:41:47 +0100 Subject: [PATCH 325/878] Test stale action. --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 8ca23def..88a26e63 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '* 8 * * *' + - cron: '*/5 * * * *' permissions: issues: write From 7c2d773e838a184dae29b89af049cb5fe581ba3f Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:56:22 +0100 Subject: [PATCH 326/878] Test. --- .github/workflows/stale.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 88a26e63..5faf85f6 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -16,5 +16,5 @@ jobs: stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues - # days-before-stale: 60 - # days-before-close: 7 + days-before-stale: 60 + days-before-close: 7 From 5d40aa62ba43b5a1ccd668fc05fd1c8a1251fb87 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 00:00:59 +0100 Subject: [PATCH 327/878] Test. --- .github/workflows/stale.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 5faf85f6..88a26e63 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -16,5 +16,5 @@ jobs: stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues - days-before-stale: 60 - days-before-close: 7 + # days-before-stale: 60 + # days-before-close: 7 From d59340b1ee2f769c5da00b8669279d6d3ebbe934 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 00:07:49 +0100 Subject: [PATCH 328/878] Update stale.yaml --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 88a26e63..be61da3d 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '*/5 * * * *' + - cron: '0 0 * * *' permissions: issues: write From a819ec5df564398e5b343d09f976c7bfc7e88619 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 00:08:46 +0100 Subject: [PATCH 329/878] Update stale.yaml --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 88a26e63..be61da3d 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '*/5 * * * *' + - cron: '0 0 * * *' permissions: issues: write From 3265f08840412a6b1c2ea59b1dff5b7357e3fd58 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:39:56 +0100 Subject: [PATCH 330/878] Updates from public SPS repo. --- .github/workflows/docker-build-releases.yaml | 30 ++++++++++++-------- .github/workflows/docker-build.yaml | 30 ++++++++++++-------- docker/Dockerfile | 13 ++++----- docker/README.md | 14 +++++++-- docker/airplay1/README.md | 2 +- docker/docker-compose.yaml | 2 +- 6 files changed, 55 insertions(+), 36 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index 80e6d263..ef355fa8 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -8,8 +8,11 @@ name: Build and push docker images on releases. on: release: - types: [ published ] + types: [ ] +env: + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + jobs: main: runs-on: ubuntu-latest @@ -22,11 +25,14 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Set up ssh agent - uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - + - name: Determine NQPTP env (part 1). + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV + + - name: Determine NQPTP env (part 2). + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -36,7 +42,7 @@ jobs: - name: Login to Docker Registry uses: docker/login-action@v1 with: - registry: registry.nova.do + registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} @@ -45,20 +51,20 @@ jobs: with: context: ./ file: ./docker/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.PLATFORMS }} push: true - tags: registry.nova.do/shairport-sync:latest registry.nova.do/shairport-sync:${{ github.event.release.tag_name }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 with: context: ./ file: ./docker/airplay1/Dockerfile-AirPlay1 - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.PLATFORMS }} push: true - tags: registry.nova.do/shairport-sync:latest-airplay1, registry.nova.do/shairport-sync:${{ github.event.release.tag_name }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest-airplay1, ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index a5a5f68c..27cb0342 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,5 +1,5 @@ # Builds & pushes a docker image when a commit is made to one of the branches specified below. -# Tag pattern: 'unstable-[BRANCH NAME]'. +# Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-airplay1' # See docker-build-releases.yaml for release build process. name: Build and push docker image based on commit to specified branches. @@ -7,6 +7,9 @@ name: Build and push docker image based on commit to specified branches. on: push: branches: [ master, development, charles-dev ] + +env: + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 jobs: main: @@ -20,11 +23,14 @@ jobs: - name: Set SHAIRPORT_SYNC_BRANCH env. run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Set up ssh agent - uses: webfactory/ssh-agent@v0.5.2 # Can be removed when no longer pulling from private repos. - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - + - name: Determine NQPTP env (part 1). + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV + + - name: Determine NQPTP env (part 2). + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} + run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -34,7 +40,7 @@ jobs: - name: Login to Docker Registry uses: docker/login-action@v1 with: - registry: registry.nova.do + registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} @@ -43,20 +49,20 @@ jobs: with: context: ./ file: ./docker/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - ssh: default=${{ env.SSH_AUTH_SOCK }} # Can be removed when no longer pulling from private repos. + NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} - name: Build and push (AirPlay 1) uses: docker/build-push-action@v2 with: context: ./ file: ./docker/airplay1/Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: registry.nova.do/shairport-sync:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 1203b1c6..98a3900c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,22 +43,21 @@ RUN make install # Shairport Sync Build System: FROM builder-base AS builder-sps -# This will be modified by the Github Action Workflow and is required -# to ensure the correct branch is being used. +# These will be modified by the Github Action Workflow and are required +# to ensure the correct branches are being used. ARG SHAIRPORT_SYNC_BRANCH RUN test -n "$SHAIRPORT_SYNC_BRANCH" +ARG NQPTP_BRANCH +RUN test -n "$NQPTP_BRANCH" COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -# TODO: Remove when no longer pulling from private repos. -RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - -RUN --mount=type=ssh git clone git@github.com:mikebrady/nqptp.git +RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp # Use development branch for now. -RUN git checkout development +RUN git checkout "$NQPTP_BRANCH" RUN autoreconf -fi RUN ./configure RUN make diff --git a/docker/README.md b/docker/README.md index ccb691a2..6043cc44 100644 --- a/docker/README.md +++ b/docker/README.md @@ -28,12 +28,20 @@ Edit the configuration file `/etc/shairport-sync.conf` in the container (or use ## Build Example (for arm7 devices) -TODO: remove `--ssh` when going public as this is only for private repos. ``` -docker buildx build --ssh default=$SSH_AUTH_SOCK --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development -t registry.nova.do/shairport-sync:unstable-development . +docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development -t shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. ## AirPlay 1 Only -The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. \ No newline at end of file + +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image build via the AirPlay 2 branch does not work with multiple instances on the same hardware. + +## GitHub Action Builds + +Requires the following secrets to be set in the repo: +- `DOCKER_REGISTRY` - docker.io if using Docker Hub, else set to your registry URL. +- `DOCKER_REGISTRY_TOKEN` - Access token for your registry. +- `DOCKER_REGISTRY_USER` - Login user for your registry. +- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub. \ No newline at end of file diff --git a/docker/airplay1/README.md b/docker/airplay1/README.md index 247754e6..f8335d76 100644 --- a/docker/airplay1/README.md +++ b/docker/airplay1/README.md @@ -1,3 +1,3 @@ # AirPlay 1 Only Docker Image -See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. For the latest build, use `latest-airplay1`. \ No newline at end of file +See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index abcce4bc..74ed09ac 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2,7 +2,7 @@ services: shairport-sync: - image: registry.nova.do/shairport-sync:unstable-development + image: mikebrady/shairport-sync:unstable-development network_mode: host restart: unless-stopped devices: From df9c6b4fa0e27328be5f4665d5d5d09ddd2c4cd7 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:44:58 +0100 Subject: [PATCH 331/878] Use 'main' branch instead of 'master' for nqptp. --- .github/workflows/docker-build-releases.yaml | 2 +- .github/workflows/docker-build.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index ef355fa8..6894a0e7 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -31,7 +31,7 @@ jobs: - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 27cb0342..33b1c8e0 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -29,7 +29,7 @@ jobs: - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 From 6564cf1600cbd101c613fc516a7f4e667852a27e Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:45:40 +0100 Subject: [PATCH 332/878] Use 'main' branch instead of 'master' for nqptp. --- .github/workflows/docker-build-releases.yaml | 2 +- .github/workflows/docker-build.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-releases.yaml index ef355fa8..6894a0e7 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-releases.yaml @@ -31,7 +31,7 @@ jobs: - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index af394e1c..e4ac6b48 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -29,7 +29,7 @@ jobs: - name: Determine NQPTP env (part 2). if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} - run: echo "NQPTP_BRANCH=master" >> $GITHUB_ENV + run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 From 77895f1dccb3845fd57a7383476305d2462ff958 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:49:22 +0100 Subject: [PATCH 333/878] Remove stale since it's now on master instead. --- .github/workflows/stale.yaml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/stale.yaml diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml deleted file mode 100644 index be61da3d..00000000 --- a/.github/workflows/stale.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Close stale issues and PRs' -on: - schedule: - - cron: '0 0 * * *' - -permissions: - issues: write - pull-requests: write - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v4 - with: - stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' - stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' - # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues - # days-before-stale: 60 - # days-before-close: 7 From ac5e4eba2f9f746567e6ffa0623926b9e1545e8f Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 13:42:33 +0100 Subject: [PATCH 334/878] Comment change. --- docker/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/start.sh b/docker/start.sh index bb790283..7221417a 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -10,7 +10,7 @@ dbus-daemon --system avahi-daemon --daemonize --no-chroot -# TODO: This should probably be implemented in a better way and using the correct nqptp group. +# TODO: This should probably be implemented in a better way. nqptp & # su-exec shairport-sync shairport-sync $@ From 07569a5786f991d08779b04d318a6744d0bc1082 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Thu, 15 Jul 2021 14:01:09 +0100 Subject: [PATCH 335/878] Try again with updated secret. --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b7d5ff89..41d81e74 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: [paypal.me/UMBr] +custom: [paypal.me/UMBr] \ No newline at end of file From 978862c2ec2c1c141ef2ed235057a024a7dbc46d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 13 Jul 2021 22:28:34 +0200 Subject: [PATCH 336/878] Update pair_ap to 0.8 (fixes incorrect pair-list response) --- pair_ap/README.md | 13 +++++++------ pair_ap/pair.h | 2 +- pair_ap/pair_homekit.c | 22 +++++++++++++++++++--- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pair_ap/README.md b/pair_ap/README.md index 1c855444..d6b00d65 100644 --- a/pair_ap/README.md +++ b/pair_ap/README.md @@ -2,9 +2,10 @@ C client implementation of pairing for: * Apple TV device verification, which became mandatory with tvOS 10.2 (this is called fruit mode in pair_ap) -* Homekit pairing (for AirPlay 2, not working for Home app) +* Homekit pairing (also for AirPlay 2) Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting. + ## Requirements - libsodium - libgcrypt or libopenssl @@ -33,7 +34,7 @@ The controller uses `/pair-add` to make sure that all devices on a network get the ID and public key of all the other devices, so that the user only needs to pair a device once. -### Normal pairing +### Normal pairing with one-time code For a normal first-time pairing, the client needs a one-time code (the device announces via mDNS whether a code is required). The client calls `/pair-pin-start` and the device displays the code. There is also QR-based @@ -42,12 +43,12 @@ pairing, which is (probably?) an encoded code. After obtaining the code, the client initiates a three step `/pair-setup` sequence, which results in both peers registering each other's ID and public key. Henceforth, a pairing is verified with the two step `/pair-verify`, where -the parties check each-others identify. Saving the peer's ID + public key isn't +the parties check eachothers identify. Saving the peer's ID + public key isn't strictly necessary if client or server doesn't care about verifying the peer, i.e. that `/pair-setup` has actually been completed. The result of `/pair-verify` is a shared secret that is used for symmetric -encryption of the following communinacation between the parties. +encryption of the following communication between the parties. ### Transient pairing Some devices don't require a code from the user for pairing (e.g. an Airport @@ -55,8 +56,7 @@ Express 2). If so, the client just needs to go through a two-step `/pair-setup` sequence which results in a shared secret, which is then used for encrypted communication. A fixed code of 3939 is used. -Such devices don't appear to be fully Homekit compatible - they will not, for -instance - appear in the Home app. +The controller can still use `/pair-add` etc. towards such devices. ## "fruit" pairing Like normal Homekit pairing, this consists of first requesting a code with @@ -69,4 +69,5 @@ shared secret. - [AirPlayAuth](https://github.com/funtax/AirPlayAuth) - [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC) - [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender) +- [airplay2-receiver](https://github.com/ckdo/airplay2-receiver) - [csrp](https://github.com/cocagne/csrp) diff --git a/pair_ap/pair.h b/pair_ap/pair.h index 6324bc15..3877e2bb 100644 --- a/pair_ap/pair.h +++ b/pair_ap/pair.h @@ -4,7 +4,7 @@ #include #define PAIR_AP_VERSION_MAJOR 0 -#define PAIR_AP_VERSION_MINOR 5 +#define PAIR_AP_VERSION_MINOR 8 #define PAIR_AP_DEVICE_ID_LEN_MAX 64 diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c index 076ea14a..ed59807e 100644 --- a/pair_ap/pair_homekit.c +++ b/pair_ap/pair_homekit.c @@ -2679,6 +2679,7 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in pair_tlv_t *device_id; pair_tlv_t *pk; char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + uint8_t *public_key = NULL; request = message_process(in, in_len, &errmsg); if (!request) @@ -2687,15 +2688,21 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in } device_id = pair_tlv_get_value(request, TLVType_Identifier); - pk = pair_tlv_get_value(request, TLVType_PublicKey); - if (!device_id || device_id->size >= sizeof(id_str) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES) + if (!device_id || device_id->size >= sizeof(id_str)) { goto error; } + // Only present when adding + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (pk && pk->size == crypto_sign_PUBLICKEYBYTES) + { + public_key = pk->value; + } + memcpy(id_str, device_id->value, device_id->size); - cb(pk->value, id_str, cb_arg); + cb(public_key, id_str, cb_arg); pair_tlv_free(request); return 0; @@ -2757,9 +2764,18 @@ static int server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg) { pair_tlv_values_t *response = cb_arg; + pair_tlv_t *previous_id; + uint8_t permissions = 1; // Means admin (TODO don't hardcode - let caller set) + + // If this isn't the first iteration (item) then we must add a separator + previous_id = pair_tlv_get_value(response, TLVType_Identifier); + if (previous_id) + pair_tlv_add_value(response, TLVType_Separator, NULL, 0); pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id)); pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES); + pair_tlv_add_value(response, TLVType_Permissions, &permissions, sizeof(permissions)); + return 0; } From 1a64a56c3f807d69b9c27e8dc2b3c47ec6db453b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 13 Jul 2021 23:21:01 +0200 Subject: [PATCH 337/878] Add Homekit pairing methods (pair-add/list/remove + configure) Also change the features flag to match airplay2-receiver --- rtsp.c | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++ shairport.c | 13 ++--- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/rtsp.c b/rtsp.c index d0cc762c..ba09ce15 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1555,6 +1555,125 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) #endif #ifdef CONFIG_AIRPLAY_2 +struct pairings { + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + uint8_t public_key[32]; + + struct pairings *next; +} *pairings; + +static struct pairings * pairing_find(const char *device_id) +{ + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + if (strcmp(device_id, pairing->device_id) == 0) + return pairing; + } + return NULL; +} + +static void pairing_add(uint8_t public_key[32], const char *device_id) { + struct pairings *pairing = calloc(1, sizeof(struct pairings)); + snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + + pairing->next = pairings; + pairings = pairing; +} + +static void pairing_remove(struct pairings *pairing) { + if (pairing == pairings) { + pairings = pairing->next; + } else { + struct pairings *iter; + for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) + ; /* EMPTY */ + + if (iter) + iter->next = pairing->next; + } + + free(pairing); +} + +static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) { + debug(1, "pair-add cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (pairing) { + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + return 0; + } + + pairing_add(public_key, device_id); + return 0; +} + +static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) { + debug(1, "pair-remove cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (!pairing) { + debug(1, "pair-remove callback for unknown device"); + return -1; + } + + pairing_remove(pairing); + return 0; +} + +static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) { + debug(1, "pair-list cb"); + + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); + } +} + +void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-add returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-add response", resp); +} + +void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-list returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-list response", resp); +} + +void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-remove returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-remove response", resp); +} + void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int ret; uint8_t *body = NULL; @@ -1747,14 +1866,56 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message msg_add_header(resp, "Content-Type", "application/octet-stream"); } +/* + Identifier + 21cc689d-d5de-4814-872c-71d1426b57e0 + Enable_HK_Access_Control + + PublicKey + + qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= + + Device_Name + MyDevice + Access_Control_Level + 0 +*/ +void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req __attribute__((unused)), rtsp_message *resp) { + uint8_t public_key[32]; + + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); + + plist_t response_plist = plist_new_dict(); + + plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); + plist_dict_set_item(response_plist, "PublicKey", plist_new_data((const char *)public_key, sizeof(public_key))); + plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); + plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "POST /configure response:", resp); +} + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { handle_pair_setup(conn, req, resp); } else if (strcmp(req->path, "/pair-verify") == 0) { handle_pair_verify(conn, req, resp); + } else if (strcmp(req->path, "/pair-add") == 0) { + handle_pair_add(conn, req, resp); + } else if (strcmp(req->path, "/pair-remove") == 0) { + handle_pair_remove(conn, req, resp); + } else if (strcmp(req->path, "/pair-list") == 0) { + handle_pair_list(conn, req, resp); } else if (strcmp(req->path, "/fp-setup") == 0) { handle_fp_setup(conn, req, resp); + } else if (strcmp(req->path, "/configure") == 0) { + handle_configure(conn, req, resp); } else { debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); diff --git a/shairport.c b/shairport.c index 45a08c7f..0f7f141f 100644 --- a/shairport.c +++ b/shairport.c @@ -1587,15 +1587,12 @@ int main(int argc, char **argv) { // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: // 496155702020608 this setting here is the source of both the plist features response and the // mDNS string. - config.airplay_features = 0x1C340405F4A00; + // note: 0x300401F4A00 works but with weird delays and stuff + config.airplay_features = 0x1C340405FCA00; // Advertised with mDNS and returned with GET /info, see // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN - // required (transient pairing), no Homekit access control 0x204: Audio cable attached, - // OneTimePairingRequired 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit - // access control - - // note: 0x300401F4A00 works but with weird delays and stuff - + // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio + // cable attached, OneTimePairingRequired, device was setup for Homekit access control config.airplay_statusflags = 0x4; // Set to NULL to work with transient pairing config.airplay_pin = NULL; @@ -1622,7 +1619,7 @@ int main(int argc, char **argv) { uuid_generate_random(binuuid); char *uuid = malloc(UUID_STR_LEN); - /* Produces a UUID string at uuid consisting of lower-case letters. */ + // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); config.airplay_pi = strdup(uuid); config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi From 5630e20eb02d28ce3d53abd38faab7c991986238 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 16 Jul 2021 11:24:05 +0100 Subject: [PATCH 338/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c14ba33e..0f0202cb 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version 4.0-dev-182-gc0ee8d1 +==== +#### New Features +- **Home App Integration** An AirPlay 2 build of Shairport Sync now offers limited integration with the Home app. Specifically, a Shairport Sync AirPlay 2 instance can now be added as a speaker to the Home app. Thanks to [ckdo](https://github.com/ckdo) and [ejurgensen](https://github.com/ejurgensen) for figuring this out and for coding it up. It means that some Siri interaction is possible. +- **Automated Docker Builds** Thanks to the work of [Charles Omer](https://github.com/charlesomer), Docker builds on the `development` branch should appear from time to time on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). + +#### Bug Fixes +- [Crashing bug fix](https://github.com/mikebrady/nqptp/blob/main/RELEASE_NOTES.md) in `nqptp` -- thanks to [ste94pz](https://github.com/ste94pz). + Version 4.0-dev-153-g9ccde50 ==== From 3a6297bc9aca9fa2b88d7761b12e9a328764dd84 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 16 Jul 2021 15:04:51 +0100 Subject: [PATCH 339/878] Honour max_output_db setting even when ignore_volume_control is true. Spell out "Shairport Sync" in the Home app information. --- player.c | 312 ++++++++++++++++++++++++++++--------------------------- rtsp.c | 71 +++++++------ 2 files changed, 198 insertions(+), 185 deletions(-) diff --git a/player.c b/player.c index 70ba36ec..ac332a43 100644 --- a/player.c +++ b/player.c @@ -1760,27 +1760,25 @@ void *player_thread_func(void *arg) { // successive rtptimes, at worst conn->output_sample_ratio = config.output_rate / conn->input_rate; - - -// Sign extending rtptime calculations to 64 bit is needed from time to time. -// The standard rtptime is unsigned 32 bits, -// so you can do modulo 2^32 difference calculations -// and get a signed result simply by typing the result as a signed 32-bit number. + // Sign extending rtptime calculations to 64 bit is needed from time to time. -// So long as you can be sure the numbers are within 2^31 of each other, -// the sign of the result calculated in this way indicates the order of the operands. -// For example, if you subtract a from b and the result is positive, you can conclude -// b is the same as or comes after a in module 2^32 order. + // The standard rtptime is unsigned 32 bits, + // so you can do modulo 2^32 difference calculations + // and get a signed result simply by typing the result as a signed 32-bit number. -// We want to do the same with the rtptime calculations for multiples of -// the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit/ -// Therefore we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 bit unsigned -// numbers on the same basis. + // So long as you can be sure the numbers are within 2^31 of each other, + // the sign of the result calculated in this way indicates the order of the operands. + // For example, if you subtract a from b and the result is positive, you can conclude + // b is the same as or comes after a in module 2^32 order. -// That is what the output_rtptime_sign_bit, output_rtptime_mask, output_rtptime_mask_not and -// output_rtptime_sign_mask are for -- see later, calculating the sync error. + // We want to do the same with the rtptime calculations for multiples of + // the rtptimes (1, 2, 4 or 8 times), and we want to do this in signed 64-bit/ + // Therefore we need to sign extend these modulo 2^32, 2^33, 2^34, or 2^35 bit unsigned + // numbers on the same basis. + // That is what the output_rtptime_sign_bit, output_rtptime_mask, output_rtptime_mask_not and + // output_rtptime_sign_mask are for -- see later, calculating the sync error. int output_rtptime_sign_bit; switch (conn->output_sample_ratio) { @@ -2022,7 +2020,6 @@ void *player_thread_func(void *arg) { } #endif - #ifdef CONFIG_AIRPLAY_2 if (conn->timing_type == ts_ntp) { #endif @@ -2940,45 +2937,46 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c // if it's both, we haven't decided whether hw or sw should be on top // we have to consider the settings ignore_volume_control and mute. - if (config.ignore_volume_control == 0) { - if (airplay_volume == -144.0) { + if (airplay_volume == -144.0) { - if ((config.output->mute) && (config.output->mute(1) == 0)) - debug(2, - "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "hardware mute is enabled.", - volume_mode, airplay_volume); - else { - conn->software_mute_enabled = 1; - debug(2, - "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "software mute is enabled.", - volume_mode, airplay_volume); - } + if ((config.output->mute) && (config.output->mute(1) == 0)) + debug(2, + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "hardware mute is enabled.", + volume_mode, airplay_volume); + else { + conn->software_mute_enabled = 1; + debug(2, + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "software mute is enabled.", + volume_mode, airplay_volume); + } + + } else { + int32_t max_db = 0, min_db = 0; + switch (volume_mode) { + case vol_hw_only: + max_db = hw_max_db; + min_db = hw_min_db; + break; + case vol_sw_only: + max_db = sw_max_db; + min_db = sw_min_db; + break; + case vol_both: + // debug(1, "dB range passed is hw: %d, sw: %d, total: %d", hw_max_db - hw_min_db, + // sw_max_db - sw_min_db, (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db)); + max_db = + (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db); // this should be the range requested + min_db = 0; + break; + default: + debug(1, "player_volume_without_notification: error: not in a volume mode"); + break; + } + double scaled_attenuation = max_db; + if (config.ignore_volume_control == 0) { - } else { - int32_t max_db = 0, min_db = 0; - switch (volume_mode) { - case vol_hw_only: - max_db = hw_max_db; - min_db = hw_min_db; - break; - case vol_sw_only: - max_db = sw_max_db; - min_db = sw_min_db; - break; - case vol_both: - // debug(1, "dB range passed is hw: %d, sw: %d, total: %d", hw_max_db - hw_min_db, - // sw_max_db - sw_min_db, (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db)); - max_db = - (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db); // this should be the range requested - min_db = 0; - break; - default: - debug(1, "player_volume_without_notification: error: not in a volume mode"); - break; - } - double scaled_attenuation = 0.0; if (config.volume_control_profile == VCP_standard) scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points else if (config.volume_control_profile == VCP_flat) @@ -2986,116 +2984,122 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points else debug(1, "player_volume_without_notification: unrecognised volume control profile"); - - // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward. - double hardware_attenuation = 0.0; - double software_attenuation = 0.0; - - switch (volume_mode) { - case vol_hw_only: - hardware_attenuation = scaled_attenuation; - break; - case vol_sw_only: - software_attenuation = scaled_attenuation; - break; - case vol_both: - // here, we now the attenuation required, so we have to apportion it to the sw and hw mixers - // if we give the hw priority, that means when lowering the volume, set the hw volume to its - // lowest - // before using the sw attenuation. - // similarly, if we give the sw priority, that means when lowering the volume, set the sw - // volume to its lowest - // before using the hw attenuation. - // one imagines that hw priority is likely to be much better - // if (config.volume_range_hw_priority) { - if (config.volume_range_hw_priority != 0) { - // hw priority - if ((sw_max_db - sw_min_db) > scaled_attenuation) { - software_attenuation = sw_min_db + scaled_attenuation; - hardware_attenuation = hw_min_db; - } else { - software_attenuation = sw_max_db; - hardware_attenuation = hw_min_db + scaled_attenuation - (sw_max_db - sw_min_db); - } - } else { - // sw priority - if ((hw_max_db - hw_min_db) > scaled_attenuation) { - hardware_attenuation = hw_min_db + scaled_attenuation; - software_attenuation = sw_min_db; - } else { - hardware_attenuation = hw_max_db; - software_attenuation = sw_min_db + scaled_attenuation - (hw_max_db - hw_min_db); - } - } - break; - default: - debug(1, "player_volume_without_notification: error: not in a volume mode"); - break; - } - - if (((volume_mode == vol_hw_only) || (volume_mode == vol_both)) && (config.output->volume)) { - config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value - // debug(1,"Hardware attenuation set to %f for airplay volume of - // %f.",hardware_attenuation,airplay_volume); - if (volume_mode == vol_hw_only) - conn->fix_volume = 0x10000; - } - - if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) { - double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000); - // debug(1,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of - // %f",software_attenuation,temp_fix_volume,airplay_volume); - - conn->fix_volume = temp_fix_volume; - - // if (config.loudness) - loudness_set_volume(software_attenuation / 100); - } - - if (config.logOutputLevel) { - inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); - } - -#ifdef CONFIG_METADATA - // here, send the 'pvol' metadata message when the airplay volume information - // is being used by shairport sync to control the output volume - char dv[128]; - memset(dv, 0, 128); - if (volume_mode == vol_both) { - // normalise the maximum output to the hardware device's max output - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, - (scaled_attenuation - max_db + hw_max_db) / 100.0, - (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); - } else { - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, - min_db / 100.0, max_db / 100.0); - } - send_ssnc_metadata('pvol', dv, strlen(dv), 1); -#endif - - if (config.output->mute) - config.output->mute(0); - conn->software_mute_enabled = 0; - - debug(2, - "player_volume_without_notification: volume mode is %d, airplay volume is %f, " - "software_attenuation: %f, hardware_attenuation: %f, muting " - "is disabled.", - volume_mode, airplay_volume, software_attenuation, hardware_attenuation); } - } + // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward. + double hardware_attenuation = 0.0; + double software_attenuation = 0.0; + + switch (volume_mode) { + case vol_hw_only: + hardware_attenuation = scaled_attenuation; + break; + case vol_sw_only: + software_attenuation = scaled_attenuation; + break; + case vol_both: + // here, we now the attenuation required, so we have to apportion it to the sw and hw mixers + // if we give the hw priority, that means when lowering the volume, set the hw volume to its + // lowest + // before using the sw attenuation. + // similarly, if we give the sw priority, that means when lowering the volume, set the sw + // volume to its lowest + // before using the hw attenuation. + // one imagines that hw priority is likely to be much better + // if (config.volume_range_hw_priority) { + if (config.volume_range_hw_priority != 0) { + // hw priority + if ((sw_max_db - sw_min_db) > scaled_attenuation) { + software_attenuation = sw_min_db + scaled_attenuation; + hardware_attenuation = hw_min_db; + } else { + software_attenuation = sw_max_db; + hardware_attenuation = hw_min_db + scaled_attenuation - (sw_max_db - sw_min_db); + } + } else { + // sw priority + if ((hw_max_db - hw_min_db) > scaled_attenuation) { + hardware_attenuation = hw_min_db + scaled_attenuation; + software_attenuation = sw_min_db; + } else { + hardware_attenuation = hw_max_db; + software_attenuation = sw_min_db + scaled_attenuation - (hw_max_db - hw_min_db); + } + } + break; + default: + debug(1, "player_volume_without_notification: error: not in a volume mode"); + break; + } + + if (((volume_mode == vol_hw_only) || (volume_mode == vol_both)) && (config.output->volume)) { + config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value + // debug(1,"Hardware attenuation set to %f for airplay volume of + // %f.",hardware_attenuation,airplay_volume); + if (volume_mode == vol_hw_only) + conn->fix_volume = 0x10000; + } + + if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) { + double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000); + + if (config.ignore_volume_control == 0) + debug(2, "Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of %f", + software_attenuation, temp_fix_volume, airplay_volume); + else + debug(2, "Software attenuation set to %f, i.e %f out of 65,536. Volume control is ignored.", + software_attenuation, temp_fix_volume); + + conn->fix_volume = temp_fix_volume; + + // if (config.loudness) + loudness_set_volume(software_attenuation / 100); + } + + if (config.logOutputLevel) { + inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); + } #ifdef CONFIG_METADATA - else { // here, send the 'pvol' metadata message when the airplay volume information // is being used by shairport sync to control the output volume char dv[128]; memset(dv, 0, 128); - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); + if (volume_mode == vol_both) { + // normalise the maximum output to the hardware device's max output + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, + (scaled_attenuation - max_db + hw_max_db) / 100.0, + (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); + } else { + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, + min_db / 100.0, max_db / 100.0); + } send_ssnc_metadata('pvol', dv, strlen(dv), 1); - } #endif + if (config.output->mute) + config.output->mute(0); + conn->software_mute_enabled = 0; + + debug(2, + "player_volume_without_notification: volume mode is %d, airplay volume is %f, " + "software_attenuation: %f, hardware_attenuation: %f, muting " + "is disabled.", + volume_mode, airplay_volume, software_attenuation, hardware_attenuation); + } + /* + } + + #ifdef CONFIG_METADATA + else { + // here, send the 'pvol' metadata message when the airplay volume information + // is being used by shairport sync to control the output volume + char dv[128]; + memset(dv, 0, 128); + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); + send_ssnc_metadata('pvol', dv, strlen(dv), 1); + } + #endif + */ // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); diff --git a/rtsp.c b/rtsp.c index ba09ce15..7e6ff7cf 100644 --- a/rtsp.c +++ b/rtsp.c @@ -50,8 +50,8 @@ #include -#include "config.h" #include "activity_monitor.h" +#include "config.h" #ifdef CONFIG_OPENSSL #include @@ -1560,10 +1560,9 @@ struct pairings { uint8_t public_key[32]; struct pairings *next; -} *pairings; +} * pairings; -static struct pairings * pairing_find(const char *device_id) -{ +static struct pairings *pairing_find(const char *device_id) { for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { if (strcmp(device_id, pairing->device_id) == 0) return pairing; @@ -1595,7 +1594,8 @@ static void pairing_remove(struct pairings *pairing) { free(pairing); } -static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) { +static int pairing_add_cb(uint8_t public_key[32], const char *device_id, + void *cb_arg __attribute__((unused))) { debug(1, "pair-add cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); @@ -1608,7 +1608,8 @@ static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *c return 0; } -static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) { +static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, + void *cb_arg __attribute__((unused))) { debug(1, "pair-remove cb for %s", device_id); struct pairings *pairing = pairing_find(device_id); @@ -1621,7 +1622,8 @@ static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), con return 0; } -static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) { +static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, + void *cb_arg __attribute__((unused))) { debug(1, "pair-list cb"); for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { @@ -1629,10 +1631,12 @@ static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __a } } -void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-add returned an error"); resp->respcode = 451; @@ -1644,10 +1648,12 @@ void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message debug_log_rtsp_message(2, "pair-add response", resp); } -void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-list returned an error"); resp->respcode = 451; @@ -1659,10 +1665,12 @@ void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message debug_log_rtsp_message(2, "pair-list response", resp); } -void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) { +void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { uint8_t *body = NULL; size_t body_len = 0; - int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, (const uint8_t *)req->content, req->contentlength); + int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, + (const uint8_t *)req->content, req->contentlength); if (ret < 0) { debug(1, "pair-remove returned an error"); resp->respcode = 451; @@ -1867,20 +1875,21 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message } /* - Identifier - 21cc689d-d5de-4814-872c-71d1426b57e0 - Enable_HK_Access_Control - - PublicKey - - qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= - - Device_Name - MyDevice - Access_Control_Level - 0 + Identifier + 21cc689d-d5de-4814-872c-71d1426b57e0 + Enable_HK_Access_Control + + PublicKey + + qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= + + Device_Name + MyDevice + Access_Control_Level + 0 */ -void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req __attribute__((unused)), rtsp_message *resp) { +void handle_configure(rtsp_conn_info *conn __attribute__((unused)), + rtsp_message *req __attribute__((unused)), rtsp_message *resp) { uint8_t public_key[32]; pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); @@ -1889,7 +1898,8 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); - plist_dict_set_item(response_plist, "PublicKey", plist_new_data((const char *)public_key, sizeof(public_key))); + plist_dict_set_item(response_plist, "PublicKey", + plist_new_data((const char *)public_key, sizeof(public_key))); plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); @@ -2238,7 +2248,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // hack. conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; // we are stuck with this for the moment. + conn->input_rate = 44100; // we are stuck with this for the moment. conn->input_num_channels = 2; conn->input_bit_depth = 16; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); @@ -2254,7 +2264,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(stream0dict, "audioBufferSize", plist_new_uint(conn->ap2_audio_buffer_size)); - // this should be cancelled by an activity_monitor_signify_activity(1) // call in the SETRATEANCHORI handler, which should come up right away activity_monitor_signify_activity(0); @@ -4414,7 +4423,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = ap1_featuresString; *p++ = firmware_version; *p++ = "md=2"; - *p++ = "am=SPS"; + *p++ = "am=Shairport Sync"; *p++ = "sf=0x4"; *p++ = "tp=UDP"; *p++ = "vn=65537"; @@ -4475,7 +4484,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { *p++ = "acl=0"; *p++ = "rsf=0x0"; *p++ = firmware_version; - *p++ = "model=SPS"; + *p++ = "model=Shairport Sync"; char piString[64]; snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); *p++ = piString; From e26d3edd5c429da2c011eed6dfbf91bf1a7349a7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 16 Jul 2021 15:12:33 +0100 Subject: [PATCH 340/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 0f0202cb..c10e455d 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,11 +1,17 @@ -Version 4.0-dev-182-gc0ee8d1 +Version 4.0-dev-185-g0c02ee2 ==== + #### New Features - **Home App Integration** An AirPlay 2 build of Shairport Sync now offers limited integration with the Home app. Specifically, a Shairport Sync AirPlay 2 instance can now be added as a speaker to the Home app. Thanks to [ckdo](https://github.com/ckdo) and [ejurgensen](https://github.com/ejurgensen) for figuring this out and for coding it up. It means that some Siri interaction is possible. - **Automated Docker Builds** Thanks to the work of [Charles Omer](https://github.com/charlesomer), Docker builds on the `development` branch should appear from time to time on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). #### Bug Fixes - [Crashing bug fix](https://github.com/mikebrady/nqptp/blob/main/RELEASE_NOTES.md) in `nqptp` -- thanks to [ste94pz](https://github.com/ste94pz). +- Honour the `max_volume_db` setting even when `ignore_volume_control` is true. Note that dithering is enabled if software attenuation is needed. + +#### Other Changes +- Spell out `Shairport Sync` instead of `SPS` in a few places. +- Clang format some source files. Version 4.0-dev-153-g9ccde50 ==== From 4c9d1f7904c972392843891669439a65c6b35518 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 12:07:14 +0100 Subject: [PATCH 341/878] Make mDNS txt record arrays global and permanent. We hope to be able to modify them dynamically later. --- rtsp.c | 140 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/rtsp.c b/rtsp.c index 7e6ff7cf..71e93b05 100644 --- a/rtsp.c +++ b/rtsp.c @@ -4053,6 +4053,25 @@ void msg_cleanup_function(void *arg) { msg_free((rtsp_message **)arg); } +// mDNS advertisement strings + +// Create these strings and then keep them updated. +// When necessary, update the mDNS service records, using e.g. Avahi +// from these sources. + +char *txt_records[64]; +char firmware_version[64]; +char ap1_featuresString[64]; +char pkString[128]; +#ifdef CONFIG_AIRPLAY_2 +char *secondary_txt_records[64]; +char deviceIdString[64]; +char featuresString[64]; +char statusflagsString[32]; +char piString[64]; +char gidString[64]; +#endif + static void *rtsp_conversation_thread_func(void *pconn) { rtsp_conn_info *conn = pconn; @@ -4383,19 +4402,12 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { maxfd = sockfd[i]; } - char **t1; // ap1 test records - char **t2; // two text records + char **t1 = txt_records; // ap1 test records + char **t2 = NULL; // possibly two text records - t1 = NULL; - t2 = NULL; - - // make up a txt record - char *txt_records[64]; - char **p = txt_records; - t1 = p; // first set of text records + int entry_number = 0; // make up a firmware version - char firmware_version[64]; #ifdef CONFIG_USE_GIT_VERSION_STRING if (git_version_string[0] != '\0') snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); @@ -4404,96 +4416,86 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 - char ap1_featuresString[64]; uint64_t features_hi = config.airplay_features; features_hi = (features_hi >> 32) & 0xffffffff; uint64_t features_lo = config.airplay_features; features_lo = features_lo & 0xffffffff; snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", features_lo, features_hi); - char pkString[128]; snprintf(pkString, sizeof(pkString), "pk="); pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); - // the ap1 text record is different if it is set up for ap2 - *p++ = "cn=0,1"; - *p++ = "da=true"; - *p++ = "et=0,4"; - *p++ = ap1_featuresString; - *p++ = firmware_version; - *p++ = "md=2"; - *p++ = "am=Shairport Sync"; - *p++ = "sf=0x4"; - *p++ = "tp=UDP"; - *p++ = "vn=65537"; - *p++ = "vs=366.0"; - *p++ = pkString; - *p++ = NULL; + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "et=0,4"; + txt_records[entry_number++] = ap1_featuresString; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "md=2"; + txt_records[entry_number++] = "am=Shairport Sync"; + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = "tp=UDP"; + txt_records[entry_number++] = "vn=65537"; + txt_records[entry_number++] = "vs=366.0"; + txt_records[entry_number++] = pkString; + txt_records[entry_number++] = NULL; #else // here, just replicate what happens in mdns.h when using those #defines - *p++ = "sf=0x4"; - *p++ = firmware_version; - *p++ = "am=ShairportSync"; - *p++ = "vs=105.1"; - *p++ = "tp=TCP,UDP"; - *p++ = "vn=65537"; + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "am=ShairportSync"; + txt_records[entry_number++] = "vs=105.1"; + txt_records[entry_number++] = "tp=TCP,UDP"; + txt_records[entry_number++] = "vn=65537"; #ifdef CONFIG_METADATA if (config.get_coverart == 0) - *p++ = "md=0,2"; + txt_records[entry_number++] = "md=0,2"; else - *p++ = "md=0,1,2"; + txt_records[entry_number++] = "md=0,1,2"; #endif - *p++ = "ss=16"; - *p++ = "sr=44100"; - *p++ = "da=true"; - *p++ = "sv=false"; - *p++ = "et=0,1"; - *p++ = "ek=1"; - *p++ = "cn=0,1"; - *p++ = "ch=2"; - *p++ = "txtvers=1"; + txt_records[entry_number++] = "ss=16"; + txt_records[entry_number++] = "sr=44100"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "sv=false"; + txt_records[entry_number++] = "et=0,1"; + txt_records[entry_number++] = "ek=1"; + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "ch=2"; + txt_records[entry_number++] = "txtvers=1"; if (config.password == 0) - *p++ = "pw=false"; + txt_records[entry_number++] = "pw=false"; else - *p++ = "pw=true"; - - *p++ = NULL; + txt_records[entry_number++] = "pw=true"; + txt_records[entry_number++] = NULL; #endif #ifdef CONFIG_AIRPLAY_2 // make up a secondary set of text records - char *secondary_txt_records[64]; - p = secondary_txt_records; - t2 = p; // second set of text records in AirPlay 2 only + t2 = secondary_txt_records; // second set of text records in AirPlay 2 only + entry_number = 0; - *p++ = "srcvers=366.0"; - char deviceIdString[64]; + secondary_txt_records[entry_number++] = "srcvers=366.0"; snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); - *p++ = deviceIdString; - char featuresString[64]; + secondary_txt_records[entry_number++] = deviceIdString; snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", features_lo, features_hi); - *p++ = featuresString; - char statusflagsString[32]; + secondary_txt_records[entry_number++] = featuresString; snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, config.airplay_statusflags); - *p++ = statusflagsString; - *p++ = "protovers=1.1"; - *p++ = "acl=0"; - *p++ = "rsf=0x0"; - *p++ = firmware_version; - *p++ = "model=Shairport Sync"; - char piString[64]; + secondary_txt_records[entry_number++] = statusflagsString; + secondary_txt_records[entry_number++] = "protovers=1.1"; + secondary_txt_records[entry_number++] = "acl=0"; + secondary_txt_records[entry_number++] = "rsf=0x0"; + secondary_txt_records[entry_number++] = firmware_version; + secondary_txt_records[entry_number++] = "model=Shairport Sync"; snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); - *p++ = piString; - char gidString[64]; + secondary_txt_records[entry_number++] = piString; snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); - *p++ = gidString; - *p++ = "gcgl=0"; - *p++ = pkString; - *p++ = NULL; + secondary_txt_records[entry_number++] = gidString; + secondary_txt_records[entry_number++] = "gcgl=0"; + secondary_txt_records[entry_number++] = pkString; + secondary_txt_records[entry_number++] = NULL; #endif mdns_register(t1, t2); From 67455c19d65d296d6f353398a8446daf91632d68 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 17:09:52 +0100 Subject: [PATCH 342/878] If using libdaemon logs default to daemonlog, otherwise to stderr. --- common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.c b/common.c index fcbf4fb5..5d922b74 100644 --- a/common.c +++ b/common.c @@ -231,7 +231,7 @@ void log_to_syslog() { #ifdef CONFIG_LIBDAEMON sps_log = daemon_log; #else - sps_log = syslog; + sps_log = do_sps_log_to_stderr; #endif } From 2ab97d980e3dffba0b1cda8d8cfda9182398bd38 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 17:10:51 +0100 Subject: [PATCH 343/878] add a simple mdns_update --- mdns.c | 7 +++++++ mdns.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/mdns.c b/mdns.c index 4e44e38f..7427dc7e 100644 --- a/mdns.c +++ b/mdns.c @@ -109,6 +109,13 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { mdns_dacp_monitor_start(); // create a dacp monitor thread } +void mdns_update(char **txt_records, char **secondary_txt_records) { +if ((config.mdns) && (config.mdns->mdns_update)) { + config.mdns->mdns_update(txt_records, secondary_txt_records); + } else + debug(1, "Can't mdns_update -- no mdns_update registered."); +} + void mdns_unregister(void) { mdns_dacp_monitor_stop(); if (config.mdns) { diff --git a/mdns.h b/mdns.h index dc6e30d9..d43b9513 100644 --- a/mdns.h +++ b/mdns.h @@ -9,6 +9,7 @@ extern int mdns_pid; void mdns_unregister(void); void mdns_register(char **txt_records, char **secondary_txt_records); +void mdns_update(char **txt_records, char **secondary_txt_records); void mdns_dacp_monitor_start(); void mdns_dacp_monitor_stop(void); void mdns_dacp_monitor_set_id(const char *dacp_id); @@ -19,6 +20,7 @@ typedef struct { char *name; int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records, char **secondary_txt_records); + int (*mdns_update)(char **txt_records, char **secondary_txt_records); void (*mdns_unregister)(void); void (*mdns_dacp_monitor_start)(); void (*mdns_dacp_monitor_set_id)(const char *); From 6d781c8c193813d71ba9eae652fc056bf4051439 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 17:11:59 +0100 Subject: [PATCH 344/878] By default, log to daemonlog/stderr --- shairport.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shairport.c b/shairport.c index 0f7f141f..2de86a9d 100644 --- a/shairport.c +++ b/shairport.c @@ -1493,6 +1493,8 @@ int main(int argc, char **argv) { usage(argv[0]); exit(EXIT_SUCCESS); } + + log_to_syslog(); #ifdef CONFIG_LIBDAEMON pid = getpid(); From 9748297737580643042fc30760079e61d7bf3151 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 17:13:27 +0100 Subject: [PATCH 345/878] Pick up the DACP-ID for AP2 streams. Move the mdns txt string globals around a bit. --- rtsp.c | 75 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/rtsp.c b/rtsp.c index 71e93b05..cead4378 100644 --- a/rtsp.c +++ b/rtsp.c @@ -92,6 +92,27 @@ #include "ptp-utilities.h" #endif +#include "mdns.h" + +// mDNS advertisement strings + +// Create these strings and then keep them updated. +// When necessary, update the mDNS service records, using e.g. Avahi +// from these sources. + +char *txt_records[64]; +char firmware_version[64]; +char ap1_featuresString[64]; +char pkString[128]; +#ifdef CONFIG_AIRPLAY_2 +char *secondary_txt_records[64]; +char deviceIdString[64]; +char featuresString[64]; +char statusflagsString[32]; +char piString[64]; +char gidString[64]; +#endif + #define METADATA_SNDBUF (4 * 1024 * 1024) enum rtsp_read_request_response { @@ -158,6 +179,14 @@ typedef struct { int respcode; } rtsp_message; +#ifdef CONFIG_AIRPLAY_2 +void mdns_update_flags(uint32_t flags) { + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, + flags); + mdns_update(NULL, secondary_txt_records); +} +#endif + #ifdef CONFIG_METADATA typedef struct { uint32_t type; @@ -2168,6 +2197,26 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t item = plist_dict_get_item(stream0, "shk"); // session key uint64_t item_value = 0; plist_get_data_val(item, (char **)&conn->session_key, &item_value); + + // get the DACP-ID for remote control stuff + + char *ar = msg_get_header(req, "DACP-ID"); + if (ar) { + debug(1, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", conn->connection_number, ar); + if (conn->dacp_id) // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", + conn->connection_number); + if (conn->dacp_id) { // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = NULL; + } + } // now, get the type of the stream. item = plist_dict_get_item(stream0, "type"); @@ -2224,6 +2273,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) ptp_send_control_message_string(conn->ap2_timing_peer_list_message); else debug(1, "No timing peer list!"); + //config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive + //mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); @@ -2266,14 +2317,16 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // this should be cancelled by an activity_monitor_signify_activity(1) // call in the SETRATEANCHORI handler, which should come up right away + //config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive + //mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(0); - player_play(conn); conn->rtp_running = 1; // hack! } break; default: debug(1, "Unhandled stream type %" PRIu64 ".", item_value); + debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); } plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); @@ -4053,25 +4106,6 @@ void msg_cleanup_function(void *arg) { msg_free((rtsp_message **)arg); } -// mDNS advertisement strings - -// Create these strings and then keep them updated. -// When necessary, update the mDNS service records, using e.g. Avahi -// from these sources. - -char *txt_records[64]; -char firmware_version[64]; -char ap1_featuresString[64]; -char pkString[128]; -#ifdef CONFIG_AIRPLAY_2 -char *secondary_txt_records[64]; -char deviceIdString[64]; -char featuresString[64]; -char statusflagsString[32]; -char piString[64]; -char gidString[64]; -#endif - static void *rtsp_conversation_thread_func(void *pconn) { rtsp_conn_info *conn = pconn; @@ -4483,6 +4517,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { secondary_txt_records[entry_number++] = featuresString; snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, config.airplay_statusflags); + secondary_txt_records[entry_number++] = statusflagsString; secondary_txt_records[entry_number++] = "protovers=1.1"; secondary_txt_records[entry_number++] = "acl=0"; From 2e4156d39ff633c185f205203afdac97ea0b2b01 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 17:16:37 +0100 Subject: [PATCH 346/878] Add the ability to update the ap1 and ap2 strings dynamically --- mdns_avahi.c | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/mdns_avahi.c b/mdns_avahi.c index bf24ddba..9b53a539 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -104,7 +104,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn while (*dacpid == '0') dacpid++; // skip any leading zeroes if (strcmp(dacpid, dbs->dacp_id) == 0) { - debug(3, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port); + debug(1, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port); #ifdef CONFIG_DACP_CLIENT dacp_monitor_port_update_callback(dacpid, port); #endif @@ -318,6 +318,50 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } +static int avahi_update(char **txt_records, + char **secondary_txt_records) { + // debug(1, "avahi_update."); + +/* + service_name = strdup(ap1name); + if (ap2name != NULL) + ap2_service_name = strdup(ap2name); + port = srvport; +*/ + int err = 0; + AvahiIfIndex selected_interface; + if (config.interface != NULL) + selected_interface = config.interface_index; + else + selected_interface = AVAHI_IF_UNSPEC; + + if (txt_records != NULL) { + if (text_record_string_list) + avahi_string_list_free(text_record_string_list); + text_record_string_list = + avahi_string_list_new_from_array((const char **)txt_records, -1); + err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + service_name, config.regtype, NULL, + text_record_string_list); + if (err != 0) + debug(1,"avahi_update error updating primary txt records."); + } + + if (secondary_txt_records != NULL) { + if (ap2_text_record_string_list) + avahi_string_list_free(ap2_text_record_string_list); + ap2_text_record_string_list = + avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); + err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + ap2_service_name, config.regtype2, NULL, + ap2_text_record_string_list); + if (err != 0) + debug(1,"avahi_update error updating secondary txt records."); + } + return 0; +} + + static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records, char **secondary_txt_records) { // debug(1, "avahi_register."); @@ -429,6 +473,7 @@ void avahi_dacp_monitor_set_id(const char *dacp_id) { avahi_strerror(avahi_client_errno(client))); } avahi_threaded_poll_unlock(tpoll); + debug(1,"dacp_monitor for \"%s\"", dacp_id); } else { warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct."); } @@ -451,6 +496,7 @@ void avahi_dacp_monitor_stop() { mdns_backend mdns_avahi = {.name = "avahi", .mdns_register = avahi_register, + .mdns_update = avahi_update, .mdns_unregister = avahi_unregister, .mdns_dacp_monitor_start = avahi_dacp_monitor_start, .mdns_dacp_monitor_set_id = avahi_dacp_monitor_set_id, From 4e6f1e5e659ed2e4db0f39629e7caadd74508153 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Jul 2021 21:51:47 +0100 Subject: [PATCH 347/878] Pick up active Remote information as well as DACP-ID. --- rtsp.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index cead4378..425bd71f 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2198,9 +2198,30 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint64_t item_value = 0; plist_get_data_val(item, (char **)&conn->session_key, &item_value); - // get the DACP-ID for remote control stuff + // get the DACP-ID and Active Remote for remote control stuff - char *ar = msg_get_header(req, "DACP-ID"); + + char *ar = msg_get_header(req, "Active-Remote"); + if (ar) { + debug(1, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", + conn->connection_number, ar); + // get the active remote + if (conn->dacp_active_remote) // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", + conn->connection_number); + if (conn->dacp_active_remote) { // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + } + + ar = msg_get_header(req, "DACP-ID"); if (ar) { debug(1, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", conn->connection_number, ar); if (conn->dacp_id) // this is in case SETUP was previously called From c4b5e0928f0abcc9fbc8f16b180ec6f28f5bdb56 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 22 Jul 2021 11:01:53 +0100 Subject: [PATCH 348/878] Use a local copy of the nqptp shared memory interface description file. --- nqptp-shm-structures.h | 48 ++++++++++++++++++++++++++++++++++++++++++ ptp-utilities.c | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 nqptp-shm-structures.h diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h new file mode 100644 index 00000000..36c8a813 --- /dev/null +++ b/nqptp-shm-structures.h @@ -0,0 +1,48 @@ +/* + * This file is part of the nqptp distribution (https://github.com/mikebrady/nqptp). + * Copyright (c) 2021 Mike Brady. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Commercial licensing is also available. + */ + +#ifndef NQPTP_SHM_STRUCTURES_H +#define NQPTP_SHM_STRUCTURES_H + +#define STORAGE_ID "/nqptp" +#define MAX_CLOCKS 32 +#define NQPTP_SHM_STRUCTURES_VERSION 5 +#define NQPTP_CONTROL_PORT 9000 + +// the control port will accept a UDP packet with the first letter being: +// "T", followed by a space and then a space-delimited +// list of ip numbers, either IPv4 or IPv6 +// the whole not to exceed 4096 characters in total +// The IPs will become the new list of timing peers, replacing any previous + +#include +#include + +struct shm_structure { + pthread_mutex_t shm_mutex; // for safely accessing the structure + uint16_t version; // check this is equal to NQPTP_SHM_STRUCTURES_VERSION + uint32_t flags; // unused + uint64_t master_clock_id; // the current master clock + char master_clock_ip[64]; // where it's coming from + uint64_t local_time; // the time when the offset was calculated + uint64_t local_to_master_time_offset; // add this to the local time to get master clock time + uint64_t master_clock_start_time; // this is when the master clock became master +}; + +#endif diff --git a/ptp-utilities.c b/ptp-utilities.c index a9cf614f..0f80e913 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -41,7 +41,7 @@ #include #include -#include "../nqptp/nqptp-shm-structures.h" +#include "nqptp-shm-structures.h" #include "ptp-utilities.h" int shm_fd; From a85a9ac0e466076e05f1c8f90f86cbbe4c25a27e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 22 Jul 2021 11:03:32 +0100 Subject: [PATCH 349/878] Add a dummy handler for the POST /feedback request. --- rtsp.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/rtsp.c b/rtsp.c index 425bd71f..d10f0905 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1939,6 +1939,10 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), debug_log_rtsp_message(2, "POST /configure response:", resp); } +void handle_feedback(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + +} + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { @@ -1955,11 +1959,12 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { handle_fp_setup(conn, req, resp); } else if (strcmp(req->path, "/configure") == 0) { handle_configure(conn, req, resp); + } else if (strcmp(req->path, "/feedback") == 0) { + handle_feedback(conn, req, resp); } else { - debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "POST request", req); - debug(3, "Unhandled POST, path \"%s\".", req->path); } } @@ -2246,7 +2251,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) switch (item_value) { case 96: { - debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug(2, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); get_play_lock(conn); conn->airplay_stream_type = realtime_stream; @@ -2303,7 +2308,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; case 103: { - debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); + debug(2, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); get_play_lock(conn); conn->airplay_stream_type = buffered_stream; @@ -2360,6 +2365,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " "and open a TCP port.", conn->connection_number); + debug_log_rtsp_message(2, "SETUP on Connection %d: No \"streams\" array has been found.", req); // first, get and retain the incoming plist. if (conn->client_setup_plist) { plist_free(conn->client_setup_plist); @@ -4172,7 +4178,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { resp = msg_init(); pthread_cleanup_push(msg_cleanup_function, (void *)&resp); resp->respcode = 400; - +/* if (strcmp(req->method, "OPTIONS") != 0) // the options message is very common, so don't log it until level 3 debug_level = 2; @@ -4180,6 +4186,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, req->method), debug_print_msg_headers(debug_level, req); +*/ apple_challenge(conn->fd, req, resp); hdr = msg_get_header(req, "CSeq"); From 707204e7e81d38186282d9e6b6fec5cbc12e19ca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 22 Jul 2021 12:43:21 +0100 Subject: [PATCH 350/878] Quieten a few temporary wanrings. --- rtsp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index d10f0905..e087fbf1 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1939,7 +1939,7 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), debug_log_rtsp_message(2, "POST /configure response:", resp); } -void handle_feedback(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { +void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { } From 4090b2d8fb383ad8873d2c4538da9de9a0a26136 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 22 Jul 2021 13:23:57 +0100 Subject: [PATCH 351/878] Avoid repreatedly asking Avahi to monitor for the same DACP-ID. Quieten some debug messages. --- mdns_avahi.c | 94 +++++++++++++++++++-------------------- rtp.c | 121 +++++++++++++++++++++++++++++---------------------- rtsp.c | 53 +++++++++++----------- 3 files changed, 142 insertions(+), 126 deletions(-) diff --git a/mdns_avahi.c b/mdns_avahi.c index 9b53a539..c4568078 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -104,7 +104,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn while (*dacpid == '0') dacpid++; // skip any leading zeroes if (strcmp(dacpid, dbs->dacp_id) == 0) { - debug(1, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port); + debug(3, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port); #ifdef CONFIG_DACP_CLIENT dacp_monitor_port_update_callback(dacpid, port); #endif @@ -318,50 +318,47 @@ static void client_callback(AvahiClient *c, AvahiClientState state, } } -static int avahi_update(char **txt_records, - char **secondary_txt_records) { +static int avahi_update(char **txt_records, char **secondary_txt_records) { // debug(1, "avahi_update."); -/* - service_name = strdup(ap1name); - if (ap2name != NULL) - ap2_service_name = strdup(ap2name); - port = srvport; -*/ + /* + service_name = strdup(ap1name); + if (ap2name != NULL) + ap2_service_name = strdup(ap2name); + port = srvport; + */ int err = 0; AvahiIfIndex selected_interface; - if (config.interface != NULL) - selected_interface = config.interface_index; - else - selected_interface = AVAHI_IF_UNSPEC; + if (config.interface != NULL) + selected_interface = config.interface_index; + else + selected_interface = AVAHI_IF_UNSPEC; if (txt_records != NULL) { if (text_record_string_list) avahi_string_list_free(text_record_string_list); - text_record_string_list = - avahi_string_list_new_from_array((const char **)txt_records, -1); - err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, - service_name, config.regtype, NULL, - text_record_string_list); + text_record_string_list = avahi_string_list_new_from_array((const char **)txt_records, -1); + err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, + 0, service_name, config.regtype, NULL, + text_record_string_list); if (err != 0) - debug(1,"avahi_update error updating primary txt records."); + debug(1, "avahi_update error updating primary txt records."); } if (secondary_txt_records != NULL) { if (ap2_text_record_string_list) avahi_string_list_free(ap2_text_record_string_list); ap2_text_record_string_list = - avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); - err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, - ap2_service_name, config.regtype2, NULL, - ap2_text_record_string_list); + avahi_string_list_new_from_array((const char **)secondary_txt_records, -1); + err = avahi_entry_group_update_service_txt_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, + 0, ap2_service_name, config.regtype2, NULL, + ap2_text_record_string_list); if (err != 0) - debug(1,"avahi_update error updating secondary txt records."); + debug(1, "avahi_update error updating secondary txt records."); } return 0; } - static int avahi_register(char *ap1name, char *ap2name, int srvport, char **txt_records, char **secondary_txt_records) { // debug(1, "avahi_register."); @@ -446,7 +443,7 @@ static void avahi_unregister(void) { void avahi_dacp_monitor_start(void) { // debug(1, "avahi_dacp_monitor_start."); memset((void *)&private_dbs, 0, sizeof(dacp_browser_struct)); - debug(1, "avahi_dacp_monitor_start Avahi DACP monitor successfully started"); + debug(2, "avahi_dacp_monitor_start Avahi DACP monitor successfully started"); return; } @@ -454,28 +451,33 @@ void avahi_dacp_monitor_set_id(const char *dacp_id) { // debug(1, "avahi_dacp_monitor_set_id: Search for DACP ID \"%s\".", t); dacp_browser_struct *dbs = &private_dbs; - if (dbs->dacp_id) - free(dbs->dacp_id); - if (dacp_id == NULL) - dbs->dacp_id = NULL; - else { - char *t = strdup(dacp_id); - if (t) { - dbs->dacp_id = t; - avahi_threaded_poll_lock(tpoll); - if (dbs->service_browser) - avahi_service_browser_free(dbs->service_browser); + if (((dbs->dacp_id) && (dacp_id) && (strcmp(dbs->dacp_id, dacp_id) == 0)) || + ((dbs->dacp_id == NULL) && (dacp_id == NULL))) { + debug(3, "no change..."); + } else { + if (dbs->dacp_id) + free(dbs->dacp_id); + if (dacp_id == NULL) + dbs->dacp_id = NULL; + else { + char *t = strdup(dacp_id); + if (t) { + dbs->dacp_id = t; + avahi_threaded_poll_lock(tpoll); + if (dbs->service_browser) + avahi_service_browser_free(dbs->service_browser); - if (!(dbs->service_browser = - avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_dacp._tcp", - NULL, 0, browse_callback, (void *)dbs))) { - warn("failed to create avahi service browser: %s\n", - avahi_strerror(avahi_client_errno(client))); + if (!(dbs->service_browser = + avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, + "_dacp._tcp", NULL, 0, browse_callback, (void *)dbs))) { + warn("failed to create avahi service browser: %s\n", + avahi_strerror(avahi_client_errno(client))); + } + avahi_threaded_poll_unlock(tpoll); + debug(2, "dacp_monitor for \"%s\"", dacp_id); + } else { + warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct."); } - avahi_threaded_poll_unlock(tpoll); - debug(1,"dacp_monitor for \"%s\"", dacp_id); - } else { - warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct."); } } } diff --git a/rtp.c b/rtp.c index 1b2cffad..e074e113 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1271,18 +1271,21 @@ int long_time_notifcation_done = 0; int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { uint64_t actual_clock_id, actual_time_of_sample, actual_offset, start_of_mastership; - int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, &start_of_mastership); + int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, + &start_of_mastership); if (response == clock_ok) { uint64_t time_now = get_absolute_time_in_ns(); int64_t time_since_sample = time_now - actual_time_of_sample; if (time_since_sample > 10000000000) { if (long_time_notifcation_done == 0) { - debug(1,"The last PTP timing sample is pretty old: %f seconds.", 0.000000001 * time_since_sample); + debug(1, "The last PTP timing sample is pretty old: %f seconds.", + 0.000000001 * time_since_sample); long_time_notifcation_done = 1; } } else if ((time_since_sample < 2000000000) && (long_time_notifcation_done != 0)) { - debug(1,"The last PTP timing sample is no longer too old: %f seconds.", 0.000000001 * time_since_sample); + debug(1, "The last PTP timing sample is no longer too old: %f seconds.", + 0.000000001 * time_since_sample); long_time_notifcation_done = 0; } @@ -1296,11 +1299,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // using the master clock timing if (actual_clock_id == conn->anchor_clock) { - // if the master clock and the anchor clock are the same - // wait at least this time before using the new master clock - // note that mastershipo may be backdated + // if the master clock and the anchor clock are the same + // wait at least this time before using the new master clock + // note that mastershipo may be backdated if (duration_of_mastership < 1500000000) { - debug(3,"master not old enough yet: %f ms", 0.000001 * duration_of_mastership); + debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || (conn->last_anchor_info_is_valid == 0)) { @@ -1314,7 +1317,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->last_anchor_info_is_valid = 1; if (conn->anchor_clock_is_new != 0) debug(1, - "Connection %d: Clock %" PRIx64 " is now the new anchor clock and master clock. History: %f milliseconds.", + "Connection %d: Clock %" PRIx64 + " is now the new anchor clock and master clock. History: %f milliseconds.", conn->connection_number, conn->anchor_clock, 0.000001 * duration_of_mastership); conn->anchor_clock_is_new = 0; } @@ -1326,16 +1330,19 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // so, if the anchor has not changed, it must be that the master clock has changed if (conn->anchor_clock_is_new != 0) - debug(1, + debug(3, "Connection %d: Anchor clock has changed to %" PRIx64 ", master clock is: %" PRIx64 ". History: %f milliseconds.", - conn->connection_number, conn->anchor_clock, actual_clock_id, 0.000001 * duration_of_mastership); + conn->connection_number, conn->anchor_clock, actual_clock_id, + 0.000001 * duration_of_mastership); if ((conn->last_anchor_info_is_valid != 0) && (conn->anchor_clock_is_new == 0)) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; if (time_since_last_update > 5000000000) { - debug(1, "Connection %d: Master clock has changed to %" PRIx64 ". History: %f milliseconds.", + debug(1, + "Connection %d: Master clock has changed to %" PRIx64 + ". History: %f milliseconds.", conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); // here we adjust the time of the anchor rtptime // we know its local time, so we use the new clocks's offset to @@ -2157,7 +2164,8 @@ void *rtp_buffered_audio_processor(void *arg) { av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, "in_sample_rate", conn->input_rate, 0); - av_opt_set_int(swr, "out_sample_rate", conn->input_rate, 0); // must match or the timing will be wrong` + av_opt_set_int(swr, "out_sample_rate", conn->input_rate, + 0); // must match or the timing will be wrong` av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); enum AVSampleFormat av_format; @@ -2280,9 +2288,14 @@ void *rtp_buffered_audio_processor(void *arg) { if ((blocks_read != 0) && (seq_no >= flushUntilSeq)) { // we have reached or overshot the flushUntilSeq block if (flushUntilSeq != seq_no) - debug(2, "flush request ended with flushUntilSeq %u overshot at %u, flushUntilTS: %u, incoming timestamp: %u.", flushUntilSeq, seq_no, flushUntilTS, timestamp); + debug(2, + "flush request ended with flushUntilSeq %u overshot at %u, flushUntilTS: %u, " + "incoming timestamp: %u.", + flushUntilSeq, seq_no, flushUntilTS, timestamp); else - debug(2, "flush request ended with flushUntilSeq, flushUntilTS: %u, incoming timestamp: %u", flushUntilSeq, flushUntilTS, timestamp); + debug(2, + "flush request ended with flushUntilSeq, flushUntilTS: %u, incoming timestamp: %u", + flushUntilSeq, flushUntilTS, timestamp); conn->ap2_flush_requested = 0; flush_request_active = 0; flush_newly_requested = 0; @@ -2301,7 +2314,6 @@ void *rtp_buffered_audio_processor(void *arg) { if (flush_newly_complete) { debug(2, "Flush Complete."); blocks_read_since_flush = 0; - } if (play_newly_stopped != 0) @@ -2329,9 +2341,10 @@ void *rtp_buffered_audio_processor(void *arg) { // is there space in the player thread's buffer system? unsigned int player_buffer_size, player_buffer_occupancy; get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn); - //debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, player_buffer_occupancy); - if (player_buffer_occupancy > - ((requested_lead_time + 0.4) * conn->input_rate / 352)) { // must be greater than the lead time. + // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size, + // player_buffer_occupancy); + if (player_buffer_occupancy > ((requested_lead_time + 0.4) * conn->input_rate / + 352)) { // must be greater than the lead time. // if there is enough stuff in the player's buffer, sleep for a while and try again usleep(1000); // wait for a while } else { @@ -2358,26 +2371,26 @@ void *rtp_buffered_audio_processor(void *arg) { // it seems that some garbage blocks can be left after the flush, so // only accept them if they have sensible lead times if ((lead_time_ms < 5000.0) && (lead_time > -1000.0)) { - // if it's the very first block (thus no priming needed) - if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { - if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || - (streaming_has_started != 0)) { - if (streaming_has_started == 0) - debug( - 2, - "Connection %d: buffered audio starting frame: %u, lead time: %f seconds.", - conn->connection_number, pcm_buffer_read_point_rtptime, - 0.000000001 * lead_time); - // else { - // if (expected_rtptime != pcm_buffer_read_point_rtptime) - // debug(1,"actual rtptime is %u, expected rtptime is %u.", - // pcm_buffer_read_point_rtptime, expected_rtptime); - //} - // expected_rtptime = pcm_buffer_read_point_rtptime + 352; + // if it's the very first block (thus no priming needed) + if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { + if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || + (streaming_has_started != 0)) { + if (streaming_has_started == 0) + debug(2, + "Connection %d: buffered audio starting frame: %u, lead time: %f " + "seconds.", + conn->connection_number, pcm_buffer_read_point_rtptime, + 0.000000001 * lead_time); + // else { + // if (expected_rtptime != pcm_buffer_read_point_rtptime) + // debug(1,"actual rtptime is %u, expected rtptime is %u.", + // pcm_buffer_read_point_rtptime, expected_rtptime); + //} + // expected_rtptime = pcm_buffer_read_point_rtptime + 352; - // this is a diagnostic for introducing a timing error that will force the - // processing chain to resync - // clang-format off + // this is a diagnostic for introducing a timing error that will force the + // processing chain to resync + // clang-format off /* if ((not_first_time_out == 0) && (blocks_read >= 20)) { int timing_error = 150; @@ -2390,21 +2403,23 @@ void *rtp_buffered_audio_processor(void *arg) { not_first_time_out = 1; } */ - // clang-format on + // clang-format on - player_put_packet(0, 0, pcm_buffer_read_point_rtptime, - pcm_buffer + pcm_buffer_read_point, 352, conn); - streaming_has_started++; + player_put_packet(0, 0, pcm_buffer_read_point_rtptime, + pcm_buffer + pcm_buffer_read_point, 352, conn); + streaming_has_started++; + } } - } } else { - debug(2,"Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); + debug(2, + "Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", + pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); } pcm_buffer_read_point_rtptime += 352; pcm_buffer_read_point += 352 * conn->input_bytes_per_frame; } else { - debug(1,"frame to local time error"); + debug(1, "frame to local time error"); } } else { usleep(1000); // wait before asking if play is enabled again @@ -2488,8 +2503,8 @@ void *rtp_buffered_audio_processor(void *arg) { // valid int64_t lead_time = should_be_time - get_absolute_time_in_ns(); debug(1, - "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: 0x%" PRIx64 - " nanoseconds, i.e. %f sec.", + "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: " + "0x%" PRIx64 " nanoseconds, i.e. %f sec.", seq_no, flushUntilTS, timestamp, lead_time, lead_time * 0.000000001); } else { debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); diff --git a/rtsp.c b/rtsp.c index e087fbf1..ca5178cd 100644 --- a/rtsp.c +++ b/rtsp.c @@ -181,8 +181,7 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 void mdns_update_flags(uint32_t flags) { - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, - flags); + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, flags); mdns_update(NULL, secondary_txt_records); } #endif @@ -1939,9 +1938,9 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), debug_log_rtsp_message(2, "POST /configure response:", resp); } -void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { - -} +void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, + __attribute__((unused)) rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) {} void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; @@ -1962,8 +1961,8 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } else if (strcmp(req->path, "/feedback") == 0) { handle_feedback(conn, req, resp); } else { - debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); + debug(2, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, + req->path, req->contentlength); debug_log_rtsp_message(2, "POST request", req); } } @@ -2202,13 +2201,12 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t item = plist_dict_get_item(stream0, "shk"); // session key uint64_t item_value = 0; plist_get_data_val(item, (char **)&conn->session_key, &item_value); - + // get the DACP-ID and Active Remote for remote control stuff - - + char *ar = msg_get_header(req, "Active-Remote"); if (ar) { - debug(1, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", + debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", conn->connection_number, ar); // get the active remote if (conn->dacp_active_remote) // this is in case SETUP was previously called @@ -2225,10 +2223,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->dacp_active_remote = NULL; } } - + ar = msg_get_header(req, "DACP-ID"); if (ar) { - debug(1, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", conn->connection_number, ar); + debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", conn->connection_number, + ar); if (conn->dacp_id) // this is in case SETUP was previously called free(conn->dacp_id); conn->dacp_id = strdup(ar); @@ -2299,8 +2298,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) ptp_send_control_message_string(conn->ap2_timing_peer_list_message); else debug(1, "No timing peer list!"); - //config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive - //mdns_update_flags(config.airplay_statusflags); + // config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive + // mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); @@ -2343,8 +2342,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // this should be cancelled by an activity_monitor_signify_activity(1) // call in the SETRATEANCHORI handler, which should come up right away - //config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive - //mdns_update_flags(config.airplay_statusflags); + // config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive + // mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(0); player_play(conn); @@ -4178,15 +4177,15 @@ static void *rtsp_conversation_thread_func(void *pconn) { resp = msg_init(); pthread_cleanup_push(msg_cleanup_function, (void *)&resp); resp->respcode = 400; -/* - if (strcmp(req->method, "OPTIONS") != - 0) // the options message is very common, so don't log it until level 3 - debug_level = 2; - debug(debug_level, - "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, - req->method), - debug_print_msg_headers(debug_level, req); -*/ + /* + if (strcmp(req->method, "OPTIONS") != + 0) // the options message is very common, so don't log it until level 3 + debug_level = 2; + debug(debug_level, + "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, + req->method), + debug_print_msg_headers(debug_level, req); + */ apple_challenge(conn->fd, req, resp); hdr = msg_get_header(req, "CSeq"); @@ -4545,7 +4544,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { secondary_txt_records[entry_number++] = featuresString; snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, config.airplay_statusflags); - + secondary_txt_records[entry_number++] = statusflagsString; secondary_txt_records[entry_number++] = "protovers=1.1"; secondary_txt_records[entry_number++] = "acl=0"; From 99f078d478546d87ea46a81da910ab9a136cfe42 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 24 Jul 2021 09:23:02 +0100 Subject: [PATCH 352/878] Simplify UFW Stuff Make is work for any source, IPv4 or IPv6, add in 7000 for AP2 --- TROUBLESHOOTING.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index fd500e60..92d8b969 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -90,15 +90,14 @@ You have installed Shairport Sync successfully, the daemon is running, you can s You have to allow connections to your Pi from remote devices. To do so, after re-enabling UFW (see last step of the previous section), enter the following commands in shell: ``` -sudo ufw allow from 192.168.1.1/16 to any port 3689 proto tcp -sudo ufw allow from 192.168.1.1/16 to any port 5353 -sudo ufw allow from 192.168.1.1/16 to any port 5000:5005 proto tcp -sudo ufw allow from 192.168.1.1/16 to any port 6000:6005 proto udp -sudo ufw allow from 192.168.1.1/16 to any port 35000:65535 proto udp +sudo ufw allow 3689/tcp +sudo ufw allow 5353 +sudo ufw allow 5000/tcp +sudo ufw allow 7000/tcp +sudo ufw allow 6000:6009/udp +sudo ufw allow 35000:65535/udp ``` -You may have to change the IP addresses range depending on your own local network settings. - You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purpose. Run your song from your remote device. Enjoy ! From 8f54828fd707b2bad28b8365009976994061a2c2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 24 Jul 2021 09:28:12 +0100 Subject: [PATCH 353/878] Update TROUBLESHOOTING.md --- TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 92d8b969..1cdac7b8 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -98,7 +98,7 @@ sudo ufw allow 6000:6009/udp sudo ufw allow 35000:65535/udp ``` -You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purpose. +You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purposes. Check out the [ufw man pages](http://manpages.ubuntu.com/manpages/man8/ufw.8.html) for more. Run your song from your remote device. Enjoy ! From e0983421854d2d999d068a9bd1839bee1d760945 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 28 Jul 2021 12:56:55 +0100 Subject: [PATCH 354/878] remove a misleading obsolete comment. --- common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.h b/common.h index 26e9e17d..4572d198 100644 --- a/common.h +++ b/common.h @@ -379,7 +379,7 @@ double flat_vol2attn(double vol, long max_db, long min_db); // dB), return an attenuation depending on the transfer function double vol2attn(double vol, long max_db, long min_db); -// return a monolithic (always increasing) time in nanoseconds +// return a time in nanoseconds // uint64_t get_absolute_time_in_fp(void); // obselete uint64_t get_absolute_time_in_ns(void); From 90740694ba136c638785ffc57ec1e0b279a5aa4a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 28 Jul 2021 12:58:20 +0100 Subject: [PATCH 355/878] When an unrecognised SETUP message is received, just say okay and ignore it. Remove reduncant client_setup_plist from the conn data structure. --- player.h | 1 - rtsp.c | 94 +++++++++++++++++++++++--------------------------------- 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/player.h b/player.h index c35e5726..2596eb78 100644 --- a/player.h +++ b/player.h @@ -328,7 +328,6 @@ typedef struct { uint16_t local_realtime_audio_port; uint16_t local_buffered_audio_port; - plist_t client_setup_plist; uint64_t audio_format; uint64_t compression; unsigned char *session_key; // needs to be free'd at the end diff --git a/rtsp.c b/rtsp.c index ca5178cd..c459dfc9 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2069,10 +2069,6 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag if (conn->event_socket) close(conn->event_socket); - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - conn->client_setup_plist = NULL; - } debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } //} else { @@ -2162,12 +2158,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // the timing peer info in the accompanying plist plist_t messagePlist = plist_from_rtsp_content(req); - plist_t setupResponsePlist = plist_new_dict(); + plist_t setupResponsePlist = NULL; // we'll create it when we need it // now see if the incoming plist contains a "streams" array plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { + setupResponsePlist = plist_new_dict(); // now that we know we need it, create it debug(2, "SETUP on Connection %d: A \"streams\" array has been found -- create control and audio " "threads and ports", @@ -2250,7 +2247,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) switch (item_value) { case 96: { - debug(2, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); get_play_lock(conn); conn->airplay_stream_type = realtime_stream; @@ -2307,7 +2304,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; case 103: { - debug(2, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); + debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); get_play_lock(conn); conn->airplay_stream_type = buffered_stream; @@ -2365,19 +2362,16 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "and open a TCP port.", conn->connection_number); debug_log_rtsp_message(2, "SETUP on Connection %d: No \"streams\" array has been found.", req); - // first, get and retain the incoming plist. - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - } - conn->client_setup_plist = messagePlist; - plist_t addresses = plist_new_array(); // to hold the device's peer interfaces // now determine which of the present device's IP numbers are in the // same subnet as any IP numbers in the timing peer info // provided by the client - plist_t timing_peer_info = plist_dict_get_item(conn->client_setup_plist, "timingPeerInfo"); + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); if (timing_peer_info) { + // first, get and retain the incoming plist. + setupResponsePlist = plist_new_dict(); // now that we know we need it, create it + plist_t addresses = plist_new_array(); // to hold the device's peer interfaces plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); if (addresses_array) { // iterate through the array of items @@ -2456,45 +2450,40 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } else { debug(1, "Can't find timingPeerInfo addresses"); } - } else { - debug_log_rtsp_message(1, "Unrecognised SETUP incoming message", req); + // make up the timing peer info list part of the response... + // debug(1,"Create timingPeerInfoPlist"); + plist_t timingPeerInfoPlist = plist_new_dict(); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); + plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); + plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); + // get a port to use as an event port + + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + + int err = + bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_event_port, &conn->event_socket); + if (err) { + die("Error %d: could not find a TCP port to use as an event port", err); + } + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy } - - // make up the timing peer info list part of the response... - // debug(1,"Create timingPeerInfoPlist"); - plist_t timingPeerInfoPlist = plist_new_dict(); - plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); - plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); - plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); - // get a port to use as an event port - - // bind a new TCP port and get a socket - conn->local_event_port = 0; // any port - - int err = - bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_event_port, &conn->event_socket); - if (err) { - die("Error %d: could not find a TCP port to use as an event port", err); - } - - pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); - - plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy } - - plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); - plist_free(setupResponsePlist); - - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + if (setupResponsePlist) { + plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); + plist_free(setupResponsePlist); + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + } else { + debug_log_rtsp_message(1, "Unrecognised SETUP incoming message", req); + warn("Unrecognised SETUP incoming message -- ignored."); + } debug_log_rtsp_message(2, " SETUP response", resp); - resp->respcode = 200; // it all worked out okay - // } else { - // resp->respcode = 453; - // debug(1, "Connection %d: SETUP failed because another connection is already playing.", - // conn->connection_number); - // } + resp->respcode = 200; } #endif @@ -4072,11 +4061,6 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { } #ifdef CONFIG_AIRPLAY_2 - if (conn->client_setup_plist) { - plist_free(conn->client_setup_plist); - conn->client_setup_plist = NULL; - } - buf_drain(&conn->ap2_control_pairing.plain_buf, -1); buf_drain(&conn->ap2_control_pairing.encrypted_buf, -1); pair_setup_free(conn->ap2_control_pairing.setup_ctx); From 5c600f9bd96a24e5bebabe34fa2ad0972d7f523b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 28 Jul 2021 15:47:21 +0100 Subject: [PATCH 356/878] Update RELEASENOTES-DEVELOPMENT --- RELEASENOTES-DEVELOPMENT.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c10e455d..be36f514 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,23 @@ +Version 4.0-dev-303-g9074069 +==== +#### New Features +- An initial [PipeWire](https://pipewire.org) backend, with thanks to [Lukas Rusak](https://github.com/lrusak). Use the `--with-pw` configuration flag at the `./configure...` stage to include support. + + +#### Bug Fixes +- Fix a bug when using the `tinysvcmdns` mDNS implementation, with thanks to [fesc2000](https://github.com/fesc2000). +- When an unrecognised SETUP message is received, just ignore it and put a warning in the log. Remove a redundant `client_setup_plist` data item from the `conn` data structure. + +#### Enhancements +- Shairport Sync no longer needs to be in the same directory of NQPTP during compilation. +- Add Docker automations -- thanks to [Charles Omer](https://github.com/charlesomer). +- Add Issue management automations -- thanks to [Charles Omer](https://github.com/charlesomer). +- Reconnect the DACP scanner for AirPlay 2. (It still isn't useful, unfortunately.) +- Add initial support to allow dynamic modification of Rendezvous advertisements. +- Simplify some UFW suggestions in the Troubleshooting Guide. +- Merge some documentation changes from the `master` branch. +- Add information to the Car Install Guide about improving boot times, thanks to [vasilisvg](https://github.com/vasilisvg) + Version 4.0-dev-185-g0c02ee2 ==== From 099d21d3971dc6c3b8e58de8c8baf5af923b91a0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 28 Jul 2021 16:59:35 +0100 Subject: [PATCH 357/878] Documentation updates --- AIRPLAY2.md | 11 ++++++----- BUILDFORAP2.md | 10 +++------- README.md | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 42a20730..b253f39c 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,9 +1,9 @@ AirPlay 2 === -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not integrate with the Home app or with Siri. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. It has limited integration with the Home app and Siri. -The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. +The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work. For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. @@ -17,13 +17,13 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi - AirPlay 2 for iOS and Mac players. ### What Does Not Work -- No integration with Siri or HomeKit - Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. - No Remote Control - No Artwork in the Metadata - No AirPlay 2 for Windows iTunes ### What Partly Works +- Incomplete functionality with Siri and HomeKit - Incomplete functionality using AppleTV or HomePod as player. A Shairport Sync player will work -- i.e. it will play -- but the selection button won't persist and the volume can't be set. @@ -51,15 +51,16 @@ Guides AirPlay 2 -- More About What Works --- * Two types of audio are received by Shairport Sync -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. Acknowledgements ---- -Huge thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: +Huge thanks are due to a number of individuals who made direct and valuable contributions to Shairport Sync: 1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. 2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. 3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes, documentation and Docker automation (forthcoming). +4. [ckdo](https://github.com/ckdo) for their pathfinding work on AirPlay 2, especially the HomeKit pairing. Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. It is a pleasure to acknowledge the work of the contributors there. diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 780a3c81..49c89227 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -5,9 +5,8 @@ Building Shairport Sync for AirPlay 2 * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * For now, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) -* Shairport Sync has a build dependency on `nqptp`, so you should build or update `nqptp` *before* building or updating Shairport Sync. +* Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. + (FYI, most computers do not have a PTP clock running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) * The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. @@ -59,17 +58,14 @@ First, install the packages needed by Shairport Sync: libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` -(Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) ### nqptp ### Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). -***Note:*** Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. - ### Shairport Sync #### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please us the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: +As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please use the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: ``` Version with git information: 4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc diff --git a/README.md b/README.md index 234dc391..b98156dc 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. -* [Charles Omer](https://github.com/charlesomer) for Docker automation (forthcoming), testing, encouragement, enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for Docker automation, repository management automation, testing, encouragement, enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. From 14c12b77ed3886bc27fc0dcca02ac236ebe5ec00 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Jul 2021 13:11:39 +0100 Subject: [PATCH 358/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 49c89227..b6106455 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -54,7 +54,7 @@ Okay, now let's get the tools and libraries for building and installing Shairpor First, install the packages needed by Shairport Sync: ``` -# apt-get install build-essential git xmltoman autoconf automake libtool \ +# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` From 3208165961d5312bf4dc7931a1dbdccab16defa4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Jul 2021 13:13:01 +0100 Subject: [PATCH 359/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index dcf78cba..27b3ae43 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -47,7 +47,7 @@ The first thing to do on a Pi would be to use the `raspi-config` tool to expand ### Shairport Sync First, install the packages needed by Shairport Sync: ``` -# apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev +# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev ``` Next, download Shairport Sync, configure it, compile and install it: ``` From 23da206c7eae2f7a83429419a8b0b9a31ae1170c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Jul 2021 19:15:17 +0100 Subject: [PATCH 360/878] revert output to syslog rather than stderr --- common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.c b/common.c index 5d922b74..fcbf4fb5 100644 --- a/common.c +++ b/common.c @@ -231,7 +231,7 @@ void log_to_syslog() { #ifdef CONFIG_LIBDAEMON sps_log = daemon_log; #else - sps_log = do_sps_log_to_stderr; + sps_log = syslog; #endif } From 5a51c43d98188d954efea2b12c97c00e48009e0d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Jul 2021 20:39:08 +0100 Subject: [PATCH 361/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index be36f514..775a74e2 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,15 +1,19 @@ +Version 4.0-dev-308-g23da206 +==== +#### Bug Fixes +- Restore logging to `syslog`. + Version 4.0-dev-303-g9074069 ==== #### New Features - An initial [PipeWire](https://pipewire.org) backend, with thanks to [Lukas Rusak](https://github.com/lrusak). Use the `--with-pw` configuration flag at the `./configure...` stage to include support. - #### Bug Fixes - Fix a bug when using the `tinysvcmdns` mDNS implementation, with thanks to [fesc2000](https://github.com/fesc2000). - When an unrecognised SETUP message is received, just ignore it and put a warning in the log. Remove a redundant `client_setup_plist` data item from the `conn` data structure. #### Enhancements -- Shairport Sync no longer needs to be in the same directory of NQPTP during compilation. +- Shairport Sync no longer needs to be in the same directory as NQPTP during compilation. - Add Docker automations -- thanks to [Charles Omer](https://github.com/charlesomer). - Add Issue management automations -- thanks to [Charles Omer](https://github.com/charlesomer). - Reconnect the DACP scanner for AirPlay 2. (It still isn't useful, unfortunately.) From 246f26afe41d992d0be2ff44b9cd5596d3a051c4 Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Fri, 6 Aug 2021 11:41:49 +0200 Subject: [PATCH 362/878] configure.ac: find sndfile through pkg-config Find sndfile through pkg-config to retrieve sndfile dependencies such as flac and avoid the following static build failure: /tmp/instance-3/output-1/host/lib/gcc/xtensa-buildroot-linux-uclibc/10.3.0/../../../../xtensa-buildroot-linux-uclibc/bin/ld: /tmp/instance-3/output-1/host/bin/../xtensa-buildroot-linux-uclibc/sysroot/usr/lib/libsndfile.a(libsndfile_la-flac.o): in function `flac_byterate': flac.c:(.text+0xfc): undefined reference to `FLAC__StreamDecoderErrorStatusString' Fixes: - http://autobuild.buildroot.org/results/92ed30a6855ca11800b779718822bcba4a69c9a3 Signed-off-by: Fabrice Fontaine --- configure.ac | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 4f27e346..ef32fda5 100644 --- a/configure.ac +++ b/configure.ac @@ -325,7 +325,14 @@ AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP if test "x$with_convolution" = "xyes" ; then AM_INIT_AUTOMAKE([subdir-objects]) AC_DEFINE([CONFIG_CONVOLUTION], 1, [Include audio DSP convolution support.]) - AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!)) + if test "x${with_pkg_config}" = xyes ; then + PKG_CHECK_MODULES( + [sndfile], [sndfile], + [CFLAGS="${sndfile_CFLAGS} ${CFLAGS}" + LIBS="${sndfile_LIBS} ${LIBS}"], AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!)) + else + AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!)) + fi fi AM_CONDITIONAL([USE_CONVOLUTION], [test "x$with_convolution" = "xyes"]) From 687fd16758e34e3e2b0686a88110d1e97399f36c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 10 Aug 2021 22:04:37 +0100 Subject: [PATCH 363/878] Update MQTT.md --- MQTT.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/MQTT.md b/MQTT.md index da21a3fc..3189a54a 100644 --- a/MQTT.md +++ b/MQTT.md @@ -1,3 +1,21 @@ +MQTT in Shairport Sync +==== +To enable Shairport Sync to act as an MQTT publisher, you need to: +1. Install the mosquitto library: +``` +# apt install libmosquitto-dev +``` +2. Add the configuration flag `--with-mqtt-client` to the list of parameters to the `./configure...` command, for example: +``` +$ ./configure --with-mqtt-client --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-systemd +``` + +If Shairport Sync has MQTT support, it will have the string `mqtt` in its configuration string. For example: +``` +$ shairport-sync -V +3.3.8-OpenSSL-Avahi-ALSA-metadata-mqtt-sysconfdir:/etc +``` + Setting Up MQTT Publishing ==== From 6beb73e99e2703101bff10207fc039ac9ebcbfd4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 23 Aug 2021 12:57:56 +0100 Subject: [PATCH 364/878] Allow debug/inform/die/warn messages up to 16384 bytes (up from 1024 bytes) --- common.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common.c b/common.c index fcbf4fb5..866222c2 100644 --- a/common.c +++ b/common.c @@ -141,7 +141,7 @@ static void (*sps_log)(int prio, const char *t, ...) = syslog; #endif void do_sps_log_to_stderr(__attribute__((unused)) int prio, const char *t, ...) { - char s[1024]; + char s[16384]; va_list args; va_start(args, t); vsnprintf(s, sizeof(s), t, args); @@ -150,7 +150,7 @@ void do_sps_log_to_stderr(__attribute__((unused)) int prio, const char *t, ...) } void do_sps_log_to_stdout(__attribute__((unused)) int prio, const char *t, ...) { - char s[1024]; + char s[16384]; va_list args; va_start(args, t); vsnprintf(s, sizeof(s), t, args); @@ -210,7 +210,7 @@ int create_log_file(const char *path) { } void do_sps_log_to_fd(__attribute__((unused)) int prio, const char *t, ...) { - char s[1024]; + char s[16384]; va_list args; va_start(args, t); vsnprintf(s, sizeof(s), t, args); @@ -459,7 +459,7 @@ char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss void _die(const char *filename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - char b[1024]; + char b[16384]; b[0] = 0; char *s; if (debuglev) { @@ -489,7 +489,7 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { void _warn(const char *filename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - char b[1024]; + char b[16384]; b[0] = 0; char *s; if (debuglev) { @@ -519,7 +519,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f return; int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - char b[1024]; + char b[16384]; b[0] = 0; pthread_mutex_lock(&debug_timing_lock); uint64_t time_now = get_absolute_time_in_ns(); @@ -541,7 +541,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f void _inform(const char *filename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - char b[1024]; + char b[16384]; b[0] = 0; char *s; if (debuglev) { @@ -1079,7 +1079,7 @@ void command_start(void) { void command_execute(const char *command, const char *extra_argument, const int block) { // this has a cancellation point if waiting is enabled if (command) { - char new_command_buffer[1024]; + char new_command_buffer[2048]; char *full_command = (char *)command; if (extra_argument != NULL) { memset(new_command_buffer, 0, sizeof(new_command_buffer)); From d2dc03d07c5696c6f5241060e0de257a688f9dca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:00:09 +0100 Subject: [PATCH 365/878] break out the POST /command stuff --- rtsp.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rtsp.c b/rtsp.c index c459dfc9..8e3fa8a7 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1942,6 +1942,14 @@ void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, __attribute__((unused)) rtsp_message *resp) {} +void handle_command(__attribute__((unused)) rtsp_conn_info *conn, + rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + + debug_log_rtsp_message(2, "POST /command", req); +} + + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { @@ -1960,6 +1968,8 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { handle_configure(conn, req, resp); } else if (strcmp(req->path, "/feedback") == 0) { handle_feedback(conn, req, resp); + } else if (strcmp(req->path, "/command") == 0) { + handle_command(conn, req, resp); } else { debug(2, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); From 5d4d75ad4eacde7681a505553c09bcb4b8242541 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:43:56 +0100 Subject: [PATCH 366/878] add a place to store the groupContainsGroupLeader from SETUP --- player.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player.h b/player.h index 2596eb78..05019417 100644 --- a/player.h +++ b/player.h @@ -146,7 +146,6 @@ typedef struct { uint32_t maximum_latency; // set if an a=max-latency: line appears in the ANNOUNCE message; zero // otherwise int software_mute_enabled; // if we don't have a real mute that we can use - int fd; int authorized; // set if a password is required and has been supplied char *auth_nonce; // the session nonce, if needed @@ -333,7 +332,8 @@ typedef struct { unsigned char *session_key; // needs to be free'd at the end uint64_t frames_packet; uint64_t type; - uint64_t networkTimeTimelineID; // the clock ID used by the player + uint64_t networkTimeTimelineID; // the clock ID used by the player + uint64_t groupContainsGroupLeader; // information coming from the SETUP char *ap2_timing_peer_list_message; From f62ec6686501d9a39650adf933af523aba477c40 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:45:27 +0100 Subject: [PATCH 367/878] Make the features more like the APX2 --- shairport.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shairport.c b/shairport.c index 2de86a9d..c40ae1cc 100644 --- a/shairport.c +++ b/shairport.c @@ -1493,7 +1493,7 @@ int main(int argc, char **argv) { usage(argv[0]); exit(EXIT_SUCCESS); } - + log_to_syslog(); #ifdef CONFIG_LIBDAEMON @@ -1590,12 +1590,13 @@ int main(int argc, char **argv) { // 496155702020608 this setting here is the source of both the plist features response and the // mDNS string. // note: 0x300401F4A00 works but with weird delays and stuff - config.airplay_features = 0x1C340405FCA00; + // config.airplay_features = 0x1C340405FCA00; + config.airplay_features = 0x1C340405D4A00; // APX + Authentication4 (b14) // Advertised with mDNS and returned with GET /info, see // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio // cable attached, OneTimePairingRequired, device was setup for Homekit access control - config.airplay_statusflags = 0x4; + config.airplay_statusflags = 0x04; // Set to NULL to work with transient pairing config.airplay_pin = NULL; @@ -1619,7 +1620,7 @@ int main(int argc, char **argv) { // with thanks uuid_t binuuid; uuid_generate_random(binuuid); - + char *uuid = malloc(UUID_STR_LEN); // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); From 620d6fae601d0ef33a8968d5bbd956f792a4d950 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:54:59 +0100 Subject: [PATCH 368/878] Rearrange code for setting bonjour strings. Improve response text from msg_write_response. Decode updateMRSupportedCommands stuff for curiosity. Capture group ID and groupContainsGroupLeader from setup and post them in the Bonjour strings. Include 'isGroupLeader=0' if it's in a group. Restore group ID and related stuff when deselected. Set bit 11 'DeviceSupportsRelay' when selected and clear when deselected. --- rtsp.c | 381 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 255 insertions(+), 126 deletions(-) diff --git a/rtsp.c b/rtsp.c index 8e3fa8a7..f40053d6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -101,11 +101,12 @@ // from these sources. char *txt_records[64]; +char *secondary_txt_records[64]; + char firmware_version[64]; char ap1_featuresString[64]; char pkString[128]; #ifdef CONFIG_AIRPLAY_2 -char *secondary_txt_records[64]; char deviceIdString[64]; char featuresString[64]; char statusflagsString[32]; @@ -180,12 +181,122 @@ typedef struct { } rtsp_message; #ifdef CONFIG_AIRPLAY_2 -void mdns_update_flags(uint32_t flags) { - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, flags); - mdns_update(NULL, secondary_txt_records); + +static void pkString_make(char *str, size_t str_size, const char *device_id) { + uint8_t public_key[32]; + if (str_size < 2 * sizeof(public_key) + 1) { + warn("Insufficient string size"); + str[0] = '\0'; + return; + } + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); + char *ptr = str; + for (size_t i = 0; i < sizeof(public_key); i++) + ptr += sprintf(ptr, "%02x", public_key[i]); } #endif +void build_bonjour_strings(rtsp_conn_info *conn) { + + int entry_number = 0; + + // make up a firmware version +#ifdef CONFIG_USE_GIT_VERSION_STRING + if (git_version_string[0] != '\0') + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); + else +#endif + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); + +#ifdef CONFIG_AIRPLAY_2 + uint64_t features_hi = config.airplay_features; + features_hi = (features_hi >> 32) & 0xffffffff; + uint64_t features_lo = config.airplay_features; + features_lo = features_lo & 0xffffffff; + snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); + + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "et=0,4"; + txt_records[entry_number++] = ap1_featuresString; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "md=2"; + txt_records[entry_number++] = "am=Shairport Sync"; + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = "tp=UDP"; + txt_records[entry_number++] = "vn=65537"; + txt_records[entry_number++] = "vs=366.0"; + txt_records[entry_number++] = pkString; + txt_records[entry_number++] = NULL; + +#else + // here, just replicate what happens in mdns.h when using those #defines + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "am=ShairportSync"; + txt_records[entry_number++] = "vs=105.1"; + txt_records[entry_number++] = "tp=TCP,UDP"; + txt_records[entry_number++] = "vn=65537"; +#ifdef CONFIG_METADATA + if (config.get_coverart == 0) + txt_records[entry_number++] = "md=0,2"; + else + txt_records[entry_number++] = "md=0,1,2"; +#endif + txt_records[entry_number++] = "ss=16"; + txt_records[entry_number++] = "sr=44100"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "sv=false"; + txt_records[entry_number++] = "et=0,1"; + txt_records[entry_number++] = "ek=1"; + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "ch=2"; + txt_records[entry_number++] = "txtvers=1"; + if (config.password == 0) + txt_records[entry_number++] = "pw=false"; + else + txt_records[entry_number++] = "pw=true"; + txt_records[entry_number++] = NULL; +#endif + +#ifdef CONFIG_AIRPLAY_2 + // make up a secondary set of text records + entry_number = 0; + + secondary_txt_records[entry_number++] = "srcvers=366.0"; + snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); + secondary_txt_records[entry_number++] = deviceIdString; + snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + secondary_txt_records[entry_number++] = featuresString; + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, + config.airplay_statusflags); + + secondary_txt_records[entry_number++] = statusflagsString; + secondary_txt_records[entry_number++] = "protovers=1.1"; + secondary_txt_records[entry_number++] = "acl=0"; + secondary_txt_records[entry_number++] = "rsf=0x0"; + secondary_txt_records[entry_number++] = firmware_version; + secondary_txt_records[entry_number++] = "model=Shairport Sync"; + snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); + secondary_txt_records[entry_number++] = piString; + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); + secondary_txt_records[entry_number++] = gidString; + if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) + secondary_txt_records[entry_number++] = "gcgl=1"; + else + secondary_txt_records[entry_number++] = "gcgl=0"; + if (strcmp(config.airplay_pi, config.airplay_gid) != 0) // if it's in a group + secondary_txt_records[entry_number++] = "isGroupLeader=0"; + secondary_txt_records[entry_number++] = pkString; + secondary_txt_records[entry_number++] = NULL; +#endif +} + #ifdef CONFIG_METADATA typedef struct { uint32_t type; @@ -960,19 +1071,6 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou free(encrypted); return count; } - -static void pkString_make(char *str, size_t str_size, const char *device_id) { - uint8_t public_key[32]; - if (str_size < 2 * sizeof(public_key) + 1) { - warn("Insufficient string size"); - str[0] = '\0'; - return; - } - pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); - char *ptr = str; - for (size_t i = 0; i < sizeof(public_key); i++) - ptr += sprintf(ptr, "%02x", public_key[i]); -} #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { @@ -1159,9 +1257,26 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { int n; unsigned int i; - n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, - resp->respcode == 200 ? "OK" : "Unauthorized"); - // debug(1, "sending response: %s", pkt); + struct response_t { + int code; + char *string; + }; + + struct response_t responses[] = {{200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}}; + int found = 0; + char *respcode_text = "Unauthorized"; + for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { + if (resp->respcode == responses[i].code) { + found = 1; + respcode_text = responses[i].string; + } + } + + if (found == 0) + debug(1, "can't find text for response code %d. Using \"%s\" instead.", resp->respcode, + respcode_text); + + n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, respcode_text); pktfree -= n; p += n; @@ -1843,7 +1958,7 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; - resp->respcode = 200; // assume it's not handled + resp->respcode = 200; // assume it's handled // uint8_t *out; // size_t out_len; @@ -1942,13 +2057,77 @@ void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, __attribute__((unused)) rtsp_message *resp) {} -void handle_command(__attribute__((unused)) rtsp_conn_info *conn, - rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - +void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { debug_log_rtsp_message(2, "POST /command", req); -} + if (rtsp_message_contains_plist(req)) { + plist_t command_dict = NULL; + plist_from_memory(req->content, req->contentlength, &command_dict); + if (command_dict != NULL) { + // we have a plist -- try to get the dict item keyed to "updateMRSupportedCommands" + plist_t item = plist_dict_get_item(command_dict, "type"); + if (item != NULL) { + char *typeValue = NULL; + plist_get_string_val(item, &typeValue); + if (strcmp(typeValue, "updateMRSupportedCommands") == 0) { + item = plist_dict_get_item(command_dict, "params"); + if (item != NULL) { + // the item should be a dict + plist_t item_array = plist_dict_get_item(item, "mrSupportedCommandsFromSender"); + if (item_array != NULL) { + // here we have an array of data items + uint32_t items = plist_array_get_size(item_array); + if (items) { + uint32_t item_number; + for (item_number = 0; item_number < items; item_number++) { + plist_t the_item = plist_array_get_item(item_array, item_number); + char *buff = NULL; + uint64_t length = 0; + plist_get_data_val(the_item, &buff, &length); + // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); + if ((length >= strlen("bplist00")) && (strstr(buff, "bplist00") == buff)) { + // debug(1,"Contains a plist."); + plist_t subsidiary_plist = NULL; + plist_from_memory(buff, length, &subsidiary_plist); + if (subsidiary_plist) { + char *printable_plist = plist_content(subsidiary_plist); + if (printable_plist) { + debug(2, "\n%s", printable_plist); + free(printable_plist); + } else { + debug(1, "Can't print the plist!"); + } + // plist_free(subsidiary_plist); + } else { + debug(1, "Can't access the plist!"); + } + } + } + } + } else { + debug(1, "POST /command no mrSupportedCommandsFromSender item."); + } + } else { + debug(1, "POST /command no params dict."); + } + resp->respcode = 400; // say it's a bad request + } else { + debug(1, + "POST /command plist type is \"%s\", but \"updateMRSupportedCommands\" expected.", + typeValue); + } + } else { + debug(1, "Could not get the \"type\" item."); + } + plist_free(command_dict); + } else { + debug(1, "POST /command plist cannot be inputted."); + } + } else { + debug(1, "POST /command contains no plist"); + } +} void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; @@ -2087,6 +2266,14 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag // resp->respcode = 451; plist_free(messagePlist); + if (config.airplay_gid != NULL) + free(config.airplay_gid); + config.airplay_gid = strdup(config.airplay_pi); + // mdns_update_gid(config.airplay_gid); + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + // mdns_update_flags(config.airplay_statusflags); + build_bonjour_strings(conn); + mdns_update(NULL, secondary_txt_records); resp->respcode = 200; } else { debug(1, "Connection %d: missing plist!", conn->connection_number); @@ -2161,8 +2348,8 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; - debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); - // debug_log_rtsp_message(1, "Connection %d: SETUP (AirPlay 2) SETUP incoming message", req); + // debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); + debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); // we need to get the timing peer interfaces. // I'm guessing they are all this device's adresses that are on the same subnets as // the timing peer info in the accompanying plist @@ -2170,6 +2357,38 @@ 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 = NULL; // we'll create it when we need it + plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); + if (groupUUID) { + char *gid = NULL; + plist_get_string_val(groupUUID, &gid); + if (gid) { + if (config.airplay_gid) + free(config.airplay_gid); + config.airplay_gid = gid; // it'll be free'd later on... + // mdns_update_gid(config.airplay_gid); + build_bonjour_strings(conn); + mdns_update(NULL, secondary_txt_records); + // debug(1,"Updated groupUUID to \"%s\"", config.airplay_gid); + } else { + debug(1, "Invalid groupUUID"); + } + } else { + debug(1, "No groupUUID in SETUP"); + } + + // now see if the group contains a group leader + plist_t groupContainsGroupLeader = plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); + if (groupContainsGroupLeader) { + uint8_t value = 0; + plist_get_bool_val(groupContainsGroupLeader, &value); + conn->groupContainsGroupLeader = value; + build_bonjour_strings(conn); + mdns_update(NULL, secondary_txt_records); + debug(1, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); + } else { + debug(1, "No groupContainsGroupLeader in SETUP"); + } + // now see if the incoming plist contains a "streams" array plist_t streams = plist_dict_get_item(messagePlist, "streams"); @@ -2305,8 +2524,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) ptp_send_control_message_string(conn->ap2_timing_peer_list_message); else debug(1, "No timing peer list!"); - // config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive - // mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); @@ -2349,8 +2566,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // this should be cancelled by an activity_monitor_signify_activity(1) // call in the SETRATEANCHORI handler, which should come up right away - // config.airplay_statusflags |= 1 << 10; // ReceiverSessionIsActive - // mdns_update_flags(config.airplay_statusflags); activity_monitor_signify_activity(0); player_play(conn); @@ -2371,7 +2586,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " "and open a TCP port.", conn->connection_number); - debug_log_rtsp_message(2, "SETUP on Connection %d: No \"streams\" array has been found.", req); // now determine which of the present device's IP numbers are in the // same subnet as any IP numbers in the timing peer info @@ -2488,9 +2702,14 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); plist_free(setupResponsePlist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay + build_bonjour_strings(conn); + mdns_update(NULL, secondary_txt_records); } else { - debug_log_rtsp_message(1, "Unrecognised SETUP incoming message", req); - warn("Unrecognised SETUP incoming message -- ignored."); + // debug(1, "Unrecognised SETUP incoming message from %s", (const char *) + // conn->client_ip_string); debug_log_rtsp_message(1, "Unrecognised SETUP incoming message.", + // req); + // warn("Unrecognised SETUP incoming message -- ignored."); } debug_log_rtsp_message(2, " SETUP response", resp); resp->respcode = 200; @@ -4459,101 +4678,11 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { char **t1 = txt_records; // ap1 test records char **t2 = NULL; // possibly two text records - - int entry_number = 0; - - // make up a firmware version -#ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); - else -#endif - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); - -#ifdef CONFIG_AIRPLAY_2 - uint64_t features_hi = config.airplay_features; - features_hi = (features_hi >> 32) & 0xffffffff; - uint64_t features_lo = config.airplay_features; - features_lo = features_lo & 0xffffffff; - snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - snprintf(pkString, sizeof(pkString), "pk="); - pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), - config.airplay_device_id); - - txt_records[entry_number++] = "cn=0,1"; - txt_records[entry_number++] = "da=true"; - txt_records[entry_number++] = "et=0,4"; - txt_records[entry_number++] = ap1_featuresString; - txt_records[entry_number++] = firmware_version; - txt_records[entry_number++] = "md=2"; - txt_records[entry_number++] = "am=Shairport Sync"; - txt_records[entry_number++] = "sf=0x4"; - txt_records[entry_number++] = "tp=UDP"; - txt_records[entry_number++] = "vn=65537"; - txt_records[entry_number++] = "vs=366.0"; - txt_records[entry_number++] = pkString; - txt_records[entry_number++] = NULL; - -#else - // here, just replicate what happens in mdns.h when using those #defines - txt_records[entry_number++] = "sf=0x4"; - txt_records[entry_number++] = firmware_version; - txt_records[entry_number++] = "am=ShairportSync"; - txt_records[entry_number++] = "vs=105.1"; - txt_records[entry_number++] = "tp=TCP,UDP"; - txt_records[entry_number++] = "vn=65537"; -#ifdef CONFIG_METADATA - if (config.get_coverart == 0) - txt_records[entry_number++] = "md=0,2"; - else - txt_records[entry_number++] = "md=0,1,2"; -#endif - txt_records[entry_number++] = "ss=16"; - txt_records[entry_number++] = "sr=44100"; - txt_records[entry_number++] = "da=true"; - txt_records[entry_number++] = "sv=false"; - txt_records[entry_number++] = "et=0,1"; - txt_records[entry_number++] = "ek=1"; - txt_records[entry_number++] = "cn=0,1"; - txt_records[entry_number++] = "ch=2"; - txt_records[entry_number++] = "txtvers=1"; - if (config.password == 0) - txt_records[entry_number++] = "pw=false"; - else - txt_records[entry_number++] = "pw=true"; - txt_records[entry_number++] = NULL; -#endif - #ifdef CONFIG_AIRPLAY_2 // make up a secondary set of text records t2 = secondary_txt_records; // second set of text records in AirPlay 2 only - entry_number = 0; - - secondary_txt_records[entry_number++] = "srcvers=366.0"; - snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); - secondary_txt_records[entry_number++] = deviceIdString; - snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - secondary_txt_records[entry_number++] = featuresString; - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, - config.airplay_statusflags); - - secondary_txt_records[entry_number++] = statusflagsString; - secondary_txt_records[entry_number++] = "protovers=1.1"; - secondary_txt_records[entry_number++] = "acl=0"; - secondary_txt_records[entry_number++] = "rsf=0x0"; - secondary_txt_records[entry_number++] = firmware_version; - secondary_txt_records[entry_number++] = "model=Shairport Sync"; - snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); - secondary_txt_records[entry_number++] = piString; - snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); - secondary_txt_records[entry_number++] = gidString; - secondary_txt_records[entry_number++] = "gcgl=0"; - secondary_txt_records[entry_number++] = pkString; - secondary_txt_records[entry_number++] = NULL; #endif - + build_bonjour_strings(NULL); // no conn yet mdns_register(t1, t2); pthread_setcancelstate(oldState, NULL); From d703314c06e844edcda932bd4be335ba8733f830 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:28:30 +0100 Subject: [PATCH 369/878] Move group ID store to the conn. Fix the size of the groupContainsGroupLeader plist boolean store. --- common.h | 1 - player.h | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common.h b/common.h index 4572d198..9c3496f6 100644 --- a/common.h +++ b/common.h @@ -302,7 +302,6 @@ typedef struct { char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist - char *airplay_gid; // UUID in the Bonjour advertisement -- initially the same as the pi #endif } shairport_cfg; diff --git a/player.h b/player.h index 05019417..e8d661b5 100644 --- a/player.h +++ b/player.h @@ -287,6 +287,7 @@ typedef struct { clock_status_t clock_status; #ifdef CONFIG_AIRPLAY_2 + char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as the pi UUID airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... timing_t timing_type; // are we using NTP or PTP on this connection? @@ -332,8 +333,8 @@ typedef struct { unsigned char *session_key; // needs to be free'd at the end uint64_t frames_packet; uint64_t type; - uint64_t networkTimeTimelineID; // the clock ID used by the player - uint64_t groupContainsGroupLeader; // information coming from the SETUP + uint64_t networkTimeTimelineID; // the clock ID used by the player + uint8_t groupContainsGroupLeader; // information coming from the SETUP char *ap2_timing_peer_list_message; From 7fb8b288f07311e552db4ca7f0f18be56dd7755f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:30:24 +0100 Subject: [PATCH 370/878] Stop initialising the airplay_gid. Now if it's NULL it means the group UUID is the same as the deviice's pi. Alos airplay_gid is moved to the conn. --- shairport.c | 1 - 1 file changed, 1 deletion(-) diff --git a/shairport.c b/shairport.c index c40ae1cc..9ab3f51a 100644 --- a/shairport.c +++ b/shairport.c @@ -1625,7 +1625,6 @@ int main(int argc, char **argv) { // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); config.airplay_pi = strdup(uuid); - config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi // now we need to create the sequence of items for a Bonjour TXT record. #else From 7d36f0402542378833bedd3273cb74f769168ec7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:31:06 +0100 Subject: [PATCH 371/878] If the airplay_gid is not NULL, free it before deleting the player. --- player.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/player.c b/player.c index ac332a43..c95588e9 100644 --- a/player.c +++ b/player.c @@ -1705,6 +1705,9 @@ void player_thread_cleanup_handler(void *arg) { if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); + if (conn->airplay_gid) + free(conn->airplay_gid); + reset_anchor_info(conn); release_play_lock(conn); conn->rtp_running = 0; From 7c4308ce3a3b304b386be6f5ef35d5021fc2aed6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:33:29 +0100 Subject: [PATCH 372/878] Move groupUUID storage to the conn, as it's a property of a connection not of the player. Improve some debug messages. Alsways update the bonjour strings dueing a SETUP, only reset grooup UUID when a TEARDOWN closes a connection, not when it closes a stream. --- rtsp.c | 81 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/rtsp.c b/rtsp.c index f40053d6..12c500d6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -284,13 +284,17 @@ void build_bonjour_strings(rtsp_conn_info *conn) { secondary_txt_records[entry_number++] = "model=Shairport Sync"; snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); secondary_txt_records[entry_number++] = piString; - snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_gid); + if ((conn != NULL) && (conn->airplay_gid != 0)) { + snprintf(gidString, sizeof(gidString), "gid=%s", conn->airplay_gid); + } else { + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_pi); + } secondary_txt_records[entry_number++] = gidString; if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) secondary_txt_records[entry_number++] = "gcgl=1"; else secondary_txt_records[entry_number++] = "gcgl=0"; - if (strcmp(config.airplay_pi, config.airplay_gid) != 0) // if it's in a group + if ((conn != NULL) && (conn->airplay_gid != 0)) // if it's in a group secondary_txt_records[entry_number++] = "isGroupLeader=0"; secondary_txt_records[entry_number++] = pkString; secondary_txt_records[entry_number++] = NULL; @@ -2225,7 +2229,6 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: TEARDOWN", conn->connection_number); // debug_log_rtsp_message(1, "TEARDOWN: ", req); // if (have_player(conn)) { @@ -2238,6 +2241,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { + debug(1, "Connection %d: TEARDOWN a stream.", conn->connection_number); // we are being asked to close a stream player_stop(conn); activity_monitor_signify_activity(0); // inactive, and should be after command_stop() @@ -2249,8 +2253,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { // we are being asked to disconnect - debug(2, "Connection %d: TEARDOWN No \"streams\" array has been found.", - conn->connection_number); + debug(1, "Connection %d: TEARDOWN a connection.", conn->connection_number); debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); pthread_cancel(conn->rtp_event_thread); pthread_join(conn->rtp_event_thread, NULL); @@ -2259,6 +2262,15 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag close(conn->event_socket); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); + if (conn->airplay_gid != NULL) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } + conn->groupContainsGroupLeader = 0; + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + // mdns_update_flags(config.airplay_statusflags); + build_bonjour_strings(conn); + mdns_update(NULL, secondary_txt_records); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2266,14 +2278,6 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag // resp->respcode = 451; plist_free(messagePlist); - if (config.airplay_gid != NULL) - free(config.airplay_gid); - config.airplay_gid = strdup(config.airplay_pi); - // mdns_update_gid(config.airplay_gid); - config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay - // mdns_update_flags(config.airplay_statusflags); - build_bonjour_strings(conn); - mdns_update(NULL, secondary_txt_records); resp->respcode = 200; } else { debug(1, "Connection %d: missing plist!", conn->connection_number); @@ -2348,8 +2352,8 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; - // debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); - debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); + debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); + // debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); // we need to get the timing peer interfaces. // I'm guessing they are all this device's adresses that are on the same subnets as // the timing peer info in the accompanying plist @@ -2362,13 +2366,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) char *gid = NULL; plist_get_string_val(groupUUID, &gid); if (gid) { - if (config.airplay_gid) - free(config.airplay_gid); - config.airplay_gid = gid; // it'll be free'd later on... - // mdns_update_gid(config.airplay_gid); - build_bonjour_strings(conn); - mdns_update(NULL, secondary_txt_records); - // debug(1,"Updated groupUUID to \"%s\"", config.airplay_gid); + if (conn->airplay_gid) + free(conn->airplay_gid); + conn->airplay_gid = gid; // it'll be free'd later on... } else { debug(1, "Invalid groupUUID"); } @@ -2382,8 +2382,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t value = 0; plist_get_bool_val(groupContainsGroupLeader, &value); conn->groupContainsGroupLeader = value; - build_bonjour_strings(conn); - mdns_update(NULL, secondary_txt_records); debug(1, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); } else { debug(1, "No groupContainsGroupLeader in SETUP"); @@ -2394,8 +2392,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (streams) { setupResponsePlist = plist_new_dict(); // now that we know we need it, create it - debug(2, - "SETUP on Connection %d: A \"streams\" array has been found -- create control and audio " + debug(1, + "Connection %d: SETUP. A \"streams\" array has been found -- create control and audio " "threads and ports", conn->connection_number); // open a player) @@ -2531,7 +2529,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; case 103: { - debug(1, "Connection %d. Buffered Audio Stream Detected.", conn->connection_number); + debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); get_play_lock(conn); conn->airplay_stream_type = buffered_stream; @@ -2543,7 +2541,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->self_scope_id, &conn->local_buffered_audio_port, &conn->buffered_audio_socket); if (err) { - die("Error %d: could not find a TCP port to use as a buffered_audio port", err); + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " + "buffered_audio port", + conn->connection_number, err); } // hack. @@ -2572,7 +2572,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; default: - debug(1, "Unhandled stream type %" PRIu64 ".", item_value); + debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", + conn->connection_number, item_value); debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); } @@ -2582,7 +2583,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "streams", streams_array); } else { - debug(2, + debug(1, "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " "and open a TCP port.", conn->connection_number); @@ -2663,16 +2664,19 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } } } else { - debug(1, "Don't recognise this as an IP address."); + debug(1, "SETUP on Connection %d: Don't recognise this as an IP address.", + conn->connection_number); } free(ip_address); } freeifaddrs(addrs); } else { - debug(1, "No timingPeerInfo addresses in the array."); + debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", + conn->connection_number); } } else { - debug(1, "Can't find timingPeerInfo addresses"); + debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", + conn->connection_number); } // make up the timing peer info list part of the response... // debug(1,"Create timingPeerInfoPlist"); @@ -2689,7 +2693,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, conn->self_scope_id, &conn->local_event_port, &conn->event_socket); if (err) { - die("Error %d: could not find a TCP port to use as an event port", err); + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event port", + conn->connection_number, err); } pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); @@ -2706,10 +2711,10 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) build_bonjour_strings(conn); mdns_update(NULL, secondary_txt_records); } else { - // debug(1, "Unrecognised SETUP incoming message from %s", (const char *) - // conn->client_ip_string); debug_log_rtsp_message(1, "Unrecognised SETUP incoming message.", - // req); - // warn("Unrecognised SETUP incoming message -- ignored."); + debug(1, "SETUP on Connection %d: Unrecognised SETUP incoming message from %s", + conn->connection_number, (const char *)conn->client_ip_string); + debug_log_rtsp_message(1, "Unrecognised SETUP incoming message.", req); + warn("Unrecognised SETUP incoming message -- ignored."); } debug_log_rtsp_message(2, " SETUP response", resp); resp->respcode = 200; From 650077bc836875144e497e9f43e48ff23b43593f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 12:08:13 +0100 Subject: [PATCH 373/878] Tiny update. Note thay SPS can not participate in a remote control connection. --- rtsp.c | 1 - 1 file changed, 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index 12c500d6..4b04aa83 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2268,7 +2268,6 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag } conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay - // mdns_update_flags(config.airplay_statusflags); build_bonjour_strings(conn); mdns_update(NULL, secondary_txt_records); } From 7fde77c45d50d92dd64ba91192af6a149b850d7f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:39:32 +0100 Subject: [PATCH 374/878] lock the avahi thread before modifying the strings --- mdns_avahi.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mdns_avahi.c b/mdns_avahi.c index c4568078..95984419 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -334,6 +334,7 @@ static int avahi_update(char **txt_records, char **secondary_txt_records) { else selected_interface = AVAHI_IF_UNSPEC; + avahi_threaded_poll_lock(tpoll); if (txt_records != NULL) { if (text_record_string_list) avahi_string_list_free(text_record_string_list); @@ -356,6 +357,8 @@ static int avahi_update(char **txt_records, char **secondary_txt_records) { if (err != 0) debug(1, "avahi_update error updating secondary txt records."); } + + avahi_threaded_poll_unlock(tpoll); return 0; } From 063b02cb11593094cc7963e2493c3b879275cfd8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:40:04 +0100 Subject: [PATCH 375/878] Mark the airplay_gid free by nulling it. --- player.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/player.c b/player.c index c95588e9..a61dc841 100644 --- a/player.c +++ b/player.c @@ -1705,8 +1705,10 @@ void player_thread_cleanup_handler(void *arg) { if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); - if (conn->airplay_gid) + if (conn->airplay_gid) { free(conn->airplay_gid); + conn->airplay_gid = NULL; + } reset_anchor_info(conn); release_play_lock(conn); From e2153d275de02798d5bb8f69cf8c37eff6b08f07 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:40:43 +0100 Subject: [PATCH 376/878] Add new type of airplasy stream --- player.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.h b/player.h index e8d661b5..48755997 100644 --- a/player.h +++ b/player.h @@ -104,7 +104,7 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; -typedef enum { realtime_stream, buffered_stream } airplay_stream_t; +typedef enum { realtime_stream, buffered_stream, remote_control_stream } airplay_stream_t; typedef struct { uint8_t *data; From f5e21ade647bda9ca1770cd9ca81677134b065a7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:53:14 +0100 Subject: [PATCH 377/878] clang --- mdns.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdns.c b/mdns.c index 7427dc7e..9a5465c1 100644 --- a/mdns.c +++ b/mdns.c @@ -110,7 +110,7 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { } void mdns_update(char **txt_records, char **secondary_txt_records) { -if ((config.mdns) && (config.mdns->mdns_update)) { + if ((config.mdns) && (config.mdns->mdns_update)) { config.mdns->mdns_update(txt_records, secondary_txt_records); } else debug(1, "Can't mdns_update -- no mdns_update registered."); From fcd57227ebf307ece3716aef092bb69462082ced Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:53:48 +0100 Subject: [PATCH 378/878] clang --- player.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/player.h b/player.h index 48755997..5e1e6e7b 100644 --- a/player.h +++ b/player.h @@ -287,7 +287,8 @@ typedef struct { clock_status_t clock_status; #ifdef CONFIG_AIRPLAY_2 - char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as the pi UUID + char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as + // the pi UUID airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... timing_t timing_type; // are we using NTP or PTP on this connection? From 6ebc2517912c603f8a7ebad58465088385c8cf38 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:54:19 +0100 Subject: [PATCH 379/878] quieten a few debug messages --- rtsp.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index 4b04aa83..4ffe4fb2 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2260,8 +2260,6 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); if (conn->event_socket) close(conn->event_socket); - - debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); if (conn->airplay_gid != NULL) { free(conn->airplay_gid); conn->airplay_gid = NULL; @@ -2270,6 +2268,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay build_bonjour_strings(conn); mdns_update(NULL, secondary_txt_records); + debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2381,7 +2380,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t value = 0; plist_get_bool_val(groupContainsGroupLeader, &value); conn->groupContainsGroupLeader = value; - debug(1, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); + debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); } else { debug(1, "No groupContainsGroupLeader in SETUP"); } From b24380d34b9433a599db93ab6cae35bdecd0c7b1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 2 Sep 2021 12:20:53 +0100 Subject: [PATCH 380/878] Move the release of the group UUID to when the connection ends, not when the play session ends. --- player.c | 5 ----- rtsp.c | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/player.c b/player.c index a61dc841..ac332a43 100644 --- a/player.c +++ b/player.c @@ -1705,11 +1705,6 @@ void player_thread_cleanup_handler(void *arg) { if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); - if (conn->airplay_gid) { - free(conn->airplay_gid); - conn->airplay_gid = NULL; - } - reset_anchor_info(conn); release_play_lock(conn); conn->rtp_running = 0; diff --git a/rtsp.c b/rtsp.c index 4ffe4fb2..8737a2da 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2267,6 +2267,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay build_bonjour_strings(conn); + debug(1,"Connection %d: TEARDOWN mdns_update.", conn->connection_number); mdns_update(NULL, secondary_txt_records); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } @@ -2707,6 +2708,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay build_bonjour_strings(conn); + debug(1,"Connection %d: SETUP mdns_update.", conn->connection_number); mdns_update(NULL, secondary_txt_records); } else { debug(1, "SETUP on Connection %d: Unrecognised SETUP incoming message from %s", @@ -4298,6 +4300,10 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { pair_setup_free(conn->ap2_control_pairing.setup_ctx); pair_verify_free(conn->ap2_control_pairing.verify_ctx); pair_cipher_free(conn->ap2_control_pairing.cipher_ctx); + if (conn->airplay_gid) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } #endif rtp_terminate(conn); From f058d118c18ddc8b6ae28f91ba2b4f0b44faaf6b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 3 Sep 2021 15:38:28 +0100 Subject: [PATCH 381/878] Quieten a really noise message on verbosity 3. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 4aa6ee9b..3afdd2ed 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1667,7 +1667,7 @@ int do_play(void *buf, int samples) { } snd_pcm_state_t prior_state = state; // keep this for afterwards.... - debug(3, "alsa: write %d frames.", samples); + // debug(3, "alsa: write %d frames.", samples); ret = alsa_pcm_write(alsa_handle, buf, samples); if (ret == samples) { stall_monitor_frame_count += samples; From 078b7080874d9eba4d336de797e034a562592a92 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 3 Sep 2021 15:41:47 +0100 Subject: [PATCH 382/878] Uncomment code to list the contents of an incoming message. Get the source airplay version. Quieten some messages. --- rtsp.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index 8737a2da..98d0a75c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -840,7 +840,7 @@ int msg_handle_line(rtsp_message **pmsg, char *line) { *p = 0; p += 2; msg_add_header(msg, line, p); - // debug(3, " %s: %s.", line, p); + debug(3, " %s: %s.", line, p); return -1; } else { char *cl = msg_get_header(msg, "Content-Length"); @@ -1408,8 +1408,19 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - if (rtsp_message_contains_plist(req)) { // it's stage one + + if (rtsp_message_contains_plist(req)) { // it's stage one + // get version of AirPlay -- it might be too old. Not using it yet. + char *hdr = msg_get_header(req, "User-Agent"); + if (hdr) { + if (strstr(hdr, "AirPlay/") == hdr) { + hdr = hdr + strlen("AirPlay/"); + //double airplay_version = 0.0; + //airplay_version = atof(hdr); + debug(1,"Connection %d: GET_INFO: Source AirPlay Version is: %s.",conn->connection_number, hdr); + } + } plist_t info_plist = NULL; plist_from_memory(req->content, req->contentlength, &info_plist); @@ -1490,7 +1501,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r } void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: FLUSHBUFFERED %s :: Content-Length %d", conn->connection_number, + debug(3, "Connection %d: FLUSHBUFFERED %s : Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); @@ -2713,7 +2724,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } else { debug(1, "SETUP on Connection %d: Unrecognised SETUP incoming message from %s", conn->connection_number, (const char *)conn->client_ip_string); - debug_log_rtsp_message(1, "Unrecognised SETUP incoming message.", req); + debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); warn("Unrecognised SETUP incoming message -- ignored."); } debug_log_rtsp_message(2, " SETUP response", resp); From ae1c4f1d2938207f2f386b5a588d7f9f012a4a78 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 4 Sep 2021 15:08:44 +0100 Subject: [PATCH 383/878] redo handle_setup to identify ptp, ntp and remote control streams, returns 400 for remote control and unrecognised stuff. --- player.h | 10 +- rtsp.c | 751 ++++++++++++++++++++++++++++++------------------------- 2 files changed, 419 insertions(+), 342 deletions(-) diff --git a/player.h b/player.h index 5e1e6e7b..d980f1c2 100644 --- a/player.h +++ b/player.h @@ -104,7 +104,13 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; -typedef enum { realtime_stream, buffered_stream, remote_control_stream } airplay_stream_t; +typedef enum { + unknown_stream_category, + ptp_stream, + ntp_stream, + remote_control_stream +} airplay_stream_c; // "c" for category +typedef enum { realtime_stream, buffered_stream } airplay_stream_t; typedef struct { uint8_t *data; @@ -290,6 +296,8 @@ typedef struct { char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as // the pi UUID airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? + airplay_stream_c + airplay_stream_category; // is it a remote control stream or a normal "full service" stream? airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... timing_t timing_type; // are we using NTP or PTP on this connection? diff --git a/rtsp.c b/rtsp.c index 98d0a75c..f0a1c495 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1409,16 +1409,16 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - if (rtsp_message_contains_plist(req)) { // it's stage one - // get version of AirPlay -- it might be too old. Not using it yet. + // get version of AirPlay -- it might be too old. Not using it yet. char *hdr = msg_get_header(req, "User-Agent"); if (hdr) { if (strstr(hdr, "AirPlay/") == hdr) { hdr = hdr + strlen("AirPlay/"); - //double airplay_version = 0.0; - //airplay_version = atof(hdr); - debug(1,"Connection %d: GET_INFO: Source AirPlay Version is: %s.",conn->connection_number, hdr); + // double airplay_version = 0.0; + // airplay_version = atof(hdr); + debug(1, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, + hdr); } } plist_t info_plist = NULL; @@ -2278,7 +2278,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1,"Connection %d: TEARDOWN mdns_update.", conn->connection_number); + debug(1, "Connection %d: TEARDOWN mdns_update.", conn->connection_number); mdns_update(NULL, secondary_txt_records); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } @@ -2362,373 +2362,442 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; - debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); + debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); // debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); - // we need to get the timing peer interfaces. - // I'm guessing they are all this device's adresses that are on the same subnets as - // the timing peer info in the accompanying plist plist_t messagePlist = plist_from_rtsp_content(req); - plist_t setupResponsePlist = NULL; // we'll create it when we need it + plist_t setupResponsePlist = plist_new_dict(); + resp->respcode = 400; - plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); - if (groupUUID) { - char *gid = NULL; - plist_get_string_val(groupUUID, &gid); - if (gid) { - if (conn->airplay_gid) - free(conn->airplay_gid); - conn->airplay_gid = gid; // it'll be free'd later on... - } else { - debug(1, "Invalid groupUUID"); - } - } else { - debug(1, "No groupUUID in SETUP"); - } - - // now see if the group contains a group leader - plist_t groupContainsGroupLeader = plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); - if (groupContainsGroupLeader) { - uint8_t value = 0; - plist_get_bool_val(groupContainsGroupLeader, &value); - conn->groupContainsGroupLeader = value; - debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); - } else { - debug(1, "No groupContainsGroupLeader in SETUP"); - } - - // now see if the incoming plist contains a "streams" array + // see if the incoming plist contains a "streams" array plist_t streams = plist_dict_get_item(messagePlist, "streams"); - - if (streams) { - setupResponsePlist = plist_new_dict(); // now that we know we need it, create it - debug(1, - "Connection %d: SETUP. A \"streams\" array has been found -- create control and audio " - "threads and ports", - conn->connection_number); - // open a player) - - // if (ptp_shm_interface_open() != 0) - // warn("Error opening the nqptp clock daemon!"); - - // get stream[0] - plist_t stream0 = plist_array_get_item(streams, 0); - - plist_t streams_array = plist_new_array(); // to hold the ports and stuff - plist_t stream0dict = plist_new_dict(); - // more stuff - // set up a UDP control stream and thread and a UDP or TCP audio stream and thread - - // bind a new UDP port and get a socket - conn->local_ap2_control_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_ap2_control_port, - &conn->ap2_control_socket); - if (err) { - die("Error %d: could not find a UDP port to use as an ap2_control port", err); - } - - pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); - - // get the session key - - plist_t item = plist_dict_get_item(stream0, "shk"); // session key - uint64_t item_value = 0; - plist_get_data_val(item, (char **)&conn->session_key, &item_value); - - // get the DACP-ID and Active Remote for remote control stuff - - char *ar = msg_get_header(req, "Active-Remote"); - if (ar) { - debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", - conn->connection_number, ar); - // get the active remote - if (conn->dacp_active_remote) // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); -#endif - } else { - debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", - conn->connection_number); - if (conn->dacp_active_remote) { // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } - } - - ar = msg_get_header(req, "DACP-ID"); - if (ar) { - debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", conn->connection_number, - ar); - if (conn->dacp_id) // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); -#endif - } else { - debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", - conn->connection_number); - if (conn->dacp_id) { // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = NULL; - } - } - - // now, get the type of the stream. - item = plist_dict_get_item(stream0, "type"); - item_value = 0; - plist_get_uint_val(item, &item_value); - - switch (item_value) { - case 96: { - debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); - debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); - get_play_lock(conn); - conn->airplay_stream_type = realtime_stream; - // bind a new UDP port and get a socket - conn->local_realtime_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_realtime_audio_port, - &conn->realtime_audio_socket); - if (err) { - die("Error %d: could not find a UDP port to use as a realtime_audio port", err); - } - - pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); - plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_realtime_audio_port)); - - conn->stream.type = ast_apple_lossless; - debug(3, "An ALAC stream has been detected."); - - // Set reasonable connection defaults - conn->stream.fmtp[0] = 96; - conn->stream.fmtp[1] = 352; - conn->stream.fmtp[2] = 0; - conn->stream.fmtp[3] = 16; - conn->stream.fmtp[4] = 40; - conn->stream.fmtp[5] = 10; - conn->stream.fmtp[6] = 14; - conn->stream.fmtp[7] = 2; - conn->stream.fmtp[8] = 255; - conn->stream.fmtp[9] = 0; - conn->stream.fmtp[10] = 0; - conn->stream.fmtp[11] = 44100; - - // set the parameters of the player (as distinct from the parameters of the decoder -- - // that's done later). - conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. - conn->input_rate = conn->stream.fmtp[11]; - conn->input_num_channels = conn->stream.fmtp[7]; - conn->input_bit_depth = conn->stream.fmtp[3]; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - debug(2, "Realtime Stream Play"); - if (conn->ap2_timing_peer_list_message) - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - else - debug(1, "No timing peer list!"); - activity_monitor_signify_activity(1); - player_prepare_to_play(conn); - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - case 103: { - debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", conn->connection_number); - debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); - get_play_lock(conn); - conn->airplay_stream_type = buffered_stream; - // get needed stuff - - // bind a new TCP port and get a socket - conn->local_buffered_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_buffered_audio_port, - &conn->buffered_audio_socket); - if (err) { - die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " - "buffered_audio port", - conn->connection_number, err); - } - - // hack. - conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; // we are stuck with this for the moment. - conn->input_num_channels = 2; - conn->input_bit_depth = 16; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - activity_monitor_signify_activity(1); - player_prepare_to_play( - conn); // get capabilities of DAC before creating the buffered audio thread - - pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); - plist_dict_set_item(stream0dict, "dataPort", plist_new_uint(conn->local_buffered_audio_port)); - plist_dict_set_item(stream0dict, "audioBufferSize", - plist_new_uint(conn->ap2_audio_buffer_size)); - - // this should be cancelled by an activity_monitor_signify_activity(1) - // call in the SETRATEANCHORI handler, which should come up right away - activity_monitor_signify_activity(0); - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - default: - debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", - conn->connection_number, item_value); - debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); - } - - plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); - - plist_array_append_item(streams_array, stream0dict); - plist_dict_set_item(setupResponsePlist, "streams", streams_array); - - } else { - debug(1, + if (streams == NULL) { + // no "streams" plist, so it must (?) be an initial setup + debug(2, "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " "and open a TCP port.", conn->connection_number); - - // now determine which of the present device's IP numbers are in the - // same subnet as any IP numbers in the timing peer info - // provided by the client - - plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); - if (timing_peer_info) { - // first, get and retain the incoming plist. - setupResponsePlist = plist_new_dict(); // now that we know we need it, create it - plist_t addresses = plist_new_array(); // to hold the device's peer interfaces - plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); - if (addresses_array) { - // iterate through the array of items - uint32_t items = plist_array_get_size(addresses_array); - if (items) { - - uint32_t item; - struct ifaddrs *addrs, *iap; - getifaddrs(&addrs); - 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, "Timing peer: %s", ip_address); - // find its family and convert it into an IPv4/6 address - - struct sockaddr_storage peer_address; - struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; - struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; - - if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == 1) { // is an IPv6 address... - peer_address.ss_family = AF_INET6; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET6)) { - struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); - struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); - unsigned int i; - int different = 0; - for (i = 0; ((i < 16) && (different == 0)); i++) { - unsigned char host_byte = - (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - unsigned char peer_byte = - (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - if (host_byte != peer_byte) - different = 1; - } - - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); - if (!different) { - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); - } - } - } - } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { - peer_address.ss_family = AF_INET; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET)) { - struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); - struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); - if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == - (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); - } - } - } + // figure out what category of stream it is, by looking at the plist + conn->airplay_stream_category = unknown_stream_category; + plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); + if (timingProtocol != NULL) { + char *timingProtocolString = NULL; + plist_get_string_val(timingProtocol, &timingProtocolString); + if (timingProtocolString) { + if (strcmp(timingProtocolString, "PTP") == 0) { + debug(1, "SETUP on Connection %d: PTP setup detected.", conn->connection_number); + conn->airplay_stream_category = ptp_stream; + conn->timing_type = ts_ptp; + } else if (strcmp(timingProtocolString, "NTP") == 0) { + debug(1, "SETUP on Connection %d: NTP setup detected.", conn->connection_number); + conn->airplay_stream_category = ntp_stream; + conn->timing_type = ts_ntp; + } else if (strcmp(timingProtocolString, "None") == 0) { + debug(2, "SETUP on Connection %d: a \"None\" setup detected.", conn->connection_number); + // 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, "SETUP on Connection %d: Remote Control setup detected.", + conn->connection_number); + conn->airplay_stream_category = remote_control_stream; } else { - debug(1, "SETUP on Connection %d: Don't recognise this as an IP address.", + debug(1, + "SETUP on Connection %d: a \"None\" setup detected, with " + "\"isRemoteControlOnly\" item set to \"false\".", conn->connection_number); } - free(ip_address); + } else { + debug(1, + "SETUP on Connection %d: a \"None\" setup detected, but no " + "\"isRemoteControlOnly\" item detected.", + conn->connection_number); } - freeifaddrs(addrs); - } else { - debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", - conn->connection_number); } + + // here, we know it's an initial setup and we know the kind of setup being requested + // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and + // timingPeerList + if (conn->airplay_stream_category == ptp_stream) { + debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); + plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); + if (groupUUID) { + char *gid = NULL; + plist_get_string_val(groupUUID, &gid); + if (gid) { + if (conn->airplay_gid) + free(conn->airplay_gid); + conn->airplay_gid = gid; // it'll be free'd later on... + } else { + debug(1, "Invalid groupUUID"); + } + } else { + debug(1, "No groupUUID in SETUP"); + } + + // now see if the group contains a group leader + plist_t groupContainsGroupLeader = + plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); + if (groupContainsGroupLeader) { + uint8_t value = 0; + plist_get_bool_val(groupContainsGroupLeader, &value); + conn->groupContainsGroupLeader = value; + debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); + } else { + debug(1, "No groupContainsGroupLeader in SETUP"); + } + + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); + if (timing_peer_info) { + // first, get and retain the incoming plist. + plist_t addresses = plist_new_array(); // to hold the device's peer interfaces + plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); + if (addresses_array) { + // iterate through the array of items + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + + uint32_t item; + struct ifaddrs *addrs, *iap; + getifaddrs(&addrs); + 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, "Timing peer: %s", ip_address); + // find its family and convert it into an IPv4/6 address + + struct sockaddr_storage peer_address; + struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; + struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; + + if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == + 1) { // is an IPv6 address... + peer_address.ss_family = AF_INET6; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET6)) { + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); + struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); + unsigned int i; + int different = 0; + for (i = 0; ((i < 16) && (different == 0)); i++) { + unsigned char host_byte = + (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + unsigned char peer_byte = + (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); + if (host_byte != peer_byte) + different = 1; + } + + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); + if (!different) { + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { + peer_address.ss_family = AF_INET; + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0) && + (iap->ifa_addr->sa_family == AF_INET)) { + struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); + struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); + if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == + (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); + } + } + } + } else { + debug(1, "SETUP on Connection %d: Don't recognise this as an IP address.", + conn->connection_number); + } + free(ip_address); + } + freeifaddrs(addrs); + } else { + debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", + conn->connection_number); + } + } else { + debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", + conn->connection_number); + } + // make up the timing peer info list part of the response... + // debug(1,"Create timingPeerInfoPlist"); + plist_t timingPeerInfoPlist = plist_new_dict(); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); + plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); + plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", + conn->connection_number); + } + } else if (conn->airplay_stream_category == ntp_stream) { + debug(1, "SETUP on Connection %d: ntp stream handling is not implemented!", + conn->connection_number, req); + warn("Shairport Sync can not handle NTP streams."); + } else if (conn->airplay_stream_category == remote_control_stream) { + debug_log_rtsp_message(1, "SETUP \"isRemoteControlOnly\" message", req); + // resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", + conn->connection_number, timingProtocolString); + warn("Shairport Sync can not handle streams of this type: \"%s\".", timingProtocolString); + } + free(timingProtocolString); } else { - debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", + debug(1, "SETUP on Connection %d: Can't retrieve timingProtocol string in initial SETUP.", conn->connection_number); } - // make up the timing peer info list part of the response... - // debug(1,"Create timingPeerInfoPlist"); - plist_t timingPeerInfoPlist = plist_new_dict(); - plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); - plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); - plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); - // get a port to use as an event port + } else { + debug(1, + "SETUP on Connection %d: Unrecognised SETUP incoming message from \"%s\": no " + "timingProtocol or streams plist found.", + conn->connection_number, (const char *)conn->client_ip_string); + debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); + warn("Unrecognised SETUP incoming message -- ignored."); + } + } else { + debug(2, + "Connection %d: SETUP. A \"streams\" array has been found -- create control and audio " + "threads and ports", + conn->connection_number); + if (conn->airplay_stream_category == ptp_stream) { + // get stream[0] + plist_t stream0 = plist_array_get_item(streams, 0); - // bind a new TCP port and get a socket - conn->local_event_port = 0; // any port + plist_t streams_array = plist_new_array(); // to hold the ports and stuff + plist_t stream0dict = plist_new_dict(); + // more stuff + // set up a UDP control stream and thread and a UDP or TCP audio stream and thread - int err = - bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_event_port, &conn->event_socket); + // bind a new UDP port and get a socket + conn->local_ap2_control_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_ap2_control_port, + &conn->ap2_control_socket); if (err) { - die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event port", - conn->connection_number, err); + die("Error %d: could not find a UDP port to use as an ap2_control port", err); } - pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); - plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + // get the session key + + plist_t item = plist_dict_get_item(stream0, "shk"); // session key + uint64_t item_value = 0; + plist_get_data_val(item, (char **)&conn->session_key, &item_value); + + // get the DACP-ID and Active Remote for remote control stuff + + char *ar = msg_get_header(req, "Active-Remote"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", + conn->connection_number, ar); + // get the active remote + if (conn->dacp_active_remote) // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", + conn->connection_number); + if (conn->dacp_active_remote) { // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + } + + ar = msg_get_header(req, "DACP-ID"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", + conn->connection_number, ar); + if (conn->dacp_id) // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", + conn->connection_number); + if (conn->dacp_id) { // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = NULL; + } + } + + // now, get the type of the stream. + item = plist_dict_get_item(stream0, "type"); + item_value = 0; + plist_get_uint_val(item, &item_value); + + switch (item_value) { + case 96: { + debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); + get_play_lock(conn); + conn->airplay_stream_type = realtime_stream; + // bind a new UDP port and get a socket + conn->local_realtime_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_realtime_audio_port, + &conn->realtime_audio_socket); + if (err) { + die("Error %d: could not find a UDP port to use as a realtime_audio port", err); + } + + pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_realtime_audio_port)); + + conn->stream.type = ast_apple_lossless; + debug(3, "An ALAC stream has been detected."); + + // Set reasonable connection defaults + conn->stream.fmtp[0] = 96; + conn->stream.fmtp[1] = 352; + conn->stream.fmtp[2] = 0; + conn->stream.fmtp[3] = 16; + conn->stream.fmtp[4] = 40; + conn->stream.fmtp[5] = 10; + conn->stream.fmtp[6] = 14; + conn->stream.fmtp[7] = 2; + conn->stream.fmtp[8] = 255; + conn->stream.fmtp[9] = 0; + conn->stream.fmtp[10] = 0; + conn->stream.fmtp[11] = 44100; + + // set the parameters of the player (as distinct from the parameters of the decoder -- + // that's done later). + conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. + conn->input_rate = conn->stream.fmtp[11]; + conn->input_num_channels = conn->stream.fmtp[7]; + conn->input_bit_depth = conn->stream.fmtp[3]; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + debug(2, "Realtime Stream Play"); + if (conn->ap2_timing_peer_list_message) + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + else + debug(1, "No timing peer list!"); + activity_monitor_signify_activity(1); + player_prepare_to_play(conn); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + case 103: { + debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", + conn->connection_number); + debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); + get_play_lock(conn); + conn->airplay_stream_type = buffered_stream; + // get needed stuff + + // bind a new TCP port and get a socket + conn->local_buffered_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_buffered_audio_port, + &conn->buffered_audio_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " + "buffered_audio port", + conn->connection_number, err); + } + + // hack. + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; // we are stuck with this for the moment. + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + activity_monitor_signify_activity(1); + player_prepare_to_play( + conn); // get capabilities of DAC before creating the buffered audio thread + + pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_buffered_audio_port)); + plist_dict_set_item(stream0dict, "audioBufferSize", + plist_new_uint(conn->ap2_audio_buffer_size)); + + // this should be cancelled by an activity_monitor_signify_activity(1) + // call in the SETRATEANCHORI handler, which should come up right away + activity_monitor_signify_activity(0); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + default: + debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", + conn->connection_number, item_value); + debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); + } + + plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); + + 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(1, "SETUP on Connection %d: Remote Control Stream received. Nothing done.", + conn->connection_number); + // resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: Stream received but no airplay category set. Nothing done.", + conn->connection_number); } } - if (setupResponsePlist) { + + if (resp->respcode == 200) { plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); plist_free(setupResponsePlist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1,"Connection %d: SETUP mdns_update.", conn->connection_number); + debug(1, "Connection %d: SETUP mdns_update.", conn->connection_number); mdns_update(NULL, secondary_txt_records); - } else { - debug(1, "SETUP on Connection %d: Unrecognised SETUP incoming message from %s", - conn->connection_number, (const char *)conn->client_ip_string); - debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); - warn("Unrecognised SETUP incoming message -- ignored."); } debug_log_rtsp_message(2, " SETUP response", resp); - resp->respcode = 200; } #endif From 8d477d73dcb0e444d27a6f3f28f3d363334bf992 Mon Sep 17 00:00:00 2001 From: Mao Liu <1684060+mao-liu@users.noreply.github.com> Date: Sat, 4 Sep 2021 14:31:15 +0000 Subject: [PATCH 384/878] small change to fix codec loading for ubuntu-18/ffmpeg-3.4 --- rtp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index e074e113..e7324570 100644 --- a/rtp.c +++ b/rtp.c @@ -2114,8 +2114,8 @@ void *rtp_buffered_audio_processor(void *arg) { // initialize all muxers, demuxers and protocols for libavformat // (does nothing if called twice during the course of one program execution) - // not needed in ffmpeg 4.0 and later... - // av_register_all(); + // not needed in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 + avcodec_register_all(); AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { From c9b34cc367e9e59e9cb3b2d95214840f439a356b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 5 Sep 2021 09:09:44 +0100 Subject: [PATCH 385/878] Update rtp.c Stop a deprecated warning. --- rtp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rtp.c b/rtp.c index e7324570..c4ac08b5 100644 --- a/rtp.c +++ b/rtp.c @@ -2115,7 +2115,11 @@ void *rtp_buffered_audio_processor(void *arg) { // initialize all muxers, demuxers and protocols for libavformat // (does nothing if called twice during the course of one program execution) // not needed in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 + + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" avcodec_register_all(); + #pragma GCC diagnostic pop AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { From dd795d9157ca4f651265ab5c7e1f211579564f7d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 5 Sep 2021 10:12:41 +0100 Subject: [PATCH 386/878] To address Issue 1107, set the number of items to buffer to 255. Thanks to https://github.com/PieGuy314 and https://github.com/Hello71 for reporting. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index f0a1c495..3ef385da 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2792,7 +2792,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); plist_free(setupResponsePlist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay + // config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay build_bonjour_strings(conn); debug(1, "Connection %d: SETUP mdns_update.", conn->connection_number); mdns_update(NULL, secondary_txt_records); @@ -4737,7 +4737,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, strerror(errno)); } else { - listen(fd, 5); + listen(fd, 255); nsock++; sockfd = realloc(sockfd, (nsock + 1) * sizeof(int)); sockfd[nsock] = fd; From 8ec99e806f86cbc29a1806b23f499a9e78d79d7c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 5 Sep 2021 10:13:17 +0100 Subject: [PATCH 387/878] Slight comment change. --- rtp.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rtp.c b/rtp.c index c4ac08b5..618e068b 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -2114,8 +2114,8 @@ void *rtp_buffered_audio_processor(void *arg) { // initialize all muxers, demuxers and protocols for libavformat // (does nothing if called twice during the course of one program execution) - // not needed in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 - + // deprecated in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" avcodec_register_all(); From 1fc00e13e96f91014d337b2b171ebf226ca0aeca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Sep 2021 09:11:24 +0100 Subject: [PATCH 388/878] Seems to work better with iOS / HomePod mini speaker selection. Does not recover discarded rtsp stream memory! --- player.h | 3 +- rtsp.c | 145 ++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 103 insertions(+), 45 deletions(-) diff --git a/player.h b/player.h index d980f1c2..64e9e3cb 100644 --- a/player.h +++ b/player.h @@ -105,7 +105,7 @@ typedef struct { typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; typedef enum { - unknown_stream_category, + unspecified_stream_category = 0, ptp_stream, ntp_stream, remote_control_stream @@ -144,6 +144,7 @@ typedef struct flush_request_t { typedef struct { int connection_number; // for debug ID purposes, nothing else... int resend_interval; // this is really just for debugging + int has_been_cancelled; // for cleaning up later. char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency uint32_t latency; // the actual latency used for this play session diff --git a/rtsp.c b/rtsp.c index 3ef385da..7d4e5ecd 100644 --- a/rtsp.c +++ b/rtsp.c @@ -624,20 +624,30 @@ static void track_thread(rtsp_conn_info *conn) { } } -void cancel_all_RTSP_threads(void) { +// note: connection numbers start at 1, so an except_this_one value of zero means "all threads" +void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_one) { + // if the stream category is unspecified_stream_category + // all categories are elegible for cancellation + // otherwise just the category itself + int i; for (i = 0; i < nconns; i++) { - debug(2, "Connection %d: cancelling.", conns[i]->connection_number); - pthread_cancel(conns[i]->thread); + if ((conns[i]->has_been_cancelled == 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + debug(2, "Connection %d: cancelling.", conns[i]->connection_number); + pthread_cancel(conns[i]->thread); + } } for (i = 0; i < nconns; i++) { - debug(2, "Connection %d: joining.", conns[i]->connection_number); - pthread_join(conns[i]->thread, NULL); - free(conns[i]); + if ((conns[i]->has_been_cancelled == 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + debug(2, "Connection %d: joining.", conns[i]->connection_number); + pthread_join(conns[i]->thread, NULL); + conns[i]->has_been_cancelled = 1; + } } } void cleanup_threads(void) { + /* void *retval; int i; // debug(2, "culling threads."); @@ -655,6 +665,7 @@ void cleanup_threads(void) { i++; } } + */ } // park a null at the line ending, and return the next line pointer @@ -1350,9 +1361,31 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { return 0; } +char *get_category_string(airplay_stream_c cat) { + char *category; + switch (cat) { + case unspecified_stream_category: + category = "unspecified stream"; + break; + case ptp_stream: + category = "PTP stream"; + break; + case ntp_stream: + category = "NTP stream"; + break; + case remote_control_stream: + category = "Remote Control stream"; + break; + default: + category = "Unexpected stream code"; + break; + } + return category; +} + void handle_record_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: RECORD (AirPlay 2)", conn->connection_number); - debug_log_rtsp_message(2, "RECORD incoming message", req); + debug(1, "Connection %d: RECORD on %s", conn->connection_number, get_category_string(conn->airplay_stream_category)); + //debug_log_rtsp_message(1, "RECORD incoming message", req); resp->respcode = 200; } @@ -1417,7 +1450,7 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r hdr = hdr + strlen("AirPlay/"); // double airplay_version = 0.0; // airplay_version = atof(hdr); - debug(1, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, + debug(2, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, hdr); } } @@ -2241,7 +2274,7 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { - // debug_log_rtsp_message(1, "TEARDOWN: ", req); + debug_log_rtsp_message(2, "TEARDOWN: ", req); // if (have_player(conn)) { resp->respcode = 200; msg_add_header(resp, "Connection", "close"); @@ -2252,7 +2285,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { - debug(1, "Connection %d: TEARDOWN a stream.", conn->connection_number); + debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream player_stop(conn); activity_monitor_signify_activity(0); // inactive, and should be after command_stop() @@ -2264,22 +2297,26 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { // we are being asked to disconnect - debug(1, "Connection %d: TEARDOWN a connection.", conn->connection_number); + debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, get_category_string(conn->airplay_stream_category)); debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); pthread_cancel(conn->rtp_event_thread); pthread_join(conn->rtp_event_thread, NULL); debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); if (conn->event_socket) close(conn->event_socket); - if (conn->airplay_gid != NULL) { - free(conn->airplay_gid); - conn->airplay_gid = NULL; + + // if we are closing a PTP stream only, do this + if (conn->airplay_stream_category == ptp_stream) { + if (conn->airplay_gid != NULL) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } + conn->groupContainsGroupLeader = 0; + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); } - conn->groupContainsGroupLeader = 0; - config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay - build_bonjour_strings(conn); - debug(1, "Connection %d: TEARDOWN mdns_update.", conn->connection_number); - mdns_update(NULL, secondary_txt_records); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } //} else { @@ -2374,44 +2411,44 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (streams == NULL) { // no "streams" plist, so it must (?) be an initial setup debug(2, - "SETUP on Connection %d: No \"streams\" array has been found -- create an event thread " + "Connection %d SETUP: No \"streams\" array has been found -- create an event thread " "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 - conn->airplay_stream_category = unknown_stream_category; plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); if (timingProtocol != NULL) { char *timingProtocolString = NULL; plist_get_string_val(timingProtocol, &timingProtocolString); if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { - debug(1, "SETUP on Connection %d: PTP setup detected.", conn->connection_number); + debug(1, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; } else if (strcmp(timingProtocolString, "NTP") == 0) { - debug(1, "SETUP on Connection %d: NTP setup detected.", conn->connection_number); + debug(1, "Connection %d: SETUP: NTP setup detected.", conn->connection_number); conn->airplay_stream_category = ntp_stream; conn->timing_type = ts_ntp; } else if (strcmp(timingProtocolString, "None") == 0) { - debug(2, "SETUP on Connection %d: a \"None\" setup detected.", conn->connection_number); + debug(2, "Connection %d: SETUP: a \"None\" setup detected.", conn->connection_number); // 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, "SETUP on Connection %d: Remote Control setup detected.", + debug(1, "Connection %d: SETUP: Remote Control setup detected.", conn->connection_number); conn->airplay_stream_category = remote_control_stream; } else { debug(1, - "SETUP on Connection %d: a \"None\" setup detected, with " + "Connection %d: SETUP: a \"None\" setup detected, with " "\"isRemoteControlOnly\" item set to \"false\".", conn->connection_number); } } else { debug(1, - "SETUP on Connection %d: a \"None\" setup detected, but no " + "Connection %d: SETUP: a \"None\" setup detected, but no " "\"isRemoteControlOnly\" item detected.", conn->connection_number); } @@ -2558,6 +2595,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + cancel_all_RTSP_threads(unspecified_stream_category, conn->connection_number); // kill all the other listeners + + config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + resp->respcode = 200; } else { debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", @@ -2568,8 +2612,26 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, req); warn("Shairport Sync can not handle NTP streams."); } else if (conn->airplay_stream_category == remote_control_stream) { - debug_log_rtsp_message(1, "SETUP \"isRemoteControlOnly\" message", req); - // resp->respcode = 200; + debug_log_rtsp_message(2, "SETUP \"isRemoteControlOnly\" message", req); + + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + cancel_all_RTSP_threads(remote_control_stream, conn->connection_number); // kill all the other remote control listeners + resp->respcode = 200; } else { debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", conn->connection_number, timingProtocolString); @@ -2590,9 +2652,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } } else { debug(2, - "Connection %d: SETUP. A \"streams\" array has been found -- create control and audio " - "threads and ports", - conn->connection_number); + "Connection %d: SETUP on %s. A \"streams\" array has been found", + conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); @@ -2779,11 +2840,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "streams", streams_array); resp->respcode = 200; } else if (conn->airplay_stream_category == remote_control_stream) { - debug(1, "SETUP on Connection %d: Remote Control Stream received. Nothing done.", + debug(1, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); - // resp->respcode = 200; + resp->respcode = 200; } else { - debug(1, "SETUP on Connection %d: Stream received but no airplay category set. Nothing done.", + debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", conn->connection_number); } } @@ -2792,10 +2853,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); plist_free(setupResponsePlist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - // config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay - build_bonjour_strings(conn); - debug(1, "Connection %d: SETUP mdns_update.", conn->connection_number); - mdns_update(NULL, secondary_txt_records); } debug_log_rtsp_message(2, " SETUP response", resp); } @@ -2808,7 +2865,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { uint16_t cport, tport; char *ar = msg_get_header(req, "Active-Remote"); if (ar) { - debug(2, "Connection %d: SETUP -- Active-Remote string seen: \"%s\".", + debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", conn->connection_number, ar); // get the active remote if (conn->dacp_active_remote) // this is in case SETUP was previously called @@ -2818,7 +2875,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); #endif } else { - debug(2, "Connection %d: SETUP -- Note: no Active-Remote information the SETUP Record.", + debug(2, "Connection %d: SETUP: Note: no Active-Remote information seen.", conn->connection_number); if (conn->dacp_active_remote) { // this is in case SETUP was previously called free(conn->dacp_active_remote); @@ -2828,7 +2885,7 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { ar = msg_get_header(req, "DACP-ID"); if (ar) { - debug(2, "Connection %d: SETUP -- DACP-ID string seen: \"%s\".", conn->connection_number, ar); + debug(2, "Connection %d: SETUP: DACP-ID string seen: \"%s\".", conn->connection_number, ar); if (conn->dacp_id) // this is in case SETUP was previously called free(conn->dacp_id); conn->dacp_id = strdup(ar); @@ -4645,7 +4702,7 @@ void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); debug(2, "rtsp_listen_loop_cleanup_handler called."); - cancel_all_RTSP_threads(); + cancel_all_RTSP_threads(unspecified_stream_category,0); // kill all RTSP listeners int *sockfd = (int *)arg; mdns_unregister(); if (sockfd) { From 7ff6d07c83fdcea71d8953a6cac59919e4211719 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:02:59 +0100 Subject: [PATCH 389/878] Don't need 'had_been_cancelled'. Remoted. --- player.h | 1 - 1 file changed, 1 deletion(-) diff --git a/player.h b/player.h index 64e9e3cb..2badc274 100644 --- a/player.h +++ b/player.h @@ -144,7 +144,6 @@ typedef struct flush_request_t { typedef struct { int connection_number; // for debug ID purposes, nothing else... int resend_interval; // this is really just for debugging - int has_been_cancelled; // for cleaning up later. char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency uint32_t latency; // the actual latency used for this play session From 8049f1e60b3c7ff435889980f9fecb6425304efc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:05:20 +0100 Subject: [PATCH 390/878] Improve cancelled connection memory recovery. Remove some trick-bicycling programming from connection memory recovery. Use mutexes. Recover cancelled connection memory immediately. --- rtsp.c | 68 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/rtsp.c b/rtsp.c index 7d4e5ecd..2c6a887a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -130,9 +130,13 @@ rtsp_conn_info **conns; int metadata_running = 0; -// always lock use this when accessing the playing conn value +// always lock this when accessing the playing conn value pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; +// always lock this when accessing the list of connection threads +pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; + + // every time we want to retain or release a reference count, lock it with this // if a reference count is read as zero, it means the it's being deallocated. static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER; @@ -615,13 +619,29 @@ void *player_watchdog_thread_code(void *arg) { // keep track of the threads we have spawned so we can join() them static int nconns = 0; static void track_thread(rtsp_conn_info *conn) { - conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); - if (conns) { - conns[nconns] = conn; - nconns++; - } else { - die("could not reallocate memnory for \"conns\" in rtsp.c."); + debug_mutex_lock(&conns_lock, 1000000, 3); + // look for an empty slot first + int i = 0; + int found = 0; + while ((i < nconns) && (found == 0)) { + if (conns[i] == NULL) + found = 1; + else + i++; } + if (found != 0){ + conns[i] = conn; + } else { + // make space for a new element + conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); + if (conns) { + conns[nconns] = conn; + nconns++; + } else { + die("could not reallocate memory for conns"); + } + } + debug_mutex_unlock(&conns_lock, 3); } // note: connection numbers start at 1, so an except_this_one value of zero means "all threads" @@ -629,43 +649,43 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o // if the stream category is unspecified_stream_category // all categories are elegible for cancellation // otherwise just the category itself - + debug_mutex_lock(&conns_lock, 1000000, 3); int i; for (i = 0; i < nconns; i++) { - if ((conns[i]->has_been_cancelled == 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { - debug(2, "Connection %d: cancelling.", conns[i]->connection_number); + if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + debug(1, "Connection %d: stopped.", conns[i]->connection_number); pthread_cancel(conns[i]->thread); } } for (i = 0; i < nconns; i++) { - if ((conns[i]->has_been_cancelled == 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { - debug(2, "Connection %d: joining.", conns[i]->connection_number); + if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + debug(1, "Connection %d: deleted.", conns[i]->connection_number); pthread_join(conns[i]->thread, NULL); - conns[i]->has_been_cancelled = 1; + free(conns[i]); + conns[i] = NULL; } } + debug_mutex_unlock(&conns_lock, 3); } void cleanup_threads(void) { - /* + void *retval; int i; // debug(2, "culling threads."); - for (i = 0; i < nconns;) { - if (conns[i]->running == 0) { + debug_mutex_lock(&conns_lock, 1000000, 3); + for (i = 0; i < nconns; i++) { + if ((conns[i] != NULL ) && (conns[i]->running == 0)) { debug(3, "found RTSP connection thread %d in a non-running state.", conns[i]->connection_number); pthread_join(conns[i]->thread, &retval); - debug(3, "RTSP connection thread %d deleted...", conns[i]->connection_number); + debug(1, "Connection %d: deleted in cleanup.", conns[i]->connection_number); free(conns[i]); - nconns--; - if (nconns) - conns[i] = conns[nconns]; - } else { - i++; + conns[i] = NULL; } } - */ + debug_mutex_unlock(&conns_lock, 3); + } // park a null at the line ending, and return the next line pointer @@ -1383,7 +1403,7 @@ char *get_category_string(airplay_stream_c cat) { return category; } -void handle_record_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { +void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, rtsp_message *resp) { debug(1, "Connection %d: RECORD on %s", conn->connection_number, get_category_string(conn->airplay_stream_category)); //debug_log_rtsp_message(1, "RECORD incoming message", req); resp->respcode = 200; From 7def758f1367ce01c34e87685369a3c5a6933cab Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Sep 2021 14:30:13 +0100 Subject: [PATCH 391/878] Clang --- rtsp.c | 83 ++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/rtsp.c b/rtsp.c index 2c6a887a..ba8b3fc2 100644 --- a/rtsp.c +++ b/rtsp.c @@ -136,7 +136,6 @@ pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; // always lock this when accessing the list of connection threads pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; - // every time we want to retain or release a reference count, lock it with this // if a reference count is read as zero, it means the it's being deallocated. static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER; @@ -629,7 +628,7 @@ static void track_thread(rtsp_conn_info *conn) { else i++; } - if (found != 0){ + if (found != 0) { conns[i] = conn; } else { // make space for a new element @@ -652,13 +651,17 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_lock(&conns_lock, 1000000, 3); int i; for (i = 0; i < nconns; i++) { - if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { debug(1, "Connection %d: stopped.", conns[i]->connection_number); pthread_cancel(conns[i]->thread); } } for (i = 0; i < nconns; i++) { - if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { + if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { debug(1, "Connection %d: deleted.", conns[i]->connection_number); pthread_join(conns[i]->thread, NULL); free(conns[i]); @@ -675,7 +678,7 @@ void cleanup_threads(void) { // debug(2, "culling threads."); debug_mutex_lock(&conns_lock, 1000000, 3); for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL ) && (conns[i]->running == 0)) { + if ((conns[i] != NULL) && (conns[i]->running == 0)) { debug(3, "found RTSP connection thread %d in a non-running state.", conns[i]->connection_number); pthread_join(conns[i]->thread, &retval); @@ -685,7 +688,6 @@ void cleanup_threads(void) { } } debug_mutex_unlock(&conns_lock, 3); - } // park a null at the line ending, and return the next line pointer @@ -1384,28 +1386,30 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { char *get_category_string(airplay_stream_c cat) { char *category; switch (cat) { - case unspecified_stream_category: - category = "unspecified stream"; - break; - case ptp_stream: - category = "PTP stream"; - break; - case ntp_stream: - category = "NTP stream"; - break; - case remote_control_stream: - category = "Remote Control stream"; - break; - default: - category = "Unexpected stream code"; - break; + case unspecified_stream_category: + category = "unspecified stream"; + break; + case ptp_stream: + category = "PTP stream"; + break; + case ntp_stream: + category = "NTP stream"; + break; + case remote_control_stream: + category = "Remote Control stream"; + break; + default: + category = "Unexpected stream code"; + break; } return category; } -void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, rtsp_message *resp) { - debug(1, "Connection %d: RECORD on %s", conn->connection_number, get_category_string(conn->airplay_stream_category)); - //debug_log_rtsp_message(1, "RECORD incoming message", req); +void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + rtsp_message *resp) { + debug(1, "Connection %d: RECORD on %s", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + // debug_log_rtsp_message(1, "RECORD incoming message", req); resp->respcode = 200; } @@ -2305,7 +2309,8 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { - debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream player_stop(conn); activity_monitor_signify_activity(0); // inactive, and should be after command_stop() @@ -2317,7 +2322,8 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { // we are being asked to disconnect - debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); pthread_cancel(conn->rtp_event_thread); pthread_join(conn->rtp_event_thread, NULL); @@ -2334,7 +2340,8 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); } debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); @@ -2615,11 +2622,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy - cancel_all_RTSP_threads(unspecified_stream_category, conn->connection_number); // kill all the other listeners + cancel_all_RTSP_threads(unspecified_stream_category, + conn->connection_number); // kill all the other listeners config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); + debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); resp->respcode = 200; @@ -2650,7 +2659,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); - cancel_all_RTSP_threads(remote_control_stream, conn->connection_number); // kill all the other remote control listeners + cancel_all_RTSP_threads( + remote_control_stream, + conn->connection_number); // kill all the other remote control listeners resp->respcode = 200; } else { debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", @@ -2671,8 +2682,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) warn("Unrecognised SETUP incoming message -- ignored."); } } else { - debug(2, - "Connection %d: SETUP on %s. A \"streams\" array has been found", + debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { // get stream[0] @@ -2860,8 +2870,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "streams", streams_array); resp->respcode = 200; } else if (conn->airplay_stream_category == remote_control_stream) { - debug(1, "Connection %d: SETUP: Remote Control Stream received.", - conn->connection_number); + debug(1, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); resp->respcode = 200; } else { debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", @@ -2885,8 +2894,8 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { uint16_t cport, tport; char *ar = msg_get_header(req, "Active-Remote"); if (ar) { - debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", - conn->connection_number, ar); + debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", conn->connection_number, + ar); // get the active remote if (conn->dacp_active_remote) // this is in case SETUP was previously called free(conn->dacp_active_remote); @@ -4722,7 +4731,7 @@ void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); debug(2, "rtsp_listen_loop_cleanup_handler called."); - cancel_all_RTSP_threads(unspecified_stream_category,0); // kill all RTSP listeners + cancel_all_RTSP_threads(unspecified_stream_category, 0); // kill all RTSP listeners int *sockfd = (int *)arg; mdns_unregister(); if (sockfd) { From 9f9513f2e3ea670e2d4831fd11d662cd53ad3913 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Sep 2021 07:44:28 +0100 Subject: [PATCH 392/878] Fix a crashing bug in cancel_all_RTSP_threads. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index ba8b3fc2..244ecd44 100644 --- a/rtsp.c +++ b/rtsp.c @@ -651,7 +651,7 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_lock(&conns_lock, 1000000, 3); int i; for (i = 0; i < nconns; i++) { - if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { debug(1, "Connection %d: stopped.", conns[i]->connection_number); @@ -659,7 +659,7 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o } } for (i = 0; i < nconns; i++) { - if ((conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { debug(1, "Connection %d: deleted.", conns[i]->connection_number); From 366eb27afc4b4e48d17aa76c52a8ef25144ab452 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 9 Sep 2021 11:23:25 +0100 Subject: [PATCH 393/878] Stop a message from repeating endlessly. --- audio_alsa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 3afdd2ed..f723cb7d 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -438,7 +438,7 @@ int actual_open_alsa_device(int do_auto_setup) { } else { char errorstring[1024]; strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - warn("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, + die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, alsa_out_dev); } return ret; From 2e123cf39acf0ccaf562d4a65af02909db424ce0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 9 Sep 2021 11:24:23 +0100 Subject: [PATCH 394/878] Fix compilation errors that accumulated with no --with-airplay-2 flag set. --- player.h | 14 +++++++++----- rtsp.c | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/player.h b/player.h index 2badc274..2097dfdd 100644 --- a/player.h +++ b/player.h @@ -101,15 +101,18 @@ typedef struct { audio_stream_type type; } stream_cfg; -#ifdef CONFIG_AIRPLAY_2 -typedef enum { ts_ntp, ts_ptp } timing_t; -typedef enum { ap_1, ap_2 } airplay_t; +// the following is used even when not built for AirPlay 2 typedef enum { unspecified_stream_category = 0, ptp_stream, ntp_stream, remote_control_stream } airplay_stream_c; // "c" for category + + +#ifdef CONFIG_AIRPLAY_2 +typedef enum { ts_ntp, ts_ptp } timing_t; +typedef enum { ap_1, ap_2 } airplay_t; typedef enum { realtime_stream, buffered_stream } airplay_stream_t; typedef struct { @@ -292,12 +295,13 @@ typedef struct { clock_status_t clock_status; + airplay_stream_c + airplay_stream_category; // is it a remote control stream or a normal "full service" stream? (will be unspecified if not build for AirPlay 2) + #ifdef CONFIG_AIRPLAY_2 char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as // the pi UUID airplay_t airplay_type; // are we using AirPlay 1 or AirPlay 2 protocol on this connection? - airplay_stream_c - airplay_stream_category; // is it a remote control stream or a normal "full service" stream? airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... timing_t timing_type; // are we using NTP or PTP on this connection? diff --git a/rtsp.c b/rtsp.c index 244ecd44..dccb21a6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -199,7 +199,11 @@ static void pkString_make(char *str, size_t str_size, const char *device_id) { } #endif +#ifdef CONFIG_AIRPLAY_2 void build_bonjour_strings(rtsp_conn_info *conn) { +#else +void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) { +#endif int entry_number = 0; @@ -1767,6 +1771,12 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) req->contentlength); resp->respcode = 500; } +void handle_post(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { + debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + resp->respcode = 500; +} #endif #ifdef CONFIG_AIRPLAY_2 @@ -2871,6 +2881,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) resp->respcode = 200; } else if (conn->airplay_stream_category == remote_control_stream) { debug(1, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); + debug_log_rtsp_message(1, "Remote Control Stream stream (second) message", req); + resp->respcode = 200; } else { debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", @@ -4166,6 +4178,8 @@ static struct method_handler { void (*handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 only } method_handlers[] = {{"OPTIONS", handle_options}, + {"GET", handle_get}, + {"POST", handle_post}, {"ANNOUNCE", handle_announce}, {"FLUSH", handle_flush}, {"TEARDOWN", handle_teardown}, From 2e4c391bf2c91273061754b67fb7771dddf4c183 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Sep 2021 17:42:47 +0100 Subject: [PATCH 395/878] Change features flags in agreement with the metadata settings in the config file. --- shairport.c | 95 ++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/shairport.c b/shairport.c index 9ab3f51a..fb7fdfea 100644 --- a/shairport.c +++ b/shairport.c @@ -1584,49 +1584,6 @@ int main(int argc, char **argv) { #ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog config.port = 7000; - // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 - // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as - // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: - // 496155702020608 this setting here is the source of both the plist features response and the - // mDNS string. - // note: 0x300401F4A00 works but with weird delays and stuff - // config.airplay_features = 0x1C340405FCA00; - config.airplay_features = 0x1C340405D4A00; // APX + Authentication4 (b14) - // Advertised with mDNS and returned with GET /info, see - // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN - // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio - // cable attached, OneTimePairingRequired, device was setup for Homekit access control - config.airplay_statusflags = 0x04; - // Set to NULL to work with transient pairing - config.airplay_pin = NULL; - - // use the config.hw_addr to generated an airplay_device_id - { - char obf[256] = {0}; - char *obfp = obf; - int i; - for (i = 0; i < 6; i++) { - snprintf(obfp, 4, "%02X:", config.hw_addr[i]); - obfp += 3; - } - obfp -= 1; - *obfp = 0; - config.airplay_device_id = strdup(obf); - } - debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); - - // now generate a UUID - // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c - // with thanks - uuid_t binuuid; - uuid_generate_random(binuuid); - - char *uuid = malloc(UUID_STR_LEN); - // Produces a UUID string at uuid consisting of lower-case letters - uuid_unparse_lower(binuuid, uuid); - config.airplay_pi = strdup(uuid); - - // now we need to create the sequence of items for a Bonjour TXT record. #else config.port = 5000; #endif @@ -1820,10 +1777,58 @@ int main(int argc, char **argv) { } #endif - debug(1, "Started!"); #ifdef CONFIG_AIRPLAY_2 - debug(1, "Started in Airplay 2 mode!"); + // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 + // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as + // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: + // 496155702020608 this setting here is the source of both the plist features response and the + // mDNS string. + // note: 0x300401F4A00 works but with weird delays and stuff + // config.airplay_features = 0x1C340405FCA00; + uint64_t mask = ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); + config.airplay_features = 0x1C340405D4A00 & ~mask; // APX + Authentication4 (b14) with no metadata (see below) + // Advertised with mDNS and returned with GET /info, see + // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN + // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio + // cable attached, OneTimePairingRequired, device was setup for Homekit access control + + // If we are asking for metadata, turn on the relevant bits + if (config.metadata_enabled != 0) + config.airplay_features |= (1 << 17) | (1 << 16); + // If we are asking for artwork, turn on the relevant bit + if (config.get_coverart) + config.airplay_features |= (1 << 15); + debug(1,"Features: 0x%" PRIx64 ".", config.airplay_features); + config.airplay_statusflags = 0x04; + // Set to NULL to work with transient pairing + config.airplay_pin = NULL; + + // use the config.hw_addr to generated an airplay_device_id + { + char obf[256] = {0}; + char *obfp = obf; + int i; + for (i = 0; i < 6; i++) { + snprintf(obfp, 4, "%02X:", config.hw_addr[i]); + obfp += 3; + } + obfp -= 1; + *obfp = 0; + config.airplay_device_id = strdup(obf); + } + + // now generate a UUID + // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c + // with thanks + uuid_t binuuid; + uuid_generate_random(binuuid); + + char *uuid = malloc(UUID_STR_LEN); + // Produces a UUID string at uuid consisting of lower-case letters + uuid_unparse_lower(binuuid, uuid); + config.airplay_pi = strdup(uuid); + debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); #else debug(1, "Started in Airplay 1 mode!"); #endif From 1438216878a66acb093d1f9cacdef24fd5cf7dfb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Sep 2021 15:24:48 +0100 Subject: [PATCH 396/878] Add full teardown actions to RTSP stream cleanup to correctly close all connections. --- rtp.c | 5 ++- rtsp.c | 112 ++++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/rtp.c b/rtp.c index 618e068b..86792369 100644 --- a/rtp.c +++ b/rtp.c @@ -1564,6 +1564,7 @@ void rtp_ap2_control_handler_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); close(conn->ap2_control_socket); + debug(1, "Connection %d: UDP control port %u closed.", conn->connection_number, conn->local_ap2_control_port); conn->ap2_control_socket = 0; conn->ap2_remote_control_socket_addr_length = 0; // indicates to the control receiver thread that the socket address need to be @@ -1748,8 +1749,9 @@ void *rtp_ap2_control_receiver(void *arg) { void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Realtime Audio Receiver Cleanup Start."); - rtsp_conn_info *conn = (rtsp_conn_info *)arg; + rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->realtime_audio_socket); + debug(1, "Connection %d: closing realtime audio port %u", conn->local_realtime_audio_port); conn->realtime_audio_socket = 0; debug(2, "Realtime Audio Receiver Cleanup Done."); } @@ -2028,6 +2030,7 @@ void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); + debug(1, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, conn->local_buffered_audio_port); conn->buffered_audio_socket = 0; debug(2, "Buffered Audio Receiver Cleanup Done."); } diff --git a/rtsp.c b/rtsp.c index dccb21a6..c8bec51c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -658,16 +658,16 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { - debug(1, "Connection %d: stopped.", conns[i]->connection_number); pthread_cancel(conns[i]->thread); + debug(1, "Connection %d: cancelled.", conns[i]->connection_number); } } for (i = 0; i < nconns; i++) { if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { - debug(1, "Connection %d: deleted.", conns[i]->connection_number); pthread_join(conns[i]->thread, NULL); + debug(1, "Connection %d: joined.", conns[i]->connection_number); free(conns[i]); conns[i] = NULL; } @@ -2305,6 +2305,47 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * "OPTIONS, POST, GET, PUT"); } +void teardown_phase_one(rtsp_conn_info *conn) { + if (conn->player_thread) { + player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + } + if (conn->session_key) { + free(conn->session_key); + conn->session_key = NULL; + } +} + +void teardown_phase_two(rtsp_conn_info *conn) { + // we are being asked to disconnect + debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); + pthread_cancel(conn->rtp_event_thread); + pthread_join(conn->rtp_event_thread, NULL); + debug(1, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); + if (conn->event_socket) { + close(conn->event_socket); + conn->event_socket = 0; + debug(1, "Connection %d: closing TCP event port %u.", conn->connection_number, conn->local_event_port); + } + + // if we are closing a PTP stream only, do this + if (conn->airplay_stream_category == ptp_stream) { + if (conn->airplay_gid != NULL) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } + conn->groupContainsGroupLeader = 0; + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + } +} + + void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { @@ -2322,38 +2363,11 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream - player_stop(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - if (conn->session_key) { - free(conn->session_key); - conn->session_key = NULL; - } + teardown_phase_one(conn); plist_free(streams); debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); } else { - // we are being asked to disconnect - debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); - pthread_cancel(conn->rtp_event_thread); - pthread_join(conn->rtp_event_thread, NULL); - debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); - if (conn->event_socket) - close(conn->event_socket); - - // if we are closing a PTP stream only, do this - if (conn->airplay_stream_category == ptp_stream) { - if (conn->airplay_gid != NULL) { - free(conn->airplay_gid); - conn->airplay_gid = NULL; - } - conn->groupContainsGroupLeader = 0; - config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay - build_bonjour_strings(conn); - debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - mdns_update(NULL, secondary_txt_records); - } + teardown_phase_two(conn); debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); } //} else { @@ -2627,6 +2641,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + debug(1, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); + pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); plist_dict_set_item(setupResponsePlist, "eventPort", @@ -2664,6 +2680,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "port", conn->connection_number, err); } + + debug(1, "Connection %d: TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); @@ -2711,6 +2729,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as an ap2_control port", err); } + debug(1, "Connection %d: UDP control port opened: %u.", conn->connection_number, conn->local_ap2_control_port); pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); @@ -2780,6 +2799,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as a realtime_audio port", err); } + debug(1, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, conn->local_realtime_audio_port); pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, (void *)conn); @@ -2842,6 +2862,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + debug(1, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); + // hack. conn->max_frames_per_packet = 352; // number of audio frames per packet. conn->input_rate = 44100; // we are stuck with this for the moment. @@ -4448,26 +4470,36 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - debug(3, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", + debug(1, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); - if (conn->player_thread) { - player_stop(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - } + teardown_phase_one(conn); + teardown_phase_two(conn); debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", conn->connection_number); - if (conn->control_socket) + if (conn->control_socket) { close(conn->control_socket); - if (conn->timing_socket) + debug(1, "Connection %d: closing control port %u", conn->local_ap2_control_port); + } + if (conn->timing_socket) { close(conn->timing_socket); - if (conn->audio_socket) + debug(1, "Connection %d: closing timing port %u", conn->local_ap2_control_port); + } + if (conn->audio_socket) { close(conn->audio_socket); + debug(1, "Connection %d: closing audio port %u", conn->local_ap2_control_port); + } + + 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: terminating 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); } if (conn->auth_nonce) { free(conn->auth_nonce); @@ -4958,7 +4990,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(2, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(1, "Connection %d: new 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); } else { From e06be8c91673e396356f4002fa03399675059aac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Sep 2021 15:31:45 +0100 Subject: [PATCH 397/878] Allow compilation with AirPlay2 not selected. --- rtsp.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index c8bec51c..62fbb7da 100644 --- a/rtsp.c +++ b/rtsp.c @@ -4472,22 +4472,21 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(1, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 teardown_phase_one(conn); teardown_phase_two(conn); +#endif debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", conn->connection_number); if (conn->control_socket) { close(conn->control_socket); - debug(1, "Connection %d: closing control port %u", conn->local_ap2_control_port); } if (conn->timing_socket) { close(conn->timing_socket); - debug(1, "Connection %d: closing timing port %u", conn->local_ap2_control_port); } if (conn->audio_socket) { close(conn->audio_socket); - debug(1, "Connection %d: closing audio port %u", conn->local_ap2_control_port); } From 41f3e8bd17e69a7776cc6ad9f1b7116079cf2154 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 17 Sep 2021 19:24:10 +0100 Subject: [PATCH 398/878] Update AIRPLAY2.md --- AIRPLAY2.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index b253f39c..33411e88 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,7 +1,7 @@ AirPlay 2 === -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. It has limited integration with the Home app and Siri. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices, Macs and HomePod minis. It does not work for iTunes on Windows. The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work. @@ -14,32 +14,26 @@ The AirPlay 2 version of Shairport Sync will not work if installed on macOS. Thi The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. ### What Works -- AirPlay 2 for iOS and Mac players. +- AirPlay 2 for iOS, HomePod mini and Mac players. ### What Does Not Work - Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. - No Remote Control -- No Artwork in the Metadata - No AirPlay 2 for Windows iTunes ### What Partly Works -- Incomplete functionality with Siri and HomeKit -- Incomplete functionality using AppleTV or HomePod as player. - A Shairport Sync player will work -- i.e. it will play -- but the selection button won't persist and the volume can't be set. +- Incomplete functionality using AppleTV?. (Reports of usage welcome!) ### Limitations - Only a single instance of Shairport Sync can run on a device. - The AirPlay 2 version of Shairport Sync will not work if installed on macOS, since `nqptp` can not be installed on macOS. -### Known Issues -- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. - AirPlay 2 -- What You Need --- AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. So, here are some guideline requirements: -* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. +* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2. * A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. * An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. From ef9c46610c8d37c7e7c8ea91e10ce88bfb73ab05 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 17 Sep 2021 19:27:41 +0100 Subject: [PATCH 399/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 775a74e2..a023f3d7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.0-dev-382-g02734e58 +==== +#### Enhancement +- **Better HomePod mini and Home Integration** An AirPlay 2 build of Shairport Sync now provides a visible volume control when used by a HomePod mini. This should work for other devices too -- reports welcome. You can use the Home app and Siri to set and adjust the volume when used to play from the HomePod mini. + Version 4.0-dev-308-g23da206 ==== #### Bug Fixes From 4a82745561aea2ff07157a678c3713ae339b0ad6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 18 Sep 2021 12:22:57 +0100 Subject: [PATCH 400/878] Allocate, deallocate and NULL the rtp_event_thread memory space so that it is only deleted once! --- player.h | 2 +- rtsp.c | 32 +++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/player.h b/player.h index 2097dfdd..effd8b12 100644 --- a/player.h +++ b/player.h @@ -305,7 +305,7 @@ typedef struct { airplay_stream_t airplay_stream_type; // is it realtime audio or buffered audio... timing_t timing_type; // are we using NTP or PTP on this connection? - pthread_t rtp_event_thread; + pthread_t *rtp_event_thread; pthread_t rtp_ap2_control_thread; pthread_t rtp_realtime_audio_thread; pthread_t rtp_buffered_audio_thread; diff --git a/rtsp.c b/rtsp.c index 3d16aea1..74263296 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2306,8 +2306,10 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * } void teardown_phase_one(rtsp_conn_info *conn) { + // this can be called more than once on the same connection -- + // by the player itself but also by the play seesion being killed if (conn->player_thread) { - player_stop(conn); + player_stop(conn); // this nulls the player_thread activity_monitor_signify_activity(0); // inactive, and should be after command_stop() } if (conn->session_key) { @@ -2318,11 +2320,17 @@ void teardown_phase_one(rtsp_conn_info *conn) { void teardown_phase_two(rtsp_conn_info *conn) { // we are being asked to disconnect + // this can be called more than once on the same connection -- + // by the player itself but also by the play seesion being killed debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, get_category_string(conn->airplay_stream_category)); - debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); - pthread_cancel(conn->rtp_event_thread); - pthread_join(conn->rtp_event_thread, NULL); + if (conn->rtp_event_thread) { + debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); + pthread_cancel(*conn->rtp_event_thread); + pthread_join(*conn->rtp_event_thread, NULL); + free(conn->rtp_event_thread); + conn->rtp_event_thread = NULL; + } debug(1, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); if (conn->event_socket) { close(conn->event_socket); @@ -2642,8 +2650,14 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } debug(1, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); + + if (conn->rtp_event_thread != NULL) + debug(1, "previous rtp_event_thread allocation not freed, it seems."); + conn->rtp_event_thread = malloc(sizeof(pthread_t)); + if (conn->rtp_event_thread == NULL) + die("Couldn't allocate space for pthread_t"); - pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); @@ -2682,8 +2696,12 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } debug(1, "Connection %d: TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); - - pthread_create(&conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + if (conn->rtp_event_thread != NULL) + debug(1, "previous rtp_event_thread allocation not freed, it seems."); + conn->rtp_event_thread = malloc(sizeof(pthread_t)); + if (conn->rtp_event_thread == NULL) + die("Couldn't allocate space for pthread_t"); + pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); From 37faed13af497b1b7ecdb133bbcc69177594772f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 27 Sep 2021 19:37:09 +0100 Subject: [PATCH 401/878] Is sufficiently different from 4.0-dev... --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index ef32fda5..9be47a11 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.0-dev], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.1-dev], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) From b5801f6302a6de77dce433f312464260b9539bba Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 28 Sep 2021 17:40:33 +0100 Subject: [PATCH 402/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index a023f3d7..17b2b8ef 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ -Version-4.0-dev-382-g02734e58 +Version-4.1-dev ==== #### Enhancement -- **Better HomePod mini and Home Integration** An AirPlay 2 build of Shairport Sync now provides a visible volume control when used by a HomePod mini. This should work for other devices too -- reports welcome. You can use the Home app and Siri to set and adjust the volume when used to play from the HomePod mini. +- **Better Siri, HomePod mini and Home Integration** An AirPlay 2 build of Shairport Sync now provides a visible volume control when used by a HomePod mini. This should work for other devices too -- reports welcome. You can use the Home app and Siri to set and adjust the volume when used to play from the HomePod mini. Version 4.0-dev-308-g23da206 ==== From af6511007e72e6adca104306681b54fbc34eb1d4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Sep 2021 09:02:53 +0100 Subject: [PATCH 403/878] Allow Shairport Sync to be build without including the metadata stuff. --- rtsp.c | 8 ++++---- shairport.c | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/rtsp.c b/rtsp.c index 74263296..4971d519 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2650,7 +2650,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } debug(1, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); - + if (conn->rtp_event_thread != NULL) debug(1, "previous rtp_event_thread allocation not freed, it seems."); conn->rtp_event_thread = malloc(sizeof(pthread_t)); @@ -3105,16 +3105,16 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, conn->initial_airplay_volume = volume; conn->initial_airplay_volume_set = 1; } - } else + } else if (strncmp(cp, "progress: ", strlen("progress: ")) == 0) { // this can be sent even when metadata is not solicited + #ifdef CONFIG_METADATA - if (!strncmp(cp, "progress: ", strlen("progress: "))) { char *progress = cp + strlen("progress: "); // debug(2, "progress: \"%s\"",progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per // second send_ssnc_metadata('prgr', progress, strlen(progress), 1); +#endif } else -#endif { debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, strlen(cp)); diff --git a/shairport.c b/shairport.c index 60f6ef0c..980fe944 100644 --- a/shairport.c +++ b/shairport.c @@ -1787,18 +1787,21 @@ int main(int argc, char **argv) { // note: 0x300401F4A00 works but with weird delays and stuff // config.airplay_features = 0x1C340405FCA00; uint64_t mask = ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); - config.airplay_features = 0x1C340405D4A00 & ~mask; // APX + Authentication4 (b14) with no metadata (see below) + config.airplay_features = 0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below) // Advertised with mDNS and returned with GET /info, see // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio // cable attached, OneTimePairingRequired, device was setup for Homekit access control +#ifdef CONFIG_METADATA // If we are asking for metadata, turn on the relevant bits if (config.metadata_enabled != 0) - config.airplay_features |= (1 << 17) | (1 << 16); + config.airplay_features |= (1 << 17) | (1 << 16); // 16 is progress, 17 is text // If we are asking for artwork, turn on the relevant bit if (config.get_coverart) - config.airplay_features |= (1 << 15); + config.airplay_features |= (1 << 15); // 15 is artwork +#endif + debug(1,"Features: 0x%" PRIx64 ".", config.airplay_features); config.airplay_statusflags = 0x04; // Set to NULL to work with transient pairing From d3c067d009b7d005d724a9315cf3518cd1fb55bb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Sep 2021 09:26:46 +0100 Subject: [PATCH 404/878] Honour the volume_range_db with a hardware attenuator, duh. --- player.c | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/player.c b/player.c index ac332a43..6408a006 100644 --- a/player.c +++ b/player.c @@ -2912,6 +2912,8 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c warn("The range requested is too large to accommodate -- ignored."); else sw_min_db = (sw_max_db - desired_sw_range); + } else { + hw_min_db = hw_max_db - desired_range_db; } } } else { @@ -3086,20 +3088,6 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c "is disabled.", volume_mode, airplay_volume, software_attenuation, hardware_attenuation); } - /* - } - - #ifdef CONFIG_METADATA - else { - // here, send the 'pvol' metadata message when the airplay volume information - // is being used by shairport sync to control the output volume - char dv[128]; - memset(dv, 0, 128); - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); - send_ssnc_metadata('pvol', dv, strlen(dv), 1); - } - #endif - */ // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); From 0fe21f3a5632105baef6ad9bf62ec37c9a56da3f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Sep 2021 09:31:13 +0100 Subject: [PATCH 405/878] Quieten a few debug messages. --- rtsp.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rtsp.c b/rtsp.c index 4971d519..85ec6692 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1411,7 +1411,7 @@ char *get_category_string(airplay_stream_c cat) { void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, rtsp_message *resp) { - debug(1, "Connection %d: RECORD on %s", conn->connection_number, + debug(2, "Connection %d: RECORD on %s", conn->connection_number, get_category_string(conn->airplay_stream_category)); // debug_log_rtsp_message(1, "RECORD incoming message", req); resp->respcode = 200; @@ -2481,7 +2481,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_get_string_val(timingProtocol, &timingProtocolString); if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { - debug(1, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); + debug(2, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; } else if (strcmp(timingProtocolString, "NTP") == 0) { @@ -2649,7 +2649,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(1, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); + debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); if (conn->rtp_event_thread != NULL) debug(1, "previous rtp_event_thread allocation not freed, it seems."); @@ -2667,7 +2667,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, + debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); @@ -2747,7 +2747,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as an ap2_control port", err); } - debug(1, "Connection %d: UDP control port opened: %u.", conn->connection_number, conn->local_ap2_control_port); + debug(2, "Connection %d: UDP control port opened: %u.", conn->connection_number, conn->local_ap2_control_port); pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); @@ -2817,7 +2817,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as a realtime_audio port", err); } - debug(1, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, conn->local_realtime_audio_port); + debug(2, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, conn->local_realtime_audio_port); pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, (void *)conn); @@ -2880,7 +2880,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(1, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); + debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); // hack. conn->max_frames_per_packet = 352; // number of audio frames per packet. @@ -2920,7 +2920,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "streams", streams_array); resp->respcode = 200; } else if (conn->airplay_stream_category == remote_control_stream) { - debug(1, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); + debug(2, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); debug_log_rtsp_message(1, "Remote Control Stream stream (second) message", req); resp->respcode = 200; From e43c8dacda4ac4d73b93e8d0b7fa5d9b10ab2bbf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Sep 2021 09:39:07 +0100 Subject: [PATCH 406/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 17b2b8ef..3a03748e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,10 @@ -Version-4.1-dev +Version 4.1-dev-4-g0fe21f3a +==== +#### Bug Fixes +- Fix a bug whereby the `volume_range_db` setting would be ignored if it was less than the range of the hardware mixer. +- Fix a bug that prevented an AirPlay 2 build if metadata was not enabled, either directly or indirectly, at the `./configure...` step. + +Version 4.1-dev ==== #### Enhancement - **Better Siri, HomePod mini and Home Integration** An AirPlay 2 build of Shairport Sync now provides a visible volume control when used by a HomePod mini. This should work for other devices too -- reports welcome. You can use the Home app and Siri to set and adjust the volume when used to play from the HomePod mini. From 5c418702d6bba13ffede2d9734b2479d8d6e480d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Sep 2021 10:08:22 +0100 Subject: [PATCH 407/878] Quieten some debug messages. --- rtp.c | 6 +++--- rtsp.c | 31 ++++++++++++------------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/rtp.c b/rtp.c index 86792369..db8ff2f0 100644 --- a/rtp.c +++ b/rtp.c @@ -1564,7 +1564,7 @@ void rtp_ap2_control_handler_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); close(conn->ap2_control_socket); - debug(1, "Connection %d: UDP control port %u closed.", conn->connection_number, conn->local_ap2_control_port); + debug(2, "Connection %d: UDP control port %u closed.", conn->connection_number, conn->local_ap2_control_port); conn->ap2_control_socket = 0; conn->ap2_remote_control_socket_addr_length = 0; // indicates to the control receiver thread that the socket address need to be @@ -1749,7 +1749,7 @@ void *rtp_ap2_control_receiver(void *arg) { void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Realtime Audio Receiver Cleanup Start."); - rtsp_conn_info *conn = (rtsp_conn_info *)arg; + rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->realtime_audio_socket); debug(1, "Connection %d: closing realtime audio port %u", conn->local_realtime_audio_port); conn->realtime_audio_socket = 0; @@ -2030,7 +2030,7 @@ void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); - debug(1, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, conn->local_buffered_audio_port); + debug(2, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, conn->local_buffered_audio_port); conn->buffered_audio_socket = 0; debug(2, "Buffered Audio Receiver Cleanup Done."); } diff --git a/rtsp.c b/rtsp.c index 85ec6692..56ac5b3c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -686,7 +686,7 @@ void cleanup_threads(void) { debug(3, "found RTSP connection thread %d in a non-running state.", conns[i]->connection_number); pthread_join(conns[i]->thread, &retval); - debug(1, "Connection %d: deleted in cleanup.", conns[i]->connection_number); + debug(2, "Connection %d: deleted in cleanup.", conns[i]->connection_number); free(conns[i]); conns[i] = NULL; } @@ -2322,7 +2322,7 @@ void teardown_phase_two(rtsp_conn_info *conn) { // we are being asked to disconnect // this can be called more than once on the same connection -- // by the player itself but also by the play seesion being killed - debug(1, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, + debug(2, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->rtp_event_thread) { debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); @@ -2331,11 +2331,11 @@ void teardown_phase_two(rtsp_conn_info *conn) { free(conn->rtp_event_thread); conn->rtp_event_thread = NULL; } - debug(1, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); + debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); if (conn->event_socket) { close(conn->event_socket); conn->event_socket = 0; - debug(1, "Connection %d: closing TCP event port %u.", conn->connection_number, conn->local_event_port); + debug(2, "Connection %d: closing TCP event port %u.", conn->connection_number, conn->local_event_port); } // if we are closing a PTP stream only, do this @@ -2347,7 +2347,7 @@ void teardown_phase_two(rtsp_conn_info *conn) { conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay build_bonjour_strings(conn); - debug(1, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, + debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); } @@ -2368,7 +2368,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { - debug(1, "Connection %d: TEARDOWN a %s.", conn->connection_number, + debug(2, "Connection %d: TEARDOWN a %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream teardown_phase_one(conn); @@ -3633,7 +3633,6 @@ void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) { #endif void metadata_init(void) { - int ret; if (config.metadata_enabled) { // create the metadata pipe, if necessary size_t pl = strlen(config.metadata_pipename) + 1; @@ -3659,24 +3658,18 @@ void metadata_init(void) { (char *)errorstring, path); } free(path); - int ret; - ret = pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL); - if (ret) + if (pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL) != 0) debug(1, "Failed to create metadata thread!"); - ret = - pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL); - if (ret) + if (pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL) != 0) debug(1, "Failed to create metadata multicast thread!"); } #ifdef CONFIG_METADATA_HUB - ret = pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL); - if (ret) + if (pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL) != 0) debug(1, "Failed to create metadata hub thread!"); #endif #ifdef CONFIG_MQTT - ret = pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL); - if (ret) + if (pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL) != 0) debug(1, "Failed to create metadata mqtt thread!"); #endif metadata_running = 1; @@ -4488,7 +4481,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - debug(1, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", + debug(2, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); #ifdef CONFIG_AIRPLAY_2 teardown_phase_one(conn); @@ -4514,7 +4507,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { close(conn->fd); debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); - debug(1, "Connection %d: terminating connection from %s:%u to self at %s:%u.", + debug(2, "Connection %d: terminating 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); } From d25a20aa9914476fb286d7ed7e6f0afcb17e2c5f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 11 Oct 2021 19:04:33 +0200 Subject: [PATCH 408/878] Update BUILDFORAP1.md --- BUILDFORAP1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 847346ba..e21ab996 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -59,6 +59,8 @@ $ sudo make install ``` By the way, the `autoreconf` step may take quite a while -- be patient! +### Configure + Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: ``` From 424cc398240c18e95c26edda1dd565c5915e4c1c Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Fri, 15 Oct 2021 08:20:12 +0200 Subject: [PATCH 409/878] configure.ac: fix build with automake 1.16.5 Fix the following build failure with convolution raised since automake 1.16.5 and https://git.savannah.gnu.org/cgit/automake.git/commit/?id=f4a3a70f69e1dbccb6578f39ef47835098a04624: configure.ac:305: error: AM_INIT_AUTOMAKE expanded multiple times /home/giuliobenetti/autobuild/run/instance-1/output-1/host/share/aclocal-1.16/init.m4:29: AM_INIT_AUTOMAKE is expanded from... configure.ac:6: the top level /home/giuliobenetti/autobuild/run/instance-1/output-1/host/share/aclocal-1.16/init.m4:29: AM_INIT_AUTOMAKE is expanded from... configure.ac:305: the top level Fixes: - http://autobuild.buildroot.org/results/464148bdccb705d8992dc860262bfdeb01b7e2a1 Signed-off-by: Fabrice Fontaine --- configure.ac | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 9be47a11..1d26d299 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ AC_PREREQ([2.50]) AC_INIT([shairport-sync], [4.1-dev], [4265913+mikebrady@users.noreply.github.com]) -AM_INIT_AUTOMAKE +AM_INIT_AUTOMAKE([subdir-objects]) AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) AC_PROG_RANLIB @@ -323,7 +323,6 @@ AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"]) # Look for Convolution flag AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP convolution support])]) if test "x$with_convolution" = "xyes" ; then - AM_INIT_AUTOMAKE([subdir-objects]) AC_DEFINE([CONFIG_CONVOLUTION], 1, [Include audio DSP convolution support.]) if test "x${with_pkg_config}" = xyes ; then PKG_CHECK_MODULES( From d2e35fc02ed7cf74a99e0715476a9f3399158250 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 22 Oct 2021 19:36:31 +0100 Subject: [PATCH 410/878] Return 501 "Not Implemented" for unexpected RTSP commands -- it seems to work better. The old code, 400, didn't seem to get the required response. --- rtsp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index 56ac5b3c..0135aaa5 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1303,7 +1303,7 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { char *string; }; - struct response_t responses[] = {{200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}}; + struct response_t responses[] = {{200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {501, "Not Implemented"}}; int found = 0; char *respcode_text = "Unauthorized"; for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { @@ -4620,7 +4620,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { pthread_cleanup_push(msg_cleanup_function, (void *)&req); resp = msg_init(); pthread_cleanup_push(msg_cleanup_function, (void *)&resp); - resp->respcode = 400; + resp->respcode = 501; // Not Implemented /* if (strcmp(req->method, "OPTIONS") != 0) // the options message is very common, so don't log it until level 3 @@ -4661,7 +4661,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { } } if (method_selected == 0) { - debug(1, "Connection %d: Unrecognised and unhandled rtsp request \"%s\".", + debug(1, "Connection %d: Unrecognised and unhandled rtsp request \"%s\". HTTP Response Code 501 (\"Not Implemented\") returned.", conn->connection_number, req->method); int y = req->contentlength; @@ -4680,6 +4680,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { *obfp = 0; debug(1, "Content: \"%s\".", obf); } + } } debug(debug_level, "Connection %d: RTSP Response:", conn->connection_number); From 4ced2cf1fd9fd81fd77c89c29d27cfd947f28bc5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 22 Oct 2021 19:52:36 +0100 Subject: [PATCH 411/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 3a03748e..ea5845b1 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-9-gd2e35fc0 +==== +#### Bug Fix +- Fix an incompatibility with iOS 15.1 betas and release candidates. The problem was this: if an unknown RTSP command was received, Shairport Sync would return a HTTP error response code of 400 ("Bad Request") but the player would not continue. After some experimentation, it was discovered that a HTTP error response code of 501 ("Not Implemented") would allow the player to continue. Thanks to [corrpel](https://github.com/corrpel) and [bloomkd46](https://github.com/bloomkd46) for reporting the issue. + Version 4.1-dev-4-g0fe21f3a ==== #### Bug Fixes From 1452bb687f15e33f910ee875c6781b79bddeb270 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:28:46 +0100 Subject: [PATCH 412/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index b6106455..6cdc288f 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -55,7 +55,7 @@ Okay, now let's get the tools and libraries for building and installing Shairpor First, install the packages needed by Shairport Sync: ``` # apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ - libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libglib2.0-dev \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` @@ -68,10 +68,10 @@ Download, install, enable and start `nqptp` from [here](https://github.com/mikeb As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please use the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: ``` Version with git information: -4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc +4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc Version without git information: -4.0-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-metadata-dbus-sysconfdir:/etc +4.0-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc ``` #### Build and Install @@ -82,7 +82,7 @@ $ cd shairport-sync $ git checkout development $ autoreconf -fi $ ./configure --sysconfdir=/etc --with-alsa \ - --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 + --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 $ make -j # make install ``` From 311a5c884c4197a7e75c332f5dbc4618d963b9fc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:36:40 +0100 Subject: [PATCH 413/878] slight tidying up --- rtsp.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index 0135aaa5..9e18a3b2 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1661,6 +1661,18 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message resp->respcode = 200; } +void handle_setrate(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SETRATE %s : Content-Length %d", conn->connection_number, + req->path, req->contentlength); + debug_log_rtsp_message(2, "SETRATE request -- unimplemented", req); + resp->respcode = 501; // Not Implemented +} + +void handle_unimplemented_ap1(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug_log_rtsp_message(1, "request not recognised for AirPlay 1 operation", req); + resp->respcode = 501; +} + void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: SETRATEANCHORI %s :: Content-Length %d", conn->connection_number, req->path, req->contentlength); @@ -4201,9 +4213,10 @@ static struct method_handler { {"RECORD", handle_record, handle_record_2}, {"GET", handle_get, handle_get}, {"POST", handle_post, handle_post}, - {"SETPEERS", handle_setpeers, handle_setpeers}, - {"SETRATEANCHORTI", handle_setrateanchori, handle_setrateanchori}, - {"FLUSHBUFFERED", handle_flushbuffered, handle_flushbuffered}, + {"SETPEERS", handle_unimplemented_ap1, handle_setpeers}, + {"SETRATEANCHORTI", handle_unimplemented_ap1, handle_setrateanchori}, + {"FLUSHBUFFERED", handle_unimplemented_ap1, handle_flushbuffered}, + {"SETRATE", handle_unimplemented_ap1, handle_setrate}, {NULL, NULL, NULL}}; #else static struct method_handler { From 6b9b45c887a23aa351bc15528508689fb60b559c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Oct 2021 13:25:35 +0100 Subject: [PATCH 414/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index ea5845b1..eb65e60a 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,20 @@ +Version 4.1-dev-15-gf4115c45 +==== +#### Bug Fix +- Fix the following [buildroot](https://buildroot.org) build failure when the `--with convolution` flag is included. The issue is due to a change in the behaviour of automake 1.16.5 -- see [`https://git.savannah.gnu.org/cgit/automake.git/commit/?id=f4a3a70f69e1dbccb6578f39ef47835098a04624`](https://git.savannah.gnu.org/cgit/automake.git/commit/?id=f4a3a70f69e1dbccb6578f39ef47835098a04624): +``` +configure.ac:305: error: AM_INIT_AUTOMAKE expanded multiple times +/home/giuliobenetti/autobuild/run/instance-1/output-1/host/share/aclocal-1.16/init.m4:29: AM_INIT_AUTOMAKE is expanded from... +configure.ac:6: the top level +/home/giuliobenetti/autobuild/run/instance-1/output-1/host/share/aclocal-1.16/init.m4:29: AM_INIT_AUTOMAKE is expanded from... +configure.ac:305: the top level +``` +Fixes: +[`http://autobuild.buildroot.org/results/464148bdccb705d8992dc860262bfdeb01b7e2a1`](http://autobuild.buildroot.org/results/464148bdccb705d8992dc860262bfdeb01b7e2a1). + +Thanks to [Fabrice Fontaine](https://github.com/ffontaine) for [Pull Request #1314](https://github.com/mikebrady/shairport-sync/pull/1314). + + Version 4.1-dev-9-gd2e35fc0 ==== #### Bug Fix From f4cad1d1ba8b49dae5a879c20f2d6ae7c3787898 Mon Sep 17 00:00:00 2001 From: Alexander Mextner Date: Mon, 1 Nov 2021 09:18:57 +0300 Subject: [PATCH 415/878] typo(MQTT): Please not -> Please note --- MQTT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MQTT.md b/MQTT.md index 3189a54a..3dee3f22 100644 --- a/MQTT.md +++ b/MQTT.md @@ -174,7 +174,7 @@ binary_sensor: off_delay: 300 ``` -Below parsed data is saved into the Home Assistant database as sensor data. Please not the conversion of the volume from dB to percentage. +Below parsed data is saved into the Home Assistant database as sensor data. Please note the conversion of the volume from dB to percentage. ```yml sensor: From 98c45cbb1bee872d001b1221d2fd9872ad15d7b7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 2 Nov 2021 09:15:04 +0000 Subject: [PATCH 416/878] Fix a bug that was causing a compiler warniong about a potentially uninitialised variable. --- rtsp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index 9e18a3b2..f1efb1cd 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3784,7 +3784,7 @@ int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char * int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, int block) { - int rc; + int rc = 0; if (config.metadata_enabled) { rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); rc = From 99c663857313a23d307b8d3c48423bdfc9554379 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Nov 2021 18:32:26 +0000 Subject: [PATCH 417/878] Ensure the frame rate calculator allows for silent frames added to disable standby. Clean up outmoded and unused stuff in audio_alsa.c. More to do. --- audio_alsa.c | 83 +++++++++++++++++++++------------------------------- player.c | 56 +++++++++++++++++++++++------------ player.h | 2 +- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index f723cb7d..32f6c90f 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -60,7 +60,8 @@ static int play(void *buf, int samples); static void stop(void); static void flush(void); int delay(long *the_delay); -int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); +int get_frames_sent_for_output(__attribute__ ((unused)) uint64_t *elapsed_time, uint64_t *frames_sent_to_dac); +// int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); void *alsa_buffer_monitor_thread_code(void *arg); static void volume(double vol); @@ -88,7 +89,8 @@ audio_output audio_alsa = { .flush = &flush, .delay = &delay, .play = &play, - .rate_info = &get_rate_information, + .rate_info = &get_frames_sent_for_output, // will also include frames of silence sent to stop standby mode +// .rate_info = NULL, .mute = NULL, // a function will be provided if it can, and is allowed to, // do hardware mute .volume = NULL, // a function will be provided if it can do hardware volume @@ -239,22 +241,13 @@ int precision_delay_available() { return (precision_delay_available_status == YNDK_YES); } -// static int play_number; -// static int64_t accumulated_delay, accumulated_da_delay; int alsa_characteristics_already_listed = 0; static snd_pcm_uframes_t period_size_requested, buffer_size_requested; static int set_period_size_request, set_buffer_size_request; -static uint64_t measurement_start_time; -static uint64_t frames_played_at_measurement_start_time; - -static uint64_t measurement_time; -static uint64_t frames_played_at_measurement_time; - static uint64_t frames_sent_for_playing; -static uint64_t frame_index; -static int measurement_data_is_valid; +static int output_error_occurred; // set to true if an underrun or similar has occurred since last requested static void help(void) { printf(" -d output-device set the output device, default is \"default\".\n" @@ -1407,8 +1400,8 @@ static void start(__attribute__((unused)) int i_sample_rate, __attribute__((unused)) int i_sample_format) { debug(3, "audio_alsa start called."); - frame_index = 0; - measurement_data_is_valid = 0; + // frame_index = 0; + // measurement_data_is_valid = 0; stall_monitor_start_time = 0; stall_monitor_frame_count = 0; @@ -1430,8 +1423,8 @@ int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } else { // not running, thus no delay information, thus can't check for frame // rates - frame_index = 0; // we'll be starting over... - measurement_data_is_valid = 0; + // frame_index = 0; // we'll be starting over... + // measurement_data_is_valid = 0; *delay = 0; } @@ -1587,8 +1580,8 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, // not running, thus no delay information, thus can't check for frame // rates - frame_index = 0; // we'll be starting over... - measurement_data_is_valid = 0; + // frame_index = 0; // we'll be starting over... + // measurement_data_is_valid = 0; } } else { debug(1, "alsa: can't get device's status."); @@ -1631,6 +1624,21 @@ int delay(long *the_delay) { return ret; } +int get_frames_sent_for_output(__attribute__ ((unused)) uint64_t *elapsed_time, uint64_t *frames_sent_to_dac) { + int ret = 0; + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); + *frames_sent_to_dac = frames_sent_for_playing; + if (alsa_handle == NULL) + ret = ENODEV; + else + ret = output_error_occurred; // will be zero unless an error occurred + output_error_occurred = 0; // reset it. + debug_mutex_unlock(&alsa_mutex, 0); + pthread_cleanup_pop(0); + return ret; +} + +/* int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { // elapsed_time is in nanoseconds int response = 0; // zero means okay @@ -1644,6 +1652,7 @@ int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { } return response; } +*/ int do_play(void *buf, int samples) { // assuming the alsa_mutex has been acquired @@ -1671,34 +1680,9 @@ int do_play(void *buf, int samples) { ret = alsa_pcm_write(alsa_handle, buf, samples); if (ret == samples) { stall_monitor_frame_count += samples; - - if (frame_index == 0) { - frames_sent_for_playing = samples; - } else { - frames_sent_for_playing += samples; - } - - const uint64_t start_measurement_from_this_frame = - (2 * config.output_rate) / 352; // two seconds of frames - - frame_index++; - - if ((frame_index == start_measurement_from_this_frame) || - ((frame_index > start_measurement_from_this_frame) && (frame_index % 32 == 0))) { - - measurement_time = get_absolute_time_in_ns(); - frames_played_at_measurement_time = frames_sent_for_playing - my_delay - samples; - - if (frame_index == start_measurement_from_this_frame) { - // debug(1, "Start frame counting"); - frames_played_at_measurement_start_time = frames_played_at_measurement_time; - measurement_start_time = measurement_time; - measurement_data_is_valid = 1; - } - } + frames_sent_for_playing += samples; } else { - frame_index = 0; - measurement_data_is_valid = 0; + output_error_occurred = -ret; // note than an output error has occurred if (ret == -EPIPE) { /* underrun */ // It could be that the DAC was in the SND_PCM_STATE_XRUN state before @@ -1748,8 +1732,6 @@ int do_play(void *buf, int samples) { "alsa: device status returns fault status %d and SND_PCM_STATE_* " "%d for play.", ret, state); - frame_index = 0; - measurement_data_is_valid = 0; } pthread_setcancelstate(oldState, NULL); @@ -1774,7 +1756,8 @@ int do_open(int do_auto_setup) { // set accordingly // do_mute(0); // complete unmute } - + output_error_occurred = 0; + frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. } } else { @@ -2005,8 +1988,8 @@ void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { "alsa_backend_state => abm_connected"); } else if ((alsa_backend_state == abm_connected) && (config.keep_dac_busy == 0)) { stall_monitor_start_time = 0; - frame_index = 0; - measurement_data_is_valid = 0; + // frame_index = 0; + // measurement_data_is_valid = 0; debug(2, "alsa: alsa_buffer_monitor_thread_code() -- closing the output " "device"); do_close(); diff --git a/player.c b/player.c index 6408a006..74c6eac2 100644 --- a/player.c +++ b/player.c @@ -1611,7 +1611,7 @@ void player_thread_cleanup_handler(void *arg) { int elapsedHours = rawSeconds / 3600; int elapsedMin = (rawSeconds / 60) % 60; int elapsedSec = rawSeconds % 60; - if (conn->frame_rate_status) + if (conn->frame_rate_valid) inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, " "output: %0.2f " "frames per second.", @@ -1714,6 +1714,12 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; + + uint64_t previous_frames_played; + uint64_t previous_frames_played_time; + int previous_frames_played_valid = 0; + + // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); conn->packet_count = 0; conn->packet_count_since_flush = 0; @@ -1866,7 +1872,7 @@ void *player_thread_func(void *arg) { conn->playstart = time(NULL); conn->frame_rate = 0.0; - conn->frame_rate_status = 0; + conn->frame_rate_valid = 0; conn->input_frame_rate = 0.0; conn->input_frame_rate_starting_point_is_valid = 0; @@ -2059,7 +2065,7 @@ void *player_thread_func(void *arg) { pthread_testcancel(); // allow a pthread_cancel request to take effect. abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not // guaranteed that they'll always be executed - + uint64_t local_time_now = get_absolute_time_in_ns(); // types okay if (inframe) { inbuf = inframe->data; inbuflength = inframe->length; @@ -2245,8 +2251,6 @@ void *player_thread_func(void *arg) { at_least_one_frame_seen = 1; - uint64_t local_time_now = get_absolute_time_in_ns(); // types okay - // This is the timing error for the next audio frame in the DAC, if applicable int64_t sync_error = 0; @@ -2780,21 +2784,32 @@ void *player_thread_func(void *arg) { conn->input_frame_rate = 0.0; } + uint64_t frames_sent_for_play; + int status = -1; if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) { - uint64_t elapsed_play_time, frames_played; - if (config.output->rate_info(&elapsed_play_time, &frames_played) == 0) - conn->frame_rate_status = 1; - else - conn->frame_rate_status = 0; - if (conn->frame_rate_status) { - conn->frame_rate = - (1.0E9 * frames_played) / - elapsed_play_time; // an IEEE double calculation with two 64-bit integers - } else { - conn->frame_rate = 0.0; + uint64_t elapsed_play_time; // dummy + status = config.output->rate_info(&elapsed_play_time, &frames_sent_for_play); + uint64_t frames_played = frames_sent_for_play - play_samples - current_delay; + // If the status is zero, it means that there were no output problems since the + // last time the rate_info call was made. Thus, the frame rate should be valid. + if ((status == 0) && (previous_frames_played_valid)) { + uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; + uint64_t interval = local_time_now - previous_frames_played_time; + conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; + conn->frame_rate_valid = 1; } + + // uncomment the if statement if your want to get as long a period for + // calculating the frame rate + // if ((status != 0) || (previous_frames_played_valid == 0)) { + // if we have just detected an outputting error, or if we have no + // starting information + previous_frames_played = frames_played; + previous_frames_played_time = local_time_now; + previous_frames_played_valid = 1; + // } } - + // we can now calculate running averages for sync error (frames), corrections (ppm), // insertions plus deletions (ppm), drift (ppm) double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; @@ -2830,7 +2845,10 @@ void *player_thread_func(void *arg) { statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); - statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); + if (conn->frame_rate_valid) + statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); + else + statistics_item(" output fps", " N/A"); statistics_item("source drift ppm", "%*.2f", 16, (conn->local_to_remote_time_gradient - 1.0) * 1000000); statistics_item("drift samples", "%*d", 13, @@ -2838,7 +2856,7 @@ void *player_thread_func(void *arg) { statistics_item( "estimated (unused) correction ppm", "%*.2f", strlen("estimated (unused) correction ppm"), - (conn->frame_rate > 0.0) + (conn->frame_rate_valid != 0) ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * conn->local_to_remote_time_gradient) * 1000000) / diff --git a/player.h b/player.h index effd8b12..d5719031 100644 --- a/player.h +++ b/player.h @@ -181,7 +181,7 @@ typedef struct { // for holding the output rate information until printed out at the end of a session double frame_rate; - int frame_rate_status; + int frame_rate_valid; // for holding input rate information until printed out at the end of a session From 04240708a948cfb8a8424bb7d8aceaef6359c2f8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Nov 2021 10:14:42 +0000 Subject: [PATCH 418/878] Try using the monotonic clock for checking output rate... --- common.c | 70 ++++++++++++++++++++++++++++++++++++++++++++------------ common.h | 1 + player.c | 24 +++++++++---------- player.h | 4 ++-- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/common.c b/common.c index 866222c2..b1525a31 100644 --- a/common.c +++ b/common.c @@ -55,9 +55,9 @@ #endif #ifdef COMPILE_FOR_FREEBSD -#include -#include #include +#include +#include #endif #ifdef COMPILE_FOR_OSX @@ -1258,6 +1258,49 @@ uint64_t get_absolute_time_in_fp() { return time_now_fp; } +uint64_t get_monotonic_time_in_ns() { + uint64_t time_now_ns; + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + struct timespec tn; + // can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt + // CLOCK_REALTIME because PTP uses it. + clock_gettime(CLOCK_MONOTONIC, &tn); + uint64_t tnnsec = tn.tv_sec; + tnnsec = tnnsec * 1000000000; + uint64_t tnjnsec = tn.tv_nsec; + time_now_ns = tnnsec + tnjnsec; +#endif + +#ifdef COMPILE_FOR_OSX + uint64_t time_now_mach; + uint64_t elapsedNano; + static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; + + // this actually give you a monotonic clock + // see https://news.ycombinator.com/item?id=6303755 + time_now_mach = mach_absolute_time(); + + // If this is the first time we've run, get the timebase. + // We can use denom == 0 to indicate that sTimebaseInfo is + // uninitialised because it makes no sense to have a zero + // denominator in a fraction. + + if (sTimebaseInfo.denom == 0) { + debug(1, "Mac initialise timebase info."); + (void)mach_timebase_info(&sTimebaseInfo); + } + + // Do the maths. We hope that the multiplication doesn't + // overflow; the price you pay for working in fixed point. + + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; +#endif + + return time_now_ns; +} + uint64_t get_absolute_time_in_ns() { uint64_t time_now_ns; @@ -1277,6 +1320,7 @@ uint64_t get_absolute_time_in_ns() { uint64_t elapsedNano; static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; + // this actually give you a monotonic clock time_now_mach = mach_absolute_time(); // If this is the first time we've run, get the timebase. @@ -1963,7 +2007,7 @@ int get_device_id(uint8_t *id, int int_length) { t = id; int found = 0; for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { - #ifdef AF_PACKET +#ifdef AF_PACKET if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; if ((strcmp(ifa->ifa_name, "lo") != 0) && (found == 0)) { @@ -1973,25 +2017,23 @@ int get_device_id(uint8_t *id, int int_length) { found = 1; } } - #else - #ifdef AF_LINK - struct sockaddr_dl * sdl = (struct sockaddr_dl *) ifa->ifa_addr; - if ((sdl) && (sdl->sdl_family == AF_LINK)) { +#else +#ifdef AF_LINK + struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + if ((sdl) && (sdl->sdl_family == AF_LINK)) { if (sdl->sdl_type == IFT_ETHER) { char *s = LLADDR(sdl); for (i = 0; i < sdl->sdl_alen; i++) { - debug(1,"char %d: \"%c\".", i, *s); - *t++ = (uint8_t)*s++; - } + debug(1, "char %d: \"%c\".", i, *s); + *t++ = (uint8_t)*s++; + } found = 1; } } - #endif - #endif - +#endif +#endif } freeifaddrs(ifaddr); } return response; } - diff --git a/common.h b/common.h index 9c3496f6..199b3d41 100644 --- a/common.h +++ b/common.h @@ -381,6 +381,7 @@ double vol2attn(double vol, long max_db, long min_db); // return a time in nanoseconds // uint64_t get_absolute_time_in_fp(void); // obselete uint64_t get_absolute_time_in_ns(void); +uint64_t get_monotonic_time_in_ns(void); // to try and get precise FPS values // time at startup for debugging timing extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message; diff --git a/player.c b/player.c index 74c6eac2..e19e6cab 100644 --- a/player.c +++ b/player.c @@ -1714,12 +1714,11 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; - + uint64_t previous_frames_played; uint64_t previous_frames_played_time; int previous_frames_played_valid = 0; - // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); conn->packet_count = 0; conn->packet_count_since_flush = 0; @@ -2787,6 +2786,7 @@ void *player_thread_func(void *arg) { uint64_t frames_sent_for_play; int status = -1; if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) { + uint64_t monotonic_time_now = get_monotonic_time_in_ns(); uint64_t elapsed_play_time; // dummy status = config.output->rate_info(&elapsed_play_time, &frames_sent_for_play); uint64_t frames_played = frames_sent_for_play - play_samples - current_delay; @@ -2794,22 +2794,22 @@ void *player_thread_func(void *arg) { // last time the rate_info call was made. Thus, the frame rate should be valid. if ((status == 0) && (previous_frames_played_valid)) { uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; - uint64_t interval = local_time_now - previous_frames_played_time; + uint64_t interval = monotonic_time_now - previous_frames_played_time; conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; conn->frame_rate_valid = 1; } - + // uncomment the if statement if your want to get as long a period for // calculating the frame rate // if ((status != 0) || (previous_frames_played_valid == 0)) { - // if we have just detected an outputting error, or if we have no - // starting information - previous_frames_played = frames_played; - previous_frames_played_time = local_time_now; - previous_frames_played_valid = 1; - // } + // if we have just detected an outputting error, or if we have no + // starting information + previous_frames_played = frames_played; + previous_frames_played_time = monotonic_time_now; + previous_frames_played_valid = 1; + //} } - + // we can now calculate running averages for sync error (frames), corrections (ppm), // insertions plus deletions (ppm), drift (ppm) double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; @@ -2931,7 +2931,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c else sw_min_db = (sw_max_db - desired_sw_range); } else { - hw_min_db = hw_max_db - desired_range_db; + hw_min_db = hw_max_db - desired_range_db; } } } else { diff --git a/player.h b/player.h index d5719031..41cdc5bd 100644 --- a/player.h +++ b/player.h @@ -109,7 +109,6 @@ typedef enum { remote_control_stream } airplay_stream_c; // "c" for category - #ifdef CONFIG_AIRPLAY_2 typedef enum { ts_ntp, ts_ptp } timing_t; typedef enum { ap_1, ap_2 } airplay_t; @@ -296,7 +295,8 @@ typedef struct { clock_status_t clock_status; airplay_stream_c - airplay_stream_category; // is it a remote control stream or a normal "full service" stream? (will be unspecified if not build for AirPlay 2) + airplay_stream_category; // is it a remote control stream or a normal "full service" stream? + // (will be unspecified if not build for AirPlay 2) #ifdef CONFIG_AIRPLAY_2 char *airplay_gid; // UUID in the Bonjour advertisement -- if NULL, the group UUID is the same as From 3f6f09328dcf58ec7ad08da2207906cbade57ecb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 14 Nov 2021 11:18:39 +0000 Subject: [PATCH 419/878] Fix a bug by ensuring certain ap2 variables are initialised by the player thread. --- player.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/player.c b/player.c index 6408a006..0084ab5e 100644 --- a/player.c +++ b/player.c @@ -1727,6 +1727,13 @@ void *player_thread_func(void *arg) { conn->flush_rtp_timestamp = 0; // it seems this number has a special significance -- it seems to // be used as a null operand, so we'll use it like that too conn->fix_volume = 0x10000; + +#ifdef CONFIG_AIRPLAY_2 + conn->ap2_flush_requested = 0; + conn->ap2_flush_from_valid = 0; + conn->ap2_rate = 0; + conn->ap2_play_enabled = 0; +#endif reset_anchor_info(conn); From fb1b2e9dff455a1e85b485808ec5c915bb103a83 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 14 Nov 2021 11:43:16 +0000 Subject: [PATCH 420/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index eb65e60a..407e85be 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-19-g3f6f0932 +=== +#### Bug fix +- Fix a bug that would occasionally mute Shairport Sync when using it with AppleTV. Combined with the [recent update](https://github.com/mikebrady/nqptp/blob/development/RELEASE_NOTES.md) of `nqptp`, this makes using Shairport Sync much more useful with an AppleTV 4K. + Version 4.1-dev-15-gf4115c45 ==== #### Bug Fix From 1032cad68485c06921fadffc81503b8917f6f6d7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 14 Nov 2021 14:13:31 +0000 Subject: [PATCH 421/878] Update to the latest version of ejurgensen's pair_ap library. With thanks. --- pair_ap/{pair_ap-master => }/.gitignore | 0 pair_ap/client-example.c | 15 ++++++++ pair_ap/{pair_ap-master => }/evrtsp/evrtsp.h | 0 pair_ap/{pair_ap-master => }/evrtsp/log.h | 0 .../evrtsp/rtsp-internal.h | 0 pair_ap/{pair_ap-master => }/evrtsp/rtsp.c | 0 pair_ap/pair-internal.h | 6 +++ pair_ap/pair.c | 21 +++++++++++ pair_ap/pair.h | 2 +- pair_ap/pair_fruit.c | 4 +- pair_ap/pair_homekit.c | 14 +++---- pair_ap/server-example.c | 15 ++++++++ shairport.c | 37 ++++++++++++++++--- 13 files changed, 99 insertions(+), 15 deletions(-) rename pair_ap/{pair_ap-master => }/.gitignore (100%) rename pair_ap/{pair_ap-master => }/evrtsp/evrtsp.h (100%) rename pair_ap/{pair_ap-master => }/evrtsp/log.h (100%) rename pair_ap/{pair_ap-master => }/evrtsp/rtsp-internal.h (100%) rename pair_ap/{pair_ap-master => }/evrtsp/rtsp.c (100%) diff --git a/pair_ap/pair_ap-master/.gitignore b/pair_ap/.gitignore similarity index 100% rename from pair_ap/pair_ap-master/.gitignore rename to pair_ap/.gitignore diff --git a/pair_ap/client-example.c b/pair_ap/client-example.c index 35e424ad..1cb766bb 100644 --- a/pair_ap/client-example.c +++ b/pair_ap/client-example.c @@ -9,6 +9,10 @@ #include "evrtsp/evrtsp.h" #include "pair.h" +#ifdef CONFIG_GCRYPT +# include +#endif + #define DEVICE_ID "AABBCCDD11223344" #define ACTIVE_REMOTE "3515324763" #define DACP_ID "FF1DB45949E6CBD3" @@ -553,6 +557,17 @@ main( int argc, char * argv[] ) content_type_setup = CONTENTTYPE_SETUP_HOMEKIT; } +// libgcrypt requires that the application initializes the library +#ifdef CONFIG_GCRYPT + if (!gcry_check_version(NULL)) + { + printf("libgcrypt not initialized\n"); + return -1; + } + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); +#endif + evbase = event_base_new(); evcon = evrtsp_connection_new(address, atoi(port)); evrtsp_connection_set_base(evcon, evbase); diff --git a/pair_ap/pair_ap-master/evrtsp/evrtsp.h b/pair_ap/evrtsp/evrtsp.h similarity index 100% rename from pair_ap/pair_ap-master/evrtsp/evrtsp.h rename to pair_ap/evrtsp/evrtsp.h diff --git a/pair_ap/pair_ap-master/evrtsp/log.h b/pair_ap/evrtsp/log.h similarity index 100% rename from pair_ap/pair_ap-master/evrtsp/log.h rename to pair_ap/evrtsp/log.h diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h b/pair_ap/evrtsp/rtsp-internal.h similarity index 100% rename from pair_ap/pair_ap-master/evrtsp/rtsp-internal.h rename to pair_ap/evrtsp/rtsp-internal.h diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp.c b/pair_ap/evrtsp/rtsp.c similarity index 100% rename from pair_ap/pair_ap-master/evrtsp/rtsp.c rename to pair_ap/evrtsp/rtsp.c diff --git a/pair_ap/pair-internal.h b/pair_ap/pair-internal.h index e7cd85e3..f9d8fa66 100644 --- a/pair_ap/pair-internal.h +++ b/pair_ap/pair-internal.h @@ -225,6 +225,12 @@ struct pair_definition }; +/* ----------------------------- INITIALIZATION ---------------------------- */ + +bool +is_initialized(void); + + /* -------------------- GCRYPT AND OPENSSL COMPABILITY --------------------- */ /* partly borrowed from ffmpeg (rtmpdh.c) */ diff --git a/pair_ap/pair.c b/pair_ap/pair.c index 6908e8bd..2b58bbe1 100644 --- a/pair_ap/pair.c +++ b/pair_ap/pair.c @@ -44,6 +44,27 @@ static struct pair_definition *pair[] = { &pair_server_homekit, }; +/* ------------------------------ INITIALIZATION ---------------------------- */ + +bool +is_initialized(void) +{ + if (sodium_init() == -1) + return false; + +#if CONFIG_GCRYPT + // According to libgcrypt documentation: "It is important that these + // initialization steps are not done by a library but by the actual + // application. A library using Libgcrypt might want to check for finished + // initialization using:" + if (!gcry_control (GCRYCTL_INITIALIZATION_FINISHED_P)) + return false; +#endif + + return true; +} + + /* -------------------------- SHARED HASHING HELPERS ------------------------ */ int diff --git a/pair_ap/pair.h b/pair_ap/pair.h index 3877e2bb..d3be0ca9 100644 --- a/pair_ap/pair.h +++ b/pair_ap/pair.h @@ -4,7 +4,7 @@ #include #define PAIR_AP_VERSION_MAJOR 0 -#define PAIR_AP_VERSION_MINOR 8 +#define PAIR_AP_VERSION_MINOR 10 #define PAIR_AP_DEVICE_ID_LEN_MAX 64 diff --git a/pair_ap/pair_fruit.c b/pair_ap/pair_fruit.c index 42e0b066..3236faf5 100644 --- a/pair_ap/pair_fruit.c +++ b/pair_ap/pair_fruit.c @@ -601,7 +601,7 @@ client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add { struct pair_client_setup_context *sctx = &handle->sctx.client; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (!pin || strlen(pin) < 4) @@ -876,7 +876,7 @@ client_verify_new(struct pair_verify_context *handle, const char *client_setup_k const char *ptr; int i; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (!client_setup_keys) diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c index bb45df7b..6540d42d 100644 --- a/pair_ap/pair_homekit.c +++ b/pair_ap/pair_homekit.c @@ -281,7 +281,7 @@ calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char * for (i=0; i < hash_len; i++ ) H_xor[i] = H_N[i] ^ H_g[i]; - + hash_init( alg, &ctx ); hash_update( alg, &ctx, H_xor, hash_len ); @@ -410,7 +410,7 @@ srp_user_start_authentication(struct SRPUser *usr, const char **username, // BN_hex2bn(&(usr->a), "D929DFB605687233C9E9030C2280156D03BDB9FDCF3CCE3BC27D9CCFCB5FF6A1"); bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); - + *len_A = bnum_num_bytes(usr->A); *bytes_A = malloc(*len_A); @@ -515,7 +515,7 @@ static int srp_create_salted_verification_key(enum hash_alg alg, SRP_NGType ng_type, const char *username, const unsigned char *password, int len_password, - unsigned char **bytes_s, int *len_s, + unsigned char **bytes_s, int *len_s, unsigned char **bytes_v, int *len_v, const char *n_hex, const char *g_hex ) { @@ -1137,7 +1137,7 @@ client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add { struct pair_client_setup_context *sctx = &handle->sctx.client; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (handle->type == &pair_client_homekit_normal) @@ -1626,7 +1626,7 @@ client_verify_new(struct pair_verify_context *handle, const char *client_setup_k struct pair_client_verify_context *vctx = &handle->vctx.client; size_t hexkey_len; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) @@ -1950,7 +1950,7 @@ server_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add { struct pair_server_setup_context *sctx = &handle->sctx.server; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (!pin) @@ -2396,7 +2396,7 @@ server_verify_new(struct pair_verify_context *handle, const char *client_setup_k { struct pair_server_verify_context *vctx = &handle->vctx.server; - if (sodium_init() == -1) + if (!is_initialized()) return -1; if (client_setup_keys) diff --git a/pair_ap/server-example.c b/pair_ap/server-example.c index 73fe4bda..4a6e6fd1 100644 --- a/pair_ap/server-example.c +++ b/pair_ap/server-example.c @@ -37,6 +37,10 @@ #include "pair.h" +#ifdef CONFIG_GCRYPT +# include +#endif + #define DEVICE_ID "FFEEDDCCBBAA9988" #define LISTEN_PORT 7000 #define CONTENT_TYPE_OCTET "application/octet-stream" @@ -668,6 +672,17 @@ main(int argc, char * argv[]) struct event_base *evbase; struct evconnlistener *listener; +// libgcrypt requires that the application initializes the library +#ifdef CONFIG_GCRYPT + if (!gcry_check_version(NULL)) + { + printf("libgcrypt not initialized\n"); + return -1; + } + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); +#endif + evbase = event_base_new(); listener = listen_add(evbase, in_accept_cb, in_error_cb, LISTEN_PORT); diff --git a/shairport.c b/shairport.c index 980fe944..edcb5e10 100644 --- a/shairport.c +++ b/shairport.c @@ -44,6 +44,7 @@ #include "config.h" #ifdef CONFIG_AIRPLAY_2 +#include #include #include #endif @@ -1786,8 +1787,10 @@ int main(int argc, char **argv) { // mDNS string. // note: 0x300401F4A00 works but with weird delays and stuff // config.airplay_features = 0x1C340405FCA00; - uint64_t mask = ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); - config.airplay_features = 0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below) + uint64_t mask = + ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); + config.airplay_features = + 0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below) // Advertised with mDNS and returned with GET /info, see // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio @@ -1802,7 +1805,7 @@ int main(int argc, char **argv) { config.airplay_features |= (1 << 15); // 15 is artwork #endif - debug(1,"Features: 0x%" PRIx64 ".", config.airplay_features); + debug(1, "Features: 0x%" PRIx64 ".", config.airplay_features); config.airplay_statusflags = 0x04; // Set to NULL to work with transient pairing config.airplay_pin = NULL; @@ -1910,6 +1913,29 @@ int main(int argc, char **argv) { } else { debug(1, "libsodium initialised."); } + + // this code is based on + // https://www.gnupg.org/documentation/manuals/gcrypt/Initializing-the-library.html + + /* Version check should be the very first call because it + makes sure that important subsystems are initialized. + #define NEED_LIBGCRYPT_VERSION to the minimum required version. */ + +#define NEED_LIBGCRYPT_VERSION "1.5.4" + + if (!gcry_check_version(NEED_LIBGCRYPT_VERSION)) { + die("libgcrypt is too old (need %s, have %s).", NEED_LIBGCRYPT_VERSION, + gcry_check_version(NULL)); + } + + /* Disable secure memory. */ + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); + + /* ... If required, other initialization goes here. */ + + /* Tell Libgcrypt that initialization has completed. */ + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); + #endif /* Mess around with the latency options */ @@ -1961,8 +1987,9 @@ int main(int argc, char **argv) { debug(1, "mdns backend \"%s\".", config.mdns_name); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); debug(1, "interpolation setting is \"%s\".", - config.packet_stuffing == ST_basic ? "basic" - : config.packet_stuffing == ST_soxr ? "soxr" : "auto"); + config.packet_stuffing == ST_basic ? "basic" + : config.packet_stuffing == ST_soxr ? "soxr" + : "auto"); debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold); debug(1, "resync time is %f seconds.", config.resyncthreshold); debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption); From c31022016b428a664a41c91c28f093409c2ca99d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 14 Nov 2021 15:03:19 +0000 Subject: [PATCH 422/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 407e85be..f07d84b7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-21-g1032cad6 +=== +#### Enhancement +- Update the `pair_ap` library to the [latest version](https://github.com/ejurgensen/pair_ap). Thanks again to [ejurgensen](https://github.com/ejurgensen). Addresses Issue [#1334](https://github.com/mikebrady/shairport-sync/issues/1334). Thanks to [iChUdY](https://github.com/iChUdY) for reporting the issue. + Version 4.1-dev-19-g3f6f0932 === #### Bug fix From c0a3dacf304346d17efa3544dd8ca93c6ab5ede2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:35:21 +0000 Subject: [PATCH 423/878] Working to check for divide / mod by 0's --- activity_monitor.c | 4 +- audio_alsa.c | 63 ++++++----- audio_pw.c | 268 +++++++++++++++++++++------------------------ mdns.h | 2 +- player.c | 2 +- player.h | 2 +- ptp-utilities.c | 4 +- rtp.c | 28 ++--- rtsp.c | 60 ++++++---- shairport.c | 2 +- 10 files changed, 221 insertions(+), 214 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 2f5efddf..1d301c49 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -169,11 +169,11 @@ void *activity_monitor_thread_code(void *arg) { do { switch (state) { case am_inactive: - debug(2,"am_state: am_inactive"); + debug(2, "am_state: am_inactive"); while (player_state != ps_active) pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex); state = am_active; - debug(2,"am_state: going active"); + debug(2, "am_state: going active"); break; case am_active: // debug(1,"am_state: am_active"); diff --git a/audio_alsa.c b/audio_alsa.c index 32f6c90f..a2126c36 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -60,7 +60,8 @@ static int play(void *buf, int samples); static void stop(void); static void flush(void); int delay(long *the_delay); -int get_frames_sent_for_output(__attribute__ ((unused)) uint64_t *elapsed_time, uint64_t *frames_sent_to_dac); +int get_frames_sent_for_output(__attribute__((unused)) uint64_t *elapsed_time, + uint64_t *frames_sent_to_dac); // int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); void *alsa_buffer_monitor_thread_code(void *arg); @@ -89,8 +90,9 @@ audio_output audio_alsa = { .flush = &flush, .delay = &delay, .play = &play, - .rate_info = &get_frames_sent_for_output, // will also include frames of silence sent to stop standby mode -// .rate_info = NULL, + .rate_info = &get_frames_sent_for_output, // will also include frames of silence sent to stop + // standby mode + // .rate_info = NULL, .mute = NULL, // a function will be provided if it can, and is allowed to, // do hardware mute .volume = NULL, // a function will be provided if it can do hardware volume @@ -247,7 +249,8 @@ static snd_pcm_uframes_t period_size_requested, buffer_size_requested; static int set_period_size_request, set_buffer_size_request; static uint64_t frames_sent_for_playing; -static int output_error_occurred; // set to true if an underrun or similar has occurred since last requested +static int output_error_occurred; // set to true if an underrun or similar has occurred since last + // requested static void help(void) { printf(" -d output-device set the output device, default is \"default\".\n" @@ -432,7 +435,7 @@ int actual_open_alsa_device(int do_auto_setup) { char errorstring[1024]; strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, - alsa_out_dev); + alsa_out_dev); } return ret; } @@ -469,14 +472,14 @@ int actual_open_alsa_device(int do_auto_setup) { ret = snd_pcm_hw_params_set_access(alsa_handle, alsa_params, access); if (ret < 0) { die("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev, - snd_strerror(ret)); + snd_strerror(ret)); return ret; } ret = snd_pcm_hw_params_set_channels(alsa_handle, alsa_params, 2); if (ret < 0) { die("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev, - snd_strerror(ret)); + snd_strerror(ret)); return ret; } @@ -522,7 +525,7 @@ int actual_open_alsa_device(int do_auto_setup) { sps_format_description_string(config.output_format)); } else { die("audio_alsa: Could not automatically set the output format for device \"%s\": %s", - alsa_out_dev, snd_strerror(ret)); + alsa_out_dev, snd_strerror(ret)); return ret; } } @@ -532,8 +535,8 @@ int actual_open_alsa_device(int do_auto_setup) { config.output_rate; // this is the requested rate -- it'll be changed to the actual rate ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir); if (ret < 0) { - die("audio_alsa: The frame rate of %i frames per second is not available for playback: %s", config.output_rate, - snd_strerror(ret)); + die("audio_alsa: The frame rate of %i frames per second is not available for playback: %s", + config.output_rate, snd_strerror(ret)); return ret; } } else { @@ -552,7 +555,9 @@ int actual_open_alsa_device(int do_auto_setup) { if (ret == 0) { speed_found = 1; if (actual_sample_rate != speeds[i]) - die("The output DAC can not be set to %d frames per second (fps). The nearest speed available is %d fps.", speeds[i], actual_sample_rate); + die("The output DAC can not be set to %d frames per second (fps). The nearest speed " + "available is %d fps.", + speeds[i], actual_sample_rate); } else { i++; } @@ -562,7 +567,7 @@ int actual_open_alsa_device(int do_auto_setup) { debug(1, "alsa: output speed chosen is %d.", config.output_rate); } else { die("audio_alsa: Could not automatically set the output rate for device \"%s\": %s", - alsa_out_dev, snd_strerror(ret)); + alsa_out_dev, snd_strerror(ret)); return ret; } } @@ -607,7 +612,7 @@ int actual_open_alsa_device(int do_auto_setup) { ret = snd_pcm_hw_params(alsa_handle, alsa_params); if (ret < 0) { die("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev, - snd_strerror(ret)); + snd_strerror(ret)); return ret; } @@ -774,10 +779,11 @@ int actual_open_alsa_device(int do_auto_setup) { break; } - if (snd_pcm_hw_params_get_rate_numden(alsa_params, &uval, &uval2) == 0) + if ((snd_pcm_hw_params_get_rate_numden(alsa_params, &uval, &uval2) == 0) && (uval2 != 0)) + // watch for a divide by zero too! debug(log_level, " precise (rational) rate = %.3f frames per second (i.e. %u/%u).", uval, - uval2, ((double)uval) / uval2); - else + uval2, ((double)uval) / uval2); + else debug(log_level, " precise (rational) rate information unavailable."); snd_pcm_hw_params_get_period_time(alsa_params, &uval, &dir); @@ -1283,16 +1289,16 @@ static int init(int argc, char **argv) { warn("Invalid use_precision_timing option choice \"%s\". It should be " "\"yes\", \"auto\" or \"no\". " "It remains set to \"%s\".", - config.use_precision_timing == YNA_NO - ? "no" - : config.use_precision_timing == YNA_AUTO ? "auto" : "yes"); + config.use_precision_timing == YNA_NO ? "no" + : config.use_precision_timing == YNA_AUTO ? "auto" + : "yes"); } } debug(1, "alsa: disable_standby_mode is \"%s\".", - config.disable_standby_mode == disable_standby_off - ? "never" - : config.disable_standby_mode == disable_standby_always ? "always" : "auto"); + config.disable_standby_mode == disable_standby_off ? "never" + : config.disable_standby_mode == disable_standby_always ? "always" + : "auto"); debug(1, "alsa: disable_standby_mode_silence_threshold is %f seconds.", config.disable_standby_mode_silence_threshold); debug(1, "alsa: disable_standby_mode_silence_scan_interval is %f seconds.", @@ -1624,18 +1630,19 @@ int delay(long *the_delay) { return ret; } -int get_frames_sent_for_output(__attribute__ ((unused)) uint64_t *elapsed_time, uint64_t *frames_sent_to_dac) { +int get_frames_sent_for_output(__attribute__((unused)) uint64_t *elapsed_time, + uint64_t *frames_sent_to_dac) { int ret = 0; pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); *frames_sent_to_dac = frames_sent_for_playing; if (alsa_handle == NULL) ret = ENODEV; - else + else ret = output_error_occurred; // will be zero unless an error occurred - output_error_occurred = 0; // reset it. + output_error_occurred = 0; // reset it. debug_mutex_unlock(&alsa_mutex, 0); pthread_cleanup_pop(0); - return ret; + return ret; } /* @@ -1682,8 +1689,8 @@ int do_play(void *buf, int samples) { stall_monitor_frame_count += samples; frames_sent_for_playing += samples; } else { - output_error_occurred = -ret; // note than an output error has occurred - if (ret == -EPIPE) { /* underrun */ + output_error_occurred = -ret; // note than an output error has occurred + if (ret == -EPIPE) { /* underrun */ // It could be that the DAC was in the SND_PCM_STATE_XRUN state before // sending the samples to be output. If so, it will still be in diff --git a/audio_pw.c b/audio_pw.c index 58a5fd72..f25c0456 100644 --- a/audio_pw.c +++ b/audio_pw.c @@ -29,8 +29,8 @@ #include #include -#include #include +#include #include #include @@ -61,26 +61,26 @@ struct pw_data { } data; -static void on_core_info(__attribute__((unused)) void *userdata, const struct pw_core_info *info) -{ - debug(1, "pw: remote %"PRIu32" is named \"%s\"", info->id, info->name); +static void on_core_info(__attribute__((unused)) void *userdata, const struct pw_core_info *info) { + debug(1, "pw: remote %" PRIu32 " is named \"%s\"", info->id, info->name); } -static void on_core_error(__attribute__((unused)) void *userdata, uint32_t id, int seq, int res, const char *message) -{ - warn("pw: remote error: id=%"PRIu32" seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); +static void on_core_error(__attribute__((unused)) void *userdata, uint32_t id, int seq, int res, + const char *message) { + warn("pw: remote error: id=%" PRIu32 " seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), + message); } static const struct pw_core_events core_events = { - PW_VERSION_CORE_EVENTS, - .info = on_core_info, - .error = on_core_error, + PW_VERSION_CORE_EVENTS, + .info = on_core_info, + .error = on_core_error, }; -static void registry_event_global(__attribute__((unused)) void *userdata, uint32_t id, - __attribute__((unused)) uint32_t permissions, const char *type, __attribute__((unused)) uint32_t version, - const struct spa_dict *props) -{ +static void registry_event_global(__attribute__((unused)) void *userdata, uint32_t id, + __attribute__((unused)) uint32_t permissions, const char *type, + __attribute__((unused)) uint32_t version, + const struct spa_dict *props) { const struct spa_dict_item *item; const char *name, *media_class; @@ -91,49 +91,47 @@ static void registry_event_global(__attribute__((unused)) void *userdata, uint3 if (!name || !media_class) return; - debug(1, "pw: registry: id=%"PRIu32" type=%s name=\"%s\" media_class=\"%s\"", id, type, name, media_class); + debug(1, "pw: registry: id=%" PRIu32 " type=%s name=\"%s\" media_class=\"%s\"", id, type, name, + media_class); - spa_dict_for_each(item, props) { - debug(1, "pw: \t\t%s = \"%s\"", item->key, item->value); - } + spa_dict_for_each(item, props) { debug(1, "pw: \t\t%s = \"%s\"", item->key, item->value); } } } -static void registry_event_global_remove(__attribute__((unused)) void *userdata, uint32_t id) -{ - debug(1, "pw: registry: remove id=%"PRIu32"", id); +static void registry_event_global_remove(__attribute__((unused)) void *userdata, uint32_t id) { + debug(1, "pw: registry: remove id=%" PRIu32 "", id); } static const struct pw_registry_events registry_events = { - PW_VERSION_REGISTRY_EVENTS, - .global = registry_event_global, - .global_remove = registry_event_global_remove, + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, }; -static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) -{ +static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, + const char *error) { struct pw_data *pipewire = userdata; - debug(1, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old), pw_stream_state_as_string(state)); + debug(1, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old), + pw_stream_state_as_string(state)); if (state == PW_STREAM_STATE_STREAMING) - debug(1, "pw: stream node %"PRIu32"", pw_stream_get_node_id(pipewire->stream)); + debug(1, "pw: stream node %" PRIu32 "", pw_stream_get_node_id(pipewire->stream)); if (state == PW_STREAM_STATE_ERROR) - debug(1, "pw: stream node %"PRIu32" error: %s", pw_stream_get_node_id(pipewire->stream), error); + debug(1, "pw: stream node %" PRIu32 " error: %s", pw_stream_get_node_id(pipewire->stream), + error); pw_thread_loop_signal(pipewire->mainloop, 0); } -static void on_process(void *userdata) -{ +static void on_process(void *userdata) { struct pw_data *pipewire = userdata; pw_thread_loop_signal(pipewire->mainloop, 0); } -static void on_drained(void *userdata) -{ +static void on_drained(void *userdata) { struct pw_data *pipewire = userdata; pw_stream_set_active(pipewire->stream, false); @@ -142,10 +140,10 @@ static void on_drained(void *userdata) } static const struct pw_stream_events stream_events = { - PW_VERSION_STREAM_EVENTS, - .state_changed = on_state_changed, - .process = on_process, - .drained = on_drained, + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed, + .process = on_process, + .drained = on_drained, }; static void deinit() { @@ -157,7 +155,7 @@ static void deinit() { } if (data.registry) { - pw_proxy_destroy((struct pw_proxy*)data.registry); + pw_proxy_destroy((struct pw_proxy *)data.registry); data.registry = NULL; } @@ -182,10 +180,10 @@ static void deinit() { } } -static int init(__attribute__((unused)) int argc, __attribute__((unused)) char** argv) { +static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { struct pw_loop *loop; - struct pw_properties* props; + struct pw_properties *props; // set up default values first config.audio_backend_buffer_desired_length = 0.35; @@ -197,13 +195,9 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char** debug(1, "pw: compiled with libpipewire %s", pw_get_headers_version()); debug(1, "pw: linked with libpipewire: %s", pw_get_library_version()); - data.props = pw_properties_new( - PW_KEY_MEDIA_TYPE, "Audio", - PW_KEY_MEDIA_CATEGORY, "Playback", - PW_KEY_MEDIA_ROLE, "Music", - PW_KEY_APP_NAME, "shairport-sync", - PW_KEY_NODE_NAME, "shairport-sync", - NULL); + data.props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", PW_KEY_APP_NAME, "shairport-sync", + PW_KEY_NODE_NAME, "shairport-sync", NULL); if (!data.props) { deinit(); @@ -268,77 +262,75 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char** static enum spa_audio_format sps_format_to_spa_format(sps_format_t sps_format) { switch (sps_format) { - case SPS_FORMAT_S8: - return SPA_AUDIO_FORMAT_S8; - case SPS_FORMAT_U8: - return SPA_AUDIO_FORMAT_U8; - case SPS_FORMAT_S16: - return SPA_AUDIO_FORMAT_S16; - case SPS_FORMAT_S16_LE: - return SPA_AUDIO_FORMAT_S16_LE; - case SPS_FORMAT_S16_BE: - return SPA_AUDIO_FORMAT_S16_BE; - case SPS_FORMAT_S24: - return SPA_AUDIO_FORMAT_S24_32; - case SPS_FORMAT_S24_LE: - return SPA_AUDIO_FORMAT_S24_32_LE; - case SPS_FORMAT_S24_BE: - return SPA_AUDIO_FORMAT_S24_32_BE; - case SPS_FORMAT_S24_3LE: - return SPA_AUDIO_FORMAT_S24_LE; - case SPS_FORMAT_S24_3BE: - return SPA_AUDIO_FORMAT_S24_BE; - case SPS_FORMAT_S32: - return SPA_AUDIO_FORMAT_S32; - case SPS_FORMAT_S32_LE: - return SPA_AUDIO_FORMAT_S32_LE; - case SPS_FORMAT_S32_BE: - return SPA_AUDIO_FORMAT_S32_BE; + case SPS_FORMAT_S8: + return SPA_AUDIO_FORMAT_S8; + case SPS_FORMAT_U8: + return SPA_AUDIO_FORMAT_U8; + case SPS_FORMAT_S16: + return SPA_AUDIO_FORMAT_S16; + case SPS_FORMAT_S16_LE: + return SPA_AUDIO_FORMAT_S16_LE; + case SPS_FORMAT_S16_BE: + return SPA_AUDIO_FORMAT_S16_BE; + case SPS_FORMAT_S24: + return SPA_AUDIO_FORMAT_S24_32; + case SPS_FORMAT_S24_LE: + return SPA_AUDIO_FORMAT_S24_32_LE; + case SPS_FORMAT_S24_BE: + return SPA_AUDIO_FORMAT_S24_32_BE; + case SPS_FORMAT_S24_3LE: + return SPA_AUDIO_FORMAT_S24_LE; + case SPS_FORMAT_S24_3BE: + return SPA_AUDIO_FORMAT_S24_BE; + case SPS_FORMAT_S32: + return SPA_AUDIO_FORMAT_S32; + case SPS_FORMAT_S32_LE: + return SPA_AUDIO_FORMAT_S32_LE; + case SPS_FORMAT_S32_BE: + return SPA_AUDIO_FORMAT_S32_BE; - case SPS_FORMAT_UNKNOWN: - case SPS_FORMAT_AUTO: - case SPS_FORMAT_INVALID: - default: - return SPA_AUDIO_FORMAT_S16; + case SPS_FORMAT_UNKNOWN: + case SPS_FORMAT_AUTO: + case SPS_FORMAT_INVALID: + default: + return SPA_AUDIO_FORMAT_S16; } } -static int spa_format_samplesize(enum spa_audio_format audio_format) -{ - switch(audio_format) { - case SPA_AUDIO_FORMAT_S8: - case SPA_AUDIO_FORMAT_U8: - return 1; - case SPA_AUDIO_FORMAT_S16: - return 2; - case SPA_AUDIO_FORMAT_S24: - return 3; - case SPA_AUDIO_FORMAT_S24_32: - case SPA_AUDIO_FORMAT_S32: - return 4; - default: - die("pw: unhandled spa_audio_format: %d", audio_format); - return -1; +static int spa_format_samplesize(enum spa_audio_format audio_format) { + switch (audio_format) { + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_U8: + return 1; + case SPA_AUDIO_FORMAT_S16: + return 2; + case SPA_AUDIO_FORMAT_S24: + return 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S32: + return 4; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return -1; } } -static const char* spa_format_to_str(enum spa_audio_format audio_format) -{ - switch(audio_format) { - case SPA_AUDIO_FORMAT_U8: - return "u8"; - case SPA_AUDIO_FORMAT_S8: - return "s8"; - case SPA_AUDIO_FORMAT_S16: - return "s16"; - case SPA_AUDIO_FORMAT_S24: - case SPA_AUDIO_FORMAT_S24_32: - return "s24"; - case SPA_AUDIO_FORMAT_S32: - return "s32"; - default: - die("pw: unhandled spa_audio_format: %d", audio_format); - return "(invalid)"; +static const char *spa_format_to_str(enum spa_audio_format audio_format) { + switch (audio_format) { + case SPA_AUDIO_FORMAT_U8: + return "u8"; + case SPA_AUDIO_FORMAT_S8: + return "s8"; + case SPA_AUDIO_FORMAT_S16: + return "s16"; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_32: + return "s24"; + case SPA_AUDIO_FORMAT_S32: + return "s32"; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return "(invalid)"; } } @@ -368,13 +360,11 @@ static void start(int sample_rate, int sample_format) { debug(1, "pw: format: %s", spa_format_to_str(data.format)); debug(1, "pw: samplesize: %d", spa_format_samplesize(data.format)); debug(1, "pw: stride: %d", data.stride); - debug(1, "pw: latency: %d samples (%.3fs)", nom, (double)nom/data.rate); + if (data.rate != 0) + debug(1, "pw: latency: %d samples (%.3fs)", nom, (double)nom / data.rate); - info = SPA_AUDIO_INFO_RAW_INIT( - .flags = SPA_AUDIO_FLAG_NONE, - .format = data.format, - .rate = data.rate, - .channels = data.channels); + info = SPA_AUDIO_INFO_RAW_INIT(.flags = SPA_AUDIO_FLAG_NONE, .format = data.format, + .rate = data.rate, .channels = data.channels); params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &info); @@ -385,15 +375,13 @@ static void start(int sample_rate, int sample_format) { die("pw: pw_stream_new() failed: %m"); } - debug(1, "pw: connecting stream: target_id=%"PRIu32"", PW_ID_ANY); + debug(1, "pw: connecting stream: target_id=%" PRIu32 "", PW_ID_ANY); pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data); - ret = pw_stream_connect(data.stream, - PW_DIRECTION_OUTPUT, - PW_ID_ANY, - PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, - params, 1); + ret = pw_stream_connect( + data.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, + PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, params, 1); if (ret < 0) { deinit(); @@ -408,12 +396,12 @@ static void start(int sample_rate, int sample_format) { debug(1, "pw: stream properties:"); pstate = NULL; while ((key = pw_properties_iterate(props, &pstate)) != NULL && - (val = pw_properties_get(props, key)) != NULL) { + (val = pw_properties_get(props, key)) != NULL) { debug(1, "pw: \t%s = \"%s\"", key, val); } } - while(1) { + while (1) { enum pw_stream_state stream_state = pw_stream_get_state(data.stream, NULL); if (stream_state == PW_STREAM_STATE_PAUSED) break; @@ -483,19 +471,17 @@ static int play(void *buf, int samples) { return 0; } -audio_output audio_pw = { - .name = "pw", - .help = NULL, - .init = &init, - .deinit = &deinit, - .prepare = NULL, - .start = &start, - .stop = &stop, - .is_running = NULL, - .flush = &flush, - .delay = NULL, - .play = &play, - .volume = NULL, - .parameters = NULL, - .mute = NULL -}; +audio_output audio_pw = {.name = "pw", + .help = NULL, + .init = &init, + .deinit = &deinit, + .prepare = NULL, + .start = &start, + .stop = &stop, + .is_running = NULL, + .flush = &flush, + .delay = NULL, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL}; diff --git a/mdns.h b/mdns.h index d43b9513..f69179ef 100644 --- a/mdns.h +++ b/mdns.h @@ -20,7 +20,7 @@ typedef struct { char *name; int (*mdns_register)(char *ap1name, char *ap2name, int port, char **txt_records, char **secondary_txt_records); - int (*mdns_update)(char **txt_records, char **secondary_txt_records); + int (*mdns_update)(char **txt_records, char **secondary_txt_records); void (*mdns_unregister)(void); void (*mdns_dacp_monitor_start)(); void (*mdns_dacp_monitor_set_id)(const char *); diff --git a/player.c b/player.c index 67a0c086..07ee1123 100644 --- a/player.c +++ b/player.c @@ -1732,7 +1732,7 @@ void *player_thread_func(void *arg) { conn->flush_rtp_timestamp = 0; // it seems this number has a special significance -- it seems to // be used as a null operand, so we'll use it like that too conn->fix_volume = 0x10000; - + #ifdef CONFIG_AIRPLAY_2 conn->ap2_flush_requested = 0; conn->ap2_flush_from_valid = 0; diff --git a/player.h b/player.h index 41cdc5bd..526572ef 100644 --- a/player.h +++ b/player.h @@ -170,7 +170,7 @@ typedef struct { player_watchdog_thread; // buffers to delete on exit - signed short *tbuf; + int32_t *tbuf; int32_t *sbuf; char *outbuf; diff --git a/ptp-utilities.c b/ptp-utilities.c index 0f80e913..7019187e 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -33,8 +33,8 @@ #include #include #ifdef COMPILE_FOR_FREEBSD -#include #include +#include #endif #define __STDC_FORMAT_MACROS #include "common.h" @@ -86,7 +86,7 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint if (actual_clock_id != NULL) *actual_clock_id = nqptp_data.master_clock_id; if (time_of_sample != NULL) - *time_of_sample = nqptp_data.local_time; + *time_of_sample = nqptp_data.local_time; if (raw_offset != NULL) *raw_offset = nqptp_data.local_to_master_time_offset; if (mastership_start_time != NULL) diff --git a/rtp.c b/rtp.c index db8ff2f0..41b07371 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1564,7 +1564,8 @@ void rtp_ap2_control_handler_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(2, "Connection %d: AP2 Control Receiver Cleanup.", conn->connection_number); close(conn->ap2_control_socket); - debug(2, "Connection %d: UDP control port %u closed.", conn->connection_number, conn->local_ap2_control_port); + debug(2, "Connection %d: UDP control port %u closed.", conn->connection_number, + conn->local_ap2_control_port); conn->ap2_control_socket = 0; conn->ap2_remote_control_socket_addr_length = 0; // indicates to the control receiver thread that the socket address need to be @@ -2030,7 +2031,8 @@ void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Buffered Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->buffered_audio_socket); - debug(2, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, conn->local_buffered_audio_port); + debug(2, "Connection %d: TCP Buffered Audio port closed: %u.", conn->connection_number, + conn->local_buffered_audio_port); conn->buffered_audio_socket = 0; debug(2, "Buffered Audio Receiver Cleanup Done."); } @@ -2119,10 +2121,10 @@ void *rtp_buffered_audio_processor(void *arg) { // (does nothing if called twice during the course of one program execution) // deprecated in ffmpeg 4.0 and later... but still needed in ffmpeg 3.6 / ubuntu 18 - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" avcodec_register_all(); - #pragma GCC diagnostic pop +#pragma GCC diagnostic pop AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { @@ -2221,7 +2223,7 @@ void *rtp_buffered_audio_processor(void *arg) { ssize_t data_remaining; uint32_t seq_no; // audio packet number // uint32_t previous_seq_no = 0; - int new_buffer_needed; + int new_buffer_needed = 0; ssize_t nread; int finished = 0; diff --git a/rtsp.c b/rtsp.c index f1efb1cd..5f9fa9ab 100644 --- a/rtsp.c +++ b/rtsp.c @@ -655,7 +655,8 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_lock(&conns_lock, 1000000, 3); int i; for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + if ((conns[i] != NULL) && (conns[i]->running != 0) && + (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { pthread_cancel(conns[i]->thread); @@ -663,7 +664,8 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o } } for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL) && (conns[i]->running != 0) && (conns[i]->connection_number != except_this_one) && + if ((conns[i] != NULL) && (conns[i]->running != 0) && + (conns[i]->connection_number != except_this_one) && ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { pthread_join(conns[i]->thread, NULL); @@ -1303,7 +1305,8 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { char *string; }; - struct response_t responses[] = {{200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {501, "Not Implemented"}}; + struct response_t responses[] = { + {200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {501, "Not Implemented"}}; int found = 0; char *respcode_text = "Unauthorized"; for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { @@ -1662,13 +1665,14 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } void handle_setrate(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: SETRATE %s : Content-Length %d", conn->connection_number, - req->path, req->contentlength); + debug(3, "Connection %d: SETRATE %s : Content-Length %d", conn->connection_number, req->path, + req->contentlength); debug_log_rtsp_message(2, "SETRATE request -- unimplemented", req); resp->respcode = 501; // Not Implemented } -void handle_unimplemented_ap1(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { +void handle_unimplemented_ap1(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { debug_log_rtsp_message(1, "request not recognised for AirPlay 1 operation", req); resp->respcode = 501; } @@ -1783,8 +1787,9 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) req->contentlength); resp->respcode = 500; } -void handle_post(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, - __attribute((unused)) rtsp_message *resp) { +void handle_post(__attribute((unused)) rtsp_conn_info *conn, + __attribute((unused)) rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); resp->respcode = 500; @@ -2321,7 +2326,7 @@ void teardown_phase_one(rtsp_conn_info *conn) { // this can be called more than once on the same connection -- // by the player itself but also by the play seesion being killed if (conn->player_thread) { - player_stop(conn); // this nulls the player_thread + player_stop(conn); // this nulls the player_thread activity_monitor_signify_activity(0); // inactive, and should be after command_stop() } if (conn->session_key) { @@ -2347,7 +2352,8 @@ void teardown_phase_two(rtsp_conn_info *conn) { if (conn->event_socket) { close(conn->event_socket); conn->event_socket = 0; - debug(2, "Connection %d: closing TCP event port %u.", conn->connection_number, conn->local_event_port); + debug(2, "Connection %d: closing TCP event port %u.", conn->connection_number, + conn->local_event_port); } // if we are closing a PTP stream only, do this @@ -2365,7 +2371,6 @@ void teardown_phase_two(rtsp_conn_info *conn) { } } - void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { @@ -2661,7 +2666,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); + debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, + conn->local_event_port); if (conn->rtp_event_thread != NULL) debug(1, "previous rtp_event_thread allocation not freed, it seems."); @@ -2707,7 +2713,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(1, "Connection %d: TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); + debug(1, "Connection %d: TCP Remote Control event port opened: %u.", + conn->connection_number, conn->local_event_port); if (conn->rtp_event_thread != NULL) debug(1, "previous rtp_event_thread allocation not freed, it seems."); conn->rtp_event_thread = malloc(sizeof(pthread_t)); @@ -2759,7 +2766,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as an ap2_control port", err); } - debug(2, "Connection %d: UDP control port opened: %u.", conn->connection_number, conn->local_ap2_control_port); + debug(2, "Connection %d: UDP control port opened: %u.", conn->connection_number, + conn->local_ap2_control_port); pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); @@ -2829,7 +2837,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (err) { die("Error %d: could not find a UDP port to use as a realtime_audio port", err); } - debug(2, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, conn->local_realtime_audio_port); + debug(2, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, + conn->local_realtime_audio_port); pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, (void *)conn); @@ -2892,7 +2901,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); + debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, + conn->local_buffered_audio_port); // hack. conn->max_frames_per_packet = 352; // number of audio frames per packet. @@ -3117,7 +3127,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, conn->initial_airplay_volume = volume; conn->initial_airplay_volume_set = 1; } - } else if (strncmp(cp, "progress: ", strlen("progress: ")) == 0) { // this can be sent even when metadata is not solicited + } else if (strncmp(cp, "progress: ", strlen("progress: ")) == + 0) { // this can be sent even when metadata is not solicited #ifdef CONFIG_METADATA char *progress = cp + strlen("progress: "); @@ -3126,8 +3137,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, send_ssnc_metadata('prgr', progress, strlen(progress), 1); #endif - } else - { + } else { debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, strlen(cp)); } @@ -3381,6 +3391,8 @@ void metadata_multicast_process(uint32_t type, uint32_t code, char *data, uint32 // ("ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data) uint32_t chunk_ix = 0; + if (config.metadata_sockmsglength == 24) + die("A divide by zero almost occurred (config.metadata_sockmsglength = 24)."); uint32_t chunk_total = length / (config.metadata_sockmsglength - 24); if (chunk_total * (config.metadata_sockmsglength - 24) < length) { chunk_total++; @@ -3673,7 +3685,8 @@ void metadata_init(void) { if (pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL) != 0) debug(1, "Failed to create metadata thread!"); - if (pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL) != 0) + if (pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, + NULL) != 0) debug(1, "Failed to create metadata multicast thread!"); } #ifdef CONFIG_METADATA_HUB @@ -4513,8 +4526,6 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { close(conn->audio_socket); } - - if (conn->fd > 0) { debug(3, "Connection %d terminating: closing fd %d.", conn->connection_number, conn->fd); close(conn->fd); @@ -4674,7 +4685,9 @@ static void *rtsp_conversation_thread_func(void *pconn) { } } if (method_selected == 0) { - debug(1, "Connection %d: Unrecognised and unhandled rtsp request \"%s\". HTTP Response Code 501 (\"Not Implemented\") returned.", + debug(1, + "Connection %d: Unrecognised and unhandled rtsp request \"%s\". HTTP Response Code " + "501 (\"Not Implemented\") returned.", conn->connection_number, req->method); int y = req->contentlength; @@ -4693,7 +4706,6 @@ static void *rtsp_conversation_thread_func(void *pconn) { *obfp = 0; debug(1, "Content: \"%s\".", obf); } - } } debug(debug_level, "Connection %d: RTSP Response:", conn->connection_number); diff --git a/shairport.c b/shairport.c index edcb5e10..4afcd86f 100644 --- a/shairport.c +++ b/shairport.c @@ -1920,7 +1920,7 @@ int main(int argc, char **argv) { /* Version check should be the very first call because it makes sure that important subsystems are initialized. #define NEED_LIBGCRYPT_VERSION to the minimum required version. */ - + #define NEED_LIBGCRYPT_VERSION "1.5.4" if (!gcry_check_version(NEED_LIBGCRYPT_VERSION)) { From 5f8e6e3989b55134c9894db100bf297f81d47f0e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 Nov 2021 12:14:23 +0000 Subject: [PATCH 424/878] attempt to increase the precision of the FPS timing --- audio.h | 4 +- audio_alsa.c | 45 +++++++++- player.c | 228 ++++++++++++++++++++++++++------------------------- 3 files changed, 162 insertions(+), 115 deletions(-) diff --git a/audio.h b/audio.h index 3413250c..f6ddcd9e 100644 --- a/audio.h +++ b/audio.h @@ -39,8 +39,8 @@ typedef struct { // will change dynamically, so keep watching it. Implemented in ALSA only. // returns a negative error code if there's a problem int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long - int (*rate_info)(uint64_t *elapsed_time, - uint64_t *frames_played); // use this to get the true rate of the DAC + int (*rate_info)(uint64_t *measurement_time, uint64_t *delay, + uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC // may be NULL, in which case soft volume is applied void (*volume)(double vol); diff --git a/audio_alsa.c b/audio_alsa.c index a2126c36..0e4268ed 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -60,8 +60,7 @@ static int play(void *buf, int samples); static void stop(void); static void flush(void); int delay(long *the_delay); -int get_frames_sent_for_output(__attribute__((unused)) uint64_t *elapsed_time, - uint64_t *frames_sent_to_dac); +int play_stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac); // int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); void *alsa_buffer_monitor_thread_code(void *arg); @@ -90,7 +89,7 @@ audio_output audio_alsa = { .flush = &flush, .delay = &delay, .play = &play, - .rate_info = &get_frames_sent_for_output, // will also include frames of silence sent to stop + .rate_info = &play_stats, // will also include frames of silence sent to stop // standby mode // .rate_info = NULL, .mute = NULL, // a function will be provided if it can, and is allowed to, @@ -1630,6 +1629,46 @@ int delay(long *the_delay) { return ret; } +int play_stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) { + // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or + // SND_PCM_STATE_PREPARED + // or SND_PCM_STATE_DRAINING + // and returns the actual delay if running or 0 if prepared in *the_delay + + // otherwise return an error code + // the error code could be a Unix errno code or a snderror code, or + // the sps_extra_code_output_stalled or the + // sps_extra_code_output_state_cannot_make_ready codes + int ret = 0; + *the_delay = 0; + + int oldState; + + snd_pcm_state_t state; + snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); + + if (alsa_handle == NULL) { + ret = ENODEV; + } else { + *the_time = get_absolute_time_in_ns(); + ret = delay_and_status(&state, &my_delay, NULL); + } + if (ret == 0) + ret = output_error_occurred; // will be zero unless an error like an underrun occurred + output_error_occurred = 0; // reset it. + if (frames_sent_to_dac != NULL) + *frames_sent_to_dac = frames_sent_for_playing; + debug_mutex_unlock(&alsa_mutex, 0); + pthread_cleanup_pop(0); + pthread_setcancelstate(oldState, NULL); + uint64_t hd = my_delay; // note: snd_pcm_sframes_t is a long + *the_delay = hd; + return ret; +} + int get_frames_sent_for_output(__attribute__((unused)) uint64_t *elapsed_time, uint64_t *frames_sent_to_dac) { int ret = 0; diff --git a/player.c b/player.c index 07ee1123..c6e360e8 100644 --- a/player.c +++ b/player.c @@ -2287,6 +2287,124 @@ void *player_thread_func(void *arg) { if (conn->buffer_occupancy > maximum_buffer_occupancy) maximum_buffer_occupancy = conn->buffer_occupancy; + + + // now, before outputting anything to the output device, check the stats + + if (play_number % print_interval == 0) { + + // here, calculate the input and output frame rates, where possible, even if statistics + // have not been requested + // this is to calculate them in case they are needed by the D-Bus interface or elsewhere. + + if (conn->input_frame_rate_starting_point_is_valid) { + uint64_t elapsed_reception_time, frames_received; + elapsed_reception_time = + conn->frames_inward_measurement_time - conn->frames_inward_measurement_start_time; + frames_received = conn->frames_inward_frames_received_at_measurement_time - + conn->frames_inward_frames_received_at_measurement_start_time; + conn->input_frame_rate = + (1.0E9 * frames_received) / + elapsed_reception_time; // an IEEE double calculation with two 64-bit integers + } else { + conn->input_frame_rate = 0.0; + } + + int stats_status = -1; + if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) { + uint64_t frames_sent_for_play; + uint64_t measurement_time; + uint64_t actual_delay; + stats_status = config.output->rate_info(&measurement_time, &actual_delay, &frames_sent_for_play); + // debug(1,"actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); + uint64_t frames_played = frames_sent_for_play - actual_delay; + // If the status is zero, it means that there were no output problems since the + // last time the rate_info call was made. Thus, the frame rate should be valid. + if ((stats_status == 0) && (previous_frames_played_valid)) { + uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; + uint64_t interval = measurement_time - previous_frames_played_time; + // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRIu64 ".", frames_played_in_this_interval, interval); + conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; + conn->frame_rate_valid = 1; + } + + // uncomment the if statement if your want to get as long a period for + // calculating the frame rate as possible without an output break or error + // if ((status != 0) || (previous_frames_played_valid == 0)) { + // if we have just detected an outputting error, or if we have no + // starting information + if (stats_status != 0) + conn->frame_rate_valid = 0; + previous_frames_played = frames_played; + previous_frames_played_time = measurement_time; + previous_frames_played_valid = 1; + // } + } + + // we can now calculate running averages for sync error (frames), corrections (ppm), + // insertions plus deletions (ppm), drift (ppm) + double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; + double moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; + double moving_average_insertions_plus_deletions = + (1.0 * tsum_of_insertions_and_deletions) / number_of_statistics; + // double moving_average_drift = (1.0 * tsum_of_drifts) / number_of_statistics; + // if ((play_number/print_interval)%20==0) + // figure out which statistics profile to use, depending on the kind of stream + + if (config.statistics_requested) { + + if (at_least_one_frame_seen) { + do { + line_of_stats[0] = '\0'; + statistics_column = 0; + was_a_previous_column = 0; + statistics_item("sync error ms", "%*.2f", 13, + 1000 * moving_average_sync_error / config.output_rate); + statistics_item("net sync ppm", "%*.1f", 12, + moving_average_correction * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item("all sync ppm", "%*.1f", 12, + moving_average_insertions_plus_deletions * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item(" packets", "%*d", 11, play_number); + statistics_item("missing", "%*" PRIu64 "", 7, conn->missing_packets); + statistics_item(" late", "%*" PRIu64 "", 6, conn->late_packets); + statistics_item("too late", "%*" PRIu64 "", 8, conn->too_late_packets); + statistics_item("resend reqs", "%*" PRIu64 "", 11, conn->resend_requests); + statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); + statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); + statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); + statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); + statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); + if (conn->frame_rate_valid) + statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); + else + statistics_item(" output fps", " N/A"); + statistics_item("source drift ppm", "%*.2f", 16, + (conn->local_to_remote_time_gradient - 1.0) * 1000000); + statistics_item("drift samples", "%*d", 13, + conn->local_to_remote_time_gradient_sample_count); + statistics_item( + "estimated (unused) correction ppm", "%*.2f", + strlen("estimated (unused) correction ppm"), + (conn->frame_rate_valid != 0) + ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * + conn->local_to_remote_time_gradient) * + 1000000) / + conn->frame_rate + : 0.0); + statistics_row++; + inform(line_of_stats); + } while (statistics_row < 2); + } else { + inform("No frames received in the last sampling interval."); + } + } + minimum_dac_queue_size = UINT64_MAX; // hack reset + maximum_buffer_occupancy = INT32_MIN; // can't be less than this + minimum_buffer_occupancy = INT32_MAX; // can't be more than this + at_least_one_frame_seen = 0; + } // here, we want to check (a) if we are meant to do synchronisation, // (b) if we have a delay procedure, (c) if we can get the delay. @@ -2771,116 +2889,6 @@ void *player_thread_func(void *arg) { number_of_statistics++; } } - if (play_number % print_interval == 0) { - - // here, calculate the input and output frame rates, where possible, even if statistics - // have not been requested - // this is to calculate them in case they are needed by the D-Bus interface or elsewhere. - - if (conn->input_frame_rate_starting_point_is_valid) { - uint64_t elapsed_reception_time, frames_received; - elapsed_reception_time = - conn->frames_inward_measurement_time - conn->frames_inward_measurement_start_time; - frames_received = conn->frames_inward_frames_received_at_measurement_time - - conn->frames_inward_frames_received_at_measurement_start_time; - conn->input_frame_rate = - (1.0E9 * frames_received) / - elapsed_reception_time; // an IEEE double calculation with two 64-bit integers - } else { - conn->input_frame_rate = 0.0; - } - - uint64_t frames_sent_for_play; - int status = -1; - if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) { - uint64_t monotonic_time_now = get_monotonic_time_in_ns(); - uint64_t elapsed_play_time; // dummy - status = config.output->rate_info(&elapsed_play_time, &frames_sent_for_play); - uint64_t frames_played = frames_sent_for_play - play_samples - current_delay; - // If the status is zero, it means that there were no output problems since the - // last time the rate_info call was made. Thus, the frame rate should be valid. - if ((status == 0) && (previous_frames_played_valid)) { - uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; - uint64_t interval = monotonic_time_now - previous_frames_played_time; - conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; - conn->frame_rate_valid = 1; - } - - // uncomment the if statement if your want to get as long a period for - // calculating the frame rate - // if ((status != 0) || (previous_frames_played_valid == 0)) { - // if we have just detected an outputting error, or if we have no - // starting information - previous_frames_played = frames_played; - previous_frames_played_time = monotonic_time_now; - previous_frames_played_valid = 1; - //} - } - - // we can now calculate running averages for sync error (frames), corrections (ppm), - // insertions plus deletions (ppm), drift (ppm) - double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; - double moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; - double moving_average_insertions_plus_deletions = - (1.0 * tsum_of_insertions_and_deletions) / number_of_statistics; - // double moving_average_drift = (1.0 * tsum_of_drifts) / number_of_statistics; - // if ((play_number/print_interval)%20==0) - // figure out which statistics profile to use, depending on the kind of stream - - if (config.statistics_requested) { - - if (at_least_one_frame_seen) { - do { - line_of_stats[0] = '\0'; - statistics_column = 0; - was_a_previous_column = 0; - statistics_item("sync error ms", "%*.2f", 13, - 1000 * moving_average_sync_error / config.output_rate); - statistics_item("net sync ppm", "%*.1f", 12, - moving_average_correction * 1000000 / - (352 * conn->output_sample_ratio)); - statistics_item("all sync ppm", "%*.1f", 12, - moving_average_insertions_plus_deletions * 1000000 / - (352 * conn->output_sample_ratio)); - statistics_item(" packets", "%*d", 11, play_number); - statistics_item("missing", "%*" PRIu64 "", 7, conn->missing_packets); - statistics_item(" late", "%*" PRIu64 "", 6, conn->late_packets); - statistics_item("too late", "%*" PRIu64 "", 8, conn->too_late_packets); - statistics_item("resend reqs", "%*" PRIu64 "", 11, conn->resend_requests); - statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); - statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); - statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); - statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); - statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); - if (conn->frame_rate_valid) - statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); - else - statistics_item(" output fps", " N/A"); - statistics_item("source drift ppm", "%*.2f", 16, - (conn->local_to_remote_time_gradient - 1.0) * 1000000); - statistics_item("drift samples", "%*d", 13, - conn->local_to_remote_time_gradient_sample_count); - statistics_item( - "estimated (unused) correction ppm", "%*.2f", - strlen("estimated (unused) correction ppm"), - (conn->frame_rate_valid != 0) - ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * - conn->local_to_remote_time_gradient) * - 1000000) / - conn->frame_rate - : 0.0); - statistics_row++; - inform(line_of_stats); - } while (statistics_row < 2); - } else { - inform("No frames received in the last sampling interval."); - } - } - minimum_dac_queue_size = UINT64_MAX; // hack reset - maximum_buffer_occupancy = INT32_MIN; // can't be less than this - minimum_buffer_occupancy = INT32_MAX; // can't be more than this - at_least_one_frame_seen = 0; - } } } } From cd9da86fc30de35cc5f32640c9a2a83f787ed8ce Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 Nov 2021 13:59:16 +0000 Subject: [PATCH 425/878] Add a 'stats' function to the audio back ends to replace the 'rate_info' function. Zero all except the alsa back end. --- audio.h | 2 +- audio_alsa.c | 6 +++--- audio_ao.c | 1 + audio_dummy.c | 1 + audio_jack.c | 1 + audio_pa.c | 1 + audio_pipe.c | 1 + audio_pw.c | 1 + audio_sndio.c | 1 + audio_soundio.c | 1 + audio_stdout.c | 1 + player.c | 8 ++++---- 12 files changed, 17 insertions(+), 8 deletions(-) diff --git a/audio.h b/audio.h index f6ddcd9e..ad574adb 100644 --- a/audio.h +++ b/audio.h @@ -39,7 +39,7 @@ typedef struct { // will change dynamically, so keep watching it. Implemented in ALSA only. // returns a negative error code if there's a problem int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long - int (*rate_info)(uint64_t *measurement_time, uint64_t *delay, + int (*stats)(uint64_t *measurement_time, uint64_t *delay, uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC // may be NULL, in which case soft volume is applied diff --git a/audio_alsa.c b/audio_alsa.c index 0e4268ed..3fcbde66 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -60,7 +60,7 @@ static int play(void *buf, int samples); static void stop(void); static void flush(void); int delay(long *the_delay); -int play_stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac); +int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac); // int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); void *alsa_buffer_monitor_thread_code(void *arg); @@ -89,7 +89,7 @@ audio_output audio_alsa = { .flush = &flush, .delay = &delay, .play = &play, - .rate_info = &play_stats, // will also include frames of silence sent to stop + .stats = &stats, // will also include frames of silence sent to stop // standby mode // .rate_info = NULL, .mute = NULL, // a function will be provided if it can, and is allowed to, @@ -1629,7 +1629,7 @@ int delay(long *the_delay) { return ret; } -int play_stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) { +int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) { // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or // SND_PCM_STATE_PREPARED // or SND_PCM_STATE_DRAINING diff --git a/audio_ao.c b/audio_ao.c index 38953b69..40a94fdc 100644 --- a/audio_ao.c +++ b/audio_ao.c @@ -135,6 +135,7 @@ audio_output audio_ao = {.name = "ao", .is_running = NULL, .flush = NULL, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_dummy.c b/audio_dummy.c index 5d16bf95..70ea755e 100644 --- a/audio_dummy.c +++ b/audio_dummy.c @@ -58,6 +58,7 @@ audio_output audio_dummy = {.name = "dummy", .is_running = NULL, .flush = NULL, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_jack.c b/audio_jack.c index 21c3e543..e87e16a3 100644 --- a/audio_jack.c +++ b/audio_jack.c @@ -63,6 +63,7 @@ audio_output audio_jack = {.name = "jack", .is_running = NULL, .flush = &jack_flush, .delay = &jack_delay, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_pa.c b/audio_pa.c index 951860be..9d37bc60 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -315,6 +315,7 @@ audio_output audio_pa = {.name = "pa", .is_running = NULL, .flush = &flush, .delay = &pa_delay, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_pipe.c b/audio_pipe.c index a7925fbc..fcece96b 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -151,6 +151,7 @@ audio_output audio_pipe = {.name = "pipe", .is_running = NULL, .flush = NULL, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_pw.c b/audio_pw.c index f25c0456..52e6a882 100644 --- a/audio_pw.c +++ b/audio_pw.c @@ -481,6 +481,7 @@ audio_output audio_pw = {.name = "pw", .is_running = NULL, .flush = &flush, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_sndio.c b/audio_sndio.c index f8582c8a..e32c1f4d 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -49,6 +49,7 @@ audio_output audio_sndio = {.name = "sndio", .is_running = NULL, .flush = &flush, .delay = &delay, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/audio_soundio.c b/audio_soundio.c index 501c6db2..28108a9f 100644 --- a/audio_soundio.c +++ b/audio_soundio.c @@ -216,6 +216,7 @@ audio_output audio_soundio = {.name = "soundio", .is_running = NULL, .flush = &flush, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = ¶meters, diff --git a/audio_stdout.c b/audio_stdout.c index 8b37883e..61fdc744 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -84,6 +84,7 @@ audio_output audio_stdout = {.name = "stdout", .is_running = NULL, .flush = NULL, .delay = NULL, + .stats = NULL, .play = &play, .volume = NULL, .parameters = NULL, diff --git a/player.c b/player.c index c6e360e8..bc2956e0 100644 --- a/player.c +++ b/player.c @@ -2019,7 +2019,7 @@ void *player_thread_func(void *arg) { } } else { #endif - if (config.output->delay) { + if (config.output->stats) { if (config.no_sync == 0) statistics_print_profile = ap1_synced_statistics_print_profile; else @@ -2311,15 +2311,15 @@ void *player_thread_func(void *arg) { } int stats_status = -1; - if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) { + if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { uint64_t frames_sent_for_play; uint64_t measurement_time; uint64_t actual_delay; - stats_status = config.output->rate_info(&measurement_time, &actual_delay, &frames_sent_for_play); + stats_status = config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); // debug(1,"actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); uint64_t frames_played = frames_sent_for_play - actual_delay; // If the status is zero, it means that there were no output problems since the - // last time the rate_info call was made. Thus, the frame rate should be valid. + // last time the stats call was made. Thus, the frame rate should be valid. if ((stats_status == 0) && (previous_frames_played_valid)) { uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; uint64_t interval = measurement_time - previous_frames_played_time; From 1797b4164b94f479fdf39474cc0ab2347312a999 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 Nov 2021 14:18:13 +0000 Subject: [PATCH 426/878] Check for the stats function before allowing expanded stats info. --- player.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player.c b/player.c index bc2956e0..a8fd3a54 100644 --- a/player.c +++ b/player.c @@ -1999,7 +1999,7 @@ void *player_thread_func(void *arg) { #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_type == ap_2) { if (conn->airplay_stream_type == realtime_stream) { - if (config.output->delay) { + if (config.output->stats) { if (config.no_sync == 0) statistics_print_profile = ap2_realtime_synced_stream_statistics_print_profile; else @@ -2008,7 +2008,7 @@ void *player_thread_func(void *arg) { statistics_print_profile = ap2_realtime_nodelay_stream_statistics_print_profile; } } else { - if (config.output->delay) { + if (config.output->stats) { if (config.no_sync == 0) statistics_print_profile = ap2_buffered_synced_stream_statistics_print_profile; else From 881a5ca25e2ed899cc25d65f47412a3f7d5042b5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 Nov 2021 15:11:57 +0000 Subject: [PATCH 427/878] Don't display output FPS, as it's not clear how accurate it is... --- player.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/player.c b/player.c index a8fd3a54..41010643 100644 --- a/player.c +++ b/player.c @@ -1545,7 +1545,9 @@ int *statistics_print_profile; // these arrays specify which of the statistics specified by the statistics_item calls will actually // be printed -- 1 means print, 0 means skip -int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + +// don't display output fps -- not sure how accurate it is (change item 14 and 17 to 1 to restore) +int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; int ap1_nosync_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0}; @@ -1556,8 +1558,9 @@ int ap2_realtime_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1 int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0}; +// don't display output fps -- not sure how accurate it is (change item 14 1 to restore) int ap2_buffered_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 1, 0, 0, 0}; + 1, 1, 0, 0, 0, 0, 0, 0}; int ap2_buffered_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}; int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, From 14bfba27242545c7c3aa426528da730ab05a7e05 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 21 Nov 2021 21:47:32 +0000 Subject: [PATCH 428/878] Add code to check divisions and mods for a potrntial divide by zero problem. MAy cause SPS to terminate but leave a log message. --- audio.h | 2 +- audio_alsa.c | 12 +-- audio_sndio.c | 7 ++ audio_soundio.c | 2 + common.c | 26 ++++-- dacp.c | 14 +-- player.c | 244 +++++++++++++++++++++++++++--------------------- player.h | 1 + rtp.c | 16 +++- shairport.c | 15 ++- 10 files changed, 208 insertions(+), 131 deletions(-) diff --git a/audio.h b/audio.h index ad574adb..25742d7b 100644 --- a/audio.h +++ b/audio.h @@ -40,7 +40,7 @@ typedef struct { // returns a negative error code if there's a problem int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long int (*stats)(uint64_t *measurement_time, uint64_t *delay, - uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC + uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC // may be NULL, in which case soft volume is applied void (*volume)(double vol); diff --git a/audio_alsa.c b/audio_alsa.c index 3fcbde66..caaad899 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -89,9 +89,9 @@ audio_output audio_alsa = { .flush = &flush, .delay = &delay, .play = &play, - .stats = &stats, // will also include frames of silence sent to stop - // standby mode - // .rate_info = NULL, + .stats = &stats, // will also include frames of silence sent to stop + // standby mode + // .rate_info = NULL, .mute = NULL, // a function will be provided if it can, and is allowed to, // do hardware mute .volume = NULL, // a function will be provided if it can do hardware volume @@ -781,8 +781,8 @@ int actual_open_alsa_device(int do_auto_setup) { if ((snd_pcm_hw_params_get_rate_numden(alsa_params, &uval, &uval2) == 0) && (uval2 != 0)) // watch for a divide by zero too! debug(log_level, " precise (rational) rate = %.3f frames per second (i.e. %u/%u).", uval, - uval2, ((double)uval) / uval2); - else + uval2, ((double)uval) / uval2); + else debug(log_level, " precise (rational) rate information unavailable."); snd_pcm_hw_params_get_period_time(alsa_params, &uval, &dir); @@ -1570,7 +1570,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, ((uint64_t)frames_played_since_last_interrupt_sized != frames_played_since_last_interrupt)) debug(1, - "overflow resizing frames_played_since_last_interrupt % " PRIx64 + "overflow resizing frames_played_since_last_interrupt %" PRIx64 " to frames_played_since_last_interrupt %lx.", frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); delay_temp = delay_temp - frames_played_since_last_interrupt_sized; diff --git a/audio_sndio.c b/audio_sndio.c index e32c1f4d..f710e200 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -204,12 +204,19 @@ static int init(int argc, char **argv) { framesize = par.bps * par.pchan; config.output_rate = par.rate; + if (par.rate == 0) { + die("sndio: par.rate set to zero."); + } + config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate; config.audio_backend_latency_offset = 0; sio_onmove(hdl, onmove_cb, NULL); pthread_mutex_unlock(&sndio_mutex); + if (framesize == 0) { + die("sndio: framesize set to zero."); + } return 0; } diff --git a/audio_soundio.c b/audio_soundio.c index 28108a9f..ccf44bf7 100644 --- a/audio_soundio.c +++ b/audio_soundio.c @@ -22,6 +22,8 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m char *read_ptr = soundio_ring_buffer_read_ptr(ring_buffer); int fill_bytes = soundio_ring_buffer_fill_count(ring_buffer); + if (outstream->bytes_per_frame == 0) + die("soundio: outstream->bytes_per_frame is zero."); int fill_count = fill_bytes / outstream->bytes_per_frame; debug(3, diff --git a/common.c b/common.c index b1525a31..a947be41 100644 --- a/common.c +++ b/common.c @@ -1187,6 +1187,8 @@ double vol2attn(double vol, long max_db, long min_db) { int i; for (i = 0; i < order; i++) { if (vol <= lines[i][0]) { + if ((-30 - lines[i][0]) == 0.0) + die("(-30 - lines[%d][0]) == 0.0!", i); double tvol = lines[i][1] * (vol - lines[i][0]) / (-30 - lines[i][0]); // debug(1,"On line %d, end point of %f, input vol %f yields output vol // %f.",i,lines[i][1],vol,tvol); @@ -1246,8 +1248,10 @@ uint64_t get_absolute_time_in_fp() { // Do the maths. We hope that the multiplication doesn't // overflow; the price you pay for working in fixed point. - // this gives us nanoseconds - uint64_t time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; + if (sTimebaseInfo.denom == 0) + die("could not initialise Mac timebase info in get_absolute_time_in_fp().") + // this gives us nanoseconds + uint64_t time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; // take the units and shift them to the upper half of the fp, and take the nanoseconds, shift them // to the upper half and then divide the result to 1000000000 @@ -1291,11 +1295,14 @@ uint64_t get_monotonic_time_in_ns() { (void)mach_timebase_info(&sTimebaseInfo); } - // Do the maths. We hope that the multiplication doesn't - // overflow; the price you pay for working in fixed point. + if (sTimebaseInfo.denom == 0) + die("could not initialise Mac timebase info in get_monotonic_time_in_ns().") - // this gives us nanoseconds - time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; + // Do the maths. We hope that the multiplication doesn't + // overflow; the price you pay for working in fixed point. + + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; #endif return time_now_ns; @@ -1336,8 +1343,11 @@ uint64_t get_absolute_time_in_ns() { // Do the maths. We hope that the multiplication doesn't // overflow; the price you pay for working in fixed point. - // this gives us nanoseconds - time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; + if (sTimebaseInfo.denom == 0) + die("could not initialise Mac timebase info in get_absolute_time_in_ns().") + + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; #endif return time_now_ns; diff --git a/dacp.c b/dacp.c index 35dbabd0..ef2297e2 100644 --- a/dacp.c +++ b/dacp.c @@ -1351,12 +1351,14 @@ int dacp_set_volume(int32_t vo) { machine_number, highest_other_volume); // set the overall volume to the highest one } - int32_t desired_relative_volume = - (vo * 100 + (highest_other_volume / 2)) / highest_other_volume; - debug(2, "Set our speaker volume relative to the highest volume."); - http_response = dacp_set_speaker_volume( - machine_number, - desired_relative_volume); // set the overall volume to the highest one + if (highest_other_volume != 0) { + int32_t desired_relative_volume = + (vo * 100 + (highest_other_volume / 2)) / highest_other_volume; + debug(2, "Set our speaker volume relative to the highest volume."); + http_response = dacp_set_speaker_volume( + machine_number, + desired_relative_volume); // set the overall volume to the highest one + } } } } diff --git a/player.c b/player.c index 41010643..7a66a1b3 100644 --- a/player.c +++ b/player.c @@ -235,7 +235,10 @@ int audio_packet_decode(short *dest, int *destlen, uint8_t *buf, int len, rtsp_c reply = -1; // output packet is the wrong size } - *destlen = outsize / conn->input_bytes_per_frame; + if (conn->input_bytes_per_frame != 0) + *destlen = outsize / conn->input_bytes_per_frame; + else + die("Unexpectedly, conn->input_bytes_per_frame is zero."); if ((outsize % conn->input_bytes_per_frame) != 0) debug(1, "Number of audio frames (%d) does not correspond exactly to the number of bytes (%d) " @@ -1354,6 +1357,8 @@ static inline int32_t mean_32(int32_t a, int32_t b) { // stuff: 1 means add 1; 0 means do nothing; -1 means remove 1 static int stuff_buffer_basic_32(int32_t *inptr, int length, sps_format_t l_output_format, char *outptr, int stuff, int dither, rtsp_conn_info *conn) { + if (length < 3) + die("buffer length expected to be 3 or more, but is %d!", length); int tstuff = stuff; char *l_outptr = outptr; if ((stuff > 1) || (stuff < -1) || (length < 100)) { @@ -1465,7 +1470,10 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, longest_soxr_execution_time = soxr_execution_time; stat_n += 1; double stat_delta = soxr_execution_time - stat_mean; - stat_mean += stat_delta / stat_n; + if (stat_n != 0) + stat_mean += stat_delta / stat_n; + else + warn("calculation error for stat_n"); stat_M2 += stat_delta * (soxr_execution_time - stat_mean); int i; @@ -2071,6 +2079,17 @@ void *player_thread_func(void *arg) { debug(2, "Play begin"); while (1) { + + // check a few parameters to ensure they are non-zero + if (config.output_rate == 0) + debug(1, "config.output_rate is zero!"); + if (conn->output_sample_ratio == 0) + debug(1, "conn->output_sample_ratio is zero!"); + if (conn->input_rate == 0) + debug(1, "conn->input_rate is zero!"); + if (conn->input_bytes_per_frame == 0) + debug(1, "conn->input_bytes_per_frame is zero!"); + pthread_testcancel(); // allow a pthread_cancel request to take effect. abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not // guaranteed that they'll always be executed @@ -2290,50 +2309,56 @@ void *player_thread_func(void *arg) { if (conn->buffer_occupancy > maximum_buffer_occupancy) maximum_buffer_occupancy = conn->buffer_occupancy; - - + // now, before outputting anything to the output device, check the stats - - if (play_number % print_interval == 0) { - // here, calculate the input and output frame rates, where possible, even if statistics - // have not been requested - // this is to calculate them in case they are needed by the D-Bus interface or elsewhere. + if (play_number % print_interval == 0) { - if (conn->input_frame_rate_starting_point_is_valid) { - uint64_t elapsed_reception_time, frames_received; - elapsed_reception_time = - conn->frames_inward_measurement_time - conn->frames_inward_measurement_start_time; - frames_received = conn->frames_inward_frames_received_at_measurement_time - - conn->frames_inward_frames_received_at_measurement_start_time; - conn->input_frame_rate = - (1.0E9 * frames_received) / - elapsed_reception_time; // an IEEE double calculation with two 64-bit integers - } else { - conn->input_frame_rate = 0.0; - } + // here, calculate the input and output frame rates, where possible, even if statistics + // have not been requested + // this is to calculate them in case they are needed by the D-Bus interface or + // elsewhere. - int stats_status = -1; - if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { - uint64_t frames_sent_for_play; - uint64_t measurement_time; - uint64_t actual_delay; - stats_status = config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); - // debug(1,"actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); - uint64_t frames_played = frames_sent_for_play - actual_delay; - // If the status is zero, it means that there were no output problems since the - // last time the stats call was made. Thus, the frame rate should be valid. - if ((stats_status == 0) && (previous_frames_played_valid)) { - uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; - uint64_t interval = measurement_time - previous_frames_played_time; - // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRIu64 ".", frames_played_in_this_interval, interval); - conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; - conn->frame_rate_valid = 1; + if (conn->input_frame_rate_starting_point_is_valid) { + uint64_t elapsed_reception_time, frames_received; + elapsed_reception_time = + conn->frames_inward_measurement_time - conn->frames_inward_measurement_start_time; + frames_received = conn->frames_inward_frames_received_at_measurement_time - + conn->frames_inward_frames_received_at_measurement_start_time; + conn->input_frame_rate = + (1.0E9 * frames_received) / + elapsed_reception_time; // an IEEE double calculation with two 64-bit integers + } else { + conn->input_frame_rate = 0.0; } - // uncomment the if statement if your want to get as long a period for - // calculating the frame rate as possible without an output break or error - // if ((status != 0) || (previous_frames_played_valid == 0)) { + int stats_status = -1; + if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { + uint64_t frames_sent_for_play; + uint64_t measurement_time; + uint64_t actual_delay; + stats_status = + config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); + // debug(1,"actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", + // frames_played: %" PRIu64 ".", actual_delay, frames_sent_for_play, + // frames_sent_for_play - actual_delay); + uint64_t frames_played = frames_sent_for_play - actual_delay; + // If the status is zero, it means that there were no output problems since the + // last time the stats call was made. Thus, the frame rate should be valid. + if ((stats_status == 0) && (previous_frames_played_valid)) { + uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; + uint64_t interval = measurement_time - previous_frames_played_time; + // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRIu64 ".", + // frames_played_in_this_interval, interval); + if (interval != 0) { + conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; + conn->frame_rate_valid = 1; + } + } + + // uncomment the if statement if your want to get as long a period for + // calculating the frame rate as possible without an output break or error + // if ((status != 0) || (previous_frames_played_valid == 0)) { // if we have just detected an outputting error, or if we have no // starting information if (stats_status != 0) @@ -2341,73 +2366,80 @@ void *player_thread_func(void *arg) { previous_frames_played = frames_played; previous_frames_played_time = measurement_time; previous_frames_played_valid = 1; - // } - } - - // we can now calculate running averages for sync error (frames), corrections (ppm), - // insertions plus deletions (ppm), drift (ppm) - double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; - double moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; - double moving_average_insertions_plus_deletions = - (1.0 * tsum_of_insertions_and_deletions) / number_of_statistics; - // double moving_average_drift = (1.0 * tsum_of_drifts) / number_of_statistics; - // if ((play_number/print_interval)%20==0) - // figure out which statistics profile to use, depending on the kind of stream - - if (config.statistics_requested) { - - if (at_least_one_frame_seen) { - do { - line_of_stats[0] = '\0'; - statistics_column = 0; - was_a_previous_column = 0; - statistics_item("sync error ms", "%*.2f", 13, - 1000 * moving_average_sync_error / config.output_rate); - statistics_item("net sync ppm", "%*.1f", 12, - moving_average_correction * 1000000 / - (352 * conn->output_sample_ratio)); - statistics_item("all sync ppm", "%*.1f", 12, - moving_average_insertions_plus_deletions * 1000000 / - (352 * conn->output_sample_ratio)); - statistics_item(" packets", "%*d", 11, play_number); - statistics_item("missing", "%*" PRIu64 "", 7, conn->missing_packets); - statistics_item(" late", "%*" PRIu64 "", 6, conn->late_packets); - statistics_item("too late", "%*" PRIu64 "", 8, conn->too_late_packets); - statistics_item("resend reqs", "%*" PRIu64 "", 11, conn->resend_requests); - statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); - statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); - statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); - statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); - statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); - if (conn->frame_rate_valid) - statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); - else - statistics_item(" output fps", " N/A"); - statistics_item("source drift ppm", "%*.2f", 16, - (conn->local_to_remote_time_gradient - 1.0) * 1000000); - statistics_item("drift samples", "%*d", 13, - conn->local_to_remote_time_gradient_sample_count); - statistics_item( - "estimated (unused) correction ppm", "%*.2f", - strlen("estimated (unused) correction ppm"), - (conn->frame_rate_valid != 0) - ? ((conn->frame_rate - conn->remote_frame_rate * conn->output_sample_ratio * - conn->local_to_remote_time_gradient) * - 1000000) / - conn->frame_rate - : 0.0); - statistics_row++; - inform(line_of_stats); - } while (statistics_row < 2); - } else { - inform("No frames received in the last sampling interval."); + // } } + + // we can now calculate running averages for sync error (frames), corrections (ppm), + // insertions plus deletions (ppm), drift (ppm) + double moving_average_sync_error = 0.0; + double moving_average_correction = 0.0; + double moving_average_insertions_plus_deletions = 0.0; + if (number_of_statistics == 0) { + debug(1, "number_of_statistics is zero!"); + } else { + moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; + moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; + moving_average_insertions_plus_deletions = + (1.0 * tsum_of_insertions_and_deletions) / number_of_statistics; + // double moving_average_drift = (1.0 * tsum_of_drifts) / number_of_statistics; + } + // if ((play_number/print_interval)%20==0) + // figure out which statistics profile to use, depending on the kind of stream + + if (config.statistics_requested) { + + if (at_least_one_frame_seen) { + do { + line_of_stats[0] = '\0'; + statistics_column = 0; + was_a_previous_column = 0; + statistics_item("sync error ms", "%*.2f", 13, + 1000 * moving_average_sync_error / config.output_rate); + statistics_item("net sync ppm", "%*.1f", 12, + moving_average_correction * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item("all sync ppm", "%*.1f", 12, + moving_average_insertions_plus_deletions * 1000000 / + (352 * conn->output_sample_ratio)); + statistics_item(" packets", "%*d", 11, play_number); + statistics_item("missing", "%*" PRIu64 "", 7, conn->missing_packets); + statistics_item(" late", "%*" PRIu64 "", 6, conn->late_packets); + statistics_item("too late", "%*" PRIu64 "", 8, conn->too_late_packets); + statistics_item("resend reqs", "%*" PRIu64 "", 11, conn->resend_requests); + statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); + statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); + statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); + statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); + statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); + if (conn->frame_rate_valid) + statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); + else + statistics_item(" output fps", " N/A"); + statistics_item("source drift ppm", "%*.2f", 16, + (conn->local_to_remote_time_gradient - 1.0) * 1000000); + statistics_item("drift samples", "%*d", 13, + conn->local_to_remote_time_gradient_sample_count); + statistics_item("estimated (unused) correction ppm", "%*.2f", + strlen("estimated (unused) correction ppm"), + (conn->frame_rate_valid != 0) + ? ((conn->frame_rate - + conn->remote_frame_rate * conn->output_sample_ratio * + conn->local_to_remote_time_gradient) * + 1000000) / + conn->frame_rate + : 0.0); + statistics_row++; + inform(line_of_stats); + } while (statistics_row < 2); + } else { + inform("No frames received in the last sampling interval."); + } + } + minimum_dac_queue_size = UINT64_MAX; // hack reset + maximum_buffer_occupancy = INT32_MIN; // can't be less than this + minimum_buffer_occupancy = INT32_MAX; // can't be more than this + at_least_one_frame_seen = 0; } - minimum_dac_queue_size = UINT64_MAX; // hack reset - maximum_buffer_occupancy = INT32_MIN; // can't be less than this - minimum_buffer_occupancy = INT32_MAX; // can't be more than this - at_least_one_frame_seen = 0; - } // here, we want to check (a) if we are meant to do synchronisation, // (b) if we have a delay procedure, (c) if we can get the delay. @@ -2737,7 +2769,7 @@ void *player_thread_func(void *arg) { #ifdef CONFIG_SOXR if ((current_delay < conn->dac_buffer_queue_minimum_length) || (config.packet_stuffing == ST_basic) || - (config.soxr_delay_index == 0) || // not computed yet + (config.soxr_delay_index == 0) || // not computed ((config.packet_stuffing == ST_auto) && (config.soxr_delay_index > config.soxr_delay_threshold)) // if the CPU is deemed too slow diff --git a/player.h b/player.h index 526572ef..b08a379e 100644 --- a/player.h +++ b/player.h @@ -30,6 +30,7 @@ #include "audio.h" #define time_ping_history_power_of_two 7 +// this must now be zero, otherwise bad things will happen #define time_ping_history \ (1 << time_ping_history_power_of_two) // 2^7 is 128. At 1 per three seconds, approximately six // minutes of records diff --git a/rtp.c b/rtp.c index 41b07371..a79e0bbd 100644 --- a/rtp.c +++ b/rtp.c @@ -154,7 +154,7 @@ void *rtp_audio_receiver(void *arg) { float stat_delta = time_interval_us - stat_mean; stat_mean += stat_delta / stat_n; stat_M2 += stat_delta * (time_interval_us - stat_mean); - if (stat_n % 2500 == 0) { + if ((stat_n != 1) && (stat_n % 2500 == 0)) { debug(2, "Packet reception interval stats: mean, standard deviation and max for the last " "2,500 packets in microseconds: %10.1f, %10.1f, %10.1f.", @@ -650,6 +650,8 @@ void *rtp_timing_receiver(void *arg) { double log_of_multiplier = log10(diffusion_expansion_factor) / time_ping_history; double multiplier = pow(10, log_of_multiplier); uint64_t dispersion_factor = (uint64_t)(multiplier * 100); + if (dispersion_factor == 0) + die("dispersion factor is zero!"); // debug(1,"dispersion factor is %" PRIu64 ".", dispersion_factor); // uint64_t first_local_to_remote_time_difference_time; @@ -1029,6 +1031,8 @@ const int use_nominal_rate = 0; // specify whether to use the nominal input rate int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_conn_info *conn) { int result = 1; + if (conn->input_rate == 0) + die("conn->input_rate is zero!"); uint32_t fs = conn->input_rate; *frames = fs; // default value to return *time = 1000000000; // default value to return @@ -1051,7 +1055,11 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con result = 1; } else { *frames = local_frames; + if (local_frames == 0) + die("local_frames is zero!"); *time = local_time; + if (local_time == 0) + die("local_time is zero!"); result = 0; } } @@ -1469,6 +1477,8 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * int32_t frame_difference = timestamp - anchor_rtptime; int64_t time_difference = frame_difference; time_difference = time_difference * 1000000000; + if (conn->input_rate == 0) + die("conn->input_rate is zero!"); time_difference = time_difference / conn->input_rate; uint64_t ltime = anchor_local_time + time_difference; *time = ltime; @@ -2246,6 +2256,10 @@ void *rtp_buffered_audio_processor(void *arg) { double requested_lead_time = 0.3; // normal lead time minimum reset_buffer(conn); // in case there is any garbage in the player // int not_first_time_out = 0; + + // quick check of parameters + if (conn->input_bytes_per_frame == 0) + die("conn->input_bytes_per_frame is zero!"); do { int flush_is_delayed = 0; int flush_newly_requested = 0; diff --git a/shairport.c b/shairport.c index 4afcd86f..9b7648e3 100644 --- a/shairport.c +++ b/shairport.c @@ -202,15 +202,24 @@ void *soxr_time_check(__attribute__((unused)) void *arg) { double soxr_execution_time_ns = (get_absolute_time_in_ns() - soxr_start_time) * 1.0; // free(outbuffer); // free(inbuffer); - config.soxr_delay_index = (int)(0.9 + soxr_execution_time_ns / (number_of_iterations * 1000000)); + if (number_of_iterations != 0) { + config.soxr_delay_index = + (int)(0.9 + soxr_execution_time_ns / (number_of_iterations * 1000000)); + } else { + debug(1, "No soxr-timing iterations performed, so \"basic\" iteration will be used."); + config.soxr_delay_index = 0; // used as a flag + } debug(2, "soxr_delay_index: %d.", config.soxr_delay_index); if ((config.packet_stuffing == ST_soxr) && (config.soxr_delay_index > config.soxr_delay_threshold)) inform("Note: this device may be too slow for \"soxr\" interpolation. Consider choosing the " "\"basic\" or \"auto\" interpolation setting."); if (config.packet_stuffing == ST_auto) - debug(1, "\"%s\" interpolation has been chosen.", - config.soxr_delay_index <= config.soxr_delay_threshold ? "soxr" : "basic"); + debug( + 1, "\"%s\" interpolation has been chosen.", + ((config.soxr_delay_index != 0) && (config.soxr_delay_index <= config.soxr_delay_threshold)) + ? "soxr" + : "basic"); pthread_exit(NULL); } From 9065ebc6f090f09118eabaed0756e9e0c4f241e6 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 24 Nov 2021 16:21:34 +0100 Subject: [PATCH 429/878] Try finding libplist as libplist-2.0 on Linux too --- configure.ac | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 1d26d299..e0dcb10f 100644 --- a/configure.ac +++ b/configure.ac @@ -403,12 +403,11 @@ 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_MSG_RESULT(>>Include libraries required for AirPlay 2) - if test "x${with_os}" = xlinux ; then - PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- libplist-dev suggested!)]) - fi - if test "x${with_os}" = xfreebsd ; then - 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-2.2.0 or later!)]) - fi + 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!) + ]) + ]) PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) AC_CHECK_LIB([gcrypt], [gcry_control], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) # PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) From ce39f00a0b9496a1945967e3b4014753e40f6b1e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 25 Nov 2021 17:42:48 +0000 Subject: [PATCH 430/878] Don't overwrite service files if they already exist. --- Makefile.am | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile.am b/Makefile.am index dd8a1bac..c8d53440 100644 --- a/Makefile.am +++ b/Makefile.am @@ -230,13 +230,13 @@ if INSTALL_SYSTEMV getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null [ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d - cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ + [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ endif if INSTALL_SYSTEMD getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null [ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir) - cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) endif if INSTALL_FREEBSD_SERVICE # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." @@ -244,9 +244,9 @@ if INSTALL_FREEBSD_SERVICE pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1 [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync chown shairport-sync:shairport-sync /var/run/shairport-sync - cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync + [ -e /usr/local/etc/rc.d/shairport_sync ] || cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync chmod 555 /usr/local/etc/rc.d/shairport_sync endif if INSTALL_CYGWIN_SERVICE - cp scripts/shairport-sync-config /usr/local/bin + [ -e /usr/local/bin/shairport-sync-config ] || cp scripts/shairport-sync-config /usr/local/bin endif From 87d887f4125d681e5969c94f703f4c93d0518a34 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:13:56 +0000 Subject: [PATCH 431/878] Remove the commas in the statistics logs -- easier to get into a spreadsheet. --- player.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/player.c b/player.c index 7a66a1b3..824aa871 100644 --- a/player.c +++ b/player.c @@ -1555,7 +1555,7 @@ int *statistics_print_profile; // be printed -- 1 means print, 0 means skip // don't display output fps -- not sure how accurate it is (change item 14 and 17 to 1 to restore) -int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; +int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; int ap1_nosync_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0}; @@ -1568,7 +1568,7 @@ int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, // don't display output fps -- not sure how accurate it is (change item 14 1 to restore) int ap2_buffered_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 0, 0, 0, 0}; + 1, 1, 0, 0, 1, 0, 0, 0}; int ap2_buffered_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}; int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, @@ -1577,7 +1577,7 @@ int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, void statistics_item(const char *heading, const char *format, ...) { if (statistics_print_profile[statistics_column] == 1) { // include this column? if (was_a_previous_column != 0) - strcat(line_of_stats, ", "); + strcat(line_of_stats, " "); if (statistics_row == 0) { strcat(line_of_stats, heading); } else { @@ -2358,15 +2358,15 @@ void *player_thread_func(void *arg) { // uncomment the if statement if your want to get as long a period for // calculating the frame rate as possible without an output break or error - // if ((status != 0) || (previous_frames_played_valid == 0)) { - // if we have just detected an outputting error, or if we have no - // starting information - if (stats_status != 0) - conn->frame_rate_valid = 0; - previous_frames_played = frames_played; - previous_frames_played_time = measurement_time; - previous_frames_played_valid = 1; - // } + if ((stats_status != 0) || (previous_frames_played_valid == 0)) { + // if we have just detected an outputting error, or if we have no + // starting information + if (stats_status != 0) + conn->frame_rate_valid = 0; + previous_frames_played = frames_played; + previous_frames_played_time = measurement_time; + previous_frames_played_valid = 1; + } } // we can now calculate running averages for sync error (frames), corrections (ppm), From 136071a62180c5d5215678d6d00638240f1321bd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 26 Nov 2021 15:17:27 +0000 Subject: [PATCH 432/878] change stats to report non-zero if output has just opened or if there has been any discontinuity in play from previous time. --- audio_alsa.c | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index caaad899..0910b9e7 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -248,8 +248,12 @@ static snd_pcm_uframes_t period_size_requested, buffer_size_requested; static int set_period_size_request, set_buffer_size_request; static uint64_t frames_sent_for_playing; -static int output_error_occurred; // set to true if an underrun or similar has occurred since last - // requested + + // set to true if there has been a discontinuity between the last reported frames_sent_for_playing + // and the present reported frames_sent_for_playing + // Not that it will be set when the device is opened, as any previous figures for frames_sent_for_playing + // (which Shairport Sync might hold) would be invalid. +static int frames_sent_break_occurred; static void help(void) { printf(" -d output-device set the output device, default is \"default\".\n" @@ -1632,13 +1636,10 @@ int delay(long *the_delay) { int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) { // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or // SND_PCM_STATE_PREPARED - // or SND_PCM_STATE_DRAINING - // and returns the actual delay if running or 0 if prepared in *the_delay - - // otherwise return an error code - // the error code could be a Unix errno code or a snderror code, or - // the sps_extra_code_output_stalled or the - // sps_extra_code_output_state_cannot_make_ready codes + // or SND_PCM_STATE_DRAINING. + // returns the actual delay if running or 0 if prepared in *the_delay + // returns the present value of frames_sent_for_playing + // otherwise return a non-zero value int ret = 0; *the_delay = 0; @@ -1657,8 +1658,10 @@ int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) ret = delay_and_status(&state, &my_delay, NULL); } if (ret == 0) - ret = output_error_occurred; // will be zero unless an error like an underrun occurred - output_error_occurred = 0; // reset it. + ret = frames_sent_break_occurred; // will be zero unless an error like an underrun occurred + else + ret = 1; // just indicate there was some kind of a break + frames_sent_break_occurred = 0; // reset it. if (frames_sent_to_dac != NULL) *frames_sent_to_dac = frames_sent_for_playing; debug_mutex_unlock(&alsa_mutex, 0); @@ -1668,22 +1671,6 @@ int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) *the_delay = hd; return ret; } - -int get_frames_sent_for_output(__attribute__((unused)) uint64_t *elapsed_time, - uint64_t *frames_sent_to_dac) { - int ret = 0; - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); - *frames_sent_to_dac = frames_sent_for_playing; - if (alsa_handle == NULL) - ret = ENODEV; - else - ret = output_error_occurred; // will be zero unless an error occurred - output_error_occurred = 0; // reset it. - debug_mutex_unlock(&alsa_mutex, 0); - pthread_cleanup_pop(0); - return ret; -} - /* int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { // elapsed_time is in nanoseconds @@ -1728,7 +1715,7 @@ int do_play(void *buf, int samples) { stall_monitor_frame_count += samples; frames_sent_for_playing += samples; } else { - output_error_occurred = -ret; // note than an output error has occurred + frames_sent_break_occurred = 1; // note than an output error has occurred if (ret == -EPIPE) { /* underrun */ // It could be that the DAC was in the SND_PCM_STATE_XRUN state before @@ -1802,7 +1789,8 @@ int do_open(int do_auto_setup) { // set accordingly // do_mute(0); // complete unmute } - output_error_occurred = 0; + frames_sent_break_occurred = 1; // there is a discontinuity with + // any previously-reported frame count frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. } From b9d3abfa2421143788f2474aeaed7aeaca73e034 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 26 Nov 2021 15:18:27 +0000 Subject: [PATCH 433/878] Add some debug messages to help check the output rate calculations. --- player.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/player.c b/player.c index 824aa871..e568cc87 100644 --- a/player.c +++ b/player.c @@ -2332,27 +2332,25 @@ void *player_thread_func(void *arg) { conn->input_frame_rate = 0.0; } - int stats_status = -1; + int stats_status = 0; if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { uint64_t frames_sent_for_play; uint64_t measurement_time; uint64_t actual_delay; stats_status = config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); - // debug(1,"actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", - // frames_played: %" PRIu64 ".", actual_delay, frames_sent_for_play, - // frames_sent_for_play - actual_delay); + debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); uint64_t frames_played = frames_sent_for_play - actual_delay; // If the status is zero, it means that there were no output problems since the // last time the stats call was made. Thus, the frame rate should be valid. - if ((stats_status == 0) && (previous_frames_played_valid)) { + if ((stats_status == 0) && (previous_frames_played_valid != 0)) { uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; - uint64_t interval = measurement_time - previous_frames_played_time; - // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRIu64 ".", - // frames_played_in_this_interval, interval); + int64_t interval = measurement_time - previous_frames_played_time; if (interval != 0) { conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; conn->frame_rate_valid = 1; + debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", rate: %f.", + frames_played_in_this_interval, interval, conn->frame_rate); } } From a05db62100b9d4dcd2b81d66f16f367135e4c390 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 26 Nov 2021 17:41:01 +0000 Subject: [PATCH 434/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index f07d84b7..9407ce55 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-TBA-TBA +==== +#### Bug Fix +- This fixes [#1345](https://github.com/mikebrady/shairport-sync/issues/1345) by checking for the `libplist` library both under the name `libplist` and `libplist-2.0` using the fallback strategy [as described in the autotools docs](https://autotools.io/pkgconfig/pkg_check_modules.html). Since we now try both names, we no longer need the FreeBSD/Linux check. Many thanks to [FW](https://github.com/fwcd) for the bug report, the fix and this documentation! + Version 4.1-dev-21-g1032cad6 === #### Enhancement From 7890188dcfaa307819211d1970013f472399a0cd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 26 Nov 2021 18:01:36 +0000 Subject: [PATCH 435/878] Quieten some debug messages. --- player.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index e568cc87..0991167c 100644 --- a/player.c +++ b/player.c @@ -2339,7 +2339,7 @@ void *player_thread_func(void *arg) { uint64_t actual_delay; stats_status = config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); - debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); + // debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); uint64_t frames_played = frames_sent_for_play - actual_delay; // If the status is zero, it means that there were no output problems since the // last time the stats call was made. Thus, the frame rate should be valid. @@ -2349,8 +2349,8 @@ void *player_thread_func(void *arg) { if (interval != 0) { conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; conn->frame_rate_valid = 1; - debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", rate: %f.", - frames_played_in_this_interval, interval, conn->frame_rate); + //debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", rate: %f.", + // frames_played_in_this_interval, interval, conn->frame_rate); } } From dc5b8f1a1171b18e704283f98c3dd2bf8e4a08ca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 26 Nov 2021 18:02:23 +0000 Subject: [PATCH 436/878] Clang format --- audio_alsa.c | 16 ++++++++-------- player.c | 7 +++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 0910b9e7..f2e0b131 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -249,10 +249,10 @@ static int set_period_size_request, set_buffer_size_request; static uint64_t frames_sent_for_playing; - // set to true if there has been a discontinuity between the last reported frames_sent_for_playing - // and the present reported frames_sent_for_playing - // Not that it will be set when the device is opened, as any previous figures for frames_sent_for_playing - // (which Shairport Sync might hold) would be invalid. +// set to true if there has been a discontinuity between the last reported frames_sent_for_playing +// and the present reported frames_sent_for_playing +// Not that it will be set when the device is opened, as any previous figures for +// frames_sent_for_playing (which Shairport Sync might hold) would be invalid. static int frames_sent_break_occurred; static void help(void) { @@ -1660,8 +1660,8 @@ int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) if (ret == 0) ret = frames_sent_break_occurred; // will be zero unless an error like an underrun occurred else - ret = 1; // just indicate there was some kind of a break - frames_sent_break_occurred = 0; // reset it. + ret = 1; // just indicate there was some kind of a break + frames_sent_break_occurred = 0; // reset it. if (frames_sent_to_dac != NULL) *frames_sent_to_dac = frames_sent_for_playing; debug_mutex_unlock(&alsa_mutex, 0); @@ -1716,7 +1716,7 @@ int do_play(void *buf, int samples) { frames_sent_for_playing += samples; } else { frames_sent_break_occurred = 1; // note than an output error has occurred - if (ret == -EPIPE) { /* underrun */ + if (ret == -EPIPE) { /* underrun */ // It could be that the DAC was in the SND_PCM_STATE_XRUN state before // sending the samples to be output. If so, it will still be in @@ -1790,7 +1790,7 @@ int do_open(int do_auto_setup) { // do_mute(0); // complete unmute } frames_sent_break_occurred = 1; // there is a discontinuity with - // any previously-reported frame count + // any previously-reported frame count frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. } diff --git a/player.c b/player.c index 0991167c..5e6e4133 100644 --- a/player.c +++ b/player.c @@ -2339,7 +2339,9 @@ void *player_thread_func(void *arg) { uint64_t actual_delay; stats_status = config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); - // debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, frames_sent_for_play - actual_delay); + // debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", + // frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, + // frames_sent_for_play - actual_delay); uint64_t frames_played = frames_sent_for_play - actual_delay; // If the status is zero, it means that there were no output problems since the // last time the stats call was made. Thus, the frame rate should be valid. @@ -2349,7 +2351,8 @@ void *player_thread_func(void *arg) { if (interval != 0) { conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; conn->frame_rate_valid = 1; - //debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", rate: %f.", + // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", + // rate: %f.", // frames_played_in_this_interval, interval, conn->frame_rate); } } From 56bef8e7ddf01e9d76860fafb65b6d7330c186b1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 28 Nov 2021 16:08:48 +0000 Subject: [PATCH 437/878] Use CLOCK_MONOLITHIC_RAW to avoid NTP effects, report raw and corrected timing from audio_alsa. Clean up statistics. Stop watchdog timing out AP2 sessions. CLean up some debug messages. --- activity_monitor.c | 2 +- audio.h | 3 +- audio_alsa.c | 12 ++-- common.c | 122 ++++++++++++++++++++++------------------- common.h | 9 ++- dacp.c | 2 +- nqptp-shm-structures.h | 2 +- player.c | 75 +++++++++++++++---------- player.h | 3 +- rtp.c | 2 +- rtsp.c | 4 ++ 11 files changed, 137 insertions(+), 99 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 1d301c49..deeab2b6 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -187,7 +187,7 @@ void *activity_monitor_thread_code(void *arg) { uint64_t time_to_wait_for_wakeup_ns = (uint64_t)(config.active_state_timeout * 1000000000); #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns; + uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns; sec = time_of_wakeup_ns / 1000000000; nsec = time_of_wakeup_ns % 1000000000; time_for_wait.tv_sec = sec; diff --git a/audio.h b/audio.h index 25742d7b..5ff14195 100644 --- a/audio.h +++ b/audio.h @@ -39,7 +39,8 @@ typedef struct { // will change dynamically, so keep watching it. Implemented in ALSA only. // returns a negative error code if there's a problem int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long - int (*stats)(uint64_t *measurement_time, uint64_t *delay, + int (*stats)(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, + uint64_t *delay, uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC // may be NULL, in which case soft volume is applied diff --git a/audio_alsa.c b/audio_alsa.c index f2e0b131..a007c419 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -60,8 +60,9 @@ static int play(void *buf, int samples); static void stop(void); static void flush(void); int delay(long *the_delay); -int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac); -// int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played); +int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, + uint64_t *frames_sent_to_dac); + void *alsa_buffer_monitor_thread_code(void *arg); static void volume(double vol); @@ -1633,7 +1634,8 @@ int delay(long *the_delay) { return ret; } -int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) { +int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, + uint64_t *frames_sent_to_dac) { // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or // SND_PCM_STATE_PREPARED // or SND_PCM_STATE_DRAINING. @@ -1654,7 +1656,9 @@ int stats(uint64_t *the_time, uint64_t *the_delay, uint64_t *frames_sent_to_dac) if (alsa_handle == NULL) { ret = ENODEV; } else { - *the_time = get_absolute_time_in_ns(); + *raw_measurement_time = + get_absolute_time_in_ns(); // this is not conditioned ("disciplined") by NTP + *corrected_measurement_time = get_monotonic_time_in_ns(); // this is ("disciplined") by NTP ret = delay_and_status(&state, &my_delay, NULL); } if (ret == 0) diff --git a/common.c b/common.c index a947be41..bbe36780 100644 --- a/common.c +++ b/common.c @@ -1210,58 +1210,6 @@ double vol2attn(double vol, long max_db, long min_db) { return vol_setting; } -uint64_t get_absolute_time_in_fp() { - uint64_t time_now_fp; -#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - struct timespec tn; - // can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt - clock_gettime(CLOCK_MONOTONIC, &tn); - uint64_t tnfpsec = tn.tv_sec; - if (tnfpsec > 0x100000000) - warn("clock_gettime seconds overflow!"); - uint64_t tnfpnsec = tn.tv_nsec; - if (tnfpnsec > 0x100000000) - warn("clock_gettime nanoseconds seconds overflow!"); - tnfpsec = tnfpsec << 32; - tnfpnsec = tnfpnsec << 32; - tnfpnsec = tnfpnsec / 1000000000; - - time_now_fp = tnfpsec + tnfpnsec; // types okay -#endif -#ifdef COMPILE_FOR_OSX - uint64_t time_now_mach; - uint64_t elapsedNano; - static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; - - time_now_mach = mach_absolute_time(); - - // If this is the first time we've run, get the timebase. - // We can use denom == 0 to indicate that sTimebaseInfo is - // uninitialised because it makes no sense to have a zero - // denominator in a fraction. - - if (sTimebaseInfo.denom == 0) { - debug(1, "Mac initialise timebase info."); - (void)mach_timebase_info(&sTimebaseInfo); - } - - // Do the maths. We hope that the multiplication doesn't - // overflow; the price you pay for working in fixed point. - - if (sTimebaseInfo.denom == 0) - die("could not initialise Mac timebase info in get_absolute_time_in_fp().") - // this gives us nanoseconds - uint64_t time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; - - // take the units and shift them to the upper half of the fp, and take the nanoseconds, shift them - // to the upper half and then divide the result to 1000000000 - time_now_fp = - ((time_now_ns / 1000000000) << 32) + (((time_now_ns % 1000000000) << 32) / 1000000000); - -#endif - return time_now_fp; -} - uint64_t get_monotonic_time_in_ns() { uint64_t time_now_ns; @@ -1308,10 +1256,57 @@ uint64_t get_monotonic_time_in_ns() { return time_now_ns; } -uint64_t get_absolute_time_in_ns() { +// all these clock things are now in macOS now since 10.13 (September 2017). Must update... +uint64_t get_monotonic_raw_time_in_ns() { + // CLOCK_MONOTONIC_RAW in FreeBSD etc, monotonic in MacOSX uint64_t time_now_ns; #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + struct timespec tn; + clock_gettime(CLOCK_MONOTONIC_RAW, &tn); + uint64_t tnnsec = tn.tv_sec; + tnnsec = tnnsec * 1000000000; + uint64_t tnjnsec = tn.tv_nsec; + time_now_ns = tnnsec + tnjnsec; +#endif + +#ifdef COMPILE_FOR_OSX + uint64_t time_now_mach; + uint64_t elapsedNano; + static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; + + // this actually give you a monotonic clock + // see https://news.ycombinator.com/item?id=6303755 + time_now_mach = mach_absolute_time(); + + // If this is the first time we've run, get the timebase. + // We can use denom == 0 to indicate that sTimebaseInfo is + // uninitialised because it makes no sense to have a zero + // denominator in a fraction. + + if (sTimebaseInfo.denom == 0) { + debug(1, "Mac initialise timebase info."); + (void)mach_timebase_info(&sTimebaseInfo); + } + + if (sTimebaseInfo.denom == 0) + die("could not initialise Mac timebase info in get_monotonic_time_in_ns().") + + // Do the maths. We hope that the multiplication doesn't + // overflow; the price you pay for working in fixed point. + + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; +#endif + + return time_now_ns; +} + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD +// Not defined for macOS +uint64_t get_realtime_in_ns() { + uint64_t time_now_ns; + struct timespec tn; // can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt // CLOCK_REALTIME because PTP uses it. @@ -1320,6 +1315,21 @@ uint64_t get_absolute_time_in_ns() { tnnsec = tnnsec * 1000000000; uint64_t tnjnsec = tn.tv_nsec; time_now_ns = tnnsec + tnjnsec; + return time_now_ns; +} +#endif + +uint64_t get_absolute_time_in_ns() { + // CLOCK_MONOTONIC_RAW in FreeBSD etc, monotonic in MacOSX + uint64_t time_now_ns; + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + struct timespec tn; + clock_gettime(CLOCK_MONOTONIC_RAW, &tn); + uint64_t tnnsec = tn.tv_sec; + tnnsec = tnnsec * 1000000000; + uint64_t tnjnsec = tn.tv_nsec; + time_now_ns = tnnsec + tnjnsec; #endif #ifdef COMPILE_FOR_OSX @@ -1521,8 +1531,8 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); struct timespec timeoutTime; - uint64_t wait_until_time = dally_time * 1000; // to nanoseconds - uint64_t start_time = get_absolute_time_in_ns(); // this is from CLOCK_REALTIME + uint64_t wait_until_time = dally_time * 1000; // to nanoseconds + uint64_t start_time = get_realtime_in_ns(); // this is from CLOCK_REALTIME wait_until_time = wait_until_time + start_time; uint64_t wait_until_sec = wait_until_time / 1000000000; uint64_t wait_until_nsec = wait_until_time % 1000000000; @@ -1530,7 +1540,7 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, timeoutTime.tv_nsec = wait_until_nsec; int r = pthread_mutex_timedlock(mutex, &timeoutTime); - uint64_t et = get_absolute_time_in_ns() - start_time; + uint64_t et = get_realtime_in_ns() - start_time; if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) { char errstr[1000]; diff --git a/common.h b/common.h index 199b3d41..d9be0327 100644 --- a/common.h +++ b/common.h @@ -379,9 +379,12 @@ double flat_vol2attn(double vol, long max_db, long min_db); double vol2attn(double vol, long max_db, long min_db); // return a time in nanoseconds -// uint64_t get_absolute_time_in_fp(void); // obselete -uint64_t get_absolute_time_in_ns(void); -uint64_t get_monotonic_time_in_ns(void); // to try and get precise FPS values +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD +// Not defined for macOS +uint64_t get_realtime_in_ns(void); +#endif +uint64_t get_absolute_time_in_ns(void); // monotonic_raw or monotonic +uint64_t get_monotonic_time_in_ns(void); // NTP-disciplined // time at startup for debugging timing extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message; diff --git a/dacp.c b/dacp.c index ef2297e2..d6964133 100644 --- a/dacp.c +++ b/dacp.c @@ -529,7 +529,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { (uint64_t)(1000000000 * config.missing_port_dacp_scan_interval_seconds); #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns; + uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns; uint64_t sec = time_of_wakeup_ns / 1000000000; uint64_t nsec = time_of_wakeup_ns % 1000000000; #endif diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 36c8a813..f4db8c6e 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -22,7 +22,7 @@ #define STORAGE_ID "/nqptp" #define MAX_CLOCKS 32 -#define NQPTP_SHM_STRUCTURES_VERSION 5 +#define NQPTP_SHM_STRUCTURES_VERSION 6 #define NQPTP_CONTROL_PORT 9000 // the control port will accept a UDP packet with the first letter being: diff --git a/player.c b/player.c index 5e6e4133..9b0b86ca 100644 --- a/player.c +++ b/player.c @@ -1299,7 +1299,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { time_to_wait_for_wakeup_ns /= 3; // two thirds of a packet time #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - uint64_t time_of_wakeup_ns = local_time_now + time_to_wait_for_wakeup_ns; + uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns; uint64_t sec = time_of_wakeup_ns / 1000000000; uint64_t nsec = time_of_wakeup_ns % 1000000000; @@ -1552,30 +1552,31 @@ int was_a_previous_column; int *statistics_print_profile; // these arrays specify which of the statistics specified by the statistics_item calls will actually -// be printed -- 1 means print, 0 means skip +// be printed -- 2 means print, 1 means print only in a debug mode, 0 means skip // don't display output fps -- not sure how accurate it is (change item 14 and 17 to 1 to restore) -int ap1_synced_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; -int ap1_nosync_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0}; -int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0}; +int ap1_synced_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1}; +int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; +int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap2_realtime_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 0, 0, 1, 0, 0, 0}; -int ap2_realtime_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 1, 1, 1, 1, 1, - 1, 1, 0, 0, 0, 0, 0, 0}; -int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 1, 1, 1, 1, 0, - 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, + 1, 1, 1, 1, 2, 2, 0, 0}; +int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, + 1, 1, 1, 1, 0, 0, 0, 0}; +int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, + 1, 1, 1, 1, 0, 0, 0, 0}; // don't display output fps -- not sure how accurate it is (change item 14 1 to restore) -int ap2_buffered_synced_stream_statistics_print_profile[] = {1, 1, 1, 1, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 1, 0, 0, 0}; -int ap2_buffered_nosync_stream_statistics_print_profile[] = {1, 0, 0, 1, 0, 0, 0, 0, 1, +int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 2, 2, 0, 0}; +int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}; int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0}; void statistics_item(const char *heading, const char *format, ...) { - if (statistics_print_profile[statistics_column] == 1) { // include this column? + if (((statistics_print_profile[statistics_column] == 1) && (debuglev != 0)) || + (statistics_print_profile[statistics_column] == 2)) { // include this column? if (was_a_previous_column != 0) strcat(line_of_stats, " "); if (statistics_row == 0) { @@ -1624,10 +1625,10 @@ void player_thread_cleanup_handler(void *arg) { int elapsedSec = rawSeconds % 60; if (conn->frame_rate_valid) inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, " - "output: %0.2f " + " Output: %0.2f (raw), %0.2f (corrected) " "frames per second.", conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate, - conn->frame_rate); + conn->raw_frame_rate, conn->corrected_frame_rate); else inform("Connection %d: Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f " "frames per second.", @@ -1727,7 +1728,8 @@ void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; uint64_t previous_frames_played; - uint64_t previous_frames_played_time; + uint64_t previous_raw_measurement_time; + uint64_t previous_corrected_measurement_time; int previous_frames_played_valid = 0; // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); @@ -1888,7 +1890,8 @@ void *player_thread_func(void *arg) { conn->playstart = time(NULL); - conn->frame_rate = 0.0; + conn->raw_frame_rate = 0.0; + conn->corrected_frame_rate = 0.0; conn->frame_rate_valid = 0; conn->input_frame_rate = 0.0; @@ -2335,10 +2338,12 @@ void *player_thread_func(void *arg) { int stats_status = 0; if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { uint64_t frames_sent_for_play; - uint64_t measurement_time; + uint64_t raw_measurement_time; + uint64_t corrected_measurement_time; uint64_t actual_delay; stats_status = - config.output->stats(&measurement_time, &actual_delay, &frames_sent_for_play); + config.output->stats(&raw_measurement_time, &corrected_measurement_time, + &actual_delay, &frames_sent_for_play); // debug(1,"status: %d, actual_delay: %" PRIu64 ", frames_sent_for_play: %" PRIu64 ", // frames_played: %" PRIu64 ".", stats_status, actual_delay, frames_sent_for_play, // frames_sent_for_play - actual_delay); @@ -2347,9 +2352,13 @@ void *player_thread_func(void *arg) { // last time the stats call was made. Thus, the frame rate should be valid. if ((stats_status == 0) && (previous_frames_played_valid != 0)) { uint64_t frames_played_in_this_interval = frames_played - previous_frames_played; - int64_t interval = measurement_time - previous_frames_played_time; - if (interval != 0) { - conn->frame_rate = (1e9 * frames_played_in_this_interval) / interval; + int64_t raw_interval = raw_measurement_time - previous_raw_measurement_time; + int64_t corrected_interval = + corrected_measurement_time - previous_corrected_measurement_time; + if (raw_interval != 0) { + conn->raw_frame_rate = (1e9 * frames_played_in_this_interval) / raw_interval; + conn->corrected_frame_rate = + (1e9 * frames_played_in_this_interval) / corrected_interval; conn->frame_rate_valid = 1; // debug(1,"frames_played_in_this_interval: %" PRIu64 ", interval: %" PRId64 ", // rate: %f.", @@ -2365,7 +2374,8 @@ void *player_thread_func(void *arg) { if (stats_status != 0) conn->frame_rate_valid = 0; previous_frames_played = frames_played; - previous_frames_played_time = measurement_time; + previous_raw_measurement_time = raw_measurement_time; + previous_corrected_measurement_time = corrected_measurement_time; previous_frames_played_valid = 1; } } @@ -2411,15 +2421,19 @@ void *player_thread_func(void *arg) { statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); - statistics_item(" actual fps", "%*.2f", 11, conn->input_frame_rate); - if (conn->frame_rate_valid) - statistics_item(" output fps", "%*.2f", 11, conn->frame_rate); - else - statistics_item(" output fps", " N/A"); + statistics_item("received fps", "%*.2f", 12, conn->input_frame_rate); + if (conn->frame_rate_valid) { + statistics_item("output fps (r)", "%*.2f", 14, conn->raw_frame_rate); + statistics_item("output fps (c)", "%*.2f", 14, conn->corrected_frame_rate); + } else { + statistics_item("output fps (r)", " N/A"); + statistics_item("output fps (c)", " N/A"); + } statistics_item("source drift ppm", "%*.2f", 16, (conn->local_to_remote_time_gradient - 1.0) * 1000000); statistics_item("drift samples", "%*d", 13, conn->local_to_remote_time_gradient_sample_count); + /* statistics_item("estimated (unused) correction ppm", "%*.2f", strlen("estimated (unused) correction ppm"), (conn->frame_rate_valid != 0) @@ -2429,6 +2443,7 @@ void *player_thread_func(void *arg) { 1000000) / conn->frame_rate : 0.0); + */ statistics_row++; inform(line_of_stats); } while (statistics_row < 2); diff --git a/player.h b/player.h index b08a379e..d37f00c7 100644 --- a/player.h +++ b/player.h @@ -180,7 +180,8 @@ typedef struct { stats_t *statistics; // for holding the output rate information until printed out at the end of a session - double frame_rate; + double raw_frame_rate; + double corrected_frame_rate; int frame_rate_valid; // for holding input rate information until printed out at the end of a session diff --git a/rtp.c b/rtp.c index a79e0bbd..5b79ff4b 100644 --- a/rtp.c +++ b/rtp.c @@ -1762,7 +1762,7 @@ void rtp_realtime_audio_cleanup_handler(__attribute__((unused)) void *arg) { debug(2, "Realtime Audio Receiver Cleanup Start."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; close(conn->realtime_audio_socket); - debug(1, "Connection %d: closing realtime audio port %u", conn->local_realtime_audio_port); + debug(2, "Connection %d: closing realtime audio port %u", conn->local_realtime_audio_port); conn->realtime_audio_socket = 0; debug(2, "Realtime Audio Receiver Cleanup Done."); } diff --git a/rtsp.c b/rtsp.c index 5f9fa9ab..079ad14d 100644 --- a/rtsp.c +++ b/rtsp.c @@ -583,7 +583,11 @@ void *player_watchdog_thread_code(void *arg) { do { usleep(2000000); // check every two seconds // debug(3, "Connection %d: Check the thread is doing something...", conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 + if ((config.dont_check_timeout == 0) && (config.timeout != 0) && (conn->airplay_type == ap_1)) { +#else if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { +#endif debug_mutex_lock(&conn->watchdog_mutex, 1000, 0); uint64_t last_watchdog_bark_time = conn->watchdog_bark_time; debug_mutex_unlock(&conn->watchdog_mutex, 0); From e3faba141c24bdbec191c46010c7d394453a9969 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 28 Nov 2021 21:51:26 +0000 Subject: [PATCH 438/878] log statsistics headers whenever verbosity goes on or off or whenever statistics are requested --- dbus-service.c | 4 ++++ player.h | 1 + 2 files changed, 5 insertions(+) diff --git a/dbus-service.c b/dbus-service.c index bbd5dcd4..05dd9726 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -423,6 +423,8 @@ gboolean notify_statistics_callback(ShairportSyncDiagnostics *skeleton, // debug(1, "\"notify_statistics_callback\" called."); if (shairport_sync_diagnostics_get_statistics(skeleton)) { debug(1, ">> start logging statistics"); + if (config.statistics_requested == 0) + statistics_row = 0; // redraw the header line config.statistics_requested = 1; } else { debug(1, ">> stop logging statistics"); @@ -437,6 +439,8 @@ gboolean notify_verbosity_callback(ShairportSyncDiagnostics *skeleton, if ((th >= 0) && (th <= 3)) { if (th == 0) debug(1, ">> log verbosity set to %d.", th); + if (((debuglev == 0) && (th != 0)) || ((debuglev != 0) && (th == 0))) + statistics_row = 0; // if the debug level changes, redraw the header line debuglev = th; debug(1, ">> log verbosity set to %d.", th); } else { diff --git a/player.h b/player.h index d37f00c7..edf80837 100644 --- a/player.h +++ b/player.h @@ -410,6 +410,7 @@ typedef struct { } rtsp_conn_info; extern pthread_mutex_t playing_conn_lock; +extern int statistics_row; // will be reset to zero when debug level changes or statistics enabled void reset_buffer(rtsp_conn_info *conn); From 7b8e293f8b4c8ea3a7d7aeb148ffed6b10d44288 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Dec 2021 09:01:31 +0000 Subject: [PATCH 439/878] Change behaviour after an unrecoverable error -- if not handled, exit Shairport Sync. --- player.c | 2 +- rtsp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/player.c b/player.c index 9b0b86ca..e9fa556a 100644 --- a/player.c +++ b/player.c @@ -1168,7 +1168,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (config.cmd_unfixable) { command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { - warn("an unrecoverable error, \"output_device_stalled\", has been " + die("an unrecoverable error, \"output_device_stalled\", has been " "detected.", conn->connection_number); } diff --git a/rtsp.c b/rtsp.c index 079ad14d..ae650a6a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -611,7 +611,7 @@ void *player_watchdog_thread_code(void *arg) { conn->unfixable_error_reported = 1; command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); } else { - warn("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", + die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", conn->connection_number); } } From dadeb718a336ed4ffb0cfc8cfb8997d19c50cf0c Mon Sep 17 00:00:00 2001 From: tecdude <92227828+tecdude@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:16:15 +0000 Subject: [PATCH 440/878] Update AIRPLAY2.md --- AIRPLAY2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 33411e88..32af85b9 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -17,12 +17,12 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi - AirPlay 2 for iOS, HomePod mini and Mac players. ### What Does Not Work -- Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. - No Remote Control - No AirPlay 2 for Windows iTunes ### What Partly Works - Incomplete functionality using AppleTV?. (Reports of usage welcome!) +- Lossless or High Definition Lossless material over AirPlay 2 (ALAC decoder must be installed in config step). ### Limitations - Only a single instance of Shairport Sync can run on a device. From b3526c54726c627b9e083ed0eed9b770bca3f829 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:58:22 +0000 Subject: [PATCH 441/878] Use PKG_CHECK_MODULES to find libavcodec, to make it work with Fedora 35 (?) --- configure.ac | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index e0dcb10f..7f31e167 100644 --- a/configure.ac +++ b/configure.ac @@ -413,7 +413,9 @@ if test "x$with_airplay_2" = "xyes" ; then # PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) # AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) - AC_CHECK_LIB([avcodec],[avcodec_find_encoder], [], [AC_MSG_ERROR([AirPlay 2 support requires libavcodec -- libavcodec-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_encoder], [], [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])]) From 60f830bc7541e374dc87d72cc956452c924a7a75 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:16:00 +0000 Subject: [PATCH 442/878] Tidy up the statistics printout table and stop outputting (non existent) nominal fps for AP2 Realtime Streams. --- player.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/player.c b/player.c index e9fa556a..257d2ee8 100644 --- a/player.c +++ b/player.c @@ -1554,25 +1554,19 @@ int *statistics_print_profile; // these arrays specify which of the statistics specified by the statistics_item calls will actually // be printed -- 2 means print, 1 means print only in a debug mode, 0 means skip -// don't display output fps -- not sure how accurate it is (change item 14 and 17 to 1 to restore) -int ap1_synced_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1}; -int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; +// clang-format off +int ap1_synced_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1}; +int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; +int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, - 1, 1, 1, 1, 2, 2, 0, 0}; -int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, - 1, 1, 1, 1, 0, 0, 0, 0}; -int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, - 1, 1, 1, 1, 0, 0, 0, 0}; +int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 0, 1, 2, 2, 0, 0}; +int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 0, 1, 0, 0, 0, 0}; +int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 0, 1, 0, 0, 0, 0}; -// don't display output fps -- not sure how accurate it is (change item 14 1 to restore) -int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 2, 2, 0, 0}; -int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, - 1, 1, 0, 0, 0, 0, 0, 0}; -int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, - 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 2, 2, 0, 0}; +int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0}; +// clang-format on void statistics_item(const char *heading, const char *format, ...) { if (((statistics_print_profile[statistics_column] == 1) && (debuglev != 0)) || From d32544d9047e3c3286819c93a56acd9a387c67bd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:17:59 +0000 Subject: [PATCH 443/878] Set the timing peer list as soon as a SETPEERS message is received -- don't wait until play is about to begin. Not sure if it works universally yet. --- rtsp.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rtsp.c b/rtsp.c index ae650a6a..ba03359f 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1756,7 +1756,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message if ((rate & 1) != 0) { // keep this outside the flush_mutex lock if (conn->ap2_timing_peer_list_message) { - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + // ptp_send_control_message_string(conn->ap2_timing_peer_list_message); } else { debug(1, "Connection %d: No timing peer list!", conn->connection_number); } @@ -2290,10 +2290,10 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp free(conn->ap2_timing_peer_list_message); conn->ap2_timing_peer_list_message = strdup(timing_list_message); // if this rtsp thread is playing... - if (try_to_hold_play_lock(conn) == 0) { + // if (try_to_hold_play_lock(conn) == 0) { ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - release_hold_on_play_lock(conn); - } + // release_hold_on_play_lock(conn); + // } } plist_free(addresses_array); resp->respcode = 200; @@ -2876,10 +2876,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->input_bit_depth = conn->stream.fmtp[3]; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); debug(2, "Realtime Stream Play"); - if (conn->ap2_timing_peer_list_message) - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - else + if (conn->ap2_timing_peer_list_message) { + // ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + } else { debug(1, "No timing peer list!"); + } activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); From 231d5242d782483a44176b8e1efddb9ca4b2be0c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 2 Dec 2021 11:19:52 +0000 Subject: [PATCH 444/878] clang format only --- player.c | 4 ++-- rtsp.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/player.c b/player.c index 257d2ee8..36c7aabf 100644 --- a/player.c +++ b/player.c @@ -1169,8 +1169,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { die("an unrecoverable error, \"output_device_stalled\", has been " - "detected.", - conn->connection_number); + "detected.", + conn->connection_number); } } } else { diff --git a/rtsp.c b/rtsp.c index ba03359f..45efd8cf 100644 --- a/rtsp.c +++ b/rtsp.c @@ -612,7 +612,7 @@ void *player_watchdog_thread_code(void *arg) { command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); } else { die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", - conn->connection_number); + conn->connection_number); } } } @@ -2291,8 +2291,8 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp conn->ap2_timing_peer_list_message = strdup(timing_list_message); // if this rtsp thread is playing... // if (try_to_hold_play_lock(conn) == 0) { - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - // release_hold_on_play_lock(conn); + ptp_send_control_message_string(conn->ap2_timing_peer_list_message); + // release_hold_on_play_lock(conn); // } } plist_free(addresses_array); From 5aeb4ab9ed759d130efc1d1c27ae5afb0b6e9c18 Mon Sep 17 00:00:00 2001 From: Anton Lindqvist Date: Thu, 2 Dec 2021 11:57:40 +0100 Subject: [PATCH 445/878] configure: fix handling of non linux operating systems The OS whitelist conditional is malformed due to missing test(1) commands causing the following error to be emitted during configure while using something other than Linux: ../configure[3340]: xopenbsd: not found Instead, favor a switch statement which makes the line a bit shorter as well. Also, the AC_MSG_ERROR argument must be wrapped in brackets due to presence of a comma. Otherwise, the tail will be interpreted as an additional argument. --- configure.ac | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index e0dcb10f..32eae4fe 100644 --- a/configure.ac +++ b/configure.ac @@ -22,10 +22,13 @@ if test "z$with_os" = "z"; then with_os="linux" fi with_os=`echo ${with_os} | tr '[[:upper:]]' '[[:lower:]]' ` - -if test "x${with_os}" != xlinux && "x${with_os}" != xfreebsd && "x${with_os}" != xdarwin ; then - AC_MSG_ERROR(--with-os= argument must linux freebsd openbsd or darwin) -fi +case "x${with_os}" in +xlinux|xfreebsd|xopenbsd|xdarwin) + ;; +*) + AC_MSG_ERROR([--with-os= argument must be one of linux, freebsd, openbsd or darwin]) + ;; +esac # Checks for programs. AC_PROG_CC From 7693b4ae1b4b45ca7c4eb1de5216179966a6d864 Mon Sep 17 00:00:00 2001 From: Anton Lindqvist Date: Thu, 2 Dec 2021 12:07:14 +0100 Subject: [PATCH 446/878] configure: openbsd is supported --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 32eae4fe..878fbf2c 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e ".git/index" ]) # Derived from the Avahi configure.ac file # Specifying the OS type, defaulting to linux. # -AC_ARG_WITH(os, AS_HELP_STRING([--with-os=OSType],[Specify the distribution to target: One of linux freebsd or darwin])) +AC_ARG_WITH(os, AS_HELP_STRING([--with-os=OSType],[Specify the distribution to target: One of linux, freebsd, openbsd or darwin])) if test "z$with_os" = "z"; then with_os="linux" fi From 3b91065c2afa9790182643c9e32f21bcf4835fa4 Mon Sep 17 00:00:00 2001 From: Anton Lindqvist Date: Thu, 2 Dec 2021 12:07:55 +0100 Subject: [PATCH 447/878] do not pass null to printf OpenBSD issues a warning using syslog whenever NULL is passed as a variadic argument to the printf-like family of functions. --- shairport.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/shairport.c b/shairport.c index edcb5e10..dab32bf2 100644 --- a/shairport.c +++ b/shairport.c @@ -120,6 +120,8 @@ int this_is_the_daemon_process = 0; #define UUID_STR_LEN 36 #endif +#define strnull(s) ((s) ? (s) : "(null)") + pthread_t rtsp_listener_thread; int killOption = 0; @@ -1976,15 +1978,15 @@ int main(int argc, char **argv) { debug(1, "udp port range is %d.", config.udp_port_range); debug(1, "player name is \"%s\".", config.service_name); debug(1, "backend is \"%s\".", config.output_name); - debug(1, "run_this_before_play_begins action is \"%s\".", config.cmd_start); - debug(1, "run_this_after_play_ends action is \"%s\".", config.cmd_stop); + debug(1, "run_this_before_play_begins action is \"%s\".", strnull(config.cmd_start)); + debug(1, "run_this_after_play_ends action is \"%s\".", strnull(config.cmd_stop)); debug(1, "wait-cmd status is %d.", config.cmd_blocking); debug(1, "run_this_before_play_begins may return output is %d.", config.cmd_start_returns_output); - debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", config.cmd_unfixable); - debug(1, "run_this_before_entering_active_state action is \"%s\".", config.cmd_active_start); - debug(1, "run_this_after_exiting_active_state action is \"%s\".", config.cmd_active_stop); + debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", strnull(config.cmd_unfixable)); + debug(1, "run_this_before_entering_active_state action is \"%s\".", strnull(config.cmd_active_start)); + debug(1, "run_this_after_exiting_active_state action is \"%s\".", strnull(config.cmd_active_stop)); debug(1, "active_state_timeout is %f seconds.", config.active_state_timeout); - debug(1, "mdns backend \"%s\".", config.mdns_name); + debug(1, "mdns backend \"%s\".", strnull(config.mdns_name)); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); debug(1, "interpolation setting is \"%s\".", config.packet_stuffing == ST_basic ? "basic" @@ -1995,7 +1997,7 @@ int main(int argc, char **argv) { debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption); debug(1, "busy timeout time is %d.", config.timeout); debug(1, "drift tolerance is %f seconds.", config.tolerance); - debug(1, "password is \"%s\".", config.password); + debug(1, "password is \"%s\".", strnull(config.password)); debug(1, "ignore_volume_control is %d.", config.ignore_volume_control); if (config.volume_max_db_set) debug(1, "volume_max_db is %d.", config.volume_max_db); From 489a77633be7d7885d50c5313196e3b0ebd79245 Mon Sep 17 00:00:00 2001 From: Anton Lindqvist Date: Sun, 5 Dec 2021 09:00:23 +0100 Subject: [PATCH 448/878] add missing includes on openbsd --- common.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common.c b/common.c index 866222c2..feec44ad 100644 --- a/common.c +++ b/common.c @@ -35,6 +35,7 @@ #include #include #include +#include // PRIdPTR #include #include #include @@ -54,7 +55,7 @@ #include #endif -#ifdef COMPILE_FOR_FREEBSD +#ifdef COMPILE_FOR_BSD #include #include #include From 57af5f3c10c54947cf160fe46a580cf40669d412 Mon Sep 17 00:00:00 2001 From: Anton Lindqvist Date: Fri, 3 Dec 2021 20:45:51 +0100 Subject: [PATCH 449/878] avoid recursive mutex acquisition in sndio backend Ending a RTSP session while running on OpenBSD using sndio backend causes the following crash: #0 thrkill () #1 0x000005208224403e in _libc_abort #2 0x00000520821b77be in _rthread_mutex_trylock #3 _rthread_mutex_timedlock #4 0x0000051e0d54e2c0 in stop () #5 0x0000051e0d544e85 in player_thread_cleanup_handler #6 0x0000052082243126 in _libc_pthread_exit #7 0x000005209a158700 in sigthr_handler #8 #9 _thread_sys_poll () #10 0x000005208223533e in _libc_poll_cancel #11 0x00000520df54c9a0 in sio_psleep #12 0x00000520df54cc1f in sio_write #13 0x0000051e0d54e27a in play #14 0x0000051e0d547fc0 in player_thread_func #15 0x000005209a158cc1 in _rthread_start #16 0x000005208223565a in __tfork_thread The player thread is blocking inside sio_write() -> poll(2) while the thread is being terminated. The stop routine tied to the same backend is invoked through player_thread_cleanup_handler() which tries to acquire the mutex which it already acquired before invoking sio_write(). Avoiding blocking writes would require switching to async I/O which is quite an undertaking. The fact that there's only one `struct sio_hdl *' instance in the compilation unit sort of implies there can only be one player thread at a time. Therefore fix the crash by only trying to acquire the mutex and continue as usual if it's already acquired. --- audio_sndio.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/audio_sndio.c b/audio_sndio.c index f8582c8a..6ada40ad 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -239,11 +239,19 @@ static int play(void *buf, int frames) { } static void stop() { - pthread_mutex_lock(&sndio_mutex); + int gotlock = 1; + + // The player thread could already be waiting in sio_write() during + // termination implying that the same thread already have acquired the mutex. + if (pthread_mutex_trylock(&sndio_mutex)) + gotlock = 0; + if (!sio_stop(hdl)) die("sndio: unable to stop"); written = played = 0; - pthread_mutex_unlock(&sndio_mutex); + + if (gotlock) + pthread_mutex_unlock(&sndio_mutex); } static void onmove_cb(__attribute__((unused)) void *arg, int delta) { From 41f55ac1cb4b371ac8dec4155ffabcccc6facd51 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:03:42 +0000 Subject: [PATCH 450/878] Stop clearing the timing peer list and stop removing the anchor clock info. --- player.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index 36c7aabf..da98c55b 100644 --- a/player.c +++ b/player.c @@ -383,6 +383,10 @@ void reset_buffer(rtsp_conn_info *conn) { debug_mutex_lock(&conn->ab_mutex, 30000, 0); ab_resync(conn); debug_mutex_unlock(&conn->ab_mutex, 0); + if (config.output->flush) { + config.output->flush(); // no cancellation points +// debug(1, "reset_buffer: flush output device."); + } } void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy, @@ -1687,7 +1691,7 @@ void player_thread_cleanup_handler(void *arg) { debug(3, "Audio thread terminated."); #ifdef CONFIG_AIRPLAY_2 } - ptp_send_control_message_string("T"); // remove all timing peers to force the master to 0 + // ptp_send_control_message_string("T"); // remove all timing peers to force the master to 0 #endif if (conn->outbuf) { @@ -1711,7 +1715,7 @@ void player_thread_cleanup_handler(void *arg) { if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); - reset_anchor_info(conn); + // reset_anchor_info(conn); release_play_lock(conn); conn->rtp_running = 0; pthread_setcancelstate(oldState, NULL); @@ -1747,7 +1751,7 @@ void *player_thread_func(void *arg) { conn->ap2_play_enabled = 0; #endif - reset_anchor_info(conn); + // reset_anchor_info(conn); if (conn->stream.type == ast_apple_lossless) init_alac_decoder((int32_t *)&conn->stream.fmtp, From fa20d88c1c2b46391a6de88f187fd8c816bd7806 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:08:35 +0000 Subject: [PATCH 451/878] Remember to add some exta fields later... --- plists/get_info_response.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plists/get_info_response.xml b/plists/get_info_response.xml index 7a3b7a05..b45e3bad 100644 --- a/plists/get_info_response.xml +++ b/plists/get_info_response.xml @@ -60,7 +60,8 @@ 366.0 statusFlags 4 - txtAirPlay - + + From be4807fff205b7022d7fed2bc27b29ce1f1d38fa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:09:29 +0000 Subject: [PATCH 452/878] Wait for that mutex for longer. (Don't know why yet...) --- ptp-utilities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 7019187e..fa1c6407 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -68,7 +68,7 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; - pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 20000, 1); + pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 50000, 1); if (actual_clock_id != NULL) *actual_clock_id = 0; if (raw_offset != NULL) From 0b21373c0e5ba8e565bf420ed2ad8eb79f23d491 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:13:20 +0000 Subject: [PATCH 453/878] Skip some invalid AAC packets, don't decode outdated frames, wait for timing information to be valid. --- rtp.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/rtp.c b/rtp.c index 5b79ff4b..d3bf06c4 100644 --- a/rtp.c +++ b/rtp.c @@ -1310,7 +1310,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock // note that mastershipo may be backdated - if (duration_of_mastership < 1500000000) { + if (duration_of_mastership < 100000000) { debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || @@ -1400,6 +1400,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } } } else { + // debug(1, "anchor_remote_info_is_valid not valid"); response = clock_no_anchor_info; // no anchor information } } @@ -2253,7 +2254,14 @@ void *rtp_buffered_audio_processor(void *arg) { int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; - double requested_lead_time = 0.3; // normal lead time minimum + double requested_lead_time = 0.0; // normal lead time minimum -- maybe it should be about 0.1 + + // wait until our timing information is valid + + // debug(1,"rtp_buffered_audio_processor ready."); + while (have_ptp_timing_information(conn) == 0) + usleep(1000); + reset_buffer(conn); // in case there is any garbage in the player // int not_first_time_out = 0; @@ -2519,28 +2527,42 @@ void *rtp_buffered_audio_processor(void *arg) { // to decipher it first // debug(1,"seq_no %u, timestamp %u", seq_no, timestamp); + uint64_t local_should_be_time = 0; + int have_time_information = frame_to_local_time(timestamp, &local_should_be_time, conn); + int64_t local_lead_time = 0; + int64_t requested_lead_time_ns = (int64_t)(requested_lead_time * 1000000000); + requested_lead_time_ns = (int64_t)(-300000000); + int outdated = 0; + if (have_time_information == 0) { + local_lead_time = local_should_be_time - get_absolute_time_in_ns(); + outdated = (local_lead_time < requested_lead_time_ns); + // if (outdated != 0) + // debug(1,"Frame is outdated %d if lead_time %" PRId64 " is less than requested lead time %" PRId64 " ns.", outdated, local_lead_time, requested_lead_time_ns); + } else { + debug(1,"Timing information not valid"); + // outdated = 1; + } + + if ((flush_requested) && (seq_no >= flushUntilSeq)) { - uint64_t should_be_time; - if ((frame_to_local_time(timestamp, &should_be_time, conn) == 0) && (play_enabled)) { + if ((have_time_information == 0) && (play_enabled)) { // play enabled will be off when this is a full flush and the anchor information is not // valid - int64_t lead_time = should_be_time - get_absolute_time_in_ns(); - debug(1, + debug(2, "flush completed to seq: %u, flushUntilTS; %u with rtptime: %u, lead time: " "0x%" PRIx64 " nanoseconds, i.e. %f sec.", - seq_no, flushUntilTS, timestamp, lead_time, lead_time * 0.000000001); + seq_no, flushUntilTS, timestamp, local_lead_time, local_lead_time * 0.000000001); } else { debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); } } - - if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || - ((flush_requested == 0) && (new_buffer_needed))) { - + // if we are here because of a flush request, it must be the case that // flushing the pcm buffer wasn't enough, as the request would have been turned off by now // so we better indicate that the pcm buffer is empty and its contents invalid - if (flush_requested) { + + // also, if the incomgin frame is outdated, set pcm_buffer_occupancy to 0; + if ((flush_requested) || (outdated)){ pcm_buffer_occupancy = 0; } @@ -2556,6 +2578,11 @@ void *rtp_buffered_audio_processor(void *arg) { //} } + + if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || + ((flush_requested == 0) && (new_buffer_needed))) { + + unsigned char nonce[12]; memset(nonce, 0, sizeof(nonce)); memcpy(nonce + 4, packet + nread - 8, @@ -2596,6 +2623,7 @@ void *rtp_buffered_audio_processor(void *arg) { data_remaining = aac_packet_length; int ret = 0; // there can be more than one av packet (? terminology) in a block + int frame_within_block = 0; while (data_remaining > 0) { if (decoded_frame == NULL) { decoded_frame = av_frame_alloc(); @@ -2608,14 +2636,20 @@ void *rtp_buffered_audio_processor(void *arg) { if (ret < 0) { debug(1, "error while parsing deciphered audio packet."); } else { + frame_within_block++; data_to_process += ret; data_remaining -= ret; // debug(1, "frame found"); // now pass each packet to be decoded if (pkt->size) { + //if (0) { + if (pkt->size <= 7) { // no idea about this... + debug(1,"malformed AAC packet skipped."); + } else { ret = avcodec_send_packet(codec_context, pkt); + if (ret < 0) { - debug(1, "error sending packet to decoder"); + debug(1, "error sending frame %d of size %d to decoder, blocks_read: %u, blocks_read_since_flush: %u.", frame_within_block, pkt->size, blocks_read, blocks_read_since_flush); } else { while (ret >= 0) { ret = avcodec_receive_frame(codec_context, decoded_frame); @@ -2672,6 +2706,7 @@ void *rtp_buffered_audio_processor(void *arg) { } } } + } } if (decoded_frame == NULL) debug(1, "decoded_frame is NULL"); From 3b9f2e1c53ede54dcbfba551bc534d2efd43c557 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:14:23 +0000 Subject: [PATCH 454/878] Don't ask for cover art if not asking for metadata too. Duh. --- shairport.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shairport.c b/shairport.c index 9b7648e3..b1faf5c9 100644 --- a/shairport.c +++ b/shairport.c @@ -1807,11 +1807,12 @@ int main(int argc, char **argv) { #ifdef CONFIG_METADATA // If we are asking for metadata, turn on the relevant bits - if (config.metadata_enabled != 0) + if (config.metadata_enabled != 0) { config.airplay_features |= (1 << 17) | (1 << 16); // 16 is progress, 17 is text - // If we are asking for artwork, turn on the relevant bit - if (config.get_coverart) - config.airplay_features |= (1 << 15); // 15 is artwork + // If we are asking for artwork, turn on the relevant bit + if (config.get_coverart) + config.airplay_features |= (1 << 15); // 15 is artwork + } #endif debug(1, "Features: 0x%" PRIx64 ".", config.airplay_features); From 1e2136821337dd73f4ac563e0ff6f3ff5790a92c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:16:57 +0000 Subject: [PATCH 455/878] Add txtAirPlay data to info response. Don't pause long RTSP message reception. --- rtsp.c | 127 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 17 deletions(-) diff --git a/rtsp.c b/rtsp.c index 45efd8cf..6c0da18f 100644 --- a/rtsp.c +++ b/rtsp.c @@ -185,6 +185,39 @@ typedef struct { #ifdef CONFIG_AIRPLAY_2 +int add_pstring_to_malloc(const char *s, void **allocation, size_t *size) { + int response = 0; + void *p = *allocation; + if (p == NULL) { + p = malloc(strlen(s) + 1); + if (p == NULL) { + debug(1, "error allocating memory"); + } else { + *allocation = p; + *size = *size + strlen(s) + 1; + uint8_t *b = (uint8_t *)p; + *b = strlen(s); + p = p+1; + memcpy(p,s,strlen(s)); + response = 1; + } + } else { + p = realloc(p, *size + strlen(s) + 1); + if (p == NULL) { // assuming we never allocate a zero byte space + debug(1, "error reallocating memory"); + } else { + *allocation = p; + uint8_t *b = (uint8_t *)p + *size; + *b = strlen(s); + p = p + *size + 1; + memcpy(p,s,strlen(s)); + *size = *size + strlen(s) + 1; + response = 1; + } + } + return response; +} + static void pkString_make(char *str, size_t str_size, const char *device_id) { uint8_t public_key[32]; if (str_size < 2 * sizeof(public_key) + 1) { @@ -1139,6 +1172,8 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes enum rtsp_read_request_response reply = rtsp_read_request_response_ok; ssize_t buflen = 4096; + if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) + buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay char *buf = malloc(buflen + 1); // add a NUL at the end if (!buf) { @@ -1228,7 +1263,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes get_absolute_time_in_ns() + ((uint64_t)15000000000); // i.e. fifteen seconds from now int warning_message_sent = 0; - const size_t max_read_chunk = 1024 * 1024 / 16; + // const size_t max_read_chunk = 1024 * 1024 / 16; while (inbuf < msg_size) { // we are going to read the stream in chunks and time how long it takes to @@ -1255,9 +1290,9 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes goto shutdown; } size_t read_chunk = msg_size - inbuf; - if (read_chunk > max_read_chunk) - read_chunk = max_read_chunk; - usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk + //if (read_chunk > max_read_chunk) + // read_chunk = max_read_chunk; + // usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk nread = read_from_rtsp_connection(conn, buf + inbuf, read_chunk); if (!nread) { reply = rtsp_read_request_response_error; @@ -1476,7 +1511,7 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - + debug_log_rtsp_message(2, "GET /info:", req); if (rtsp_message_contains_plist(req)) { // it's stage one // get version of AirPlay -- it might be too old. Not using it yet. char *hdr = msg_get_header(req, "User-Agent"); @@ -1519,6 +1554,43 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r if (response_plist == NULL) { debug(1, "GET /info Stage 1: response plist not created from XML!"); } else { + void *qualifier_response_data = NULL; + size_t qualifier_response_data_length = 0; + if (add_pstring_to_malloc("acl=0", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(deviceIdString, &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(featuresString, &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("rsf=0x0", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("flags=0x4", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("model=Shairport Sync", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("manufacturer=", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("serialNumber=", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("protovers=1.1", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("srcvers=366.0", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(piString, &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(gidString, &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("gcgl=0", &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); + if (add_pstring_to_malloc(pkString, &qualifier_response_data, &qualifier_response_data_length) == 0) + debug(1, "Problem"); + // debug(1,"qualifier_response_data_length: %u.", qualifier_response_data_length); + + plist_dict_set_item(response_plist, "txtAirPlay", plist_new_data(qualifier_response_data, qualifier_response_data_length)); + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); @@ -1526,15 +1598,16 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); char *vs = get_version_string(); - plist_dict_set_item(response_plist, "model", plist_new_string(vs)); +// plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); free(vs); - char pkString[128]; - pkString_make(pkString, sizeof(pkString), config.airplay_device_id); - plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); + //pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + //plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); plist_to_bin(response_plist, &resp->content, &resp->contentlength); if (resp->contentlength == 0) debug(1, "GET /info Stage 1: response bplist not created!"); plist_free(response_plist); + free(qualifier_response_data); } msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); debug_log_rtsp_message(2, "GET /info Stage 1 Response:", resp); @@ -1554,11 +1627,11 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); char *vs = get_version_string(); - plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); free(vs); - char pkString[128]; - pkString_make(pkString, sizeof(pkString), config.airplay_device_id); - plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); + // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); plist_to_bin(response_plist, &resp->content, &resp->contentlength); plist_free(response_plist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); @@ -1702,12 +1775,12 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message } uint64_t networkTimeSecs; plist_get_uint_val(item, &networkTimeSecs); - debug(2, "anchor networkTimeSecs is 0x%" PRIx64 ".", networkTimeSecs); + debug(2, "anchor networkTimeSecs is %" PRIu64 ".", networkTimeSecs); item = plist_dict_get_item(messagePlist, "networkTimeFrac"); uint64_t networkTimeFrac; plist_get_uint_val(item, &networkTimeFrac); - debug(2, "anchor networkTimeFrac is 0x%" PRIx64 ".", networkTimeFrac); + debug(2, "anchor networkTimeFrac is 0%" PRIu64 ".", networkTimeFrac); // it looks like the networkTimeFrac is a fraction where the msb is work 1/2, the // next 1/4 and so on // now, convert the network time and fraction into nanoseconds @@ -1718,7 +1791,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message networkTimeSecs = networkTimeSecs * 1000000000; // turn the whole seconds into ns uint64_t anchorTimeNanoseconds = networkTimeSecs + networkTimeFrac; - debug(2, "anchorTimeNanoseconds looks like 0x%" PRIx64 ".", anchorTimeNanoseconds); + debug(2, "anchorTimeNanoseconds looks like %" PRIu64 ".", anchorTimeNanoseconds); item = plist_dict_get_item(messagePlist, "rtpTime"); uint64_t rtpTime; @@ -2158,7 +2231,27 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) {} + __attribute__((unused)) rtsp_message *resp) { + /* not finished yet + plist_t payload_plist = plist_new_dict(); + plist_dict_set_item(payload_plist, "type", plist_new_uint(103)); + plist_dict_set_item(payload_plist, "sr", plist_new_real(44100.0)); + + plist_t array_plist = plist_new_array(); + plist_array_append_item(array_plist, payload_plist); + + plist_t response_plist = plist_new_dict(); + plist_dict_set_item(response_plist, "streams",array_plist); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + // plist_free(array_plist); + // plist_free(payload_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "FEEDBACK response:", resp); + */ +} void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { From 70821f81278abbf204ec9353b2808f8cad8bffec Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:45:25 +0000 Subject: [PATCH 456/878] Clang Format --- player.c | 4 +- rtp.c | 170 ++++++++++++++++++++++++++++--------------------------- rtsp.c | 75 ++++++++++++++---------- 3 files changed, 133 insertions(+), 116 deletions(-) diff --git a/player.c b/player.c index da98c55b..6e1ee80d 100644 --- a/player.c +++ b/player.c @@ -384,8 +384,8 @@ void reset_buffer(rtsp_conn_info *conn) { ab_resync(conn); debug_mutex_unlock(&conn->ab_mutex, 0); if (config.output->flush) { - config.output->flush(); // no cancellation points -// debug(1, "reset_buffer: flush output device."); + config.output->flush(); // no cancellation points + // debug(1, "reset_buffer: flush output device."); } } diff --git a/rtp.c b/rtp.c index d3bf06c4..f80e32ae 100644 --- a/rtp.c +++ b/rtp.c @@ -2255,14 +2255,14 @@ void *rtp_buffered_audio_processor(void *arg) { int play_enabled = 0; uint32_t flush_from_timestamp; double requested_lead_time = 0.0; // normal lead time minimum -- maybe it should be about 0.1 - + // wait until our timing information is valid - + // debug(1,"rtp_buffered_audio_processor ready."); while (have_ptp_timing_information(conn) == 0) usleep(1000); - - reset_buffer(conn); // in case there is any garbage in the player + + reset_buffer(conn); // in case there is any garbage in the player // int not_first_time_out = 0; // quick check of parameters @@ -2532,17 +2532,17 @@ void *rtp_buffered_audio_processor(void *arg) { int64_t local_lead_time = 0; int64_t requested_lead_time_ns = (int64_t)(requested_lead_time * 1000000000); requested_lead_time_ns = (int64_t)(-300000000); - int outdated = 0; + int outdated = 0; if (have_time_information == 0) { local_lead_time = local_should_be_time - get_absolute_time_in_ns(); outdated = (local_lead_time < requested_lead_time_ns); // if (outdated != 0) - // debug(1,"Frame is outdated %d if lead_time %" PRId64 " is less than requested lead time %" PRId64 " ns.", outdated, local_lead_time, requested_lead_time_ns); + // debug(1,"Frame is outdated %d if lead_time %" PRId64 " is less than requested lead time + // %" PRId64 " ns.", outdated, local_lead_time, requested_lead_time_ns); } else { - debug(1,"Timing information not valid"); + debug(1, "Timing information not valid"); // outdated = 1; } - if ((flush_requested) && (seq_no >= flushUntilSeq)) { if ((have_time_information == 0) && (play_enabled)) { @@ -2556,33 +2556,31 @@ void *rtp_buffered_audio_processor(void *arg) { debug(2, "flush completed to seq: %u with rtptime: %u.", seq_no, timestamp); } } - - // if we are here because of a flush request, it must be the case that - // flushing the pcm buffer wasn't enough, as the request would have been turned off by now - // so we better indicate that the pcm buffer is empty and its contents invalid - - // also, if the incomgin frame is outdated, set pcm_buffer_occupancy to 0; - if ((flush_requested) || (outdated)){ - pcm_buffer_occupancy = 0; - } - // decode the block and add it to or put it in the pcm buffer + // if we are here because of a flush request, it must be the case that + // flushing the pcm buffer wasn't enough, as the request would have been turned off by now + // so we better indicate that the pcm buffer is empty and its contents invalid - if (pcm_buffer_occupancy == 0) { - // they should match and the read point should be zero - // if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { - // debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", - // pcm_buffer_read_point_rtptime, timestamp); - pcm_buffer_read_point_rtptime = timestamp; - pcm_buffer_read_point = 0; - //} - } + // also, if the incomgin frame is outdated, set pcm_buffer_occupancy to 0; + if ((flush_requested) || (outdated)) { + pcm_buffer_occupancy = 0; + } + // decode the block and add it to or put it in the pcm buffer + + if (pcm_buffer_occupancy == 0) { + // they should match and the read point should be zero + // if ((blocks_read != 0) && (pcm_buffer_read_point_rtptime != timestamp)) { + // debug(2, "set pcm_buffer_read_point_rtptime from %u to %u.", + // pcm_buffer_read_point_rtptime, timestamp); + pcm_buffer_read_point_rtptime = timestamp; + pcm_buffer_read_point = 0; + //} + } if (((flush_requested != 0) && (seq_no == flushUntilSeq)) || ((flush_requested == 0) && (new_buffer_needed))) { - unsigned char nonce[12]; memset(nonce, 0, sizeof(nonce)); memcpy(nonce + 4, packet + nread - 8, @@ -2642,71 +2640,75 @@ void *rtp_buffered_audio_processor(void *arg) { // debug(1, "frame found"); // now pass each packet to be decoded if (pkt->size) { - //if (0) { + // if (0) { if (pkt->size <= 7) { // no idea about this... - debug(1,"malformed AAC packet skipped."); + debug(1, "malformed AAC packet skipped."); } else { - ret = avcodec_send_packet(codec_context, pkt); - - if (ret < 0) { - debug(1, "error sending frame %d of size %d to decoder, blocks_read: %u, blocks_read_since_flush: %u.", frame_within_block, pkt->size, blocks_read, blocks_read_since_flush); - } else { - while (ret >= 0) { - ret = avcodec_receive_frame(codec_context, decoded_frame); - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) - break; - else if (ret < 0) { - debug(1, "error %d during decoding", ret); - } else { - av_samples_alloc(&pcm_audio, &dst_linesize, codec_context->channels, - decoded_frame->nb_samples, av_format, 1); - // remember to free pcm_audio - ret = swr_convert(swr, &pcm_audio, decoded_frame->nb_samples, - (const uint8_t **)decoded_frame->extended_data, - decoded_frame->nb_samples); - dst_bufsize = av_samples_get_buffer_size( - &dst_linesize, codec_context->channels, ret, av_format, 1); - // debug(1,"generated %d bytes of PCM", dst_bufsize); - // copy the PCM audio into the PCM buffer. - // make sure it's big enough first + ret = avcodec_send_packet(codec_context, pkt); - // also, check it if needs to be truncated but to an impending delayed - // flush_is_delayed - if (flush_is_delayed) { - // see if the flush_from_timestamp is in the buffer - int32_t samples_remaining = - (flush_from_timestamp - pcm_buffer_read_point_rtptime); - if ((samples_remaining > 0) && - ((samples_remaining * conn->input_bytes_per_frame) < dst_bufsize)) { - debug(2, - "samples remaining before flush: %d, number of samples %d. " - "flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", - samples_remaining, dst_bufsize / conn->input_bytes_per_frame, - flush_from_timestamp, pcm_buffer_read_point_rtptime); - dst_bufsize = samples_remaining * conn->input_bytes_per_frame; - } - } - if ((pcm_buffer_size - pcm_buffer_occupancy) < dst_bufsize) { - debug(1, - "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " - "(frames): %u", - pcm_buffer_read_point / conn->input_bytes_per_frame, - pcm_buffer_occupancy / conn->input_bytes_per_frame); - pcm_buffer_size = dst_bufsize + pcm_buffer_occupancy; - debug(1, "fatal error! pcm buffer too small at %d bytes.", - pcm_buffer_size); + if (ret < 0) { + debug(1, + "error sending frame %d of size %d to decoder, blocks_read: %u, " + "blocks_read_since_flush: %u.", + frame_within_block, pkt->size, blocks_read, blocks_read_since_flush); + } else { + while (ret >= 0) { + ret = avcodec_receive_frame(codec_context, decoded_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + else if (ret < 0) { + debug(1, "error %d during decoding", ret); } else { - memcpy(pcm_buffer + pcm_buffer_occupancy, pcm_audio, dst_bufsize); - pcm_buffer_occupancy += dst_bufsize; + av_samples_alloc(&pcm_audio, &dst_linesize, codec_context->channels, + decoded_frame->nb_samples, av_format, 1); + // remember to free pcm_audio + ret = swr_convert(swr, &pcm_audio, decoded_frame->nb_samples, + (const uint8_t **)decoded_frame->extended_data, + decoded_frame->nb_samples); + dst_bufsize = av_samples_get_buffer_size( + &dst_linesize, codec_context->channels, ret, av_format, 1); + // debug(1,"generated %d bytes of PCM", dst_bufsize); + // copy the PCM audio into the PCM buffer. + // make sure it's big enough first + + // also, check it if needs to be truncated but to an impending delayed + // flush_is_delayed + if (flush_is_delayed) { + // see if the flush_from_timestamp is in the buffer + int32_t samples_remaining = + (flush_from_timestamp - pcm_buffer_read_point_rtptime); + if ((samples_remaining > 0) && + ((samples_remaining * conn->input_bytes_per_frame) < + dst_bufsize)) { + debug(2, + "samples remaining before flush: %d, number of samples %d. " + "flushFromTS: %u, pcm_buffer_read_point_rtptime: %u.", + samples_remaining, dst_bufsize / conn->input_bytes_per_frame, + flush_from_timestamp, pcm_buffer_read_point_rtptime); + dst_bufsize = samples_remaining * conn->input_bytes_per_frame; + } + } + if ((pcm_buffer_size - pcm_buffer_occupancy) < dst_bufsize) { + debug(1, + "pcm_buffer_read_point (frames): %u, pcm_buffer_occupancy " + "(frames): %u", + pcm_buffer_read_point / conn->input_bytes_per_frame, + pcm_buffer_occupancy / conn->input_bytes_per_frame); + pcm_buffer_size = dst_bufsize + pcm_buffer_occupancy; + debug(1, "fatal error! pcm buffer too small at %d bytes.", + pcm_buffer_size); + } else { + memcpy(pcm_buffer + pcm_buffer_occupancy, pcm_audio, dst_bufsize); + pcm_buffer_occupancy += dst_bufsize; + } + // debug(1,"decoded %d samples", decoded_frame->nb_samples); + // memcpy(sampleBuffer,outputBuffer16,dst_bufsize); + av_freep(&pcm_audio); } - // debug(1,"decoded %d samples", decoded_frame->nb_samples); - // memcpy(sampleBuffer,outputBuffer16,dst_bufsize); - av_freep(&pcm_audio); } } } } - } } if (decoded_frame == NULL) debug(1, "decoded_frame is NULL"); diff --git a/rtsp.c b/rtsp.c index 6c0da18f..177a4b81 100644 --- a/rtsp.c +++ b/rtsp.c @@ -197,8 +197,8 @@ int add_pstring_to_malloc(const char *s, void **allocation, size_t *size) { *size = *size + strlen(s) + 1; uint8_t *b = (uint8_t *)p; *b = strlen(s); - p = p+1; - memcpy(p,s,strlen(s)); + p = p + 1; + memcpy(p, s, strlen(s)); response = 1; } } else { @@ -210,7 +210,7 @@ int add_pstring_to_malloc(const char *s, void **allocation, size_t *size) { uint8_t *b = (uint8_t *)p + *size; *b = strlen(s); p = p + *size + 1; - memcpy(p,s,strlen(s)); + memcpy(p, s, strlen(s)); *size = *size + strlen(s) + 1; response = 1; } @@ -1173,8 +1173,8 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes enum rtsp_read_request_response reply = rtsp_read_request_response_ok; ssize_t buflen = 4096; if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) - buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded - int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay + buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded + int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay char *buf = malloc(buflen + 1); // add a NUL at the end if (!buf) { warn("Connection %d: rtsp_read_request: can't get a buffer.", conn->connection_number); @@ -1290,7 +1290,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes goto shutdown; } size_t read_chunk = msg_size - inbuf; - //if (read_chunk > max_read_chunk) + // if (read_chunk > max_read_chunk) // read_chunk = max_read_chunk; // usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk nread = read_from_rtsp_connection(conn, buf + inbuf, read_chunk); @@ -1556,41 +1556,56 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r } else { void *qualifier_response_data = NULL; size_t qualifier_response_data_length = 0; - if (add_pstring_to_malloc("acl=0", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("acl=0", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc(deviceIdString, &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc(deviceIdString, &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc(featuresString, &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc(featuresString, &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("rsf=0x0", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("rsf=0x0", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("flags=0x4", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("flags=0x4", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("model=Shairport Sync", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("model=Shairport Sync", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("manufacturer=", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("manufacturer=", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("serialNumber=", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("serialNumber=", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("protovers=1.1", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("protovers=1.1", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("srcvers=366.0", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("srcvers=366.0", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc(piString, &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc(piString, &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc(gidString, &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc(gidString, &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); - if (add_pstring_to_malloc("gcgl=0", &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc("gcgl=0", &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); snprintf(pkString, sizeof(pkString), "pk="); pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), config.airplay_device_id); - if (add_pstring_to_malloc(pkString, &qualifier_response_data, &qualifier_response_data_length) == 0) + if (add_pstring_to_malloc(pkString, &qualifier_response_data, + &qualifier_response_data_length) == 0) debug(1, "Problem"); // debug(1,"qualifier_response_data_length: %u.", qualifier_response_data_length); - - plist_dict_set_item(response_plist, "txtAirPlay", plist_new_data(qualifier_response_data, qualifier_response_data_length)); - + + plist_dict_set_item(response_plist, "txtAirPlay", + plist_new_data(qualifier_response_data, qualifier_response_data_length)); + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); @@ -1598,11 +1613,11 @@ void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *r plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); char *vs = get_version_string(); -// plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); free(vs); - //pkString_make(pkString, sizeof(pkString), config.airplay_device_id); - //plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); + // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); plist_to_bin(response_plist, &resp->content, &resp->contentlength); if (resp->contentlength == 0) debug(1, "GET /info Stage 1: response bplist not created!"); @@ -2236,14 +2251,14 @@ void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, plist_t payload_plist = plist_new_dict(); plist_dict_set_item(payload_plist, "type", plist_new_uint(103)); plist_dict_set_item(payload_plist, "sr", plist_new_real(44100.0)); - + plist_t array_plist = plist_new_array(); plist_array_append_item(array_plist, payload_plist); - + plist_t response_plist = plist_new_dict(); plist_dict_set_item(response_plist, "streams",array_plist); - - plist_to_bin(response_plist, &resp->content, &resp->contentlength); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); plist_free(response_plist); // plist_free(array_plist); // plist_free(payload_plist); From 3de7e9a5c5110831070e97f3723d9b3d430f5690 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 12:09:49 +0000 Subject: [PATCH 457/878] Look for at least 0.1 seconds of leadtime, ensure a master clock is at least 0.275 seconds old before use, quieten a few debug messages. --- player.c | 5 +++-- rtp.c | 6 +++--- rtsp.c | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/player.c b/player.c index 6e1ee80d..6131ad4c 100644 --- a/player.c +++ b/player.c @@ -1062,8 +1062,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->first_packet_time_to_play = should_be_time; int64_t lt = conn->first_packet_time_to_play - local_time_now; - - debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + + if (lt < 100000000) + debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); int64_t lateness = local_time_now - conn->first_packet_time_to_play; diff --git a/rtp.c b/rtp.c index f80e32ae..401f4dba 100644 --- a/rtp.c +++ b/rtp.c @@ -1310,7 +1310,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock // note that mastershipo may be backdated - if (duration_of_mastership < 100000000) { + if (duration_of_mastership < 275000000) { debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || @@ -2254,7 +2254,7 @@ void *rtp_buffered_audio_processor(void *arg) { int streaming_has_started = 0; int play_enabled = 0; uint32_t flush_from_timestamp; - double requested_lead_time = 0.0; // normal lead time minimum -- maybe it should be about 0.1 + double requested_lead_time = 0.1; // normal lead time minimum -- maybe it should be about 0.1 // wait until our timing information is valid @@ -2642,7 +2642,7 @@ void *rtp_buffered_audio_processor(void *arg) { if (pkt->size) { // if (0) { if (pkt->size <= 7) { // no idea about this... - debug(1, "malformed AAC packet skipped."); + debug(2, "malformed AAC packet skipped."); } else { ret = avcodec_send_packet(codec_context, pkt); diff --git a/rtsp.c b/rtsp.c index 177a4b81..4b0827bf 100644 --- a/rtsp.c +++ b/rtsp.c @@ -697,7 +697,7 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { pthread_cancel(conns[i]->thread); - debug(1, "Connection %d: cancelled.", conns[i]->connection_number); + debug(2, "Connection %d: cancelled.", conns[i]->connection_number); } } for (i = 0; i < nconns; i++) { @@ -706,7 +706,7 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o ((stream_category == unspecified_stream_category) || (stream_category == conns[i]->airplay_stream_category))) { pthread_join(conns[i]->thread, NULL); - debug(1, "Connection %d: joined.", conns[i]->connection_number); + debug(2, "Connection %d: joined.", conns[i]->connection_number); free(conns[i]); conns[i] = NULL; } @@ -2625,7 +2625,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: SETUP: Remote Control setup detected.", + debug(2, "Connection %d: SETUP: Remote Control setup detected.", conn->connection_number); conn->airplay_stream_category = remote_control_stream; } else { @@ -2825,7 +2825,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } - debug(1, "Connection %d: TCP Remote Control event port opened: %u.", + debug(2, "Connection %d: TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); if (conn->rtp_event_thread != NULL) debug(1, "previous rtp_event_thread allocation not freed, it seems."); @@ -3056,7 +3056,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) resp->respcode = 200; } else if (conn->airplay_stream_category == remote_control_stream) { debug(2, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); - debug_log_rtsp_message(1, "Remote Control Stream stream (second) message", req); + debug_log_rtsp_message(2, "Remote Control Stream stream (second) message", req); resp->respcode = 200; } else { From dbebdca84d95ee80d34d7eef8c6b29e581b315bd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 15:08:16 +0000 Subject: [PATCH 458/878] quieten a debug message --- dacp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dacp.c b/dacp.c index d6964133..7184ed73 100644 --- a/dacp.c +++ b/dacp.c @@ -481,7 +481,7 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) { // metadata_store.dacp_server_active = dacp_server.scan_enable; // metadata_hub_modify_epilog(ch); } else { - debug(1, "dacp port monitor reporting on an out-of-use remote."); + debug(2, "dacp port monitor reporting on an out-of-use remote."); } } pthread_cond_signal(&dacp_server_information_cv); From fd3d461a164bd4afcfeb76b26bdc7140f2570353 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 15:09:05 +0000 Subject: [PATCH 459/878] stop removing the play lock at the end of a play session. That's the job of the rtsp thread. --- player.c | 2 +- rtsp.c | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/player.c b/player.c index 6131ad4c..1feee08f 100644 --- a/player.c +++ b/player.c @@ -1717,7 +1717,7 @@ void player_thread_cleanup_handler(void *arg) { terminate_decoders(conn); // reset_anchor_info(conn); - release_play_lock(conn); + // release_play_lock(conn); conn->rtp_running = 0; pthread_setcancelstate(oldState, NULL); debug(2, "Connection %d: player terminated.", conn->connection_number); diff --git a/rtsp.c b/rtsp.c index 4b0827bf..959944ac 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1879,6 +1879,7 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) req->contentlength); resp->respcode = 500; } + void handle_post(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, __attribute((unused)) rtsp_message *resp) { @@ -2480,6 +2481,9 @@ void teardown_phase_two(rtsp_conn_info *conn) { debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); + debug(1, "Connection %d: TEARDOWN (AP2) release play lock on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + release_play_lock(conn); } } @@ -2533,6 +2537,9 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message "TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).", conn->connection_number); player_stop(conn); + release_play_lock(conn); + debug(1, "TEARDOWN: release play lock on RTSP conversation thread %d.", + conn->connection_number); activity_monitor_signify_activity(0); // inactive, and should be after command_stop() debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", conn->connection_number); @@ -2800,6 +2807,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); + + resp->respcode = 200; } else { @@ -2862,6 +2871,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { + debug(1, "Connection %d: SETUP get_play_lock.", + conn->connection_number); + get_play_lock(conn); // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); @@ -2939,7 +2951,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) case 96: { debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); - get_play_lock(conn); + // get_play_lock(conn); conn->airplay_stream_type = realtime_stream; // bind a new UDP port and get a socket conn->local_realtime_audio_port = 0; // any port @@ -2999,7 +3011,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); - get_play_lock(conn); + // get_play_lock(conn); conn->airplay_stream_type = buffered_stream; // get needed stuff @@ -3229,7 +3241,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (!strncmp(cp, "volume: ", strlen("volume: "))) { float volume = atof(cp + strlen("volume: ")); - debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, + debug(1, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); // if we are playing, go ahead and change the volume if (try_to_hold_play_lock(conn) == 0) { @@ -3969,7 +3981,7 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(2, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { @@ -4078,6 +4090,7 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: ANNOUNCE", conn->connection_number); + debug(1, "Connection %d: ANNOUNCE get_play_lock()", conn->connection_number); int get_play_status = get_play_lock(conn); if (get_play_status != -1) { debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); From 6a58eae3141c26ebc07721cc4a49d3daf84009c7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 17:37:16 +0000 Subject: [PATCH 460/878] Quieten a few debug messasges. --- rtsp.c | 14 ++++---------- shairport.c | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/rtsp.c b/rtsp.c index 959944ac..6bc6c1a0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1345,7 +1345,8 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { }; struct response_t responses[] = { - {200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {501, "Not Implemented"}}; + {200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {451, "Unavailable"}, {501, "Not Implemented"}}; + // 451 is really "Unavailable For Legal Reasons"! int found = 0; char *respcode_text = "Unauthorized"; for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { @@ -2481,8 +2482,6 @@ void teardown_phase_two(rtsp_conn_info *conn) { debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); - debug(1, "Connection %d: TEARDOWN (AP2) release play lock on %s.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); release_play_lock(conn); } } @@ -2538,8 +2537,6 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message conn->connection_number); player_stop(conn); release_play_lock(conn); - debug(1, "TEARDOWN: release play lock on RTSP conversation thread %d.", - conn->connection_number); activity_monitor_signify_activity(0); // inactive, and should be after command_stop() debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", conn->connection_number); @@ -2871,8 +2868,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { - debug(1, "Connection %d: SETUP get_play_lock.", - conn->connection_number); get_play_lock(conn); // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); @@ -3241,7 +3236,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (!strncmp(cp, "volume: ", strlen("volume: "))) { float volume = atof(cp + strlen("volume: ")); - debug(1, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, + debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); // if we are playing, go ahead and change the volume if (try_to_hold_play_lock(conn) == 0) { @@ -3981,7 +3976,7 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + debug(2, "Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { @@ -4090,7 +4085,6 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: ANNOUNCE", conn->connection_number); - debug(1, "Connection %d: ANNOUNCE get_play_lock()", conn->connection_number); int get_play_status = get_play_lock(conn); if (get_play_status != -1) { debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); diff --git a/shairport.c b/shairport.c index b1faf5c9..ecba335a 100644 --- a/shairport.c +++ b/shairport.c @@ -1459,10 +1459,10 @@ void exit_function() { if (config.daemonise) debug(1, "libdaemon parent exit"); else - debug(1, "exit"); + debug(1, "exit_function libdaemon exit"); } #else - debug(1, "exit"); + debug(1, "exit_function exit"); #endif } else { debug(1, "emergency exit"); From 107bc71e56058bd1aee25f89ccb394417958924a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 17:47:45 +0000 Subject: [PATCH 461/878] Stop checking that the timing peers are within the same subnet. --- rtsp.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/rtsp.c b/rtsp.c index 6bc6c1a0..7f78b211 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2725,10 +2725,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) char buf[32]; memset(buf, 0, sizeof(buf)); inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); - if (!different) { + // don't insist there are in the same subnet... + //if (!different) { // debug(1, "%s is in the same subnet as %s.", buf, ip_address); plist_array_append_item(addresses, plist_new_string(buf)); - } + //} } } } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { @@ -2738,15 +2739,17 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) ((iap->ifa_flags & IFF_LOOPBACK) == 0) && (iap->ifa_addr->sa_family == AF_INET)) { struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); - struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); - if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == - (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { + // not needed if not checking for same subnet + // struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); + // if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == + // (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { char buf[32]; memset(buf, 0, sizeof(buf)); inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + // no longer insisting they are in the same subnet // debug(1, "%s is in the same subnet as %s.", buf, ip_address); plist_array_append_item(addresses, plist_new_string(buf)); - } + // } } } } else { From 765a489db49cd9a512d53564ae22e0b2da9f9dd0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:37:01 +0000 Subject: [PATCH 462/878] Possibly fix a few memory leaks. Add a bogus exit to allow SPS to quit at the end of a play session, to make it easier to use valgrind. --- rtsp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index 7f78b211..076ba702 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2298,7 +2298,7 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * uint64_t length = 0; plist_get_data_val(the_item, &buff, &length); // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); - if ((length >= strlen("bplist00")) && (strstr(buff, "bplist00") == buff)) { + if ((buff != NULL) && (length >= strlen("bplist00")) && (strstr(buff, "bplist00") == buff)) { // debug(1,"Contains a plist."); plist_t subsidiary_plist = NULL; plist_from_memory(buff, length, &subsidiary_plist); @@ -2315,6 +2315,8 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * debug(1, "Can't access the plist!"); } } + if (buff != NULL) + free(buff); } } } else { @@ -2521,6 +2523,8 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(1, "Connection %d: missing plist!", conn->connection_number); resp->respcode = 451; // don't know what to do here } + debug(1,"Bogus exit for valgrind."); + exit(EXIT_SUCCESS); // } #endif @@ -3080,6 +3084,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_free(setupResponsePlist); msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); } + plist_free(messagePlist); debug_log_rtsp_message(2, " SETUP response", resp); } #endif From 7258a45bea6cac73fe581f41df177470120b4dfa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:58:00 +0000 Subject: [PATCH 463/878] Fix a few more memory leaks --- mdns_avahi.c | 2 +- rtsp.c | 8 +++++++- shairport.c | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mdns_avahi.c b/mdns_avahi.c index 95984419..7103b1ee 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -112,7 +112,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn char portstring[20]; memset(portstring, 0, sizeof(portstring)); snprintf(portstring, sizeof(portstring), "%u", port); - send_ssnc_metadata('dapo', strdup(portstring), strlen(portstring), 0); + send_ssnc_metadata('dapo', portstring, strlen(portstring), 0); #endif } } else { diff --git a/rtsp.c b/rtsp.c index 076ba702..6f46c3f5 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2282,7 +2282,7 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * if (item != NULL) { char *typeValue = NULL; plist_get_string_val(item, &typeValue); - if (strcmp(typeValue, "updateMRSupportedCommands") == 0) { + if ((typeValue != NULL) && (strcmp(typeValue, "updateMRSupportedCommands") == 0)) { item = plist_dict_get_item(command_dict, "params"); if (item != NULL) { // the item should be a dict @@ -2331,6 +2331,8 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * "POST /command plist type is \"%s\", but \"updateMRSupportedCommands\" expected.", typeValue); } + if (typeValue != NULL) + free(typeValue); } else { debug(1, "Could not get the \"type\" item."); } @@ -2523,6 +2525,10 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(1, "Connection %d: missing plist!", conn->connection_number); resp->respcode = 451; // don't know what to do here } + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } debug(1,"Bogus exit for valgrind."); exit(EXIT_SUCCESS); // } diff --git a/shairport.c b/shairport.c index ecba335a..e2fd0b5e 100644 --- a/shairport.c +++ b/shairport.c @@ -1843,7 +1843,7 @@ int main(int argc, char **argv) { char *uuid = malloc(UUID_STR_LEN); // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); - config.airplay_pi = strdup(uuid); + config.airplay_pi = uuid; debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); #else debug(1, "Started in Airplay 1 mode!"); From 9eea4e4c0c7b3ef0c740062d781bb8dc4f61693c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:05:37 +0000 Subject: [PATCH 464/878] Another memory leak. --- rtsp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rtsp.c b/rtsp.c index 6f46c3f5..af7066f9 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2529,6 +2529,10 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag free(conn->dacp_active_remote); conn->dacp_active_remote = NULL; } + if (conn->ap2_timing_peer_list_message) { + free(conn->ap2_timing_peer_list_message); + conn->ap2_timing_peer_list_message = NULL; + } debug(1,"Bogus exit for valgrind."); exit(EXIT_SUCCESS); // } From d9b6a28307cbea7656a1eee3b2a6c6b5ca7fce1d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:29:00 +0000 Subject: [PATCH 465/878] Add a bogus exit for AP1 to make it easier to evaluate valgrind results. --- rtsp.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index af7066f9..4396a363 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2533,7 +2533,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag free(conn->ap2_timing_peer_list_message); conn->ap2_timing_peer_list_message = NULL; } - debug(1,"Bogus exit for valgrind."); + debug(1,"Bogus exit for valgrind -- remember to comment it out!."); exit(EXIT_SUCCESS); // } #endif @@ -2559,6 +2559,8 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message conn->connection_number); resp->respcode = 451; } + debug(1,"Bogus exit for valgrind -- remember to comment it out!."); + exit(EXIT_SUCCESS); } void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { From 31a34ae6846f464237f4cbb2293b1bbdd8b070bf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:34:23 +0000 Subject: [PATCH 466/878] Fix another memory leak. --- rtsp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rtsp.c b/rtsp.c index 4396a363..2f7da6c5 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2559,6 +2559,10 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message conn->connection_number); resp->respcode = 451; } + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } debug(1,"Bogus exit for valgrind -- remember to comment it out!."); exit(EXIT_SUCCESS); } From b938064baebf1f2e8193a03a95bc09e63cc9be78 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Dec 2021 21:34:53 +0000 Subject: [PATCH 467/878] Add client and server ip metadata for AP2. Remove timing peer list and DACP code only for a full AP2 teardown. --- rtp.c | 5 ----- rtsp.c | 47 +++++++++++++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/rtp.c b/rtp.c index 401f4dba..220bb919 100644 --- a/rtp.c +++ b/rtp.c @@ -1003,11 +1003,6 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport conn->request_sent = 0; conn->rtp_running = 1; - -#ifdef CONFIG_METADATA - send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); - send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); -#endif } } diff --git a/rtsp.c b/rtsp.c index 2f7da6c5..e350b5a4 100644 --- a/rtsp.c +++ b/rtsp.c @@ -526,6 +526,7 @@ void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { } void release_play_lock(rtsp_conn_info *conn) { + debug(1, "Connection %d: release play lock.", conn->connection_number); debug_mutex_lock(&playing_conn_lock, 1000000, 3); if (playing_conn == conn) { // if we have the player playing_conn = NULL; // let it go @@ -535,6 +536,7 @@ void release_play_lock(rtsp_conn_info *conn) { } int get_play_lock(rtsp_conn_info *conn) { + debug(1, "Connection %d: request play lock.", conn->connection_number); // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but // interrupted an existing session int response = 0; @@ -590,7 +592,7 @@ int get_play_lock(rtsp_conn_info *conn) { } if ((have_the_player == 1) && (interrupting_current_session == 1)) { - debug(2, "Connection %d: Got player lock", conn->connection_number); + debug(1, "Connection %d: Got player lock", conn->connection_number); response = 1; } else { debug(1, "Connection %d: failed to get player lock", conn->connection_number); @@ -599,7 +601,7 @@ int get_play_lock(rtsp_conn_info *conn) { } if ((have_the_player) && (interrupting_current_session == 0)) { - debug(2, "Connection %d: got player lock.", conn->connection_number); + debug(1, "Connection %d: Got player lock.", conn->connection_number); response = 0; } return response; @@ -2486,6 +2488,14 @@ void teardown_phase_two(rtsp_conn_info *conn) { debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + if (conn->ap2_timing_peer_list_message) { + free(conn->ap2_timing_peer_list_message); + conn->ap2_timing_peer_list_message = NULL; + } release_play_lock(conn); } } @@ -2509,10 +2519,10 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag // we are being asked to close a stream teardown_phase_one(conn); plist_free(streams); - debug(2, "Connection %d: Stream TEARDOWN complete", conn->connection_number); + debug(1, "Connection %d: TEARDOWN phase one complete", conn->connection_number); } else { teardown_phase_two(conn); - debug(2, "Connection %d: non-stream TEARDOWN complete", conn->connection_number); + debug(1, "Connection %d: TEARDOWN phase two complete", conn->connection_number); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2525,16 +2535,8 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag debug(1, "Connection %d: missing plist!", conn->connection_number); resp->respcode = 451; // don't know what to do here } - if (conn->dacp_active_remote != NULL) { - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } - if (conn->ap2_timing_peer_list_message) { - free(conn->ap2_timing_peer_list_message); - conn->ap2_timing_peer_list_message = NULL; - } - debug(1,"Bogus exit for valgrind -- remember to comment it out!."); - exit(EXIT_SUCCESS); // + // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); + // exit(EXIT_SUCCESS); // } #endif @@ -2563,8 +2565,8 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message free(conn->dacp_active_remote); conn->dacp_active_remote = NULL; } - debug(1,"Bogus exit for valgrind -- remember to comment it out!."); - exit(EXIT_SUCCESS); + // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); + // exit(EXIT_SUCCESS); } void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { @@ -2637,6 +2639,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; + get_play_lock(conn); +#ifdef CONFIG_METADATA + send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); + send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); +#endif } else if (strcmp(timingProtocolString, "NTP") == 0) { debug(1, "Connection %d: SETUP: NTP setup detected.", conn->connection_number); conn->airplay_stream_category = ntp_stream; @@ -2891,7 +2898,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { - get_play_lock(conn); // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); @@ -3214,7 +3220,12 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } else { debug(1, "Connection %d: SETUP doesn't contain a Transport header.", conn->connection_number); } - if (resp->respcode != 200) { + if (resp->respcode == 200) { +#ifdef CONFIG_METADATA + send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); + send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); +#endif + } else { debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number); release_play_lock(conn); } From 9f81ad643e46750a23f19be3d81070c7538a7eca Mon Sep 17 00:00:00 2001 From: Lori Li Date: Wed, 8 Dec 2021 11:27:00 +0900 Subject: [PATCH 468/878] fixed dependency --- BUILDFORAP2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 6cdc288f..231d5ebd 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -54,7 +54,7 @@ Okay, now let's get the tools and libraries for building and installing Shairpor First, install the packages needed by Shairport Sync: ``` -# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ +# apt install --no-install-recommends build-essential git xxd xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev ``` From 429c5dc002b5fcf23a2fe920e1697dc03208a1eb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 8 Dec 2021 11:59:09 +0000 Subject: [PATCH 469/878] Add update to ejurgensen's pair_ap library. Thanks as always. --- pair_ap/client-example.c | 4 +- pair_ap/pair-internal.h | 5 +- pair_ap/pair.c | 34 +++++++++---- pair_ap/pair.h | 2 +- pair_ap/pair_fruit.c | 7 ++- pair_ap/pair_homekit.c | 102 ++++++++++++++++++++++++++++++--------- rtsp.c | 12 ++--- 7 files changed, 121 insertions(+), 45 deletions(-) diff --git a/pair_ap/client-example.c b/pair_ap/client-example.c index 1cb766bb..c5736ed5 100644 --- a/pair_ap/client-example.c +++ b/pair_ap/client-example.c @@ -108,7 +108,7 @@ make_request(const char *url, const void *data, size_t len, const char *content_ else if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT) evrtsp_add_header(req->output_headers, "X-Apple-HKP", "4"); - printf("Making request %d to '%s'... ", cseq, url); + printf("Making request %d to '%s' (len %zu)... ", cseq, url, len); return evrtsp_make_request(evcon, req, EVRTSP_REQ_POST, url); } @@ -136,7 +136,7 @@ make_request_options(const char *url, const void *data, size_t len, const char * // evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE); evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3"); - printf("Making request %d to '%s'... ", cseq, url); + printf("Making request %d to '%s' (len %zu)... ", cseq, url, len); return evrtsp_make_request(evcon, req, EVRTSP_REQ_OPTIONS, url); } diff --git a/pair_ap/pair-internal.h b/pair_ap/pair-internal.h index f9d8fa66..3de74daa 100644 --- a/pair_ap/pair-internal.h +++ b/pair_ap/pair-internal.h @@ -367,7 +367,7 @@ int hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len); bnum -H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2); +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2, int padded_len); bnum H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes); @@ -384,4 +384,7 @@ hash_num(enum hash_alg alg, const bnum n, unsigned char *dest); #ifdef DEBUG_PAIR void hexdump(const char *msg, uint8_t *mem, size_t len); + +void +bnum_dump(const char *msg, bnum n); #endif diff --git a/pair_ap/pair.c b/pair_ap/pair.c index 2b58bbe1..e444ab5b 100644 --- a/pair_ap/pair.c +++ b/pair_ap/pair.c @@ -25,6 +25,7 @@ #include #include #include // for isprint() +#include #include @@ -188,24 +189,27 @@ hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_le return hash_final(alg, &ctx, md); } +// See rfc5054 PAD() bnum -H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2) +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2, int padded_len) { bnum bn; unsigned char *bin; unsigned char buff[SHA512_DIGEST_LENGTH]; int len_n1 = bnum_num_bytes(n1); int len_n2 = bnum_num_bytes(n2); - int nbytes = 2 * len_n1; + int nbytes = 2 * padded_len; + int offset_n1 = padded_len - len_n1; + int offset_n2 = nbytes - len_n2; - if ((len_n2 < 1) || (len_n2 > len_n1)) - return 0; + assert(len_n1 <= padded_len); + assert(len_n2 <= padded_len); - bin = calloc( 1, nbytes ); + bin = calloc(1, nbytes); - bnum_bn2bin(n1, bin, len_n1); - bnum_bn2bin(n2, bin + nbytes - len_n2, len_n2); - hash( alg, bin, nbytes, buff ); + bnum_bn2bin(n1, bin + offset_n1, len_n1); + bnum_bn2bin(n2, bin + offset_n2, len_n2); + hash(alg, bin, nbytes, buff); free(bin); bnum_bin2bn(bn, buff, hash_length(alg)); return bn; @@ -221,8 +225,8 @@ H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes) unsigned char *bin = malloc(nbytes); bnum_bn2bin(n, bin, len_n); - memcpy( bin + len_n, bytes, len_bytes ); - hash( alg, bin, nbytes, buff ); + memcpy(bin + len_n, bytes, len_bytes); + hash(alg, bin, nbytes, buff); free(bin); bnum_bin2bn(bn, buff, hash_length(alg)); return bn; @@ -289,6 +293,16 @@ hexdump(const char *msg, uint8_t *mem, size_t len) } } } + +void +bnum_dump(const char *msg, bnum n) +{ + int len_n = bnum_num_bytes(n); + uint8_t *bin = calloc(1, len_n); + bnum_bn2bin(n, bin, len_n); + hexdump(msg, bin, len_n); + free(bin); +} #endif diff --git a/pair_ap/pair.h b/pair_ap/pair.h index d3be0ca9..35a5aeb8 100644 --- a/pair_ap/pair.h +++ b/pair_ap/pair.h @@ -4,7 +4,7 @@ #include #define PAIR_AP_VERSION_MAJOR 0 -#define PAIR_AP_VERSION_MINOR 10 +#define PAIR_AP_VERSION_MINOR 12 #define PAIR_AP_DEVICE_ID_LEN_MAX 64 diff --git a/pair_ap/pair_fruit.c b/pair_ap/pair_fruit.c index 3236faf5..86f23636 100644 --- a/pair_ap/pair_fruit.c +++ b/pair_ap/pair_fruit.c @@ -60,6 +60,7 @@ typedef struct { bnum N; bnum g; + int N_len; } NGConstant; struct SRPUser @@ -122,6 +123,8 @@ new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) bnum_hex2bn(ng->N, n_hex); bnum_hex2bn(ng->g, g_hex); + ng->N_len = bnum_num_bytes(ng->N); + return ng; } @@ -348,7 +351,7 @@ srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, in bnum_bin2bn(s, bytes_s, len_s); bnum_bin2bn(B, bytes_B, len_B); - k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g, usr->ng->N_len); bnum_new(v); bnum_new(tmp1); bnum_new(tmp2); @@ -357,7 +360,7 @@ srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, in if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) goto cleanup1; - u = H_nn_pad(usr->alg, usr->A, B); + u = H_nn_pad(usr->alg, usr->A, B, usr->ng->N_len); x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); if (!u || !x) goto cleanup2; diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c index 6540d42d..d08baca2 100644 --- a/pair_ap/pair_homekit.c +++ b/pair_ap/pair_homekit.c @@ -55,6 +55,8 @@ #define REQUEST_BUFSIZE 4096 #define ENCRYPTED_LEN_MAX 0x400 +// #define DEBUG_SHORT_A 1 + enum pair_keys { PAIR_SETUP_MSG01 = 0, @@ -140,6 +142,7 @@ typedef enum typedef struct { + int N_len; bnum N; bnum g; } NGConstant; @@ -184,6 +187,7 @@ struct SRPVerifier struct NGHex { + int N_len; const char *n_hex; const char *g_hex; }; @@ -192,6 +196,7 @@ struct NGHex static struct NGHex global_Ng_constants[] = { { /* 2048 */ + 256, "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" @@ -202,6 +207,7 @@ static struct NGHex global_Ng_constants[] = "2" }, { /* 3072 */ + 384, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" @@ -215,7 +221,7 @@ static struct NGHex global_Ng_constants[] = "D120A93AD2CAFFFFFFFFFFFFFFFF", "5" }, - {0,0} /* null sentinel */ + {0} /* null sentinel */ }; @@ -226,13 +232,17 @@ new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) if ( ng_type != SRP_NG_CUSTOM ) { - n_hex = global_Ng_constants[ ng_type ].n_hex; - g_hex = global_Ng_constants[ ng_type ].g_hex; + n_hex = global_Ng_constants[ng_type].n_hex; + g_hex = global_Ng_constants[ng_type].g_hex; } bnum_hex2bn(ng->N, n_hex); bnum_hex2bn(ng->g, g_hex); + ng->N_len = bnum_num_bytes(ng->N); + + assert(ng_type == SRP_NG_CUSTOM || ng->N_len == global_Ng_constants[ng_type].N_len); + return ng; } @@ -247,6 +257,15 @@ free_ng(NGConstant * ng) free(ng); } +static int +N_len(SRP_NGType ng_type) +{ + if (ng_type == SRP_NG_CUSTOM) + return -1; + + return global_Ng_constants[ng_type].N_len; +} + static bnum calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) { @@ -401,13 +420,27 @@ srp_user_get_session_key(struct SRPUser *usr, int *key_length) return usr->session_key; } +#ifdef DEBUG_SHORT_A +// This value of "a" will yield a 383 byte A +static uint8_t short_a[] = { + 0xef, 0xb5, 0x93, 0xf5, 0x03, 0x97, 0x69, 0x8e, 0x15, 0xed, 0xee, 0x5b, 0xf2, 0xf9, 0x23, 0x6c, + 0xf0, 0x59, 0x6c, 0xe2, 0x77, 0xf2, 0x14, 0x16, 0xac, 0x99, 0xfa, 0x31, 0xae, 0x2b, 0xd3, 0x41, +}; +#endif + /* Output: username, bytes_A, len_A */ static void srp_user_start_authentication(struct SRPUser *usr, const char **username, const unsigned char **bytes_A, int *len_A) { - bnum_random(usr->a, 256); // BN_hex2bn(&(usr->a), "D929DFB605687233C9E9030C2280156D03BDB9FDCF3CCE3BC27D9CCFCB5FF6A1"); + bnum_random(usr->a, 256); +#ifdef DEBUG_SHORT_A + bnum_bin2bn(usr->a, short_a, sizeof(short_a)); +#endif +#ifdef DEBUG_PAIR + bnum_dump("Random value of usr->a:\n", usr->a); +#endif bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); @@ -443,7 +476,8 @@ srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, in bnum_bin2bn(s, bytes_s, len_s); bnum_bin2bn(B, bytes_B, len_B); - k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g, usr->ng->N_len); bnum_new(v); bnum_new(tmp1); @@ -453,7 +487,7 @@ srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, in if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) goto cleanup1; - u = H_nn_pad(usr->alg, usr->A, B); + u = H_nn_pad(usr->alg, usr->A, B, usr->ng->N_len); x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); if (!u || !x) goto cleanup2; @@ -535,6 +569,9 @@ srp_create_salted_verification_key(enum hash_alg alg, goto error; bnum_random(s, 128); // MODIFIED from csrp's BN_rand(s, 32, -1, 0) +#ifdef DEBUG_PAIR + bnum_dump("Random value of s:\n", s); +#endif x = calculate_x(alg, s, username, password, len_password); if (!x) @@ -599,8 +636,11 @@ srp_verifier_start_authentication(enum hash_alg alg, SRP_NGType ng_type, bnum_bin2bn(v, bytes_v, len_v); bnum_random(b, 256); // MODIFIED from BN_rand(b, 256, -1, 0) +#ifdef DEBUG_PAIR + bnum_dump("Random value of b:\n", b); +#endif - k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) + k = H_nn_pad(alg, ng->N, ng->g, ng->N_len); // MODIFIED from H_nn(alg, ng->N, ng->g) if (!k) goto error; @@ -708,8 +748,8 @@ srp_verifier_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, if (bnum_is_zero(tmp1)) goto error; - k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) - u = H_nn_pad(alg, A, B); // MODIFIED from H_nn(alg, A, B) + k = H_nn_pad(alg, ng->N, ng->g, ng->N_len); // MODIFIED from H_nn(alg, ng->N, ng->g) + u = H_nn_pad(alg, A, B, ng->N_len); // MODIFIED from H_nn(alg, A, B) // S = (A *(v^u)) ^ b bnum_modexp(tmp1, v, u, ng->N); @@ -1163,6 +1203,11 @@ client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add crypto_sign_keypair(sctx->public_key, sctx->private_key); +#ifdef DEBUG_PAIR + hexdump("Client public key:\n", sctx->public_key, sizeof(sctx->public_key)); + hexdump("Client private key:\n", sctx->private_key, sizeof(sctx->private_key)); +#endif + return 0; } @@ -1397,18 +1442,24 @@ client_setup_response1(struct pair_setup_context *handle, const uint8_t *data, s } pk = pair_tlv_get_value(response, TLVType_PublicKey); - salt = pair_tlv_get_value(response, TLVType_Salt); - if (!pk || !salt) + if (!pk || pk->size > N_len(SRP_NG_3072)) // max 384 bytes { - handle->errmsg = "Setup response 1: Missing or invalid pk/salt"; + handle->errmsg = "Setup response 1: Missing or invalid public key"; goto error; } - sctx->pkB_len = pk->size; // 384 + salt = pair_tlv_get_value(response, TLVType_Salt); + if (!salt || salt->size != 16) + { + handle->errmsg = "Setup response 1: Missing or invalid salt"; + goto error; + } + + sctx->pkB_len = pk->size; sctx->pkB = malloc(sctx->pkB_len); memcpy(sctx->pkB, pk->value, sctx->pkB_len); - sctx->salt_len = salt->size; // 16 + sctx->salt_len = salt->size; sctx->salt = malloc(sctx->salt_len); memcpy(sctx->salt, salt->value, sctx->salt_len); @@ -1436,13 +1487,13 @@ client_setup_response2(struct pair_setup_context *handle, const uint8_t *data, s } proof = pair_tlv_get_value(response, TLVType_Proof); - if (!proof) + if (!proof || proof->size != SHA512_DIGEST_LENGTH) { - handle->errmsg = "Setup response 2: Missing proof"; + handle->errmsg = "Setup response 2: Missing or invalid proof"; goto error; } - sctx->M2_len = proof->size; // 64 + sctx->M2_len = proof->size; sctx->M2 = malloc(sctx->M2_len); memcpy(sctx->M2, proof->value, sctx->M2_len); @@ -2050,17 +2101,22 @@ server_setup_request2(struct pair_setup_context *handle, const uint8_t *data, si } pk = pair_tlv_get_value(request, TLVType_PublicKey); - proof = pair_tlv_get_value(request, TLVType_Proof); - if (!pk || !proof) + if (!pk || pk->size > N_len(SRP_NG_3072)) // 384 bytes or less { - RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Missing pkA or proof"); + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Missing og invalid public key"); } - sctx->pkA_len = pk->size; // 384 + proof = pair_tlv_get_value(request, TLVType_Proof); + if (!proof || proof->size != SHA512_DIGEST_LENGTH) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Missing or invalid proof"); + } + + sctx->pkA_len = pk->size; sctx->pkA = malloc(sctx->pkA_len); memcpy(sctx->pkA, pk->value, sctx->pkA_len); - sctx->M1_len = proof->size; // 64 + sctx->M1_len = proof->size; sctx->M1 = malloc(sctx->M1_len); memcpy(sctx->M1, proof->value, sctx->M1_len); @@ -2072,7 +2128,7 @@ server_setup_request2(struct pair_setup_context *handle, const uint8_t *data, si goto out; } - sctx->M2_len = 64; // 512 bit hash + sctx->M2_len = SHA512_DIGEST_LENGTH; srp_verifier_verify_session(sctx->verifier, sctx->M1, &sctx->M2); if (!sctx->M2) { diff --git a/rtsp.c b/rtsp.c index e350b5a4..54f3b73a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -526,7 +526,7 @@ void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { } void release_play_lock(rtsp_conn_info *conn) { - debug(1, "Connection %d: release play lock.", conn->connection_number); + debug(2, "Connection %d: release play lock.", conn->connection_number); debug_mutex_lock(&playing_conn_lock, 1000000, 3); if (playing_conn == conn) { // if we have the player playing_conn = NULL; // let it go @@ -536,7 +536,7 @@ void release_play_lock(rtsp_conn_info *conn) { } int get_play_lock(rtsp_conn_info *conn) { - debug(1, "Connection %d: request play lock.", conn->connection_number); + debug(2, "Connection %d: request play lock.", conn->connection_number); // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but // interrupted an existing session int response = 0; @@ -592,16 +592,16 @@ int get_play_lock(rtsp_conn_info *conn) { } if ((have_the_player == 1) && (interrupting_current_session == 1)) { - debug(1, "Connection %d: Got player lock", conn->connection_number); + debug(2, "Connection %d: Got player lock", conn->connection_number); response = 1; } else { - debug(1, "Connection %d: failed to get player lock", conn->connection_number); + debug(1, "Connection %d: failed to get player lock after waiting.", conn->connection_number); response = -1; } } - if ((have_the_player) && (interrupting_current_session == 0)) { - debug(1, "Connection %d: Got player lock.", conn->connection_number); + if ((have_the_player == 1) && (interrupting_current_session == 0)) { + debug(2, "Connection %d: Got player lock.", conn->connection_number); response = 0; } return response; From 159f9a980d7c26191d5951d7fb789764d4a5611e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:29:05 +0000 Subject: [PATCH 470/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 9407ce55..61fb4202 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,5 +1,41 @@ -Version 4.1-dev-TBA-TBA +Version 4.1-dev-81-g24a5bbbe ==== +This is a pretty big under-the-hood update. + +#### Significant Changes +* Change clock base to improve stability. You need to use the updated `nqptp` that has also been changed. +* Avoid skipping the first 100 milliseconds or so of a newly-selected track. +* Clean up the output of statistics — shorter when log verbosity is zero. +* Change behaviour after an unrecoverable error -- if not handled, exit Shairport Sync. +* Add client and server ip metadata for AP2 Buffered Audio Stream. + +#### Changes +* Add client and server ip metadata for AP2. Remove timing peer list and DACP code only for a full AP2 teardown. +* Fix a few memory leaks. Add bogus exits to allow SPS to quit at the end of a play session, to make it easier to use `valgrind`. +* Stop checking that the timing peers are within the same subnet. +* Look for at least 0.1 seconds of leadtime, ensure a master clock is at least 0.275 seconds old before use, quieten a few debug messages. +* Add txtAirPlay data to the get info response. +* Don't pause reception of long RTSP messages. +* Don't ask for cover art if not asking for metadata too, duh. +* Skip some types of invalid AAC packets. +* Don't decode outdated frames. +* Wait for timing information to be valid in AP2 Buffered Audio Streams processing. +* Set the timing peer list as soon as a SETPEERS message is received -- don't wait until play is about to begin. Not sure if it works universally yet. +* Tidy up the statistics printout table and stop outputting (non existent) nominal fps for AP2 Realtime Streams. +* Use PKG_CHECK_MODULES to find libavcodec, to make it work with Fedora 35 (?) +* Change behaviour after an unrecoverable error -- if not handled, exit Shairport Sync. +* log statistics headers whenever verbosity goes on or off or whenever statistics are requested +* Clean up statistics. +* Stop watchdog timing out AP2 sessions. +* Remove the commas in the statistics logs -- easier to get into a spreadsheet. +* Don't overwrite service files if they already exist. +* Try finding libplist as libplist-2.0 on Linux too. +* Move to using CLOCK_MONOLITHIC_RAW to avoid NTP effects. Bump interface version. +* Add code to check divisions and mods for a potential divide by zero problem. This may cause SPS to terminate but it will leave a log message. +* Add a 'stats' function to the audio back ends to replace the 'rate_info' function. Zero all except the alsa back end. +* Attempt to increase the precision of the FPS timing +* Update to the latest version of ejurgensen's pair_ap library. With thanks. + #### Bug Fix - This fixes [#1345](https://github.com/mikebrady/shairport-sync/issues/1345) by checking for the `libplist` library both under the name `libplist` and `libplist-2.0` using the fallback strategy [as described in the autotools docs](https://autotools.io/pkgconfig/pkg_check_modules.html). Since we now try both names, we no longer need the FreeBSD/Linux check. Many thanks to [FW](https://github.com/fwcd) for the bug report, the fix and this documentation! From 3626acba85b6fe2bd87a2eb93775ee410129c36f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:33:52 +0000 Subject: [PATCH 471/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 61fb4202..c3243392 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -3,7 +3,7 @@ Version 4.1-dev-81-g24a5bbbe This is a pretty big under-the-hood update. #### Significant Changes -* Change clock base to improve stability. You need to use the updated `nqptp` that has also been changed. +* Change clock base to improve stability. You need to use the updated `nqptp` that has also been changed. If you forget, Shairport Sync will not work properly and will leave a message in the log. * Avoid skipping the first 100 milliseconds or so of a newly-selected track. * Clean up the output of statistics — shorter when log verbosity is zero. * Change behaviour after an unrecoverable error -- if not handled, exit Shairport Sync. From 03b7c830ed075130a03018a8c620482eec09c4fa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:37:09 +0000 Subject: [PATCH 472/878] Update AIRPLAY2.md --- AIRPLAY2.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 32af85b9..757d017e 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,7 +1,7 @@ AirPlay 2 === -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices, Macs and HomePod minis. It does not work for iTunes on Windows. +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs. It does not work for iTunes on Windows. The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work. @@ -14,15 +14,14 @@ The AirPlay 2 version of Shairport Sync will not work if installed on macOS. Thi The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. ### What Works -- AirPlay 2 for iOS, HomePod mini and Mac players. +- AirPlay 2 for iOS, HomePod mini, AppleTV and Mac players. ### What Does Not Work - No Remote Control - No AirPlay 2 for Windows iTunes ### What Partly Works -- Incomplete functionality using AppleTV?. (Reports of usage welcome!) -- Lossless or High Definition Lossless material over AirPlay 2 (ALAC decoder must be installed in config step). +- Lossless or High Definition Lossless material over AirPlay 2 is automatically transcoded to ALAC or AAC before sending it via AirPlay 2 to Shairport Sync. ### Limitations - Only a single instance of Shairport Sync can run on a device. From 6d899621dd3f0a22bc429f83f834f47959158c52 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 9 Dec 2021 14:36:58 +0000 Subject: [PATCH 473/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c3243392..bf8c66ad 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version 4.1-dev-92-g3ab16b5d +==== +#### Enhancement +* Thanks to [Anton Lindqvist](https://github.com/mptre) for updates and bug fixes to building for OpenBSD. + +#### Other Updates +* Thanks to [tekdude](https://github.com/tecdude) for a documentation update. +* Various documentation updates. + Version 4.1-dev-81-g24a5bbbe ==== This is a pretty big under-the-hood update. From 48e2679c386948b947105ffa13a23d4e2d85a6ea Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 9 Dec 2021 15:16:54 +0000 Subject: [PATCH 474/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 231d5ebd..77ddba95 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -43,8 +43,8 @@ Remove it as follows: ``` Do this until no more copies of `shairport-sync` are found. -### Remove Old Startup Scripts -You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. +### Remove Old Service Files +You should also remove any of the following service files that may be present: `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf`, `/etc/dbus-1/system.d/shairport-sync-mpris.conf`, and `/etc/init.d/shairport-sync`. New ones will be installed if necessary at the `# make install` stage. ### Reboot after Cleaning Up If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. @@ -150,4 +150,6 @@ Connect and enjoy... ### Restart Your Mac! -At the time of writing, there seems to be a bug in the Mac Music app, particularly on Apple silicon Macs. It appears that, if the Mac has been sleeping, the AirPlay 2 infrastructure is not fully reenabled, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only known solution is to reboot. +At the time of writing, there seems to be a bug in AirPlay 2 on Apple Silicon Macs and on the Apple TV. It appears that, if the Mac/ATV has been sleeping, the AirPlay 2 infrastructure is not fully reenabled when it wakens up. The `nqptp` program incorporates a workaround which fixes this issue for ATVs and mostly fixes it for macOS. However, it does not completely fix macOS Music on Apple Silicon Macs. The only known solution is to reboot the Mac. + +Hopefully, this bug, which is [widely known](https://discussions.apple.com/thread/253041243) and which doesn't just affect Shairport Sync, will be rectified by Apple soon. From a7a02083607fff77af5bc33e710c4eb359592cdb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:15:30 +0000 Subject: [PATCH 475/878] Only include code to modify the RTSP buffer length if metadata support is included in the configuration. --- rtsp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtsp.c b/rtsp.c index 54f3b73a..3e5915b9 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1174,8 +1174,10 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes enum rtsp_read_request_response reply = rtsp_read_request_response_ok; ssize_t buflen = 4096; +#ifdef CONFIG_METADATA if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded +#endif int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay char *buf = malloc(buflen + 1); // add a NUL at the end if (!buf) { From 1d023f80713484c28032e0aba7f2dfe2f3f545a7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:21:34 +0000 Subject: [PATCH 476/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index bf8c66ad..38c2ed3d 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-95-ga7a02083 +==== +#### Bug Fix +* Fixed a compilation bug that occurs if metadata support is not included in the build configuration. The fix was to add the appropriate conditional code statements. Thanks to [Daniel Gooß](https://github.com/Dangoo) and [winstonsetiawan](https://github.com/winstonsetiawan) for reporting. + Version 4.1-dev-92-g3ab16b5d ==== #### Enhancement From 26409ab1a34ce85a767f68b03d051ff071201f54 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:25:12 +0000 Subject: [PATCH 477/878] Update MOREINFO.md --- MOREINFO.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/MOREINFO.md b/MOREINFO.md index 7b957974..0ae21c16 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -209,7 +209,22 @@ Playback synchronisation is allowed to wander — to "drift" — a small amount Some Statistics --------------- -If you turn on the `general` `statistics` setting, a heading like this will be output to the log file (or to `STDERR` if the `-u` command line option is chosen): +If you set the `statistics` setting in the `diagnostics` section of the configuration file to `"YES"`, some statistics will be logged at regular intervals. The items logged will depend on the type of stream being processed: AirPlay 1, AirPlay 2 Buffered Audio or AirPlay 2 Realtime Audio. If the `log_verbosity` is set to 1, 2 or 3, additional items will be logged. + +From an audio enthusiast's point of view, the most important figure is probably the `all sync ppm` figure. This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added or removed from the audio stream, expressed in parts per million (ppm) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. The lower the `all sync ppm` number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever. You may also find that the number might be higher at the start while the sysstem settles down. + +The second most important figure is probably the `sync error ms` is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. + +Two possibly interesting measurements of the output rate may be available -- `output fps (r)` and `output fps (c)`. The `(r)` means "raw" and the `(c)` means "corrected". + +The "raw" figure is the rate at which the output device (typically a DAC) accepts data measured relative to the computer's own system clock (specifically the `CLOCK_MONOTONIC_RAW` clock). The accuracy of the number depends on the accuracy of the clock, which will typically be accurate to within anything from 20 to 100 ppm. + +The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a long period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate. + +Here is a brief description of the figures that might be provided. + + + ``` sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm ``` From ed7ffb91cf0cdf6cf45def5010aa125f006f1d31 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:31:01 +0000 Subject: [PATCH 478/878] Update MOREINFO.md --- MOREINFO.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MOREINFO.md b/MOREINFO.md index 0ae21c16..eb4933ef 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -223,7 +223,10 @@ The "corrected" figure is the rate at which the output device accepts data relat Here is a brief description of the figures that might be provided. - +| Title | Description | +----------------------- +| All Sync PPM | See above | +| Net Sync PPM | See above | ``` sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm From 679e1bb839053509bbca321a6f49993ca222f5db Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:16:43 +0000 Subject: [PATCH 479/878] Update MOREINFO.md --- MOREINFO.md | 181 ++++++++++++++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 91 deletions(-) diff --git a/MOREINFO.md b/MOREINFO.md index eb4933ef..226bb908 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -207,110 +207,109 @@ Tolerance Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. * You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. -Some Statistics +Statistics --------------- If you set the `statistics` setting in the `diagnostics` section of the configuration file to `"YES"`, some statistics will be logged at regular intervals. The items logged will depend on the type of stream being processed: AirPlay 1, AirPlay 2 Buffered Audio or AirPlay 2 Realtime Audio. If the `log_verbosity` is set to 1, 2 or 3, additional items will be logged. -From an audio enthusiast's point of view, the most important figure is probably the `all sync ppm` figure. This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added or removed from the audio stream, expressed in parts per million (ppm) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. The lower the `all sync ppm` number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever. You may also find that the number might be higher at the start while the sysstem settles down. +From an audio enthusiast's point of view, the most important figure is possibly the `all sync ppm` figure. This is the total amount of interpolation needed by Shairport Sync to keep the audio stream in sync. The number represents is the ratio of frames added and removed from the audio stream relative to all the frames output in the last interval, expressed in parts per million (ppm). For reference, adding or removing one frame per second into a 44,100 frames per second stream is ± 22.68 ppm. The lower this number number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever. You may also find that the number might be higher at the start while the system settles down. -The second most important figure is probably the `sync error ms` is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. +The second most important figure is possibly the `sync error ms`. This is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync has a tolerance of a sync error of ± 2.0 milliseconds without triggering interpolation. -Two possibly interesting measurements of the output rate may be available -- `output fps (r)` and `output fps (c)`. The `(r)` means "raw" and the `(c)` means "corrected". +Two other interesting measurements of the output rate may be available -- `output fps (r)` and `output fps (c)`, where `(r)` means "raw" and the `(c)` means "corrected". The "raw" figure is the rate at which the output device (typically a DAC) accepts data measured relative to the computer's own system clock (specifically the `CLOCK_MONOTONIC_RAW` clock). The accuracy of the number depends on the accuracy of the clock, which will typically be accurate to within anything from 20 to 100 ppm. -The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a long period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate. +The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a long period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate indeed. Here is a brief description of the figures that might be provided. - -| Title | Description | ------------------------ -| All Sync PPM | See above | -| Net Sync PPM | See above | +##### sync error ms +Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. +##### net sync ppm +This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added and removed from the audio stream, expressed in parts per million (ppm) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. +##### all sync ppm +This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added less the number removed from the audio stream, expressed in parts per million (ppm) in the last interval. +##### packets +This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames. +##### missing +This is the number of packets of audio frames that were not received in the last interval. It should be zero, and if not it can indicate a problem with the network. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +##### late +This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +##### too late +This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets may already have been received, so those frames might not actually be missing when the time comes to play them. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +##### resend reqs +This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +##### min DAC queue +The is the smallest number of frames of audio in the DAC's hardware queue. If it goes too low, the DAC may begin to underrun. +##### min buffers +The is the smallest number of packets of audio in the queue to be processed in the last interval. It is related to the overall latency in AirPlay 1 and AirPlay 2 Realtime Streams. If it comes close to zero it's often a sign that the network is poor. +##### max buffers +The is the largest number of packets of audio in the queue to be processed in the last interval. +##### nominal fps +This is the rate specified in the AirPlay stream itself. Only available for AirPlay 1 streams. +##### received fps +This is the rate at which frames are received from the network averaged since the start of the play session. Only available for AirPlay 1. +##### output fps (r) +Output rate measured relative to the computer system's clock since the start of the play session. See above for a discussion. +##### output fps (c) +Output rate measured relative to the network-clock-disciplined computer system's clock since the start of the play session. See above for a discussion. +##### source drift ppm +This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. Only available for AirPlay 1. +##### drift samples +This is the number drift samples have been accepted for calculating the source drift. Only available for AirPlay 1. +#### Example +The following example is of an AirPlay 2 Buffered Audio stream from a HomePod mini to a Raspberry Pi 3 equipped with a [Pimoroni Audio DAC SHIM (Line-Out)](https://shop.pimoroni.com/products/audio-dac-shim-line-out) with `log_verbosity` set to `1`. ``` -sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm + sync error ms net sync ppm all sync ppm packets min DAC queue min buffers max buffers output fps (r) output fps (c) + -1.88 8.5 8.5 526575 8455 62 63 44099.98 44099.92 + -1.92 31.2 31.2 527578 8460 62 63 44099.98 44099.92 + -1.87 2.8 2.8 528581 8467 62 63 44099.98 44099.93 + -1.98 22.7 22.7 529584 8466 62 63 44099.98 44099.93 + -2.01 73.6 73.6 530587 8468 62 63 44099.98 44099.93 + -1.99 93.5 93.5 531590 8463 62 62 44099.98 44099.94 + -1.77 5.7 5.7 532593 8466 62 63 44099.98 44099.94 + -1.84 2.8 2.8 533596 8456 62 62 44099.98 44099.95 + -1.54 0.0 0.0 534599 8477 62 63 44099.98 44099.95 + -1.19 0.0 0.0 535602 8501 62 62 44099.98 44099.95 + -1.03 0.0 0.0 536605 8505 62 62 44099.98 44099.96 + -1.10 0.0 0.0 537608 8497 62 63 44099.98 44099.96 + -1.39 0.0 0.0 538611 8485 62 63 44099.98 44099.97 + -1.43 0.0 0.0 539614 8485 62 63 44099.98 44099.97 + -1.75 0.0 0.0 540617 8481 62 62 44099.98 44099.97 + -1.52 0.0 0.0 541620 8474 62 63 44099.98 44099.98 + -1.37 0.0 0.0 542623 8488 62 62 44099.98 44099.98 + -1.63 0.0 0.0 543626 8481 62 62 44099.98 44099.98 + -2.01 102.0 102.0 544629 8465 62 62 44099.98 44099.99 + -1.88 14.2 14.2 545632 8474 62 62 44099.98 44099.99 + -1.42 0.0 0.0 546635 8486 62 63 44099.98 44099.99 + -1.92 17.0 17.0 547638 8435 62 63 44099.98 44100.00 + -1.27 0.0 0.0 548641 8479 62 62 44099.98 44100.00 + -0.64 0.0 0.0 549644 8528 62 62 44099.98 44100.00 + -1.12 0.0 0.0 550647 8476 62 62 44099.98 44100.01 + -1.88 25.5 25.5 551650 8471 62 62 44099.98 44100.01 + -1.79 0.0 0.0 552653 8477 62 62 44099.98 44100.01 + -1.79 0.0 0.0 553656 8477 62 63 44099.98 44100.02 + -1.81 19.8 19.8 554659 8467 62 62 44099.98 44100.02 + -2.04 110.5 110.5 555662 8468 62 63 44099.98 44100.02 + -1.64 2.8 2.8 556665 8476 62 62 44099.98 44100.02 + -1.29 0.0 0.0 557668 8496 62 63 44099.98 44100.03 + -1.20 0.0 0.0 558671 8500 62 63 44099.98 44100.03 + -1.33 0.0 0.0 559674 8483 62 63 44099.98 44100.03 + -1.43 0.0 0.0 560677 8468 62 63 44099.98 44100.03 + -1.47 0.0 0.0 561680 8484 62 62 44099.98 44100.04 + -0.88 0.0 0.0 562683 8512 62 62 44099.98 44100.04 + -1.16 0.0 0.0 563686 8500 62 63 44099.98 44100.04 + -1.42 0.0 0.0 564689 8488 62 62 44099.98 44100.05 + -1.58 0.0 0.0 565692 8476 62 63 44099.98 44100.05 + -1.50 0.0 0.0 566695 8488 62 63 44099.98 44100.05 + -1.79 0.0 0.0 567698 8471 62 63 44099.98 44100.05 + -1.82 8.5 8.5 568701 8473 62 62 44099.98 44100.06 + -1.87 19.8 19.8 569704 8468 62 63 44099.98 44100.06 + -1.96 85.0 85.0 570707 8456 62 63 44099.98 44100.06 + -1.80 2.8 2.8 571710 8452 62 63 44099.98 44100.06 + -1.66 0.0 0.0 572713 8477 62 62 44099.98 44100.07 ``` -This will be followed by the statistics themselves at regular intervals, for example: -``` - 0.15, 0.0, 0.0, 1003, 0, 0, 0, 0, 8539, 208, 225, 44099.95, 44274.21, 44099.68, 0.00, 0, -6.12 - 0.34, 0.0, 0.0, 2006, 0, 0, 0, 0, 8591, 206, 225, 44099.99, 44169.96, 44099.76, 0.00, 0, -5.27 - 0.89, 0.0, 0.0, 3009, 0, 0, 0, 0, 8571, 218, 225, 44100.01, 44152.81, 44099.85, 0.00, 0, -3.66 - 0.85, 0.0, 0.0, 4012, 0, 0, 0, 0, 8619, 218, 225, 44100.00, 44142.80, 44099.80, 0.00, 0, -4.62 - 1.29, 0.0, 0.0, 5015, 0, 0, 0, 0, 8623, 218, 225, 44100.00, 44129.39, 44099.83, 0.00, 0, -3.85 - 1.41, 0.0, 0.0, 6018, 0, 0, 0, 0, 8619, 216, 225, 44099.99, 44127.74, 44099.86, 0.00, 0, -2.76 - 1.53, 0.0, 0.0, 7021, 0, 0, 0, 0, 8603, 218, 225, 44100.00, 44109.60, 44099.81, 0.00, 0, -4.21 - 1.81, 0.0, 0.0, 8024, 0, 0, 0, 0, 8655, 215, 225, 44100.00, 44110.01, 44099.82, 0.00, 0, -4.16 - 2.00, -82.1, 82.1, 9027, 0, 0, 0, 0, 8481, 208, 225, 44100.00, 44110.69, 44099.83, 0.00, 0, -3.97 - 1.96, -8.5, 8.5, 10030, 0, 0, 0, 0, 8599, 209, 225, 44099.99, 44108.84, 44099.86, 0.00, 0, -3.02 - 1.94, 0.0, 0.0, 11033, 0, 0, 0, 0, 8583, 217, 225, 44100.00, 44109.33, 44099.87, 0.00, 0, -2.93 - 1.90, -5.7, 5.7, 12036, 0, 0, 0, 0, 8601, 219, 225, 44100.00, 44110.01, 44099.85, 20.47, 10, -23.88 - 2.02, -36.8, 36.8, 13039, 0, 0, 0, 0, 8488, 216, 225, 44100.00, 44107.18, 44099.85, 20.39, 12, -23.83 - 1.96, -17.0, 17.0, 14042, 0, 0, 0, 0, 8554, 218, 226, 44100.00, 44108.80, 44099.84, 20.63, 15, -24.13 - 1.99, -31.2, 31.2, 15045, 0, 0, 0, 0, 8592, 216, 225, 44100.00, 44106.85, 44099.85, 20.95, 18, -24.39 - 1.98, -28.3, 28.3, 16048, 0, 0, 0, 0, 8627, 219, 225, 44099.99, 44109.01, 44099.88, 21.23, 20, -23.90 - 2.01, -31.2, 31.2, 17051, 0, 0, 0, 0, 8582, 217, 225, 44100.00, 44108.86, 44099.84, 21.57, 22, -25.10 - 1.96, -19.8, 19.8, 18054, 0, 0, 0, 0, 8556, 215, 226, 44100.00, 44103.79, 44099.86, 21.59, 24, -24.85 - 1.98, -25.5, 25.5, 19057, 0, 0, 0, 0, 8563, 219, 225, 44100.00, 44102.62, 44099.86, 22.26, 25, -25.47 - 2.00, -28.3, 28.3, 20060, 0, 0, 0, 0, 8569, 214, 225, 44100.00, 44103.22, 44099.88, 22.57, 27, -25.41 - 1.98, -42.5, 42.5, 21063, 0, 0, 0, 0, 8570, 216, 225, 44100.00, 44104.14, 44099.85, 22.74, 28, -26.08 - 1.89, -31.2, 31.2, 22066, 0, 0, 0, 0, 8524, 218, 225, 44100.00, 44104.66, 44099.87, 22.89, 30, -25.89 - 1.96, 0.0, 0.0, 23069, 0, 0, 0, 0, 8650, 218, 225, 44100.00, 44105.36, 44099.88, 22.92, 32, -25.73 - 1.98, -34.0, 34.0, 24072, 0, 0, 0, 0, 8475, 219, 225, 44100.00, 44106.04, 44099.88, 22.90, 34, -25.70 - 2.07, -138.8, 138.8, 25075, 0, 0, 0, 0, 8490, 218, 226, 44100.00, 44104.93, 44099.87, 23.87, 35, -26.92 - 1.33, 0.0, 0.0, 26078, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44104.66, 44099.88, 23.68, 38, -26.43 - 1.67, -22.7, 22.7, 27081, 0, 0, 0, 0, 8562, 217, 225, 44100.00, 44106.16, 44099.87, 25.06, 40, -27.93 - 1.98, -28.3, 28.3, 28084, 0, 0, 0, 0, 8544, 218, 225, 44100.00, 44105.21, 44099.87, 25.91, 41, -28.84 - 1.99, -22.7, 22.7, 29087, 0, 0, 0, 0, 8569, 219, 225, 44100.00, 44102.30, 44099.87, 25.91, 41, -28.74 - 1.66, -56.6, 56.6, 30090, 0, 0, 0, 0, 8554, 218, 225, 44100.00, 44103.07, 44099.86, 26.38, 44, -29.46 - 1.44, 0.0, 0.0, 31093, 0, 0, 0, 0, 8642, 218, 225, 44100.00, 44102.18, 44099.87, 26.08, 46, -28.98 - 1.56, 0.0, 0.0, 32096, 0, 0, 0, 0, 8647, 218, 225, 44100.00, 44102.76, 44099.87, 25.79, 48, -28.64 - 1.68, 0.0, 0.0, 33099, 0, 0, 0, 0, 8647, 217, 225, 44100.00, 44103.41, 44099.88, 25.66, 49, -28.51 - 1.92, -14.2, 14.2, 34102, 0, 0, 0, 0, 8639, 217, 225, 44100.00, 44102.38, 44099.87, 25.45, 51, -28.40 - 2.00, -45.3, 45.3, 35105, 0, 0, 0, 0, 8593, 216, 225, 44100.00, 44103.23, 44099.86, 25.35, 53, -28.61 - 1.98, -17.0, 17.0, 36108, 0, 0, 0, 0, 8609, 218, 225, 44100.00, 44102.54, 44099.87, 25.21, 55, -28.12 - 1.99, -31.2, 31.2, 37111, 0, 0, 0, 0, 8575, 217, 225, 44100.00, 44101.98, 44099.88, 25.11, 57, -27.88 - 1.99, -14.2, 14.2, 38114, 0, 0, 0, 0, 8624, 218, 225, 44100.00, 44104.44, 44099.87, 24.95, 60, -27.97 - 1.99, -28.3, 28.3, 39117, 0, 0, 0, 0, 8582, 209, 225, 44100.00, 44104.91, 44099.86, 24.88, 62, -27.98 - 1.86, -17.0, 17.0, 40120, 0, 0, 0, 0, 8504, 207, 225, 44100.00, 44104.52, 44099.88, 24.66, 63, -27.39 - 1.96, -56.6, 56.6, 41123, 0, 0, 0, 0, 8517, 212, 226, 44100.00, 44102.29, 44099.88, 24.54, 66, -27.34 - 1.90, -36.8, 36.8, 42126, 0, 0, 0, 0, 8363, 218, 240, 44100.00, 44116.79, 44099.87, 24.55, 68, -27.48 - 1.89, 0.0, 0.0, 43129, 0, 0, 0, 0, 8487, 227, 241, 44100.00, 44117.57, 44099.88, 24.56, 69, -27.32 - 1.99, -22.7, 22.7, 44132, 0, 0, 0, 0, 8576, 227, 240, 44100.00, 44118.53, 44099.87, 24.57, 70, -27.44 - 1.98, -39.7, 39.7, 45135, 0, 0, 0, 0, 8593, 228, 240, 44100.00, 44113.88, 44099.87, 24.60, 73, -27.43 - 2.00, -34.0, 34.0, 46138, 0, 0, 0, 0, 8577, 227, 240, 44100.00, 44117.10, 44099.88, 24.65, 75, -27.46 - 1.98, -22.7, 22.7, 47141, 0, 0, 0, 0, 8591, 204, 240, 44100.00, 44102.78, 44099.87, 24.67, 77, -27.59 - 1.98, -17.0, 17.0, 48144, 0, 0, 0, 0, 8587, 216, 225, 44100.00, 44102.38, 44099.88, 24.67, 79, -27.42 - 1.99, -28.3, 28.3, 49147, 0, 0, 0, 0, 8590, 214, 225, 44100.00, 44102.70, 44099.87, 24.66, 80, -27.57 - 1.85, -62.3, 62.3, 50150, 0, 0, 0, 0, 8518, 217, 226, 44100.00, 44103.16, 44099.87, 24.70, 83, -27.55 - 1.90, -28.3, 28.3, 51153, 0, 0, 0, 0, 8617, 216, 225, 44100.00, 44102.60, 44099.88, 24.66, 85, -27.42 - 1.89, -5.7, 5.7, 52156, 0, 0, 0, 0, 8615, 218, 225, 44100.00, 44103.37, 44099.88, 24.65, 86, -27.39 - 1.96, -19.8, 19.8, 53159, 0, 0, 0, 0, 8600, 219, 225, 44100.00, 44103.77, 44099.87, 24.62, 89, -27.53 - 1.99, -22.7, 22.7, 54162, 0, 0, 0, 0, 8567, 216, 225, 44100.00, 44103.12, 44099.87, 24.57, 92, -27.45 -``` - -"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output is on average 0.15 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made. - -"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. In this case, the drift is negligible, but it can routinely be up to 150 ppm, especially with older machines. - -"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections are being made. Third party programs tend to have much larger levels of corrections. - -"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.2 seconds = 8,820 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up. - -"Source nominal frames per second" is the rate at which audio is being sent to Shairport Sync according to information supplied by the source itself. - -"Source actual frames per second" is the rate at which frames of audio are actually received by Shairport Sync. This can vary a great deal due to network conditions, but over a long time (more than 30 minutes) it should settle down to an accurate value. It does not account for retransmitted packets. - -"Output frames per second" is the actual rate at which frames of audio are taken by the output device. On a system with a well-conditioned `ntp`-based clock (and without output underruns) this figure should be very accurate after playing material continuously for a period. - -"Source clock drift in ppm" is an estimate of the difference in timekeeping between the audio source and the Shairport Sync device. It is calculated from a linear regression of drift sample data. The number of samples the estimate is based on is given in the next column, "Source clock drift sample count". - -"Rough calculated correction in ppm" is a very crude estimate of the amount of interpolation that needs to applied, on average, to keep sync. It is not really to be relied on at this time. - -For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an iPhone XS Max to Shairport Sync running on a WiFi-connected Raspberry Pi 3 using an [IQaudIO Pi-DigiAMP+](http://iqaudio.co.uk/hats/9-pi-digiamp.html). - -It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway... - +For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. WiFi Issues --------- From 8330f30bf86fbc36e753b4a142902a4d1cca606e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:29:30 +0000 Subject: [PATCH 480/878] Update MOREINFO.md --- MOREINFO.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/MOREINFO.md b/MOREINFO.md index 226bb908..68736eda 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -223,21 +223,21 @@ The "corrected" figure is the rate at which the output device accepts data relat Here is a brief description of the figures that might be provided. ##### sync error ms -Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. +Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. Positive means late, negative means early. ##### net sync ppm This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added and removed from the audio stream, expressed in parts per million (ppm) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. ##### all sync ppm -This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added less the number removed from the audio stream, expressed in parts per million (ppm) in the last interval. +This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added plus the number removed from the audio stream, expressed in parts per million (ppm) in the last interval. The magnitude of this should be the same as the `net sync ppm'. If it is much larger it means that Shairport Sync is overcorrecting for sync errors -- try increasing the drift tolerance to reduce it. ##### packets This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames. ##### missing -This is the number of packets of audio frames that were not received in the last interval. It should be zero, and if not it can indicate a problem with the network. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +This is the number of packets of audio frames that were not received in the last interval. It should be zero, and if not it can indicate a problem with the network. AirPlay 1 and AirPlay 2 Realtime Streams only. ##### late -This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. AirPlay 1 and AirPlay 2 Realtime Streams only. ##### too late -This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets may already have been received, so those frames might not actually be missing when the time comes to play them. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets may already have been received, so those frames might not actually be missing when the time comes to play them. AirPlay 1 and AirPlay 2 Realtime Streams only. ##### resend reqs -This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. Only relevant in AirPlay 1 or AirPlay 2 Realtime Streams. +This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. AirPlay 1 and AirPlay 2 Realtime Streams only. ##### min DAC queue The is the smallest number of frames of audio in the DAC's hardware queue. If it goes too low, the DAC may begin to underrun. ##### min buffers @@ -245,19 +245,19 @@ The is the smallest number of packets of audio in the queue to be processed in t ##### max buffers The is the largest number of packets of audio in the queue to be processed in the last interval. ##### nominal fps -This is the rate specified in the AirPlay stream itself. Only available for AirPlay 1 streams. +This is the rate specified in the AirPlay stream itself. AirPlay 1 only. ##### received fps -This is the rate at which frames are received from the network averaged since the start of the play session. Only available for AirPlay 1. +This is the rate at which frames are received from the network averaged since the start of the play session. AirPlay 1 only. ##### output fps (r) Output rate measured relative to the computer system's clock since the start of the play session. See above for a discussion. ##### output fps (c) Output rate measured relative to the network-clock-disciplined computer system's clock since the start of the play session. See above for a discussion. ##### source drift ppm -This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. Only available for AirPlay 1. +This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. AirPlay 1 only. ##### drift samples -This is the number drift samples have been accepted for calculating the source drift. Only available for AirPlay 1. +This is the number drift samples have been accepted for calculating the source drift. AirPlay 1 only. #### Example -The following example is of an AirPlay 2 Buffered Audio stream from a HomePod mini to a Raspberry Pi 3 equipped with a [Pimoroni Audio DAC SHIM (Line-Out)](https://shop.pimoroni.com/products/audio-dac-shim-line-out) with `log_verbosity` set to `1`. +The following example is of an AirPlay 2 Buffered Audio Stream from a HomePod mini to a WiFi-connected Raspberry Pi 3 equipped with a Pimoroni "Audio DAC SHIM (Line-Out)" with `log_verbosity` set to `1`. ``` sync error ms net sync ppm all sync ppm packets min DAC queue min buffers max buffers output fps (r) output fps (c) From fbce590609a161066cfd3c28b189771541467b7a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 10 Dec 2021 13:07:20 +0000 Subject: [PATCH 481/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 38c2ed3d..7caf9166 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-100-g8330f30b +==== +#### Enhancement +* Updated information on the statistics provided -- see [MOREINFO.md](https://github.com/mikebrady/shairport-sync/blob/development/MOREINFO.md#statistics). + Version 4.1-dev-95-ga7a02083 ==== #### Bug Fix From 9bf4557422a6a3c65f88c4c6ee76a684c6e99324 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Dec 2021 12:53:40 +0000 Subject: [PATCH 482/878] Make debug messages easier to see on macOS Terminal. --- common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.c b/common.c index d5ce6a5a..53cc13b5 100644 --- a/common.c +++ b/common.c @@ -535,7 +535,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f va_start(args, format); vsnprintf(s, sizeof(b) - (s - b), format, args); va_end(args); - sps_log(LOG_DEBUG, "%s", b); + sps_log(LOG_INFO, b); // LOG_DEBUG is hard to read on macOS terminal pthread_setcancelstate(oldState, NULL); } From 8ea3d57315d8e7d88a9ac7f2e1b94e692c9e94c0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Dec 2021 12:57:07 +0000 Subject: [PATCH 483/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 7caf9166..3b831e3c 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-102-g9bf45574 +==== +#### Enhancement +* Make debug messages in the system log easier to see on macOS Terminal (!) by outputting them at LOG_INFO level rather than LOG_DEBUG level. + Version-4.1-dev-100-g8330f30b ==== #### Enhancement From 84dd2dc41fbccd0dc8f8b14a47b9f29bb9495e98 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Dec 2021 13:28:10 +0000 Subject: [PATCH 484/878] Begin to try to figure out how to respond to remote control connections. --- rtsp.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index 3e5915b9..6b46310a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2338,7 +2338,7 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * if (typeValue != NULL) free(typeValue); } else { - debug(1, "Could not get the \"type\" item."); + debug(2, "Could not find a \"type\" item."); } plist_free(command_dict); @@ -2521,10 +2521,10 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag // we are being asked to close a stream teardown_phase_one(conn); plist_free(streams); - debug(1, "Connection %d: TEARDOWN phase one complete", conn->connection_number); + debug(2, "Connection %d: TEARDOWN phase one complete", conn->connection_number); } else { teardown_phase_two(conn); - debug(1, "Connection %d: TEARDOWN phase two complete", conn->connection_number); + debug(2, "Connection %d: TEARDOWN phase two complete", conn->connection_number); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2874,6 +2874,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", + plist_new_uint(0)); cancel_all_RTSP_threads( remote_control_stream, conn->connection_number); // kill all the other remote control listeners @@ -3095,7 +3097,12 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) } else if (conn->airplay_stream_category == remote_control_stream) { debug(2, "Connection %d: SETUP: Remote Control Stream received.", conn->connection_number); debug_log_rtsp_message(2, "Remote Control Stream stream (second) message", req); - + plist_t coreResponseDict = plist_new_dict(); + plist_dict_set_item(coreResponseDict, "streamID", plist_new_uint(1)); + plist_dict_set_item(coreResponseDict, "type", plist_new_uint(130)); + plist_t coreResponseArray = plist_new_array(); + plist_array_append_item(coreResponseArray, coreResponseDict); + plist_dict_set_item(setupResponsePlist, "streams", coreResponseArray); resp->respcode = 200; } else { debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", From 54d810895f400eb8b23446a29c728a649d95c689 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Dec 2021 13:30:31 +0000 Subject: [PATCH 485/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 3b831e3c..73d8eb79 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version-4.1-dev-104-g84dd2dc4 +==== +* Begin to explore some more connection types. + Version-4.1-dev-102-g9bf45574 ==== #### Enhancement From 52359dae8f5e12d3f2b2c6134c44263bc90a7d77 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 14 Dec 2021 16:26:46 +0000 Subject: [PATCH 486/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 73d8eb79..e845aa33 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-106-ge98111fb +==== +#### Enhancement +* Try to improve the reliability of restarting a "silent clock" device. + Version-4.1-dev-104-g84dd2dc4 ==== * Begin to explore some more connection types. From 5a23ff5224d692669076bf8f784dff6e696eda69 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 16 Dec 2021 09:47:50 +0000 Subject: [PATCH 487/878] slightly simplify function generate_preliminary_string. --- common.c | 13 ++++--------- configure.ac | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/common.c b/common.c index 53cc13b5..4ac0e2fe 100644 --- a/common.c +++ b/common.c @@ -432,27 +432,22 @@ void set_requested_connection_state_to_output(int v) { requested_connection_stat char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss, double tsl, const char *filename, const int linenumber, const char *prefix) { - size_t space_remaining = buffer_length; char *insertion_point = buffer; if (config.debugger_show_elapsed_time) { - snprintf(insertion_point, space_remaining, "% 20.9f", tss); + snprintf(insertion_point, buffer_length, "% 20.9f", tss); insertion_point = insertion_point + strlen(insertion_point); - space_remaining = space_remaining - strlen(insertion_point); } if (config.debugger_show_relative_time) { - snprintf(insertion_point, space_remaining, "% 20.9f", tsl); + snprintf(insertion_point, buffer_length, "% 20.9f", tsl); insertion_point = insertion_point + strlen(insertion_point); - space_remaining = space_remaining - strlen(insertion_point); } if (config.debugger_show_file_and_line) { - snprintf(insertion_point, space_remaining, " \"%s:%d\"", filename, linenumber); + snprintf(insertion_point, buffer_length, " \"%s:%d\"", filename, linenumber); insertion_point = insertion_point + strlen(insertion_point); - space_remaining = space_remaining - strlen(insertion_point); } if (prefix) { - snprintf(insertion_point, space_remaining, "%s", prefix); + snprintf(insertion_point, buffer_length, "%s", prefix); insertion_point = insertion_point + strlen(insertion_point); - space_remaining = space_remaining - strlen(insertion_point); } return insertion_point; } diff --git a/configure.ac b/configure.ac index 0af2d024..a3c9a529 100644 --- a/configure.ac +++ b/configure.ac @@ -373,7 +373,7 @@ AC_ARG_WITH(mpris-test-client, [AS_HELP_STRING([--with-mpris-test-client],[compi AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$with_mpris_test_client" = "xyes"]) if test "x$with_mpris_test_client" = "xyes" || test "x$with_dbus_test_client" = "xyes" || test "x$with_mpris_interface" = "xyes" || test "x$with_dbus_interface" = "xyes" ; then - PKG_CHECK_MODULES([glib], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${glib_CFLAGS} ${CFLAGS}" LIBS="${glib_LIBS} ${LIBS}"],[AC_MSG_ERROR(MPRIS client support requires the glib 2.0 library -- libglib2.0-dev suggested!)]) + PKG_CHECK_MODULES([glib], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${glib_CFLAGS} ${CFLAGS}" LIBS="${glib_LIBS} ${LIBS}"],[AC_MSG_ERROR(the glib 2.0 library is required -- libglib2.0-dev suggested!)]) fi # Look for mqtt flag From 3705d8e2e0708ca742799a50dc64c059b30903d1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 16 Dec 2021 11:12:51 +0000 Subject: [PATCH 488/878] Update sample dbus commands --- documents/sample dbus commands | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/documents/sample dbus commands b/documents/sample dbus commands index 96219c19..5c0d2be1 100644 --- a/documents/sample dbus commands +++ b/documents/sample dbus commands @@ -1,5 +1,17 @@ # For the "Native" Shairport Sync D-Bus Interface support, Shairport Sync must be built with the D-Bus interface support. Add the '--with-dbus-interface' flag at the ./configure stage. -# There is a simple test client that you can have built -- add the '--with-dbus-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-dbus-test-client + +# There is a simple test client that you can build -- add the '--with-dbus-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-dbus-test-client +# After attempting to send some commands, it will listen for property changes on the D-Bus interface and report them on the console. + +# Additionally, the following are examples of using the CLI command "dbus-send" to send D-Bus commands to Shairport Sync: + +# Control the player using Remote Control. Remote Control commands are sent as requests to the player (iOS, iTunes, macOS Music, etc.). +# Different versions of the players implement different subsets of the following commands. iOS seems to implement most of them (the first nine in the list). +dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play +#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs + +# Set Airplay Volume using Remote Control. Airplay Volume is between -30.0 and 0.0 and maps linearly onto the slider, with -30.0 being lowest and 0.0 being highest. +dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.SetAirplayVolume double:-10.0 # Get Log Verbosity dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync.Diagnostics string:Verbosity @@ -23,10 +35,6 @@ dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/Shair # Include Elapsed Time in Log Entries dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync.Diagnostics string:ElapsedTime variant:boolean:true -# Play using regular Remote Control -dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play -#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs - # Get Drift Tolerance dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:DriftTolerance # Set Drift Tolerance to 1 millisecond @@ -62,19 +70,13 @@ dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/Shair # Set Convolution Impulse Response File: dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:ConvolutionImpulseResponseFile variant:string:"/etc/shairport-sync/boom.wav" -# Set Airplay Volume using Remote Control. Airplay Volume is between -30.0 and 0.0 and maps linearly onto the slider, with -30.0 being lowest and 0.0 being highest. -dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.SetAirplayVolume double:-10.0 - - # AdvancedRemoteControl interface. # Some commands and properties are accessible only through the AdvancedRemoteControl interface. -# However, only iTunes or the macOS Music app from 10.15.2 onwards -# provide the functionality needed for the AdvancedRemoteControl interface to work. +# However, support for this is very limited in recent implementations. # You can check to see if AdvancedRemoteControl is available using the command: dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync.AdvancedRemoteControl string:Available - # Set Volume using Advanced Remote Control -- only works if the org.gnome.ShairportSync.AdvancedRemoteControl is available. dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.AdvancedRemoteControl.SetVolume int32:50 @@ -83,5 +85,6 @@ dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSy # For MPRIS support, Shairport Sync must be built with the MPRIS interface support. Add the '--with-mpris-interface' flag at the ./configure stage. # There is a simple test client that you can have built -- add the '--with-mpris-test-client' flag at the ./configure stage. You'll get an executable called shairport-sync-mpris-test-client. # This is mostly compatible with the MPRIS standard, except that Volume is read-only, with a separate SetVolume method. + # Set Volume, which must be between 0.0 and 1.0 and maps linearly onto the slider. dbus-send --system --print-reply --type=method_call --dest=org.mpris.MediaPlayer2.ShairportSync '/org/mpris/MediaPlayer2' org.mpris.MediaPlayer2.Player.SetVolume double:0.3 From 6f7e894777a69133ac3df8a1054c5395ed468102 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 20 Dec 2021 12:15:59 +0000 Subject: [PATCH 489/878] Set the minimum bus mastership time to two seconds. Turn on a debug message. --- ptp-utilities.c | 2 +- rtp.c | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index fa1c6407..2b60aaaf 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -148,7 +148,7 @@ int ptp_shm_interface_close() { } void ptp_send_control_message_string(const char *msg) { - debug(2, "Send control message to NQPTP: \"%s\"", msg); + debug(1, "Send control message to NQPTP: \"%s\"", msg); int s; unsigned short port = htons(NQPTP_CONTROL_PORT); struct sockaddr_in server; diff --git a/rtp.c b/rtp.c index 220bb919..4bd07a60 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -1305,7 +1305,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock // note that mastershipo may be backdated - if (duration_of_mastership < 275000000) { + if (duration_of_mastership < 2000000000) { debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || From d6d8eb49713c10743feb94640c7a4bb844ae2719 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 20 Dec 2021 23:57:12 +0000 Subject: [PATCH 490/878] Automated docker tagging & s6-overlay implementation. --- ...r-build.yaml => docker-build-on-push.yaml} | 3 +- ...releases.yaml => docker-build-on-tag.yaml} | 35 +++++--- .github/workflows/stale.yaml | 20 ----- README.md | 2 +- docker/Dockerfile | 88 +++++++------------ docker/README.md | 22 ++--- docker/airplay1/Dockerfile | 4 +- docker/airplay1/start.sh | 2 +- docker/docker-compose.yaml | 6 +- docker/etc/services.d/01-dbus-daemon/finish | 2 + docker/etc/services.d/01-dbus-daemon/run | 2 + docker/etc/services.d/02-avahi/run | 2 + docker/etc/services.d/03-nqptp/run | 2 + .../etc/services.d/04-shairport-sync/finish | 2 + docker/etc/services.d/04-shairport-sync/run | 2 + docker/start.sh | 18 ---- 16 files changed, 85 insertions(+), 127 deletions(-) rename .github/workflows/{docker-build.yaml => docker-build-on-push.yaml} (96%) rename .github/workflows/{docker-build-releases.yaml => docker-build-on-tag.yaml} (65%) delete mode 100644 .github/workflows/stale.yaml create mode 100644 docker/etc/services.d/01-dbus-daemon/finish create mode 100644 docker/etc/services.d/01-dbus-daemon/run create mode 100644 docker/etc/services.d/02-avahi/run create mode 100644 docker/etc/services.d/03-nqptp/run create mode 100644 docker/etc/services.d/04-shairport-sync/finish create mode 100644 docker/etc/services.d/04-shairport-sync/run delete mode 100644 docker/start.sh diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build-on-push.yaml similarity index 96% rename from .github/workflows/docker-build.yaml rename to .github/workflows/docker-build-on-push.yaml index cb186043..072e9218 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build-on-push.yaml @@ -1,6 +1,5 @@ # Builds & pushes a docker image when a commit is made to one of the branches specified below. # Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-airplay1' -# See docker-build-releases.yaml for release build process. name: Build and push docker image based on commit to specified branches. @@ -9,7 +8,7 @@ on: branches: [ development ] env: - DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 jobs: main: diff --git a/.github/workflows/docker-build-releases.yaml b/.github/workflows/docker-build-on-tag.yaml similarity index 65% rename from .github/workflows/docker-build-releases.yaml rename to .github/workflows/docker-build-on-tag.yaml index 6894a0e7..d345bd89 100644 --- a/.github/workflows/docker-build-releases.yaml +++ b/.github/workflows/docker-build-on-tag.yaml @@ -1,15 +1,16 @@ -# Builds & pushes a docker image when a release occurs. +# Builds & pushes a docker image when a tag occurs. +# It seems this yaml has to exist on the branch the tag refers to. -# The following tags are created: -# 'latest' & 'latest-airplay1' -# '[RELEASE TAG]' & '[RELEASE TAG]-airplay1' +# The following docker tags are created & pushed: +# [RELEASE TAG], [RELEASE TAG]-airplay1, name: Build and push docker images on releases. on: - release: - types: [ ] - + push: + tags: + - '*' # Push events to every tag (not containing '/') + env: DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 @@ -21,9 +22,15 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - + - name: Set SHAIRPORT_SYNC_BRANCH env. - run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV + run: | + raw=$(git branch -r --contains ${{ github.ref }}) + branch=${raw##*/} + echo "SHAIRPORT_SYNC_BRANCH=${branch}" >> $GITHUB_ENV + + - name: Set tag env + run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Determine NQPTP env (part 1). if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} @@ -51,9 +58,9 @@ jobs: with: context: ./ file: ./docker/Dockerfile - platforms: ${{ env.PLATFORMS }} + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -62,9 +69,9 @@ jobs: uses: docker/build-push-action@v2 with: context: ./ - file: ./docker/airplay1/Dockerfile-AirPlay1 - platforms: ${{ env.PLATFORMS }} + file: ./docker/airplay1/Dockerfile + platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_IMAGE_NAME }}:latest-airplay1, ${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}-airplay1 build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml deleted file mode 100644 index be61da3d..00000000 --- a/.github/workflows/stale.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Close stale issues and PRs' -on: - schedule: - - cron: '0 0 * * *' - -permissions: - issues: write - pull-requests: write - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v4 - with: - stale-issue-message: 'This issue has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' - stale-pr-message: 'This PR has been inactive for 60 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' - # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues - # days-before-stale: 60 - # days-before-close: 7 diff --git a/README.md b/README.md index b98156dc..547398d6 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ For the development of AirPlay 2 support, special thanks are due to: * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. * [Charles Omer](https://github.com/charlesomer) for Docker automation, repository management automation, testing, encouragement, enthusiasm. -And of course, thanks to everyone who has supported and improved Shairport Sync over the years. +And of course, thanks to everyone who has supported and improved Shairport Sync over the years. \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 98a3900c..d8241654 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,12 @@ -FROM alpine:3.12 AS builder-base -# Version 3.12 is required for now since libplist has breaking changes in v3.13 and above which are not yet standard -# in debian/ubuntu. +FROM alpine:3.12 AS builder + +# Check required arguments exist. These will be provided by the Github Action +# Workflow and are required to ensure the correct branches are being used. +ARG SHAIRPORT_SYNC_BRANCH +RUN test -n "$SHAIRPORT_SYNC_BRANCH" +ARG NQPTP_BRANCH +RUN test -n "$NQPTP_BRANCH" -# General Build System: RUN apk -U add \ git \ build-base \ @@ -10,9 +14,7 @@ RUN apk -U add \ automake \ libtool \ dbus \ - su-exec \ alsa-lib-dev \ - libdaemon-dev \ popt-dev \ mbedtls-dev \ soxr-dev \ @@ -30,68 +32,42 @@ RUN apk -U add \ libplist-dev \ libgcrypt-dev -# ALAC Build System: -FROM builder-base AS builder-alac - +##### ALAC ##### RUN git clone https://github.com/mikebrady/alac WORKDIR /alac RUN autoreconf -fi RUN ./configure RUN make RUN make install +WORKDIR / +##### ALAC END ##### -# Shairport Sync Build System: -FROM builder-base AS builder-sps - -# These will be modified by the Github Action Workflow and are required -# to ensure the correct branches are being used. -ARG SHAIRPORT_SYNC_BRANCH -RUN test -n "$SHAIRPORT_SYNC_BRANCH" -ARG NQPTP_BRANCH -RUN test -n "$NQPTP_BRANCH" - -COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ -COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc -COPY --from=builder-alac /usr/local/include /usr/local/include - +##### NQPTP ##### RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp -# Use development branch for now. RUN git checkout "$NQPTP_BRANCH" RUN autoreconf -fi RUN ./configure RUN make RUN make install WORKDIR / +##### NQPTP END ##### +##### SPS ##### WORKDIR /shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" RUN autoreconf -fi -# RUN ./configure \ -# --with-alsa \ -# --with-dummy \ -# --with-pipe \ -# --with-stdout \ -# --with-avahi \ -# --with-ssl=mbedtls \ -# --with-soxr \ -# --sysconfdir=/etc \ -# --with-dbus-interface \ -# --with-mpris-interface \ -# --with-mqtt-client \ -# --with-apple-alac \ -# --with-convolution \ -# --with-airplay-2 -# RUN make -j $(nproc) -RUN CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata --with-dummy --with-pipe \ - --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-dbus-interface --with-stdout \ - --with-mpris-interface --with-mqtt-client --with-apple-alac --with-convolution --with-airplay-2 +RUN ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-airplay-2 \ + --with-metadata --with-dummy --with-pipe --with-dbus-interface \ + --with-stdout --with-mpris-interface --with-mqtt-client \ + --with-apple-alac --with-convolution RUN make -j RUN make install +##### SPS END ##### -# Shairport Sync Runtime System: -FROM alpine:3.12 +# Shairport Sync Runtime System +FROM crazymax/alpine-s6:3.12 RUN apk -U add \ alsa-lib \ @@ -104,21 +80,20 @@ RUN apk -U add \ libconfig \ libsndfile \ mosquitto \ - su-exec \ libuuid \ ffmpeg \ libsodium \ libplist \ libgcrypt -RUN rm -rf /lib/apk/db/* +# Copy build files. +COPY --from=builder /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 /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ +COPY --from=builder /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ -COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ -COPY --from=builder-sps /etc/shairport-sync* /etc/ -COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ -COPY --from=builder-sps /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ -COPY --from=builder-sps /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync -COPY --from=builder-sps /usr/local/bin/nqptp /usr/local/bin/nqptp +COPY ./docker/etc /etc # Create non-root user for running the container -- running as the user 'shairport-sync' also allows # Shairport Sync to provide the D-Bus and MPRIS interfaces within the container @@ -129,8 +104,7 @@ RUN adduser -D shairport-sync -G shairport-sync # Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio -COPY ./docker/start.sh / +# Remove anything we don't need. +RUN rm -rf /lib/apk/db/* -RUN chmod +x ./start.sh - -ENTRYPOINT [ "/start.sh" ] \ No newline at end of file +ENTRYPOINT [ "/init" ] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 6043cc44..66f3d470 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,18 +2,17 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync -## Example Docker Compose File +## Docker Compose See the `docker-compose.yaml` file in this folder for an example. -## Basic Usage +## Docker Run ``` $ docker run -d --restart unless-stopped --net host --device /dev/snd \ mikebrady/shairport-sync ``` -The above command will run Shairport Sync as a daemon in a Docker container, accessing the computer's ALSA audio infrastructure. It will send audio to the default output device and make no use of any hardware mixers the default device might have. The AirPlay service name will be the host's `hostname` with the first letter capitalised, e.g. `Ubuntu`. -## Options +### Options Any options you add to the command above will be passed to Shairport Sync. Here is an example: ``` @@ -24,24 +23,25 @@ This will sent audio to alsa hardware device `hw:0` and make use of the that dev ## Configuration File -Edit the configuration file `/etc/shairport-sync.conf` in the container (or use the `-v` option to mirror an external copy of `shairport-sync.conf` in to `/etc/shairport-sync.conf`) to get access to the full range of configuration options. +To get access to the full range of configuration options, pass the configuration file to `/etc/shairport-sync.conf` in the container using the `-v` option or docker compose. -## Build Example (for arm7 devices) +## Building +### Build Example (for arm7 devices) ``` docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development -t shairport-sync:unstable-development . ``` -`SHAIRPORT_SYNC_BRANCH` is required to ensure the image is built using the expected branch. +`SHAIRPORT_SYNC_BRANCH` and `NQPTP_BRANCH` are required to ensure the image is built using the expected branch. -## AirPlay 1 Only +### AirPlay 1 Only -The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image build via the AirPlay 2 branch does not work with multiple instances on the same hardware. +The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image built via the AirPlay 2 branch has missing functionality, e.g. it does not work with multiple instances on the same hardware; does not support iTunes for Windows etc. -## GitHub Action Builds +### GitHub Action Builds Requires the following secrets to be set in the repo: - `DOCKER_REGISTRY` - docker.io if using Docker Hub, else set to your registry URL. - `DOCKER_REGISTRY_TOKEN` - Access token for your registry. - `DOCKER_REGISTRY_USER` - Login user for your registry. -- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub. \ No newline at end of file +- `DOCKER_IMAGE_NAME` - The name of the image, for example `your-registry.com/shairport-sync` or just `your-username/shairport-sync` if using Docker Hub. diff --git a/docker/airplay1/Dockerfile b/docker/airplay1/Dockerfile index e60fd0b5..6276f08d 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/airplay1/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder-base +FROM alpine AS builder-base # General Build System: RUN apk -U add \ git \ @@ -63,7 +63,7 @@ RUN make -j $(nproc) RUN make install # Shairport Sync Runtime System: -FROM alpine:3.12 +FROM alpine RUN apk -U add \ alsa-lib \ diff --git a/docker/airplay1/start.sh b/docker/airplay1/start.sh index f68a122f..5e24ff59 100644 --- a/docker/airplay1/start.sh +++ b/docker/airplay1/start.sh @@ -10,4 +10,4 @@ dbus-daemon --system avahi-daemon --daemonize --no-chroot -su-exec shairport-sync shairport-sync $@ \ No newline at end of file +su-exec shairport-sync shairport-sync "$@" \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 74ed09ac..13b5bac7 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -8,4 +8,8 @@ services: devices: - "/dev/snd" # volumes: - # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. \ No newline at end of file + # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. + logging: + options: + max-size: "200k" + max-file: "10" \ No newline at end of file diff --git a/docker/etc/services.d/01-dbus-daemon/finish b/docker/etc/services.d/01-dbus-daemon/finish new file mode 100644 index 00000000..1809d88d --- /dev/null +++ b/docker/etc/services.d/01-dbus-daemon/finish @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +rm -f /var/run/dbus.pid \ No newline at end of file diff --git a/docker/etc/services.d/01-dbus-daemon/run b/docker/etc/services.d/01-dbus-daemon/run new file mode 100644 index 00000000..488884bd --- /dev/null +++ b/docker/etc/services.d/01-dbus-daemon/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +dbus-daemon --system --nofork \ No newline at end of file diff --git a/docker/etc/services.d/02-avahi/run b/docker/etc/services.d/02-avahi/run new file mode 100644 index 00000000..848dd8dd --- /dev/null +++ b/docker/etc/services.d/02-avahi/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +avahi-daemon --no-chroot \ No newline at end of file diff --git a/docker/etc/services.d/03-nqptp/run b/docker/etc/services.d/03-nqptp/run new file mode 100644 index 00000000..7ddde248 --- /dev/null +++ b/docker/etc/services.d/03-nqptp/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +/usr/local/bin/nqptp \ No newline at end of file diff --git a/docker/etc/services.d/04-shairport-sync/finish b/docker/etc/services.d/04-shairport-sync/finish new file mode 100644 index 00000000..c9e22a21 --- /dev/null +++ b/docker/etc/services.d/04-shairport-sync/finish @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -S0 +s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/docker/etc/services.d/04-shairport-sync/run b/docker/etc/services.d/04-shairport-sync/run new file mode 100644 index 00000000..395ff814 --- /dev/null +++ b/docker/etc/services.d/04-shairport-sync/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +/usr/local/bin/shairport-sync -vu --statistics \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100644 index 7221417a..00000000 --- a/docker/start.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -set -e - -rm -rf /var/run/dbus.pid -#mkdir -p /var/run/dbus - -dbus-uuidgen --ensure -dbus-daemon --system - -avahi-daemon --daemonize --no-chroot - -# TODO: This should probably be implemented in a better way. -nqptp & - -# su-exec shairport-sync shairport-sync $@ - -shairport-sync -vu --statistics \ No newline at end of file From 6d1469839191a24c0d734e8a29e879f7b20bf2ff Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:02:43 +0000 Subject: [PATCH 491/878] Send an empty timing list when a player is acquired. --- rtsp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index 6b46310a..cd76c537 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2538,7 +2538,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag resp->respcode = 451; // don't know what to do here } // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); - // exit(EXIT_SUCCESS); // + // exit(EXIT_SUCCESS); // } #endif @@ -2679,6 +2679,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and // timingPeerList if (conn->airplay_stream_category == ptp_stream) { + ptp_send_control_message_string("T"); // remove all previous history debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { @@ -2836,8 +2837,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); - - + + resp->respcode = 200; } else { From 2e6eb23a04c0bef5bbc318ef5ce5cc0a57b22512 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:03:28 +0000 Subject: [PATCH 492/878] Wait for 300 secodns before complaining about the age of a sync. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index 4bd07a60..17ee7d8b 100644 --- a/rtp.c +++ b/rtp.c @@ -1280,7 +1280,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (response == clock_ok) { uint64_t time_now = get_absolute_time_in_ns(); int64_t time_since_sample = time_now - actual_time_of_sample; - if (time_since_sample > 10000000000) { + if (time_since_sample > 300000000000) { if (long_time_notifcation_done == 0) { debug(1, "The last PTP timing sample is pretty old: %f seconds.", 0.000000001 * time_since_sample); From f0134387b9b11ad947b6c196300c84074100647d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:04:15 +0000 Subject: [PATCH 493/878] Flush if the first packet is less than 100 ms in the future. --- player.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/player.c b/player.c index 1feee08f..7750c504 100644 --- a/player.c +++ b/player.c @@ -1062,17 +1062,20 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->first_packet_time_to_play = should_be_time; int64_t lt = conn->first_packet_time_to_play - local_time_now; - - if (lt < 100000000) - debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds.", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + if (lt < 100000000) { + debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + do_flush(conn->first_packet_timestamp + 5 * 4410, conn); + } +/* int64_t lateness = local_time_now - conn->first_packet_time_to_play; if (lateness > 0) { debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", lateness); - do_flush(conn->first_packet_timestamp + 5 * 4410, conn); + } +*/ } if (conn->first_packet_time_to_play != 0) { From fc736cd12f907bffb93ca1a313c8d97ec703648c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:18:14 +0000 Subject: [PATCH 494/878] Look for 0.15 seconds of a lead time... --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 7750c504..37757f8b 100644 --- a/player.c +++ b/player.c @@ -1063,7 +1063,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - if (lt < 100000000) { + if (lt < 150000000) { debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); From 8656149d68d3a4321fdc558f2041258667587904 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:32:16 +0000 Subject: [PATCH 495/878] Adjust the time needed to allow the clock to settle and the lead time. --- player.c | 3 +++ rtp.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/player.c b/player.c index 37757f8b..2e9819c9 100644 --- a/player.c +++ b/player.c @@ -1067,6 +1067,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); + } else { + debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); } /* int64_t lateness = local_time_now - conn->first_packet_time_to_play; diff --git a/rtp.c b/rtp.c index 17ee7d8b..348ac869 100644 --- a/rtp.c +++ b/rtp.c @@ -1305,7 +1305,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock // note that mastershipo may be backdated - if (duration_of_mastership < 2000000000) { + if (duration_of_mastership < 1500000000) { debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; } else if ((duration_of_mastership > 5000000000) || From ce7bbd3999912c557f8bc513cee0f27e42043eee Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:28:38 +0000 Subject: [PATCH 496/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index e845aa33..c91f6307 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version-4.1-dev-115-g8656149d +==== +* Simplify and improve timing when a play session from a new device is started. + Version-4.1-dev-106-ge98111fb ==== #### Enhancement From e0d901bacb6e5048d4ac9a8b68bac620dadee4bf Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 21 Dec 2021 21:18:46 +0100 Subject: [PATCH 497/878] edited docker readme to include clarity on different tag. --- docker/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/README.md b/docker/README.md index 6043cc44..1ee9b4ba 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,6 +2,13 @@ Available at: https://hub.docker.com/r/mikebrady/shairport-sync +Please note if you want the development version of the image including Airplay 2 support, please pull the image with the ```unstable development``` tag using the following command: +``` +docker pull mikebrady/shairport-sync:unstable-development +``` + +When using the below commands, you should replace mikebrady/shairport-sync with mikbrady/shairport-sync:unstable-development. + ## Example Docker Compose File See the `docker-compose.yaml` file in this folder for an example. From 96b4df513b2baf898ecbe924da37b76059894735 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 21 Dec 2021 21:20:19 +0100 Subject: [PATCH 498/878] main development repo readme edit. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b98156dc..6025505c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Guides * A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP1.md). * A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). * A guide to building for AirPlay 2 on FreeBSD is forthcoming. +* Please note, a Docker image including Airplay 2 support is available under the docker folder of this repo. More Information --- From 96358bc60cee77eb869a9be325a6010037322b22 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 21 Dec 2021 21:20:46 +0100 Subject: [PATCH 499/878] main readme edit for docker reference From 30b9d15e36f70636abc6717773e3ca8dc7ddcf4d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:47:49 +0000 Subject: [PATCH 500/878] Quieten a few debug mssages, slightly shorten the minimum lead time needed for good initial sync. --- player.c | 4 ++-- ptp-utilities.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index 2e9819c9..db7ec169 100644 --- a/player.c +++ b/player.c @@ -1063,12 +1063,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - if (lt < 150000000) { + if (lt < 130000000) { debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); } else { - debug(1, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); } /* diff --git a/ptp-utilities.c b/ptp-utilities.c index 2b60aaaf..fa1c6407 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -148,7 +148,7 @@ int ptp_shm_interface_close() { } void ptp_send_control_message_string(const char *msg) { - debug(1, "Send control message to NQPTP: \"%s\"", msg); + debug(2, "Send control message to NQPTP: \"%s\"", msg); int s; unsigned short port = htons(NQPTP_CONTROL_PORT); struct sockaddr_in server; From 02852d2305aa68c6f29225ce99b7242f93e4cff5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Dec 2021 15:23:55 +0000 Subject: [PATCH 501/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c91f6307..001f7c43 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-?? +==== +#### Enhancement +* Update the Docker workflow to automatically do Docker tagging and to use an s6-overlay implementation. Many thanks to [Robert Casties](https://github.com/robcast) for his s6-overlay Pull Request [#1349](https://github.com/mikebrady/shairport-sync/pull/1349) which formed the basis of this work. + Version-4.1-dev-115-g8656149d ==== * Simplify and improve timing when a play session from a new device is started. From 4d0460b94a76e7fe2b2b552efe7f17d0dd9600a6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Dec 2021 15:32:03 +0000 Subject: [PATCH 502/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 001f7c43..e5f6ee66 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,9 @@ Version-4.1-dev-?? ==== #### Enhancement -* Update the Docker workflow to automatically do Docker tagging and to use an s6-overlay implementation. Many thanks to [Robert Casties](https://github.com/robcast) for his s6-overlay Pull Request [#1349](https://github.com/mikebrady/shairport-sync/pull/1349) which formed the basis of this work. +* Update the Docker workflow to automatically do Docker tagging and to use an s6-overlay implementation. Thanks to [Charles Omer](https://github.com/charlesomer) for all this work, and to [Robert Casties](https://github.com/robcast) for his s6-overlay Pull Request [#1349](https://github.com/mikebrady/shairport-sync/pull/1349) which formed the basis of the s6-overlay work. +* Slight documentation update, thanks to [michaeldeborst](https://github.com/michaeldeborst). + Version-4.1-dev-115-g8656149d ==== From 4cb485d206b8df08d525ab64e6e120ab335b18bb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:08:49 +0000 Subject: [PATCH 503/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index e5f6ee66..79c87738 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,4 @@ -Version-4.1-dev-?? +Version-4.1-dev-127-g4d0460b9 ==== #### Enhancement * Update the Docker workflow to automatically do Docker tagging and to use an s6-overlay implementation. Thanks to [Charles Omer](https://github.com/charlesomer) for all this work, and to [Robert Casties](https://github.com/robcast) for his s6-overlay Pull Request [#1349](https://github.com/mikebrady/shairport-sync/pull/1349) which formed the basis of the s6-overlay work. From 035f41bdb43432dbff6981013c914f9e49ce9619 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:15:17 +0000 Subject: [PATCH 504/878] Make the hw_addr 8 bytes long to that it can be read from as a 64-bit number. --- common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.h b/common.h index d9be0327..f4139e56 100644 --- a/common.h +++ b/common.h @@ -152,7 +152,7 @@ typedef struct { int mqtt_publish_cover; int mqtt_enable_remote; #endif - uint8_t hw_addr[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; int udp_port_range; From b413a29da61dd24aaa1e7675f8ffcd6f33663518 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:17:34 +0000 Subject: [PATCH 505/878] Be more careful to create default file names based on the name of the app. Create the airplay_device_id in a way that includes the PID. --- airplayd | Bin 0 -> 1204288 bytes shairport.c | 54 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 20 deletions(-) create mode 100755 airplayd diff --git a/airplayd b/airplayd new file mode 100755 index 0000000000000000000000000000000000000000..b22fe8c961bc9e475fd34bb9dc1f03feaca5705e GIT binary patch literal 1204288 zcmb@v3tW`d+BW_SprB%ohDk~}9x^J-QOU5ZP*Ev0sW2(60bvl5!w7?7(hd(5r4=O= zl_izC*kwB^D=KSaTCvlL%3V}epibd2Dl00>@4B96Jyk@B-UKr&oIJ_Zt;f8=z+Z9#xUmll3xusl3$H5 zLj>d)!c^!EWZ8H~GPqEH!OjmwUMk|LXqVw+#2J{6WHSI1gACeY=%&&rlk$2vLJu>< z4>63&cMXH>vUCXkPeXnh@)>GTfPwk84nD+vXqzDk=?r$>WdVN{qJR3Zyc6Max`t@9$s{^->^5{b;TOxVW;(UPAHEWYV<>< zb3?a8m!sS``H}Jw6^U+FlG{;Z+!nej(HL^tWkyZJDq|5eaaY}bncGs%WPg)Un*TzkW zMg!DKdZb54RC(C&5ypen!$Zo$X3t%jwz+cHl_!?BhIC6bLMGm{z3<4xzD9^6BjKEg zyQUf;lgq=JTw$(=>JZmS+b+L5#5nV&p|?Jg722(@5jO8?|07AG!$L3u`c+Mw-Xp9h z2Grd>%8Q~_ln%)WgFho`BN7KX(hcWLH9Z{@4h=cO(Pzj5M%eXmRriU;m}J99H=+_< z4JlWT1|1p}!m$vJzz^pk$HWPY;Davz*B_3%{s_?soR9nrw7?hyJVkNBse-$}hawCU zX~Pvi4R|`j83<=0oQ1$ooCU^6;3xz)0=i><#^8UhALm$6V;nF+aT~_l1b8081cVC^ zE<(^hiAb1)Fc~2UVG4r%nF=yZMPOOuVuVW&W~j88KujL9Ko8kR@xju8p2fw zS0l_t;O80ykIAh6G63@tya+i6xd_)G@Usvh&t$>>@VnT{Rj^rtV7`EVGE4) zz()`sLwFqF2?YJ~50$VH_#DFX2rnSKgiw$0GQz6}{Je(nI>NsY8W7$<;Aa~`Bf@qB zEb+#B2>iT{@BzX{2p=OfBYcjq3xS_6EinIY!T-Av_9A?P(1x%NVL!qlgl`er5%~EY z;Rll$#u4C82)`iwhHw<&cLej}z#5H!Rm%uRh(PFtz)x@B2?%`KwD-gKn^D|rFl?vwouTuPK z;9P`xDt;}{qj)+n17SWwCW03s8({%L4ni)%bqM_A0SguP0T(H50Eh_#7aI50mfNv_!GXDmD2VuKN+X4PA!g~lGAbg0x&qu(W ziZlK(_$Mms; z5C$MbBMd~aKZ69f(@sI!A1i9l+jZ~0=g-MHe_QV%+dI0yxoY)>ISF^&v1;G*gA#At zKY#4wsWsMEd2@b= zOMCqv8BH(rS~TaeIT!9rd;Gz*G5@;x+Y`S1=*h&KTg#WfS-fKNb$1PTYGqpQf4=+f zs{FbK2Yf5fS`1aiRU;Z&D|I~LcihO3!DWz{6xZt6(q5Y0+ z`gupMfzvL!chBcP8`pk1`s;_1hi&h8?ABR*Uro>a?a-HNW8(JQ-sgkcPdph-FM@hk3wI3<({=Mk2n$sT(SC&8)yCW`H=Mw z-2dBaY57-mTRn8wO;6u`qhoEkW5^{3dcQJ0WZUe?X>&4Tx_y!|uxQ+l!l59r@}uZe_vKBPJ^G2!9lnstrhCUc+<8sffY)oUxqVCEp0bO6Ikx#{_r@`c3-|cO zA5I#bx%ZOEgLBG1E0|DKHtNy~e!A++bJuU2^v$uyw>^Dp|IH)5>KQrWKgGzq=egPU zjo5MXu^&zdEMJ`|l4sVe`J{x6l6Vxj(AjdGD7Er;eUH1Qko<6Hi`FhXOZw*ShdhdyIUtKk#;x4(3B&XV-IKOg+v*5bCE zHD8|k%*L0CX6{bjJbXn~_^K#xY`-_}dm`(W_m)*YF!{F;%S!)|nsj00Gq<$gTlUTR zH6M?^dyntl`3aBj8T0GIzwZAacIN%N3UA22@93P**TyWpXW4e=vUO$MmONDY%CEh? z=`nd<%q2@6==0($eSVzrVEWCoS2rcLgbo<@@aZr8x@A_*;a|`0_xTNn`t}*S?dN{? zEIs_v!<9LnC!XwmZo-;zk6ngx4Sm-*Z!0+PME>QEzPsVZajX= z;M?!)bNbN!vqsmi=$-VzgV(O^5$=qPy*vMb7w=wi!=M%Cgmin{7HXa~n=L{k%;BDj_@Y;fh|fK7XhzVVwJ&q7e^zZW!?K^E2n%I`^_?xBhn0 z{>6W6JTdNc$20M5ITLCR&b@Z(fWGV2+NMBAzb<~@$rtYb;MVi^R(F57U&EKzpA^09z}h=wvie-z=WieEp0;h1G-Z@PQy%I71R!zbK-O?zIS z<-PZ2&fYjGXW;seU;Cl^>(9JAyt?YvvWzPp`s##1m-TIk`1#{!_h&ACYQ-PVKDp`G z!BtTgU;F;;?_PLTPVPlp-6Q7CIrRQjg&|L84h{QYN8SxLtg5KX9AC2dBG>kg^Qs?y z^MgOyzg@DUx!Wgu>o+~}=nq*VHzi#D$ea5gjeq9mBj=yqUi0OfBif!E{mx4hXFgQ+ zZh}93@{vgnt`-W+@Dt}Tl^wP!6_y&ZmVPrB{c z{0lc_o$<+(sflYG-v6|{*X=I&!ZY}?MNv~CpSfXX;Ebfx-n>0#>Eqij>mC2rv<=Ih zSABcm(vE+38}nw$+`8wv$Nu%%oI&4wI>vMO zkF1XStG?K|deVzizxwy$p69(V?Y(P0JlgcxQ~iHjyY$3a1H9G4?}0tO+jwEf`!g4R z^xpp8d&hL2E=fpV`1J1+2A-R`vTXiSEBY+z{`w31uKq5+ zXRnl!i`R`hkkk72b6fWOGAeX*Ov($j&WMe_+&ST?J8pdKaOFF1A9$x@$vH1KeR4`% zVgBM}n+GJt?TI@h=ELd>CvEyq>E5)L&sy03p)14h`o(i(#y>~gairv}-KWpIV@tapyZPU9l#1$oRfvpDlQ|>d>p3ev0aL<$0l7FZt}*n_hq9+@J4_DZlvXie7i+ zti1KrJ$IKqbmZW#|5$SCE??6Hy%z`WEqj0UxjADtTowELqd)fx+_7VJa`v1xBfIs> zDCu|biOMG;7jGy_b!~s>)^YEjk$w5rJ8n6)>8$%te*1)$3x2!y?PJY5PrN4Y<-Z-B zTY1Lh;a85!np-ht%$xh~$RBjd=|9}OFfg}QpBMgq#kY^WJg$F#Z{`JGZNL7v+ddp| z;)Bn;SyXW#bjVpb-z^;XM*KxTBrN&l!LszR*?sOgV^;dE*zSF!_bku*K50q%r;E~J zdY`%KP!G@Q+C!tB`ugRS8<*ez)0jm=8a6*Tz2WtwVcFN8lJ`~4%t6l|t1O2kU=VlYd65Z%41L_4!yxeMrk(cFgME`I>y` zu&((#hjxvx(DVxo@0x%8NnPX5xVpxF)9Q0;<>zVnb2NKz6Aom5o<5~({rfcg|E|UF z*V>z=_2+rb-f?}p)?cgTPwd?_KUA~#2Ce-^H2XfQ)jx-Uz;_1f6={b``>iy z8Xv38$9bCk=dP~u+cf+2)ciG5EB~fe{%p-2H*5AgC{`HuH%_Z>rM6z2HlnM3_i63T z)Z}l~{2hN{*YY>Ubd9^U@wZ>|-xAH=TLyM5f3D`=Yc&78s^veb^&dCmJ3q8Xl{Wsi zY5H!~#>1~eI*Xg*?E}sJkFiqqAuWisW7hcWtM$Jc>qXU9JUX+qL=noTmQ_Eq}J= zFR!NGzqRtuYURgj^YKB=Kj&%tORHx83pM`@($?=6wf1h*@&nrXH%^Ps)9gPN^;Kd# zhrz5I&nIi+zf#lhJ-()*`KREo zNZzQ$hyA--^Iwjp&uN;y_$<}_u>4}JzprZcN!Qk&yR`8&i~~h|Sf5W@A9`x?8@2Uk zw>G|R(c0gu*((J5bL8KG{l81I->0@RKUo`*RtHp&qss{%zX%utgiM zb98%XdE_Nc-*?$5^&#D^*}sQ2p5D~#JxJ5NIy z@iai2FIQ^$?`ZZb*ZOy#tt^oG4%77gQk(DhY3rZ7pkn!Wtv|n_KWl{kq1yN@)5h09 zE&j1K-d@-G_lY*1hiK#B7cKuK&HmlB@jp?s|EX9X?GO9s(DeIWv*+J6{d#Eo>t;

Ty|@gKT0(J{aaffZq)3V6D)4}bFWtZ+#x{$_UG?fe-pI*{YtI>K5hNW)YkK9 ztWU zaQ`1*jMMa)rM2HjvtOKM?_ae2?~Y(|=6HNvTR-taSLcW0r9q3I-@P;2EMKFQ@1yy5 zhi1RUntw-V^Q~61=PYeKxJ2vkY^}W&+W5ax(|@Ek9}=|ju`;Y{{|;;Yov+pB)AT!E zTc0n}^j)C&vs&AKPtxo;R?9z4v)^!SJqvSm)qjpQzFyPTpEcTg@PpP~Pi_2MB&HT#{e)gP*j-#xCb{rP2B*LaTRFRx~gxoFS7G^pR{+WcwJ z{Bwn7pWn6d5uug;Olz-)rvEO@{&m`ZyjWY`?$rF7tL-oIwedDqv&R%o{};6Uh1z(& zP}6_4w*C*&#>Y0zzazByoutiQ`Q8J^$J0Ugnd`@sT755Q^J9oMUixbG{ZX4QU!(qJ zzF2_|+ncBL|32+{tX}iaVy(Z+hMHBG|FORQn*CqV`rlt0k2#t@-_+KJIhLxO_0QMF z+kCCRQ?&S0mszy4{5##d#y^}QJVf^wD#ZD>c3XA zhfgbivSyzvwedPelivXQ$o>2&ZGOF>U2lhJ_M55MXQJlM3$^|IA#Hq|rRn3->~%zI z|9P$b!`B|jxch_qE_(nSqJ+Jxu8Lj?gZ9I?B#{0Kg{SIxu__}o+e}&q3 zouSRYP_4dmH2dUg_NdXu%OTAl)tdijYvb<=&3^yV`Wvam2W$F1qP2HZNZ0Y!W zwf$v<7H`n}f0H&|+qL}J+VxYdwqCB+`d6v#zt3p%`4Y{4i?#Z$(dOS!%|8b;`Ij|) zbG7yPG;O?PX!=do=Fef44S1 zKGn`^Cu;T!*ZNnf`R6gs|9iFdr49C%>-YWI`uT-s?=^Ii`j7_n_BDSOYyC@wJtA6y z_j7O8)=Q7pzpu6R)}`6=u-3n)HTgF+dw->sKSNvZUeWB`UCY0to85bBytZrO@0ZZ7 z`J**^+^4mFv9_MIYv;L>wDsePVD;vFS)Ss3NNev3%|2VT@itp)?=-ExW=+3| z+I;J;>9bnv|IJ#y{mv@8#qsosW{(TB`8h(Xed4&bu)QtIgUc=*AkY8BfDfVX+`8^&>^tgho&a=Q<;4RAV zdp$)N1zCmphM8DW;L9i~_ImOPmw1cVa`BQPZ*fMxFVAazp$c(NZb7c!AjvE&SnMsb zct&1cVWydyUF7w$XvX5~!lL{PKMS6dV5MXhW_dGJ!UB)CsHm{WW6Ce^6wbfSo9XwI z6cl;0jf}+}QxVlz#$vOe$L@^>twF|0EcAH`#&pI^J*b)ATk0PxDl+Q}%4Oyi7JEBO z6?+S^&|>C7ufH>`$eZcSUF?N^8Tpj7HL^=8dFq0m+uCI3q9$|_9WAm7u~Ms(rcp}# zxp}4q#xdKhyt4{Z%*XUXpWjoAa+d8q+4Yf!aw26c)pDay6G$+3VcJF|jvsvyQW$B!kT(z;k3)3B&8 z&s<0fvvN!FIq=7fHOx|l7_18lFjxE;uv%7^MEK2@ky~V^%`YtF*pNvXFp(!S2Sqc+ zxP66rOGl4&pX15(`m;IvOs!-q-mEd_oHKg7r+A^qU+D2Iv}-n1DE4}@^!kf4GxG37 z(vmzZZ>H+jvM1|=|8ujK20MWH&A}QD=TaP)JJjI12MG$9iF)a%*AFzmU_0P!n6#KlHwyt(Lt4F06!kxQVW?n{d zv8NCtq$oEFEf-X@4}78F^Nr4;nRv?!pSYD47OeZALHQ0T{CGj+8j*(xd< zJH|}QgZ@lWW?bOO&&VynHeTrSEGofv9qjh}lH5G(P1eNjG=`mO?(GYt2`~@x=3{3> z6{bD0AfR0wG74zYqTI#is>p`1-q83MG)kGQ!V;cKI=eyzg()~NIA1(CjnLTUK*=t_ z*1o`FrnBoFl@P4e9IBmlcG@M_VGJkKAp8Y0ApsgL_F(B-;LkxUSRV(YmrK4UzXXQ{ zYo619+}GV6X4pdpbFK)xV=1eHgW*!F|Cn3F#Tg5{9&f1+`;zT9OAF;hYnXAd$;k6~ z^K<>!EtLwHg+A$4)5hj5f;9<3!8o=$4yw$IVm5E-T2cTVtwBwr(~3%0hzJV=``D!` z=D9$P6?J6djPUq#xC3Ej%g*2e*x4sI5RqJLAod8cdZ{{=k(pV76*I$+Q=PvE`sCnf znP<+6MI{+USbA~5=p3Q8Cic+7Tu0@0s@0S-pPHBGoq1$5#q2@G;V>?~OH$ATc5SBP z?6GZm!P49ou$9QfP@og-bT1U=cr@Kmz=L2g&o0x&1~6j*&#~tEssV6Ib~VZI`q@iB9Yp}A<1H3fYs#zyCiw1)?$zwJ`%OdV7lIXPGain!}{_S6E+jdk|UjnQ#Y#x%uN?=V!iAC;2j#ZW8c+$!W!#o9eM#^G2Fw^(BU>*K-_ALG`NZ`(jcGjLMP z!T^`daRaB z{mpZ-^ms<`(t=E01Yni6wts6JTOF`#_zQEWMmR5c`@p*DvvjxG#FyY!zW*e-S)oHg=}*D|yH&>eRG za5Ki+SwYv%1@&QrxI)Rrg5}S}A%_i0o3lP&UdB>h2zat4@rKo(WHNAV-6^ve7kF8d zI@|5Eai$;VUwYBnIfY%V=FD3Rc6V_z$GU}Rt?qVmSG6_kcg>V{#aO(vdFw8RU8C|c ztw0*ISFko(z*45OQ$a_IZg6;*mFITK1uJ0#fAU%|n{8Nb=OL8JNgtR#5mq}$G#SIo(= z25Oh)Fy)w)+*jw_rIGmCP4+@3p| z>FN(Lg0Mp>x3GIWq2i3w*_b)1JJnFfEi+f@X*$PtA|?sE!h4X)kvI!k7dZC7>}r56 zlOqfF6h!svtZEm;&1>F$I8LXif$CJ}RPP)U!jU*BFZjy})LLQQ#IlO^bhQd+_i?h!R;=ZhdUPFV)&dt?0@?R2BcZbiv*yAQzpsSrSXOSK zM~sS2A7hMK_cp9YG8s9!BQp#0u^n0WEi)D)Mco0wEePCN&BH+lHyNyqj64kO0vu@Z zWCS-r7U$y9%3R%#SFYHPF=}O4$Nb=Z9hGd|X|UUYOOKmwj+^5(pzePhFAJW*!SHxF zyawr1%9?LhR5T;ig}k+dSa&v$SEYH6jpr5`Z)9d(F0Or~Ic-bLFDhIDZrz}?DuN{L ziYzE9#7R{L^jP*oG`lWd$L4!0%$Y=S?jtf~`Q2zdjfI@`K!B1=xcrKV=2_90 zbnz@)mtmhIq3-6Y!H9M8Aj26DyDW>CoiimpIQn{VJBz9I<*)rb!>-D#lYtsN*2L8kvvFiGedoh-Rm-bnQ{k7%){|m` z@o8SajHd8W_{5?Gi?6WmkWge&M%FCrxWHJlH_vB9i_HokXssuzwo7rV82MJ7*FOu- z5sNH&mZW)n)&i(>QukepmTKe2&4d&%emAfD#(>xq`tmS|^fq65dv$S=;izkUJQ`Lfu zg2LiFuNUh+E{o82oJ1CPN-H&0x0Yn&`h7St+O1w!h^IMFhc{3=6U_$9K6mMe)vi5Q z>>A9zu-dN0tqqJ1`Td2aLVS>+G!Pc!o7C(?uhU zXiLyKSKD<5`+3|>>50njMN3Vq2UXM+4Ca&+EaAHe=Hm;yQEw?sg<)7^&Wh0nt`9Nr zuxeUI!<>BUqyY(>Co<1Ei{o@03z$!G#~HjD+eu<$S@2bbk}7$(>t@d%o+V#8a?VO*?= zFUZKn0n0wC`-=+j=*z%*0Q(xgT->|E`53RZ7#Ki!mWQRsyzXXgxdm9y3V19+`2x0v z!&KBlCe)jS zd)j70KIl#>nvbeH*%`UGYWEkRfWd1iqj+gC@6PhBCg0t`>W^kQ57ZT(*N^OcT-S3U zFwYqnyXdDEPn~d67Z2{UvS3f#G{R2AENE&Q615q4IGe>RS_kn0>V;A;lmUu6J2@r( zEZk!=78e&N%X)av4pm)e7Qr1Nztt1AWa+-pQ)Jc&)A8jEn8{CXvQo5x)yXKaRA!{u z%eHAe^JXqpr!^s*QM3T^co2$em%`rpMt(^Fjl#V+H=FI@b{gJmQIz_#@+>wQpTMk~ z!eYPK-4biT%eJ<5JpRH9O#Z@Lm?Jyenn=(kr^v{*hmWPQS(eRpn$a4MXv!Qu=xBCk zfgf)=!DDcl7l(A**0<*_#s>znAA6J!gCsi_ZlD3sUk{Gi*!s;Auz6SCz_4LMY|$Dz z7=k%@2LCUHcxGt^E)~qh04mV?hR16@!SZUf>&)BoiF=o%c zILR{_&+liYOu6)mD`t2`X%iEWjEGqeJF30vHDSef+v=n2ndLXu z%eGHI>vlVq6o0p#USUf;@DJa0HXE_Dj5IZ3KbRYCmSwgj$@H$C3V{SY&oxO@9omOE_87!+(yAab7 z%(3jicBrLgJCk;d_lgOtbC5dU~3_=1w27l%<~o67%uiFumnMh|0}{qJ4~wBM`xd`D_1_ ze?pi1M#$Oa>pMJ9v$yf0D8EePuN?gkrVd+W{NQOiSq04JqUxZ=%8oiLuv?w#)%{W=bm!7@^>Vz4mca{$^CJ(b@ z-H@-i>t0muG~9N1(;uN-`g4iuFT{=Zo~hv+m*=bck{f#%{b6DL&P3F^*6%e$eQN#g zOw{ex?VEVi4hJXf%rUrOP_cYwjtt2pp%H_IE-ZCQx0PW@d1=uLDOjYN#Z9mF+;(L}_LTkj-}$M;Z)lO4t` zB7V%dg@_++-b+lu@6HqPquB?EXJH%=$KrS9iTHu>4kCVF`Y3TahTIk)egryf#1B{} z5R2e{;#&?Qk%;d`B@y4p@4XW-W@ivzhX09A8%8SeF8mz?A{Lf;M0_VVornc3i+BB7PXGp7?M4jydsC_@6iz{wF5G|HOOX ze_{asC*u1kJBj$-L^JUM_@9{TFj|On9mZZFR-HB?ejNQE@k98Z_zL__Og4-T;#&Bh zI3Eke8^8waGQ@WHpO}G_nD{OHPh1ZF6CZ&8iBXvUM0~NuMLY%mCwk$3;+Oc%JmPx% zT>#?anEyol=z0S2a`>NkKjuF%8~!I^{Yob8g8zvt;eTQh=0EWS%zxrz4r3niP0WAd zlMW+`*cbjM?!o*gJ`evBf5-eM{s#XOyE%+9;s*GiI0*hH_JaS3uVMZZUxfdOUt|6g zKZE~?qwu#Xh*+3wi1-EL;D2H}{7+m1{}b~Z#suO&;eX;w@IMiMd?1;4G5l|)!~eu@F#n0~!T-ed@ITQD z{}UtOf8sOv8x6z{9Y!9pH`agR+nE2vfWs&y-UZQ#1Gq75V6OvA>xlB zR1$xO|A{s5Kk;MupZGZZPy7o0CtiW|pLhoRPrMWJpO^#x6X(GH#Ax`RSONbNFMljy;Dg}BvWL=*R8{U@Gm7%t)p%zxqt%zq;O zutFU1TCD%XyRrTgXW?(`5MRUmC*qGGBoc?f|HL}Xe_{drPuvOr6HmwdC;kHe6TgH1 ziTA<(#BK0D@d3<#;xCy0#3$i@;vD#&_$uZ{tgYX80$ar6ZoHqKj5&Fh#yvKCSHyCPaFmR6VHVIiTI08ZA5&(^B^(BVYCxx z!vDm-Vf`l_g8zw`27XKyI29*P;y>YkVl4bm{2u2&;#crL@hZ%J;tcqoxCs6y_JIG1 zxtRaNA(;QfM);qY4gV7x;D6#3nE%8TnE%93{LLg{Dds=%XZW9(2>%oDM<`N>h1maz zE8&0QBiR3mBeDMz@kcRoh%xX#@jviC@nNk0#B{3qg%cC--jgBE*<-Ld`?AIAJA)?@w? zAI17lycPZ@;s*(j5|6?EZvnr9|A|>x|A`afe_|fif8r}x|B2f%|A|Af{u9SK3>Wbo z_@CH>`A^(}`A=L5{}XS5|A~Fze_{pZKM{X~B$0SC{7-aZ{u6J)`JY${{}VsQ{3m{l z`A@t6=Re{%@IUb?%zxrZSpSJj;eTQ=_J3j%{7*ar{}a>Ue9NjWhV=YQgj@IUbj z%zxtf@IP?|{7<|O=YL|H!&pc3!T-d0@ISE@^Pl)S=0EW*%zxr0od1X?!T-denE%9E z;eX<_@IUco_@B5K{wKzS7)`{#!T-cotpCIdG5?8enE%8tF#m}^!vDkrnE%B8VEz*q z!2d)){7*cF`A_^e*8jJGhv0wW0QjHy9{f+d81tWaa)=Q{OoRW42QmMN^WlHu+1USy z1K@vR_YfnF_%`-`Vm|hN;xz34#BrGa#CWX#L?gsVGUM<+u~&#OgBTKGq!7P^|A~h& z|B02D|HLx*pI8q66Q{%f#J=!9@jA?Z;+60}@hSM9ScUbUh(B;sPCOCwpLh+{f8qwr zf8wj~KXC#4Ph1556Zc{LCteBv6G!9vkLbtzCr*O@i32hJiIwm_@f@uG#MPMp#0l^} z@gn%2n2PnExCQG!aTn%4u?+h^@jCdQI1=-p_$d5OY=i%aE!h8wKVtq9hhhE`Tj77= z_3%IOIryK*Km3FH0w*no|B26F{U;XS`j40Z{}VsO{3mvY|B2%<|A{`V|HQ+X|HOsx zKd}VsKXC&5Py9RPKk+m8pI8U~6Q9HSPy7}0pEwomCyv}VvAb~~qWrbCu$Ga5u$J|K z6I$+UiD-X4P(M8a>(!mRSG}`lcl~dh12(S{yjJiU!K($Y6ud(4a=}Xl_X(aOc)H+e zf~N?cEO?^e34*%?j}_b{c(mY7!6O7W1n>AYXrFe$+XQbByjk!j!5al{5WHUSI>BoN zuMxaj@Jhie1TPo7RB)f*IfADPo+fyT;K_m~3Z5XiTku%HU4lmo?i4&ia6|BpUxfVy zZxg&l@MgiA1aB0)LGXIP>jbYAyhiY9!7Bx?5WHOQQo((K=LnuIc$(lTf+q`}D0qV4 zZoy*(cL^RXxKr>5!41JXI)wcNZxg&l@MgiA1aB0)LGXIP>jbYAyhiY9!7Bx?5WHOQ zQo((K=LnuIc$(lTf+q`}D0qV4Zoy*(cL^RXxKr>5!41JXeirr@yiM>H!J7qd61-9H z2EpqEuM@mh@EXCZ1+NsmLhy3IO9l4{o+Eg=;Aw)V2%ao>qTmUFy9JLG+$DIl;7-9K z1UCfl_(|Ab@HW9)1aB6+N$^I&8w9TxyiV|1!D|Gs7Q9mM3cIJW=oj!QFz#3hoj-T5zY}5rP|ncYH7GFL;~aErK@--XwUV;0=P; z3tlI9t>86+R|{S#c!l8Qf|m;J6Ff)obivaEPZ2y>@I=8A1a}J_E4WMWXu+L=M+j~R z-tnEVzu;|xw+P-Wc$46bf;R|WFL<5cwSw0OUM+Z~;1z z7CcsPm*CNYI|Yvr+z`Cu4`F}7+XQbByjk!j!5al{5WHUSI>BoNuMxaj@Jhie1TPo7 zRB)f*IfADPo+fyT;K_m~3Z5XiTku%HU4lmo?i4&ia6|Bp--Z1JZxg&l@MgiA1aB0) zLGXIP>jbYAyhiY9!7Bx?5WHOQQo((K=LnuIc$(lTf+q`}D0qV4Zoy*(cL^RXxKr>5 z!41JXjtct=-X?g9;LU#P&A$YmqrGone&k;Ob@HD|w z1Wy(`QSb!8-Gavo?h-s&aHrrAf*XQ&sC&`o`Y(8!;4OkT3*IDnqu>pK*9%@Jc&*?y zf>#S(DR_n8<${+A?h`ym@N~h`1Wyq>nY^xMkI_=IC(QZK-rla7)-V^IYZxN~&W81Y zu!fO=NMn5<(!DPXb0EJ<7E{5bD0OCB(HBZ^B%@=hS1P z9FP5eZ2zV)5k@i|iA1A(G~^89idN=1EpE!4cx+TFe>>Rz{ovt{9qxpzb7ITHzBR3n z3|`){#aZ{imUH5{Omrg3xp%UiJfUdKh0HJGS3Q9=R{9 z?)KdgO*41<)A20F=oN@?4>HSz;U(P&V^H8a^p#i#MB0Ap?{L-Zxzky*#|a(py3)W? zOxUVv`fj7*wYFWbMF&DV>W)I)m8iP{^`3NL%^sJ#SHOk(SGsFj9q!4yhl_Y9>M&5J zfw~Rox_nGcYt02KTg!*mwAy9NyKyB`CtLjOuJO>brYqmg#_m;y`det318UF6ytJk6akZn}?Z!i1(aOHUe z4yU0F*1r;SBo>(F925xYyYd?|jlOlQ+^wfmN9q>i8f1=vP~-B}{~SB^N(gi=Lpre( zNIPsBvp#U-bjuETOhaA{kmJDY!?OCrxEdeU=Pf_ zn${yDrnhojaE-uY^`6nNALe!I3d}c$Yf?+d*qYW5_v9ANBiOkm6mfezoji9C#>-8I zlgx2(?qPFWoH^jIbM*Q^%#f9VaE!kQaIO;{Ibm1F`oJx-jRWmm?~q2Bz2GsU*P6B` z4?+6Jkll$jr3|v$kQR-6^lMKK!x*@U@;(*SrRnu^ih$9D4Q4Q}twtdK`xFP7dOD%Vs~*IUdr1)V~FFC!+2-s2l6to~wU@ z4M*kR4J)kYBa>2p8`2|@yu2a+Y&dr8*jE$Z-NDbni;VJRNNc|+;RbwfCdpbCX8^Mh zS`gY0+7aYBY_7rZMGkCMi?$-b)4~4^-V^N`@L?+AVZan%2yh0F>B+!9l-y6i35fp+ zOaOKO%J8=CESQZ$U*QU)F3n? zBu<5&5E7<^8$N^$2w}c?3j3(zJgi;VH#did z25jDNVyL-xC98FE1~GnpV8pVuhmLCNb_(+LVvk8e-j7;dD)M$9FL`XQz+!jJo}{tU z&3Y3N=h`?yr6;I(yh1my8s*}E3n3S)$_+<+71CWwE=FOr!YE)4@}0mel#5j5A`nkS zdYFCpj9K>iWnbtu<9#?td3;=_^NrsVc2Y*Dxi7=ip|U?$4#L^+;`nh-yN z^?ZktYgD*RVFR!k`I}X|UbVLgaqd%fDt)8ET7?^cwaBkg@pYdPVG1w``7?kS zkWWTA+9e6`WcT#liArvQ!UTo!z!t3WZeStg<5am=#CITlxRP@zj8Pa3tV4bja1zQn zRk=vS*C9PZ$%QF26doPjD^QO74&W4&J2KkRvmNm~q#p!woVF?4tFQ%_g8W^;CCG0^ zIm+!sd;-#&l-v%5jS9B`UC3_$=0kq7YOfygNThF4a&-zfDy#)^pW6VOfpRq{M?b7X zd>7KIm0Xp=N`-5HT+1qeS3rK1YOfse8Sq1yN-qUcf4@Q>kb7Ppun6f%*u(7oh-*sR zW#L9VLNw0&6M!+mB%lj851940o<&t^ytfMqG|-C4@DI4+mBP6M!|qDqR00gQp?PL&(6! zyb73nV^5HxDZH?gKU02AnEA3W(QK_ebHZ%^Ha$`f%{{&iy?U z=WyDB=TO+Vm2Ek34&?c>_D7u8a6QfIE4G6(@WA+C7Sd8Nh4*`#&8sw{;@U5aU_)yY=^FAuV=2-lJB*5Zq4tZ!qfrvD{zP zog(U+AoyR&X9<}OyjS0XAZ1IDpArvSz+d#myy4_40>jb&moGIA;8pMac}UBP8XQOn z8yxu0<;DSgt!sZN(n^sw18Gf2i$>Ziq^&|)I?}cy4cAZmE0N|IjCC1lZz1gjq}3p; z267ch+k!M)$L!yTw2eqxhqTQ|3qx8x(&~{`hqRZF20ypbf0N*Aj;Av)hUqu@`;KGB zo+Nh|#e49zsV5yyoVg%-^e4zB*0eH@WAP8hITkU7FmGC;P8%GE>W|QEaG)Q?2+N#? z@sWaYWM9|qoxA=}w`DgUjxo=VD-WfLyf0K9&z4EcRvzlWS&G+3Ik=WPcp|Ppz|)Y{ zK732-E?~;A^?_{|b4@CbJJ_Vz6o{ILdFHY5xfyRO35^V^Xy3Zo#02&RyXiwaQ17)SZnP{AtIe=^M>^bq#>Ra zvOcgCer!aUI9`XsU-Zvr*xrqK%=;DeD{c=C++fSVXFRiopsu)n>jUdh$4l@T{{HNK zQy%%zDt{@)Tp9A8LB88LC~ySVUXEXa>ri8Nqo*6bZs@>0K(xIR`O(lP0p%NqtPdox z|KJIj7axt9zI$6=+{1)^@7d!Ax?wHrIb-Y~oCncAA-hh=ZWXe4Q{9wpM%|k*znXAX zvg`IK`In9(Ukmw-kgrkl=bSx#caoCdsOACfgn8971-g)J2HlZQ5B)w>z!AR{dYp-{8WT5aoEyn<(mULwYIdZ*g1wrT*(w{bi#5 z1f@UkPe8ZNZ2djSZ}muLX; z9z`QCDh+2y1fCHafIAU*woJMz+(<>Z9A$aNVqaXY!GSW^Ar@yL+wbX4YkwSn9KWYP zeg@>zg!~#MKT^nJoi@kS29>v2#cfw)*^2$%B({f7s_l>WvJt3Rpyf;!8M{MbZkPICge4t41QnW0CltXg~Mv? zcgI_6e=N}M^M(_xIkZ))uM%rE*Y1u`tYcbzpQ-v=VwtL z_Q;-}Tr_=mlWS}1M&vuOXGCGos1fDjRJnbk9BkZK&X4?#Gq$wmsWP<1(SBB2RiX_2 z)rR%|0oZQ>bZ>%>X$#uc_Ft&*Ujt-$j^I8{IqoByP_Kcq2`J0G!nRe7YOhqZC)cy% z)t|1_&v-iOsR_Zo71X~@)z7tihN|Bq#vkT_Id52oc0DZeIB({mJm<|W>;v{(NKmqS zG+FLRoC}oYTxh|X`Uvb41zmo}8On)#_LaIcs+J% znrt*=sS9Okmt9!HcVP|Z{m+-6ONZJMOuMK(;U_T$@bxRxN1IjNOl90~vStslg1G^+a#_T)RU#`9om0VUr}ORKkgq^pz;HI_=?t`e1oq(lR=fZ8O5PCi_&E?$zDDKUDe}zzqddoU8`{SC&$M-^l3k_A zHjlRSq%6)trfiCmU8>1$glvea7d|j$W0h=yCd+%OxZhyvVEe#OvYDD}DrCcyEPY_F zb9}yZ1AGvVeA^z4YJJ))d@xa+<}ddrQaxv zc;`BYZ$>AUVXgTWoYfAD^x*u85Q8ur!Cs$|!1?!`tAHC3nh@F$+7UVsBGOR~!G-Vx zSHL+A^mQ~`$o_X3X~M9vR4#xfna5uqNT0bwH25;Mb% z$v}IJtVBEuYf%=~9IlbEu&KR9`jB3#)}oa-()XJ2Jon-(y$)yFbvWDlaJKb8Prpj1K3qRn zA_YppK=b7;o z=!@ZGUJKp}j{8KP@me4T_cbfTwLt37!GToNlYsktkIXO*VCa}2}XzN@C zS@!XBr5o)sOpS{wm3N)!59%9OiSo2XEb8LDF0TKZm8?g|;@;E1&5#`q{X*QKyID8N z_iREM#|6vtn&t>}K7#TGp|b(oQ)dJBD-GPEG(ral3ek@++@qw<+;1b1PMv=icEZmc zblGoVA9KImCiYvLzdQF^-gDx;BHnMa_gda_YR3CB9mcz@+;{h44d2@j_g1j)egYkM z&&k%I3F*|~4H3yxl{#&*BohVOTa!~(N)XzECrt0~dkj2k{4dlBs)P=H$ci9Jd?l&5W66>+A z%z~oHS$MYr;cD;*FV5o#UhoEBJHjIHiflaZL#RXOK!{opZrq0S9AFtj6~ab@w%>6d z33w18GAG=45;Ez)H3*v!4kEa6@$Lgc1Hw@R*LC5>e;`)|tV6(tV8krMwFSZ<@D0Fi z2=*Sm6Z{lRZ0?T{dDeAAHTJ=3>^a;Y(_!lv>_fJ#HzJ+$cD=AQ)>U&KOhUOdoY`gw z`DP`*_Bir>$j^g(nUd#N3{!hR1@;;HT9Ng0EN*~&I^;J({)SO_{tkcG*AXl`L6u!9 z%Jx!aZ|D=ef0>TG3ik$EL)|sc;{MMg@ZkO%E~?oRhV(4d&pp!aZ-uHe^Emp)L%tFB zl_m)JO-g=_khkYWnacZG!VeN0k zzWd`P#(_rU+q%aioxX|}b=baYhwcZ_*A5|{uH<8nBhPl)As;&?SpOO&A9Eb}6v!Wi ze43EApW!)=BVP`A1GcPC^4zEPs`_UNe@4Nd^12ZIz1rS?wn4u8Da*}wIPGb!b{%BtbIQ`^2cg43$Zp2oUJ9RYM!xNHBg*pmOUKdWsH%Ue zR{v3yr!Emfm*J|O^EKJncuN<`QWyJqUUJyrz-;J}jC@;{a#cr_(1rIK@*ucw>;vcF}c!f%6X zOgFVoM4UD_umS#co)gqD2Hfsbk}B&FeX@N$PvspJdGz%>Ro4b#-&IO>k0x6IS@wgn z>_;v1sfBE9x50tmFE$R;BA>c&e$=ZvZW8_Ad6)C!?PWh7-U-?3uHSk{&KHdTcNlLM zX9=9KJLeG3Z5wefg7<*q!|=`_+DwV(yEfK)qGzH%iHO_wsDK>Tm_ec+yno_H9lRgh zj&s-hr~`iApM$fP3%c`tQ_S0ee3luFIGl5e~~C3 zi}V=hTJ!lK%SCJD_#EGfG$+cHqTI7+w+dx!U60~SL|xm3y=>p{oXWf#ML*~}>wXO0 zmlnR;tYrO~EcK)BC`;c}^)n z`t!RFyN2LiYhS!?hJMULKX`AM&$j1Tb~L|pP>cTZx%f=H_izTUQ}EuyV@PZN7k*c8 z%b&yWhCinrPwu}>GyWgyt5v#H1MPR+UAQL3Pj>B}HyUFeV~p2ayoR2EIDN}Ci_bUh zwdOj=x)Hb6v*C(!9m+&rl#=B+)`NH$;;E|Mxu|yy&b4ctcus8Kc{gl}pJ&@Y1AZ+- z8pq=t;q$SO=R4bMi_gj~fvkD{G_Goh!`baLOzsL`4Z@k=d{!BqA8w2SPX>+w`hk@Q z)d)2R7b1=Cf*l2456*YNq6=^j1zbL(Ohekgk;eDE-UIR-H@@FiiTBB>kQZHuvnN6v z!gt6wpH+g-L*RXHA21KWk5GnCj$q!iMql%sgU!Cih`w^}@%bj#0ru6rmkQY`CEHh% zO@%CdLRmC#+Hf<@i<==|Weg7dGdcLq$D0SObzi>oG3zhh`MB{gWD|q$d@N_2&jHmt zAAD9Bjk6k`Ro;U(I4^1;6N_^d@42XJ_JQ#tj%)UwiHsX-&9kQYzO}2SH3jv&0o@!% z&7RB9Ki0vq!u1l*v&g{p77=976L_-?{p=*D!ev7yHEXa~>ZoGSu#?vGmQ-EVY}XI*ED zy4tBX#=oqS_KU|^k3LJlIgIyP=3xBuevF&b*{=?f z&N|HN<9@hD1Ha#dXN3v4hu1K6ZXn%SHy$9r9vtKOHgXqyRqX!u>xQw@O&wU?g>zW~ z%9o)01eA}%`7RD=6OiUZS|ZW{c>bIOOmYnhBq1*mdAZ2LT7dU&@Gd3nOC1JdPhwrC z09l^e@BF?raV2_=Ez3FYV&3A5?Hs@5_A4A$3Nb7gdI51q4#eKD&(Mm1~ zWev#HQ*R|h9nzu0QLg2jZ%VJ(NIQr$*5B3_Yqn#3;7OFL`3&C=Qt2n*9z`9}Hz0k^ zJG>sVw)okCW*|X%I7M1|lL8ea4cj~-G#UoW3*O4++ z51(o98Ntn{CrikU#djXlmCQWI@cShYkiQXep4I4I=C4z6=5L37 zYY^vJY3G-zIP)`5z5;QsC##%z&WEu$8U5tkEmi4$q;rg3VA-36 z&%}wje6rey=!XcbGaU2O-g)2#9MG|Dxh+?T3c`}C3fV(`70yT6*b zJ0S$`2lvBwM#f-Wf$wmQ{@Da@?nCU`nrQ1RGhFnCV^_-LDH(Ykfi-*J*}Trfyto}@ z?fP-Ax=a1>s%+%{P=Az?2^BKv%0TKs9XWyJuj4Bj^|4oP`=&R+PV(N_iL&LSodpX+uVcb z>o|;qIIPo?Fy75~7s2PMJnj|q&chslE~&)eTo{-PeUedcGTJSk%6HlZ1=e8=K8F5` z$J~g{?Xy<1Cy?G9O#a9&Ix{7D!vmtetlq(>!U!+IQDIAfO{6-#U?hN zfjsvcfh6=F@6QLu0KYW-hVtrrgad0(jy9VBoAG^K=GTh+a^yQy zelL~3M&zd>pWpLfd8`ln{UdOVc@oakJ=O=tq0jVPLoJ?>V*K0B(YT+dLM|0@agd9H z9LApc42{pWj=?UW&YISQ?&||f4q(lPFXPZA-;<0*xmdIPwQc5k2j6$a9{~wCA^QVl z=`Z@kpbTt-KgQ5`M&h}s?*H)iK5&&6_5S~dKP$*mo?TIqB{{p|iZ0e!MMXu$7L$~^ zq*Rntl&Dx-BgG=yy6i63_8Jx{B`PWv>bh-8$t^N0%(ck7eIuph>Rvr899`MfwxZf< zYroIed}d%cvi|A*zQ6N$%wy&=^Pcy-=RNOv&wKtJ?4m8DVLPde@kMtfzCl}#;!k?{ zhffakZ#`@-zidO5IRKsk@H9YIH*m$Qk<}6wpSD9|l?{&*E=R`OkHG<_b67FbT7lDg zMj>gFI>kHiF%YY*YcKvz)sLi|4tNE4%ay=LWmRk+T@MV!)Jh z=AwdgVq1YLxp&5*sO7a;>pMx@fJw;rTw=(%Iup;CS}M@LvDc^kDkU$MsI8oXWi@?){p(zvc0X zwCec8i)V9hoPM2JpUz$2Me#v!q?T>ivA2HB#1_JCzE_|z&8Ce<*G!~k+CDkFexl+h z+*^okn9yCYRts|@YdoXDPuRm*Gtod?qor52K#hkd_24-?RTfVrcw!djW)DxkmrrXw z9d^w^>p0oS^{3(2UfMC8ei5wT+&N*!oXJeWGuTPT&=xbP)E}pK@iSV)xK*@%VtFZd zo>yEjvBlX@^o^gK-D_Y|hdbkS(3TeadR+J8Zn0+#1uq=$M_*(@%i3XV>Ui(OA;FJ! z@(*h}gV>$4)3(GSV2aR`M%TxK z=o73Pfj71;UKL%Fm`A(rV!svIc?&_b%)>pL8m}?@OpEIdUve43UxQq75B?36;qH}4 z?viop&4zsNN}rq#e?_n06|efcybm*9ZRGBf#;qr(=X6mSa-2?Eb=b61k>6wB)qNI5 z=gXB=842ezmlK~!f2pL5>=V}~_R$9EC(eg7S_TL*FKmn!>Y z`d%Gyx?80*$%J%WDrHup>#j(GKMr`Um-GRzGzYKtECW6Tyl*1>3Fwl&sk@IypT)kA zov%y*v*g6y$t~npyvx6YJjJ9TOB1@cF^=As{`ec?XbS%48T0s_2+sdddHJI$K^$9V z3bzM0gNrcFkj)msp3BAUBku2ucnh9z$z%Md0e;0vE0(NSQgPCf6(>~`eJUe3nuVb{ z{8jkH9OWnkM>pTSxISEXe*C_(_ucoqC*Jk3`m<}71;I(?_vqQ5!icHZ!hhoccOCh4 zcTj67<9rugl@Y9Dx>WuJ?*0)zk8mw-pmgH){fu|vp{0>{w*P_-e6j?6_$~NaYuQHl zGlZw`XK-r?=U+`Z_!ap7f!~DRf!~FH6MjE_7ybzTDDE4C_u)T;-wfg*{L{Fk_3Uwr zf-$~-!gs|rtnJ{ca6jd{3IB2YG5r6+-;1B~>5Sl4gp2W?z^}l662A_A48Ils*Z6(- zzri2He+qvm{y2V=iu^Zz9sbk!UHH%7Z^Pe?KZE~2_?fNPruaHfuXFWUyDB7Z2XPhn zllaZ}Q}`YD3H$;4=kZ7JUr;(_?jgLBaQbI6f*0`%@ps{u;lG4mi~n2vKKwu6XI`5T z{1Lw%|4;aB_^B zqwvS@3-I^hN02w3(J^zHDB+U`YmQLG1y9Xsy0@m7`B3&_{AUVb&CNfFexVOe^qg2= zY~()qcAPS6DSS(Z3@l|Wi#4)mv8$KTAHTPA|E_+t#@i1W~QHI!swp3&8I!U z4)TqXuPRuUcme&PIo)2HzK`@Rq>qrUGcKlV@@ErPxsgK)65B{0COsYl7TTn@PPqqr z5xQrH>&KqnC*RVI?U|=NV8MzmtsUjheh1U_;I2O!2kxSjg2XLruun)={!Cn{Y!!`1 z!8vi`FYld@{gN787uWim=HyMlmjSE!5^I}W842IcPF=$B1A zV&k_F-$(rQ#4jR#+{SMweu(&X;-&9?ZuP$K0=IcDaN>*h0;hnx7C7-Uvm}hq&SBq{ z_!i=G(hCyru(p-*BMwHVp5x-+b1C^s3vuKtNu`b9g~kSrIo0T1g;x@O7vXx^Y}oP? zL|KjGYb0M2`JSM>X1<&GUd#9Y;algV96bAfS-tjK_W-qj58by7*f`w-ba7jtX$#-F z12_V%w?Wf(;No_@kiCWIq64}UPF3|P0zLIpJ#2Vp7dp;w~*dy(@Si6Df!z- zKa}(?(%0JbDw|$SdN1jVNgp6xcCgBCu<4DY50joo`WWd(j!9o@)7waolm0jOGDG@q z<)?im=jqfj{N^4G@WQ*r=IbY4ZhArDCGr&mx5vv@Hv7$aIh$|0%@-$MHTj++UjuOI zY6tMn*z~=mHFuOv+Vt#GLViXLNB&9wCFy;n@Ab+ZnEj?L!6&?BHeWgUM#%RF z`L+YM&&#)a_M1Hv;FYh*=4&Qju&5yMAo+5H)5?wbQ#kw0`DmN3$L8xLUn%+SCZF2s zpfl%WXVAh70aFJ|4=~NZ40trP&3*^dUBHW`oi^WY@^z8#%jDA@u;~}12d9R5BK-(( zk$xTN;)CAxQJXTvWR2Tz=^E`D7Ap_3AH!E4D3K4YQhfD+vLpCDs337QxN<1p-Ic1g z={2NBN&f`tC8UoZ_ozAV*J#t5NG~V-64Gl(?^PYa-)7U>Nv|jUV$xekH~Va)ciZ$H z(%VSCi1aSfmz*`n#{mm72uvR^=K~|&R(X6Jnf>PcH1H}rZu3o%Z#((Q$hQZ$HcQ)H zo4$|qeWaI=9wDyX;zjpmMfu)n;iIJ?uL^Su62%#e1Hem<#%z2E@fF0cAih@ltV~zh z^eWODNY5j^mGlmqUT@PINN*>7De2Hy@I%1Q0RN}Avpzyxmz5u*EBQ9MQa*X6@FgcZbE$V`LE=U7 zlo99hWFcc!e0PIGW9>W7Vobm;SKi#y!o2xMQh)M3O`0b=G^7><&g=q(7H!y?11jJqF>H8iF(+f=G z;DW>hz!Va9xF?T;7G?;T5@7BDrjocuXij(EIt+5^di!SN#8)a$}Y0$Wj4K>^f>9C zBVA=~vFSB7y_WPKi~dh~4skl)ZO8vCm0`XqZ>@!I13m`)<-n^CblCV#;wy>&DDic~ z`FFG5i~U!P?Weo(!p-@PtTqc%jqT*lf{a>RW8mrn*So=0jU9L?b|>vVJ>c*{&y0oL z3+ynkYk(ai&aA(Pe`i?!Eu;RVpFz6vm_1eT&!!iV9xN_MoJ4vKadq^a{8i{I?~F>N zg{=a%7}z3U%ZY2S{jlDqH;`UMdX#ju?3n)&n3N$IGwXmTXxy>?q&3C z(j%nz68G_IpM2?sYrp?;@VOga+G1h)f$0Y(0~q?*0_jSk@S^`VAtdKhG|naZHlxu()Qs0ur_7a3}pqX8+g^HZ+hO$wM{sG z^nx9GzQi6PKlh$iA^Q+f+P5&Gb(8S!zRs!?s=dLZcRF-_MQzW0yF9%scxQ-HZ{eAB zjkWgMhgE;-SbbMQ`sXLm(w=fx;^*g3H)KJ$_2!4x>diVyIcJ5yyV2rFi?zqyyNQBP z`MLukn18b{S4V&Qyt&^Hebl6j7L_lUPdhx!8=?`)BJL&R7@az)x}63Mez_la@G4hz z!uD65Yp}_4;Ek)l>NFe+>-06$sJBeqMGkMv|A&l84gWuWI zU1vgySu4vHE$}0VJ(5YV<wGrQ9 zFRZP1m;_bSnazx$+colm^^JZU%beyuqdz#4gkwwgMcyBmkH zAF*Wp3x51I;&t~UIevinp#|c5h&N{hlkl_?Z|#4KEPCo%qqEudMG@;sf|5J~)|}$0yRa*?es$hP>82cFD(3Dmsh) z=EL`p)@R|9+q2W=>mZ-{sh_Wbv=*CBXBq<5ZX1b1C^p;Padh#z_{<=>O&>>zE(!W+FrKa-81 zaMtM?c9fmMSQP4#d2z}ooVugl3{LvybYRPCwrNeIU6E8?K4~>c<+0u}i@W#Ku&=XJ zo8|Lu3OyFJ`BavlHbPov5>9O6S)8s8(jR`lPV$YR!;<@0EAbTz#McuaP-i2H0eAcu zk7Jyj>3ibfU;rIfnwAv|$!FboD}F2A zTxlw>_>~H$X9YF*t@us&gZQoZ8*!buZd^ZZocKZfT-Mfy@kel@I5$S*+Vcw8k;Mt= z9gR`xvD;_c)`YQ#rWhYn86Q5tS$@4)y6x=H)*K^Vw!kQS*-HFRiI=W7{D2p~p`G=e zi@tYpS51X+m0sMx+PDZ~&RT2(<;f=8X~P0&r#?qiIBe;wSnB$esjhMGOpx5x;zT1A-v+vqbp2E4iCA`(S zJRJX3PWkE(?^nTFX~Vh?Tb^R}AtzNQ^X>r+O zI!mo|#mmm9Si;*ylorRQ z`uuX`xqj)+V`c;2DV^l;R5iu=ql)}7@;BNtlvYL_@jFUdA!(O`+oyjD9xs70 zl8vSK$vi)dIK7+a>R5zaXe}u`yC0Un8~vj4ccOE4qH`*(42sW^AL$(V>Syw$zZ?&S z$SXSg@!eZu1K=3pe2d}?A8Ai?QeN*0>VaO;IZw%z@Rrh!8!epv2Sfc@W4KFiCQtKH z=B41gwJdyJP&R<@w%fH-?Pt3BlmQ>mzvW!NE&*P;>S)G!>C_1I6Moevm-1cSV1c~* z&~xI=UgjR#&~qu|9VbrZ3uim`B7{pkaX1HUy4D}lkhOQCH^eXTX@InL=0}>>b%5^= z@JTvOGJh`h=q6pdd^vPE9&a@GZb($&SChAzyt=#pN%AVr@fdxQS4Mg=k6qb1lb%U> zHu?3R2xH`THr*of77_PjXw^9K(KFWUIR30P#-?!N3S#uMgMSMBuCUhS^|pq_GleDR zv;3sKUU^E@kz2pAD`pBcmu3>+7E0!u=fD#^n)u4+rX6NbcA1)v)6B7 zOpj4{?li(@=HK&7pd)l-Wb(Q_BvF282l^*+qX!Z0`6L@^v`F+?Hm9m|YW${jS zvii?^JX_#L@o?OhTsxVfUFX>Xf|pF*4y+50VXF$CWOyesa27H!g{*9nexgnP!h1Di zeD~S!KhpMUZ>=4a+qHu|z$otf*__o3+RWKdjW@mIlic^<_aJ+JrM{1_moC^N?S75c z=aTa(&mCv8?n0i1C3hu`LsqqxlNP&4|HG5@57u<`ojP0{uX#lJ=#jYIKtIo(AAN6u z&U-#Gaqn~RB+5B9>@KbQW!rkl&d_*N2d96r6z*bD!f!Lir!Sm#oe z?)LID9_m~8dUR*Y!s@%4GS#O2%wK44gI9EwMOn8aO?}~!(}P`K)PHCJ7d(C&PjB!> z6a#fz`%z{s<0@oV^45Xe_2Jg>9sE1%6}amawsSZq(@rj$8$+nmbQy2~cA)8xyiXpkHHUIWF|&G~@U&nwuaUfGDj#)n<0LSUi z>}lzo=5Lm)4*+DpE`sSezi-h&EI0=m7@9O6D|D{ zdUqcn-R#H1n-hR(IRK3KZ1!3h-;V~)=_kgEtnvfok$x#fzc~Akxno|=0bpI2OR!mx)ol8zFKa_dvNp(j_l{4zg)`lGApG#)9n?o_vJ3Ii@##!p z%(%S}uRT6-M|mEfau0wvnNP>Sr`?jl1@MbcK_r}Cw4fWMw-$m^dx=--BjT0D8<~?daDJVwxG9Kmf(;Dogr~;eI9MyZ~&f57nIRH zvZ-ECSL)>2TKCMO2Y_)rPA6Tq!1oGzC#R@uBRqb!_RD6UE;VBI@+Q(~L+y7iM1#Y> ziFCDLAG~Y_e>M0!4vjU zv`^$&)(NPmD+jwiOZ_kf{;MC#zIg7maNeM|k`rmuzd3-xEZHy_ft zC3$NGR|B}VfvXz-=d2I;a95NsNFKpV5yJjaK_q37egRVDqUJ?R(4C+aG` zsJ`{kBAFKtYM>{Gt~0ttu-0la28T+875#o8sqaSQl4+(HtdU{ZDc%Iwi*?v`Ypu;nSx6tTvoS8~k&0 z)xf{iN&Jf<%enMp)j{QqY8=4dhQEz5M{^o6n)~q)`MMJCAY0)wzd@%f&YhoxU50LbEB~XyPrCZ>)v^80Ykuo&{1oU+DGk?*w5MhG=qYpxJE@yq9hOYa1>D2AsdlS-MYsB!WJCSW=^eF^_UsH_ ziPtyv$uoV2Fl(>I-jWW`e2{E|ad+ zKBZ({Vd2&IuCVfI{8G4(aJCJnM{h@ehw;);IfPXgjR(S`F;?|cTc|EMw%$&^@K3l| zexLSv=VVXwtaiKJwTSwR(2mj>Tak~g$jr60h+8r*=5Y<}>yIHk#8xuAeY|WeI-= zdQtS+eq`_%I?4ZfXcis9bF0UTV=e9nh|@U@^<^`^p>8?0ZqmDlP=?BOb^FW#>ZZP> zKIH0_Ve=)|&GjACP59qS-IUkWO<~ndW6*)>Cf($0Oxem?kX@sHR6e?d`Q76Vr_Se? zustiPd6B+FYb!F>imcyy3S%I&h!_4`Mr|ePH=ZX zVivX-SdIPbfOUG@$*t}Q>)l`Jr{gWAS~t^XsX0NXIUfe56I!$Mr4Us5&!Y zG<|6{9eGM!rEx6OeRHtA69;*1sQ4ff_6K8=MMK%V#mSt(-#c-I{Z@NQH=2CVBH!er zJn3I&&wtUv&(dS*l}=trXU^`oM&|4{Sn-hfFC3B^)syzTK5UcF{#!#i*&6#86Vx7` zqHY>X4GjI<_%45BJ?D=-e6hF3W%rzq&Y2eXw#>`ZE` zcQxxp%hl%$k4Q6pfIhEr{~YL5S?VXUGk+wRGj*HOX|DZKp61LmvYzrd_wR?RcH{)v ziIgnDLHUl{NRUX$BOE!0_X<)H`p?dE{=C17sE+6cI0brzeRIjJuotJSyWqfYF#G8=wch)q~!; z=w!vkC!_F7I!7|iddjr&M(JC!lXTw9$ZzZ%bLPy+`W@r*?Qi(I~E3z{rf9@QsXmUJJ8*0Db#i`HBp4fCsxW=Qp zDD9s74LdeP))}3taXR0&f!3~+uD+x(z{$$r=*!}nPnTQIS;_y|XG2r#!HW}XkHnoO zy{S0{`j4_@N2Ht{j=4>zU~k!ex(GV!_-?e{8YlJUjmBo>(_BvDmm807{_l_m|9l}_ z$!k@4;K*5dSK@qZI=z|nz7p1~z`>c&>2%7_`9PgXlx_bt-ty2}mg?iurK;1Z7Iz8h zCBTV~;LcL}Yo4gHi+(=kSxTPdxOdWiqP++_lBZ&o2Tw&~yFHJAEMz&FCA*C$hx46= z!{E&#)-R!5Z6>~*3jIaUVCD|!bjhK<)2u#kJ(#iGwp}UkrIvQenB|Mn5w?s>%Gkr5 zj9Vx}YyB$YD9Uj8xL36K%gD3ZT0BjM4~)`zmr{p~Ui@O>8K35r(a#+c^{Fj6*bJ2M z1JV)rOKWx^|XnPx@&YW#Yf}E@N!?^NGwUBXhQ~quI1AIvgE3 zky{w9haSm1M>b$4_%t3%hsu6Z*sL+oXT?Xyi&gMq9_~VHabV}I+0&0^>9y&}YxWv9 z!8y@|Uex^k&&-`(4;`75m9p6ElRjQ&$A?<>-lG73NVi!93UDPd=x?Sw`uPVxxy}iku%t#A3C=e_ufU}d}ur2J-B_ij}lkZkr7;hUya|0yOMAh zemCw5gty|~gufI27X0LM6Y7uVzo7X)3AIJ_^3%;ZqMsRLUmCrbmydD;mqZhy<+0(f%;roTP z{Y`se^A=L3`oGS@>s;d<@Kic=gt$J=MH*UI=g~XtamxE1MEi3~t-)yh*X@Ob`uJw_ z;0?SD65=kVZxquvt`lzh-hJNx2%bv2D^Cu0ZZ$6Gtcb(0FNz%wU3;MG&V-O;>_eA`TYcmv_jMyAdB_z-cjY4i5Z-^0_}r#j0cz0f^2e{L?F za%O}vleE*YncUumXjMOUI$6B^4r>CMXPbFH@K-{Y+S}Eq0=g;;UEJ-WKBBplILBuv zyZhktkmYmEm$@TFxEj9}*NAJuwc(y8{!u2uP6n!Ii*m}Zh;W`ciXR0(Z|kPQGucRb zWB<9oxITW-4yJ7zjFu6k$M@x7d|=6)K3 zTwmRGYS{K8M{qVWgZ)3+UgNFi(-pvXqGRMs=N8(1hBkDP_78i=BYr-NuJQK=scv37 zc%=i{fN2Kr@8IK=%ty6;sCkgaU$u>3>n&^@uob{Q59}qt2DGQ(Ut$c9>=xNIeaYrm zz)__7SR92GM-&_ZIDQR|3+)`+%#%r1|H&jh&N{*`NLL+1|E1WT!liJWuF);aF})>fd4MUJ zx6$Hh2TwWacY~+I%hOEUU{W5v->b4~$)k67yOZ)%5cjR5JW-pcm^^w*`b$Z9a>;XN zQXakEt8(-9={$qz2z%=OmBR2gU6Rk(YQ1T_mH)Xi@@Ci z?%%(S`%C4^*dOJbk@^|9)Grt#CeqL)>IaJ3hb^5>+*jy>f}aA9d*cM%XzZ$8VgEGn zdP79;8!Y?)@Orz#;dgnS_44%EJfF6C+Q?(-2cADle_C4BTDT?)R|}lpn~;v!<-y7R z7Y!e?a53Pj(HVl9@Zd@;TrF@LSSQh1jp`VMKTmmZdOt+?-VL0_z#(*NF@7a{>x^vJ zA-@BCt~yQw6Mlzdvh>Kfwc*FG;rB9@-z?sX=ipogu9ywqL)hJ0s|5d>{sgWlZ|Rc{ zzDLR9eou0W>AI<7c7W z@mhC&Rp%wd34XV5Ezpi?3ztXde3U2M=F#0V*Nz)Je(Jwb1ov0!E8IH6?%MIQ9$cq| z+hgGxfphKnNe`~c!aWb%653JoM&YfczL$D%RTgeLa6031zVPBN)Q+vHgKd|e$lhO$ z{X>4W%e{XL+a-r@wab-B?c&x%MzAx+aBa2#yw98?FP_jqAbn;RbL+ zxH{l=<411H2%@+$oLj4@#vw1^wj=5O^G5nC{b{nD zzSmBF{OMbHix7C}0P%bYJjbr$OdN0Ca`(!Gb+5{Wb)U+GXO0fnNHvcrqYc0K=g^Lg zfls#Ur;rohhNG$Sa-kuWGSvSAXt*01^d5lL%Y-N2+77bwwjeLM17_ZnRo^1t6n#i{ zTW%rWcET#>{YmBM%u*(DW^JY{-FwkkGXT8a1=>hn?C9xD#0#h15^5oSJ@GmtS?JO5 zC1`NCq<7X@T&?8My6!dPac@aAT6_(p>Hq((B(09HXnboDjpBvw`iZV`U}or79|Jy% z^7WpV>@e}*6XF3p7Y~en0X`S_t-!wzc)cqm{D&`qfA8uL{|tRfYXRq5{3Ra#i@-ld zI|{$fuZkbrfNuc)Ea25&zrmfqICuWkMw+Mf11DRv9DmdD@SN*bow2Yy7oU_*^E~9u zjDdaZ->DuhzGvlzgvxc_-F&<7M!PPdb#NEgVdJ!B>dGNKPx9elx`fj!qtnAFI+U*i zIvl*>RHuzL&ec=|=}yf>@yPU|1i zE80&@)+;5{8D386Tv&`c_fu!V>x_MB!2b&T2|WN0bkE(5sg4Jgl>a>CiwCWQ|H%B& z(UQDRES&k^OabTf7H4DuoIOc+zRTU(?o{5PTe>*WyJB&o`^0d5*s~(6QxETrC)Y{; z*P=QVV+R*w^L~yx_3)o6@1|Uh&1UU{I!PW*Vcwbzyw-3fV_IXr+q<_r`l~RHzwb-` z&&n1oeOG(4f=@#>z74DkKTn??#J1E~nWLTzacCb{^K_N_6>!KFR{G&KUGrJZ_pF@e zIXinTaI$ybNj#aTYtDTGw1LS{; z%4dJgjXCH`qfgAf8+o<@cVk)**BtuB)RcHiq%E%h!M!QEf%VIEar*b{9^FDXM0eW) zIGU1hXg_caIz(qG`-VgI|5*n4&y5FYJM9^2Y;?L>_Oa?w3~v3WEynGxLc-$1w>&=Z zpHN2EoAbfgL}Ro=Dl}_Oy_fN!_7gY1B;HGx7Gf)Qo}TgY<)0gTIpcFHU;f$W{{C|3 z=|{Y*dAHv_Jnw=!(!253|L>NWmQzXz$GXmC2zjTIW>q)T;{U2iw&PQ+=fOX1?NawP&NX5%n9vtKU2~x__4o=hAOPr{i0%#s$8mHzX&)aB6&u z{jZu$wBNUE`|0jEeS>?jl!eX{-Lj)q&yFRG>5Pe=rJlm`=gzRcZj4iXHO66kO#At3 zZT=eaPoW#HAU|4q@`a@QvbmJs)!|s` zp|9yjlOyO--3zOLCZ}@_rd(eyF_usFS@>SyHKvsU-wTfx8q>C+d;ehddG4Fjg7hx_ z*92FL`#bTq_$d(7ihmG(H~uEvV#2z&zK!tVglF)z@3{{@{i_*45pl=iG=A*_Z$5D+ z@Lh(#8ov(zEc`b7^YGmm^3RNC8>z3_yq)@L&tM<#s%h+5%eTg}^O&oMN4h7WwSUQ9 zDKe*VK{6`)M{AE7E7i9iVy#nloy#M+^6%A&r<0VSG4fLGza@{6oz&^w)JbFH5aAKr zDDEP!o*FAmZVqW9; z>dMpotOoj&`otG3els>wZ^^ILn9E5YXM7Uwa(EVnw|XB+xObv=wC3;bx7M@ot~;fM z4(|2QS7yuKO`R8_!Q6F)ez)gn_)l4?Q!}`9KTvna8a&=;e?w&?*PFb?zEr%!Hv=ym zU$n=scB*H;$>fJ#jo}^ApB}9~oyljQ#TUtd?lY<%)qt18# zlXYH}ROcP+;ki2RJOeFF-Tu!4bsqYET<2q6N1Yr0$vQvFf35oMe79HU8tObs-JV;Z z&U?H%C-09Y%XTjErT*n)+wGspeoIAHxx0v`QU}4x{^_@KbI$PO>HLA#Cnf);ACN~j zm-@g1W9SF^!)!k6ve~f3q4Rtl;E-O_8(OW{T=|w?pSNS<8v4a-zLn6b{xKZ-+P{m? z7qjV;qCscNU!YuHpVF^3&*rCGjYDIi4Oo?{HOPQ+|8ZQ{AN8h|`r|Q4z^7?!k@c5#0;%+Y2jy@56x<6KrzWojKVd+du zryWR7$mURfjayadj4G=$F0isx&bMrurO1--xIUD5OZeYyvN>djmGb>ZWa}Te-=ITk z`Oc)VW_1bQPM7?%GN$!#$=FW#uXn2Mq<*fQwpc!(!1J_DmAUlh#0Y+}tVzz!d}2O7 zw|M+?coveg^|rkilCv$ZL(Z=Modta}W3lF$I`69a7#dXn}F&y2e?$>wZYw0A;dv;AMStnJ9A`l|MC#*t+%FRR_n8n=Ae6Aq8`mybv5 zH^TE)Wc|xYu+kk4*7oZ>%`>DYZ(?38`Ez)ESiRkc4luBKTkwmo0!!JmutmVGeHB>o z+i4rgj?Ul7&Qe(VRCbnoV@-Qn z|Iq%grz6w5S--WiTQz!cupQU*EB2Rg9lR%>Px{wA*>(CcS#EQr$5z}td+tLz?3!P* z?gcN@o-;AlShP<_+1FYAq#jctcDyz|^t6ju9?cd6G`?t#bS>WYwg+`yJ z_rcTBsP@dmai|%)&gZcid*EXQt{&HfYsI;Ju};GNIXLml-769v|Gb*xe>Hj|`CL+z zwK zzuU%r1cNVJs}0xxB>P?P&#m458!!rYS{zZ!Z{csH-w40b0?*R0Edvy(J` z5AK7cX`XmGW%*~B*yA=dsoz!6cg=hXxDm?LTuAkoe#A~Oa-_XKosaO(D(!iE?yS;@ z;M4w&=*KYSL3k*B*8`>FY^n z{?EJeVSE|!M*k5nI#sszpF)~BzX6_1;&rav=&NYB=AruB&$(xnv5!8bGWH_h+tD>w z*fMt8_?^UWCH@lPE5UW7J$ImW86ykSW5mMV#MvLcT~Q0HlM~d7krVOa6VN02dw^HH zyTPq@QmTNLya;9rc@SM~7N^#GIFC>TQ8DiTC^C7W&%|ZYwU1+ledX9zy1= zIGC?-Je%-h!iD&U;FsYaf!~6khu?)C#~njBxQ+dF{384l^lje5+-9sL|r{Z`Z`BR?Xy-J>2$uoNgoVlCE-2?42R>eZOt^{u-viY@@;kvA| zEhl~pZItKcFqOzu4Xy##jBCSn;JR^rxPIIa?mXm9a=VAHJG-j;++~atIs>kIefdFn z7e?V|WG-A7oeSsd4l;GnIr(he@8*uN>Y}^P+1z`Ma$h@npHb%tQt7MG<*n?ae3Ux5 zGNaK$6OF`cT-r?CTzn1i+0ixTY`y4_PG0PNOJ7QtM(q7^Lo>8#+;TXge9K;P-x2UA z-=pZ|xwu13`)U00$BIkQIU)YV3EsLi=jc_2?qIuhU5zgqHykcUQ;tVdj><&GYcJ>5 zl;L27OLCLY7(u_@|JF-*%LqQ&wotnEQv|cZ%a>Dr=xlio$JndjNT+P!SmNVQ8=~jY zBj)_S@Z=CDxI=-H9-6&Jt>bJQ`#i$+&4=e?M{S|FMh|}>aZZP-pSUws!R_oX*gg;=T+7=qP7iBM*0-tD zKedLCicBs-CN=&^<~81GJs6rNwJxbQ*6u$h+|zJn7Dm}aBTe?ls@?PZY6a!i;J#?- zXd=87=jh0^bf`RcHeYKRvMDcKYsRR_rPnk6Azi$0vwL5lGi&EAPCWTKJaKSNF56C> zzt`aOfP3pt?Y_|cg*&uvf{eQR3;O@ERCutKe!G_b{I8Y=D!odw2fXqe?oo%^&YK1QY2E-&$3E(hJsI+dHS$(lW(#HNJYk_% zrtXDku6lvSaZ9`V?=Q8bA16BAxL-@^G%NJnnAF%A~g4hJ2@jPh(*(^8Kg6VAn8V`qJc&3PM?5O#aGJ+J!nedR?68 zO||qY&hf3XG;9-*AF65C8Sb_UuD$RgZID2>X-k)7$7fUk8cCwY`MDNw}yd zBe)O02LFD1&8^x9|4?E6uguB3{*Tn(|Ik{nY~fVunfY=!&vAXa2wutt(LTf}{MWtg z>wkX3A4_-_Gb`8jE%`qGf2y@2uW!i?7XP=xbJ@&agZ~;k z{)i2seX|qbkHKYSyU)V+0^bGv^}tirY2`VHy(u47_ONiv9+u7gZ@`M)TyV;EcJ-C* z?B~;dR0{c|6Lt3KQ{)qTd35fqlWUVL@Olt8jN67A!;M3b+GvLGcEZs+Si{G!!haF} zQPx_W4qQkV>-?Tv#k^KScd8);?@7fUW-NFkU-`N=IV5i$O7g4^fe0WQ~<4nGA?^Q7Fe^s=e zee2O-A8LUPt+%T_uJ5@1j%a31D$(z|I4fQS+-byzXN`&on1Y<;kWA9MY_&f{+7Bp8%e&_AlpdO zd8<3&t@@XdBiey8r&*1pNiOq$HoxyICe7XVdYHcB_bb^bYCqW{;>F3VW6kz0#^YRQ zl1zVtd85|m9)~9N)0g=F2lXrQw8-L6n78OceC6R=R1WWk5#Zsi)=D+bxp`@-`X=!M z$c$v-JBLUX*pIMq^8J0F4&d}A?!CZ?ck1iSDqE}w&EzxcOAkz!jxafe_Fj`H*!JSz68$-@$1QRq@B~} z11tWoV(cs;P4pVRrw4gH?!9M*xM$wN{~3Y14%|u?znqMhGRBRQE__FTuNZvA7T^2Q zLi@Z6nd{Q}Nu+n87`T1NPnU(M07l_%a7cDG(vO@>Xx-VTr`6Ka0&D<1rAhQiE<}${ zqv9$R2T$~0WJ){kf(<)b%iDLD@EL@y-0ni=XXOi8BX8pWOc|G5o=sk!O*YT7NqOSg z{9hn>-jA%PUSs)Ty)N|XML(NYuYs~qclQHZ^k#UTR4-)g74?!#t6rM-sGL??PN`SU zDM{tjQjYpz4Y0%1ZRZO9*N3p$ZWD5%J&c%bmje2M%J^#Rf&|XAT@1J{{dE7Xuf{Gm z@}l>_#2Y7z=@x$g&U)~Vf?w%QUbMHN_R(9De!j6Y!upRcqyFUk3Hh+N=fMnGm;qqY zf%$=l+u3RdrET2N#YuT(LIZ0 zz*O*EVPRf^My**t1|KSkue9;c65l79;n7#Xp?c_S@~C(dp|0eqMsBOfqqI$=ZL)Q& zB5e=&^nZfY#LKTyI{B)>ue-L=MOn1-f2%K|kEO>Ok^O9NoPYM39a+?|pL4N&z-#>b z5as3&&X#=!?E9ck-x2%GTA2CP`w=Ozv>k$Zmlv05<0>h`;nBQ8@B6Ru;%X_&;jJUA zx@pf(zV-m4&`}%3wxewArKv7K^vOhB#Ko-(<6=)T&ZNaZMB7)t0r(FC{~=`K!}M3J z1!GcVNgs*^m8o%6bSSJh;~D#|H*3~}d9yX^z3@5;?}A7;w>uX8)&iRu*|6iiZzBy{ z;`^V77p&Shux%h+UryV7i?$OFq$9+869)gCoPj!@`l}slNY~sI5el^K|r}y^lP=-QpqcZC?)Fc+!1S7+L?&cgi}IvUBRJJ7N_|G>A?QEpz&xNF+h#lMUBy7puD&>m^S zy%VOKW#LD_yZ*K-Db{SJ3tNc=~~O=KgFDYVb&M+K~@FHSVU zBlT<`7~c0-T%Dw5Hf4z^LMm~kyUuLje8Pmr~gnKbP3OkUV6VxUuWArUQ5p7cAZf79$U%xKKS9<)0gvR;Kav4;HrVE0#0)Nhplt6HkkOgf{FfSUpi6k(=B1C=^q-L~+)mEX zt&>CicbhMR$kMEy?LyZ%S=!3nS3DR7R~2iji{U{RZ8E@m)c|p&#C@K6DNZ_D^X}`@ z=5T3_skXMHR5pE}(xrx0$+?NSAT=CHctqD}}eGA{6 z_;=$E zb%C*!WD~Vf#(+H+)DG9Hd@+cI~zI~%Qt%ZYYTLxfxBPjr4}Tf z!G02)zaDCA8ZS&unrt~Cu1YKpoWCQb`3(&=qs;QHsRdj8&K! zP`=>{`8p{7t6upPcHhP4i{ey%pXJL%3-G1F^5vbc#h09dL^Qo1@tFnqQc@D~r4*P# zU_QA3T|QqNU0+y$FBPm`&rmSz)81kuRFOobay* zU&9+sPQKKBW0uZw=-dXK=R&8(rzyVGw>n>#8_!)kRcIX#8IcXAz8=n(|9A6-Mz62e zy-Hs%#^zk8uXi3(kmx)Nw+Olq(APz$-`59E4*U8L`6|eFkZmLN^%~pP$G|JS*MTh5 z!w-csy}4BfdDp`CUfXUyPgGX2eg7BWoPEE?=DpG8O{E{TamKXblu)KC4@U<6}^{ju5W1;UdDiKWEyCaDxrMnQ)^GtIQ@F)_*)T+puu9c;QwX z-m;SV+OXa|mrOWW$XyYZwKgff(8f0<#pm1jx}^B1jTbNTnZLMoyy%LX5*f(GHEJ>8x(x%7EJlTuRV8dC+=v zD!S*Xt3!WrOQ>U&U-guJY%E!y_->L8+-~v3!Pf!47VxPL?dDs3=*Q2`??b|q+=sG| zP4QgoDy~j%bng9euMgE9pbx2@7w66IL;l>`jGv-BzSUkEh=+%t#hRPup;mt=E;tPxnG~nB_WY!{2ysy_?v2QX>iy+) zVY_}r?MwOU=e4$zyrXHx9nl@GHC!}#yf^RHShVx>PlqVQJmVH$0luKIJmnrvrOQw5yHo z_UOuAM*pMiGl^So)9>)oOKtke#J%w2{kuBB(*gaR;HXFMOz+TM;j+Yj z>R3-3Z6jXwet>!-1GDF|+7K|~fG?J=9ALB__xJ*I`FwFU>3>6) zqt)?6Ht8tqdg9BK@MZR#0d3AkO_tuN(HWfGKbn8G;R(`oenDp&ZUC=z^~Dp}$Y|!U{a=sZh6FM` z(Rnm``-k3@C{N?R9T=b4ADI>ojki9o*>WPi3zlKlozVM?pA!|}tN^FpoB1g?XzI3qa zydJr9bSjVDrZ@cX=v2Hr`zJZ_;RSo%A^N`!?g_JQCZ2SxxGRyuIiYvap66*_i#&C# zK^7r%cjvnwP zsCC@mUALn@@~erIIP`h(=`Ox}e!O6F;df5?4T(q`{z1F>Uy5Ps5iq}69qNM7CE*)t zFVPMwT-##zisysC$R5=?r0$(e0fU@{?QdavEKD~r`M^j_Rw5@1)ul~=#=F!;zFxxrLRf2jV}xr7=YBgQD8!ZEDsWd5H;VsR{L1@zp9Z%D zH-Ni|xO^Us_!@pK{;l{O_&xYr@$bTS>$E|CMsP3RW%%F5cjLJ373lm=afEXp(eT`k z-jmRtqR#N>4!ibjFM$W{U5}IF^YyCEl&Or`t|OG@yQ1prqyqSBNI8(*)okK z+wC}`zAal!c{N8?f4-VFEB{gUo- zxza=G`{;zp=t=7noQat>X9#3L06dy2GSDfyn$=AKw50I|At-`H*$jm7;hUdM-J!@8sj(MkPp^E&WRV1mb&`p7gVO& z^?AyR2{--OzxVpB&6_t@@b+yx&YdOJ`%TUCW%plpI*U`!x7KDP(--lr`;qnb`(OB` zPfcpy*U zS6{kC^~)en3S}(E&YJ>8dO$qX8&2uCOk6h3odwtW$tK{;niBPXpY8jK)Bl%t;HwV$ zzj^Vc;o-2)3$NbO_%8Vy@f(0q`erX(@v58di@Um^+p^3XM9>~{`M@Q+=uFC{?$dgk z=Na~Pqv(!jj?j2Tp8~hLm!`e2F4`jVJDf{JDM?2R^S|<=xFX_Q8|nQc*FO4h2i<=Y z9cly7df6ko6C1|qduB*)_RrDEf1zgPd+C#EGyOj*u<{)pD%X{%dp6>WD_7_9TllWE z{AsiNkqvL|ydrPzobbgqO*+TOG5u8ZDX(~oiu zpT#oU$BspJ>OEn>Nq5CmryqtgW$tqWXU=j_wtU&Yg45Z?ZLb4P<&@Izb&ulf@WZ^H zh^!eu!u!tP z!|Q;PouF~99~pDJk{u@f3r}zB_Tzz5nrt}L^#tlFJ5A^QG`5K^4U$#b*Z8!r(sI4_ ztyVg^QEj*%`uy=h<9&$h?Q!v1v`Kfvm9R8cIkpGY@3GE4$v&EAy%G&(ff9&V&&#-+}eh)Ht&YSfHQ~8dy zC81r_g0Ffs<7*z5vaEOVN%&E(eC0PM&awY#Dm`|&WDeah8;2}%er4hh4`~iDSI>6J zX%$||yn;HbEQK#4?DA&XvV`~hl($E6m{i^c2Pp5Ur1Cyac`8Hgrg9|*=m~QcQhi+g z{WQwQ{=V%r+Dma&_)&Q0=+XaIz2mLn{owovIFO5L{~q=+(a!oAXQRTgQ)fnM=$m!) z%~H{={!_>ugQLRx#H!Q%NiNC+-w$;W1(8YXn+TqvTABVph{51>UpE>~k<|O>^&B!}- zv2q{!&L)@c@0Yye(!a}l{Tv+9xEm#0gmdGc>>?*)ZhUj+lPSa8C5@ni#FKshv)|0O z)hBhXRqclkFfjTS%t5k8@ZH$&%5nEu-$~!e#!jf_9d;kCdr{UbO?R^H+nBmI(Q9Pj zhQ#O3W^RWL?Lt1fkdHs{e)2|SNbjbJX6Xv);9FTkkzFD>-ez^1t;bE@BU61h*0H!M_vVtrbWH{5f5+ zyzoY?k(aZQq24v-L)ZfWUi6<+X3m%WG z^SG+X#4;B{y9ngwK|=hpC@ zTIq~4-olwr;6CBWMl4A-zK_nBp}pg@mD(%+#!xo)68Dgmi?RXUa3$P=@7n(D*l9J` zPfln48+uan1UFYZ%<^%ex!N9RP`~%*YMC}~IyRHWgipXvn zuKlAEEiTz7ONi4rsky+`<=oRI&sN&}$ei##bsDfrll=7~11a!GcWQfqSpkoP|5)~^ zWK(pLF1x=|KDhP2pvPk$i5}6exPvI~7|L7X;;^Mt&Itd6s2D$~!aA zpmjeV*6l?UTDqdpl?h$@p#3j{woIE>uzp_EN9z}RqU`@68>6)OrQ}n67KL-DN!e2x zkH<-C@pveHjUZRzh1Q(9NPmvJ)NxX4-^!!=wU;O5>9cuszg@EUgw4}s^GKdQn3PBN z+*OY5xfhfE7eT%(=uL-?*lw)dd_`uRST?|RS98HNtEspM)EJQ?6S6e~`Vf4Qy{WM3a>>)7Jt`RSP;!+Xxt(@qkDj|O>NCo# z|5(b7-k#7LU-Qy8qxUnRLw2n4aevj|R2X~R*t7c2_oehb>DN|xr}bI=uc+*v$S`-f z;^d9rnaGK=mOjkBSdwifJ*&2KdiGe_Qgk?5`nYGbzri{+_*E9HJ78H0;aB?ze_14a zhv5+L`|}&K#te^EV4tcC@yp5i9@?tOwpGpdu+a#+Hqu+R(tUd8J~t}fh4!`VY^^am zInx^{Y9q53h%f!1xt8>T>}}1xbmy`aIsO7;b;$46hOYwtsy6_yHOI?l;pf*=GANr= z_J?m5=?^)WjKP@G`2W}|#s zGxGB|n^SoPZ63w>dE8uuaVAgeC@Iij^fbPBD;>#LF)96bBlTVkZGxLEE6UtAn9Ha7 z*nHfH=k70gREAkYr!JyXVcGOr)7h#ud{0&`z3P*f97r}KW8$0o%}L0EzQwytzTYPL zZMu6)?d#t|XGGN}!+a|57~oVkf6&DLOE8WUK0150OYs4jZbwASL%fArKv!~eg`gfCZFzGJJH{TID?ir$#kzC#LSl}6Zqhfilv zmiXlM+kKvFqR&XS`tg5Y`%#?m4DQE#yLF}H_cV;HCVtV5`VUOmOS;<$(!$~1+^`uuo+uo(&J0(hoS9w*Wi&vGT7m~imOJ~p9tgWOZy{U1( z+6;L_*&Z)z;Z^C0++nqS|8vf6 zwX*8V$kTt`zf1p%;`*V+D0kLRwwKyMwpX?KKe)B_k`nYz9!6hd7BZVp{Z}@Z;5P!V zG3af;7bWQnw-%TG{fywJR!``EDBOE2WyIIwULdUl|3&{`%~oH06>TRKa0kCXQ~_)i=69)so|C#K4NoXb~f^Hq?qlzhJc{2Wr$~;|n<75my`>>P&td);=N|eqqDSSz<2H13E8(l$e+5}xDBK!z z=DpY&Hu9NoJWNxYplcO3H}JiewC#?pJEGl`T}s;|B*h|{%@v*i!A_`0i5b2d{ak+ z<*r-+PWZ$Ny&FDk;qn)N)A>5_N_QmmKkur?(gomjrcXQ+T&so4JOG?{Tu$A^S7(cB zja~eZuDh4>B1Yd6E+W48)Nouc0`ClGTHWAF&cEtBy&uOoV$Re1 zaqc|5i_>}fX4+D3rYv;V!olgTMa0sMeP!N{zKyxMdxJ#dQiam-o1jJGxZWx#qAn*; z7h?xgSGJn6w68g2`6in~W#rg8=4f01N6g~TncpeSw&sJw=qQUrbCz!<;b70q)IZna znBnZJ=-9*A*IsZe28Zk{@&6!X)7h2S0(qiA{i+xo+CR{@c&~4Ti-nVXrO@AK?_No_ z_M+n|(I<_-L`X*`lFnJEV;Dy$w}^5ZB7#A$LhoC-izR$9WJ&jSDkGtNek^H%`5WVe z!!NsKBlz_;OCxYioRiQvR7L!+iLWMJ;g`wN1WuK&d8)#hl%GlYn>h1ZLHX6dO#v4K zy%TS;H0ZyiL_@Wu;UH)b+|$4{fJ60d0-xxW%_%t?ED3o!z&YJ2aDNZnO0O3l%PV(J zN%7*T?kOqk?kOo;M0~xC*SRj;Q&POUr=+mT)IBAI-906Rg;V#GTv+#%6y9?*whCeC zwix}6bFGs#_+~AyT=swa4#n3}N23Rr`%3ng60h@B+9TtA74~5-*fEanr8&tX1Kj<9 z&$TKSe#mard7T=*WvlOGZlJO?2XwM5J+FIFig)*-6jr?MMJe2r1h0Ehig)*-9K7yD zx$qn5tIE@6W%d~4I~(3)Q>Sihv|-*}b9%o7ekd%xUrJbaJxllw&w+*O2l^gc!CiK2 zvRd-@lfMKW{sZ2?Yjc*n1fGb0!l(ZTSNfl9dRnZ)oO2TXQgCTpOzhwP_$Me!_GJsO zPqSuS2cPRyHtG7W9MwT%vviAz$FH^aq25W=Tcw91e|6-4MrEN(oSmjRNsq{uu8SgX zz!yRL!TYf7Y?%)ym8rfhT>s_hwz`S47N!l7X^CTdNk+G0d+7}CAK;5@P2pJeAZtq0 zr{1g2+akz3Y1^2K_-UG_3C^9NxkmlemYr?O&Q@DepH^FD3*~IX#`+Cqs=V7@qyCk^ zYwzyof8)*$Ww|+=`i8!Xmxp&m9M4r&K4nP<>im}G)<2{y!Mbz9qT7A%j=~H6uS9FX zK90Dx3AbYt=7VE1IAl}h+5gIO_TUh9S|N6t*2=~RA4@p>2b{;nRpDxJ^|&V7T3iRN z2iFXo?$B=|{AI#>@o&b@eTX-{@k{aV#c#y_F21>=kKe<0FD_1a6n`iFIR0+j@Bc4z z?*pH8RrmjY_6Ih`)C-+D$IOeII(6!$O`W>(0%k6U{U%|NP))_!tz=92775)ASV@Yx zfQpQ6r&1DOVmBo>lFg(f!=xf5B=2jCB~6&qR#G>{_xU=X^WF}Z!?=HUzu)8L@fhcG zozFS%^FHr?=Y8Ji&xhxaUwiYd@;UXL`Rd2GBa*ziq;34=vk~js|8w)LKP4u*g5L># z50D;5YH@*m_?i9ie;~m(T$9|#h0G1m?<6u8o}2h0^lHUUtwA@|VBc5!v8A7~bC44| z=Uc+>5sY8q{8r=E0DK#OKLhY~Jbvab>MI7IGzIS>WKMSMYskl1c&9Ofv2Xh(>gkS+ z4bO!oFi$zM~K`+lh_ol+e=kD%{-({j6`5#{B-%HK@z?j+v9&Zm>Kp}gJjVpqi4 zxqc_c_WTt)qq`h;Q&#;^ykQb~+QVH*o*x^^=As9Rt7|R!_eVr8E5=Ys*=K)n@coSP z1*=p~JOWqE$?B=MkFqxqixvER)KT5PkRn;V8oMoCxn%>g%iknqPCBEH`%UIqbE9&e zZ{fL))Z|fn8YBza2Yr_K0N#$q^cy)^)ePRvw6A+_9|rG)h7uDii+PlENP zB=UtHH%ay{=8^kG$j9(u#0$xb@)d)5l6Z&grprfmCQzmgA7~GFO@OAM-laJErg=s) z@1Bl7Udt}x8$VM%S0*d znD2Wm&*sA(&9nLcNAdi2=p`My%;O=s)ttogFC{;ewS4M0x%2X)a^=)NKd3J~wET0j z>i-2EX4jWbeM(SYda3%#KPjvJ?qTXnW{(Z(YtE6xNb_J^`h1@-M40e7lCb|DQ8x!|gDk5bNXy^{BP;QLbId@hBDDzkPNSn8MR3HN=B z4Jvmld7{cWaj644zlOhc{B`oTnZIrPW%ygo7`BV_JQf%=o_(2) z=Rb~5$e)d2TB9=mNB)Dx8M7HR^q~;E-FSfSx`laaE;6b6^foh3EoMy9_;eF@n<%|q zX;6N(pVzLW{Bp`iD$je(*>;^jIL5HPHv#`MzAz zk87;6Z8to%j+5kD!xo>7JkI;EWxH7?_zg0lvZ^asudW&DV|Lxm*gEV&_a^GDrtU^x z*PX`zhMk#IJ;jEuID7uik@!|?lE3cC?S!x7+gZxO1Z6l&c_n8lNBT39iG)8F$vUuG zvtTW_i8ga-b2@F-&?e{N9KMylugI^XuYL>vb?Bq(J9R3(FZgZFiETtqqeRBZ*EP?@ zWt6`z$e-f##XtM*=Er|9#8Z8*R&Aso9m>;pR+aaSbwhb}=dk8A#%JZJT)W3lZOF#T zM(N&R$ybsyaVqc5p$6ZA2=fHva~?)`Cz8%ttDN?a${#Vh=(pCR9+u{;31FQVz{+@7qYe{Rp@*e8!ba?75wP~-yGr)=VYjrWWQ2#m zj(Z$LziGhMyj5j1Z(T&0{i9=ks*H`d--I@uzT5`N$>)E~mwPne$BV`<@e|t=?t@5C zhY8J~oG-*SYaF&Va%F?T$$aM;>|vd^k*z#8@Vtri1Ed3_A0$owxFGrv=?u~i{@Qrn zMS44FNoPTH2WcJYBGT!kUm~4F+D^KN^sA(1b8U~ZWcrC;AB>Cn>mT|3l{?F^%{l(t zOq&aazK6B>!ej8MM)%yw+*SS?<3iV@1oz=%EBcd9bWNhKMl0*{51_yDZ|2Op(n+Ud zj~UA=p@n`+J|zcM{yUUcEVPk#l6S^K&Pnmi`kl*z=DD<62%JLJXbM@Q`4sne87#@C z!TRc9!rD~H{Fyb14Xn#tAHb5{7_189<8bh516CWbT7cCUz>+>2td_&XD-EnPuquG{ zPGHs4#QU_>SFMG7nb~2aqf34WbXPKYn@m}+k*?ufn{S&a=@;jQzZ}#|< zGbd|djYM-2mDSwh8sC>x(3em7zD(!6#S+yQ&Aa4Vt1n?a_2nw+WallUFOz>2<1rUl zT3@LoolD#Dk>2myE)ClLfN#5r_e}vED|xQ+ZHM*L_IcFF&ifbIo=cs!Q)6FEw9}YA zJ3%}-8T)24d>n5E>m!15hsRYkpGtY@f#&IgGnG2EzTA5$r@R!fQ$FuKPR%d$mUlt25Ib5>^E~ha7oAEd!~3V9aM~PDX`?L<$`M;{WCba z>svCSx+Wv~9*7_J$^;`n=)2@YJY*fo!Pl5*INCe;!K$ZqJ>lZ;pqya-j&-+c;l*>U z?}Nt0+0bYy{*8@|<@7^+D`lRqdsEo9=VG50C$I4^@XNRBIj6|QkGAJr%zhc#RG+o3 zTFdhUFDq%DMW;Ga&ChBDgE`o`3V4|4=uXqOyP=KpH4jtWG|$A@m~+N={VQMf>%dp- zY7Jt~fdlJR4*Tk2N!FgmnkIZi^d{ZPj*PU!JNv;tMb0gd*$o?Y4Y8=Up~*5dAYK0)am}2Qq{*+$QJ5Z z^2fQ_L0E#Lx^sNqBFd@lg``HCMTek`Vja4hb(V*FBke6bFQ$#wS4vs;*gPtNena@; z6FU8s9O#Ug$-UNBa-pBrS2Flsk5kWZJ5-%2Uq@>!tEuw{b&|CCE%3UWHpOevtIoHt z`@fmjltt8=Y5gNMFcW$Dm+!^40pF`@W7Y<+XFuN z#P~d%+v`o86maee-=C%Zt`i3Jd!hFW+wcqb;e)k-Gv6cW6%O^nf%esw^h^6Xl|LcK zcXOH~`s3l$0Y|>%Ex;KK9Pv!FZ4PJ~?kR7h?8hi;bMtzYhb~35Wp$+k`@DZSfPbky zIM+}|?@h+_Y`S^o-wzaJkIBbDg9K%49FL-o?)Nh0?D|Viv;tpm6~3P4+uL}4i08s5 z3Zh3y8%fua=C93-X8%WS^k?Sdi^yBb-wOVgvG02|Df{4}YKod z0fck^i{Oj)&P#5!cV2#eLbSzhsK0BTAtne7ijki`=dmB3XVIGbmfSZv3du*FdJD9lF|w3{Zz#S9 zN7XBoY>}FupZFrWO^EMJl7e4uw9Ll1I#@tpfRSScaMMGKspOse`?SV$^U-= zPqcP>=hH*qms>}9^>1w{do!8Sl+i!!S3KbHpNoveV{5!#V$W_r2l6Ak*Mn?m?MLsF zuRV^^Z~0JKV=N(GW1ybZhjRKU`P8%ccr`wwWL9`Sgggq4^7~kWiBg&FO3GgE&#r4; zrLm5_bZedMwX%hLut&y~u-%6#&?c8syeL#~U?qSTqSD>Eo zRiC@yRhqK$(M2oyx6^|DTDjvrPp9yHit5uZWT>cGb$~0sJ*+3#ufg~1ywe$>qq)U!{XdC26VxVt<0I(#eNUbjuYKgorem*kPf*Qq$2b}}+JaMFsx{;E zIl6V{Nqk$1d!|_PZJL||O%mJ-l$V&-**BQOy)DIuJt+~eB=v)L{TzlkoIOR8DBkjM<&eQ(eR^F}j?Y5yuE5�h}JdUC%lp!U8-qbXlfEA1d82W_;9tQVkM5fay2qNb!Xbnq+(H5QcCx7%HnhoQhj$?O_}D zZ(Cc|7vZgW1h!xh?`kBO1 zh%cNio+a4ViM;-!^17!|<&t6>#=-OVGa8Mu*JqS(>oZfSe>m$iI`=I(oy}aP zf_dDf$Z0;hJJaVklV42!MdS;Ie<41rbjr9`4?DeVB%z!61-Pw6@9|?rcotXw;Zx94 z^%Wxu%V=)p>iZbLz9e%reD8LA@4^vr+sn018yy}*{2^R)%i{NxGRCblKLs?e+Kv+njGro7Da^{bEBqp>`(g{=M$K8)H081{MX<`%mXq48U^~b_BckR^KD(w-`osv2e z-bmjhe@ngGYX7?S&Od;?U%+49mMhu6b=N_8HTG)^oCyrY zt+e+H+sN6Xu{~YbF!@XPkZ%8g%E}K>eLZXMmG%;-jO=qFNo)%mYwy!aVxL1dC7O>=#X}s20t{7M~ev9__m`oD~S#ND?-{U&Q7wL)R3D@V{eUS0` zdzcU2WgDsUA?VhLZ2dkzzV}yWnuQy&hCw*OO=oI^TN~-2aMOOH3gJw<;4z3>qsL9- zuW+#bhxl}ZXC<)Lc^Ye+t)kvd;HNV@Rp3Xg#?4K`wftISZ~3oBMRlZ2{5A7ub97rX z(*2A&b7-)cixk!S-frr%R&P>J2ObyI`rn%Oc-j^jt(0dtmQqeOXui`0&i_EO z5PoMCeurwuom+@`IM|NzvfKHhZ|C`}b_#zU^TPULe3%mSOYtS`-4jnre7{DJCtGNF z+4NW6+CBZ(`!+33A^J}mrd`<%#Xly&vwvk=5a0Eid9;hO#P%Ee#+6pNn79cFpJxpQaE>Dm)X3^A44WAAcCk;ffp`Ke=wp?;$$ z=Y)>#IrK$xBK)mRc>)=s-LF1~??dY7SO25k#ar&$`8MRS{Xa)VB~KUpQ}OuVTi`M4 z5O^%|co;2o_FZF&<`?3NY@&3b0hy^pPowX|zMRGjn=jY#PImkqw6A%$WI?evjRUI7 zVd^6J?V6jP#Moqedh|{)X6V!XH}KOw%dOB&%XX`cANjIM5 zos9>AB^#!(A@`CeyU~>{_OiKhKlSBAixGk+Sv1~rN5la-@dOHyQyl$M`ThblU6x{f zTJjg;NBn}hhnM}^{XW@QD#JWxrZ200V#U`_x4K^58xyhYeggjo8{Ye+t@S=*O5#LtZ@o(?6gMT2}_PI=95Me7mhYrBW9yM1++;F{D= zx`?y(O14!_eUN<5mXAp4&SRzKXS5cV7hTnbyt57#_SK*SEp3k^bMuyUEP0bf07^Fl1AOOF2BLIv;yP8E7$B#(eQyPjB5T<7h;k z6TD8#e#uVjPGqI>8I_j;zubs#5=j=QZ+Fr^{H<6f)Slu9TGKR~02boaIcFKq7( z-zULV8MUWWF$Zgp9i!b7X;1mprzl3teY&(=n$S5G*4KU8l0V4<_x!AC&!UyeJxtym zLH-0{bNR&OY_0GV`aa*@BPSC)O(nCr&|PPvMzBXt=cisJHaZcRXic&2f&NtxAG*Sq zm#nG1yWpXXHHO!X1|MAz4^{4qyXp3R7`&Dvhnk~mUi=I2()wjSdV9sY zCb}4q_@VLC@Uyv{#YC2S*y~dbZV$1Qv-!J~TO5`{W)ey04KKvc^~q^n(%ozu(BgG> z8EFG1(`>@A>=lzPq|k+GbfE@as10;s4E)VSj#KEuRq)=xPOM*N+7I$1AQY$t>v9TSd2{553x zZsu>T=vNiroM|`*dDdN_v?F|eh)hVv-*f4ZjB9O9Wt={WezN=UZRwlJx6pszEA-elxtHxG%i(gi>V;ti>cl+r6 zIoW6Vs&kwz8ryS>WDGf0+qwSS-bV1=7S!MR_PG9(k6_eQE>mzURc{cbd z`nCixuJJHl6RopwkPQ+3nwRRiHmEnr*K>T#s;7R4-o_t|xBb6hKLUUGo7scFe6Alq zE%(l|_Lk$TX--!IO*L;TM|P{2)7f(>k=LU$J(95;^z0P+YVW|eyCON!#i+z%;d(B( zE@vz_iT?bSc;o)uhA!=Y-h$rl;5oOcF_YKwL}uXnhAzca2lLb9H$IWsKe3@}#u#Yl z=>0@jBe1&Z>(;!6uG`S@<3^9)GilDfdkgOTLf;PZ{ttP_l6SfCZu)$m;pXu5{gI8* zT(q3IXa#?Tv@6|8rN*%Kcy!OZ$FAwC$IsOK^alKY&0jcj*K4pUq|c{!bZfrl2zl8j0{tA(aQvNiL zkbLBm_dk)5|0GA#~MqWie zB_me`GO~v}$w>WwLq>`nzQfCi?wmT5jOYxK@s>l$NhPvT9m+}Ste$K;Qs?bR5*al+ zqG#C=^;^%?$vhXkwmESUy{JYHwHALh=Zr&pGs?@7>PeQ8*cF1QFm7%^cCPcXbFr75ZRCB(%g*_M>wzAaEzms;;dn6<9xQp_z{~G}pBgWk zJm0d%i%DUAU<<0@r`dueus#4kWefU{9od3(U<>|8o@~J-0Y7Dnji2t>^V7x)`D};c z=kK`ZTDAcH`=IgSe`@#7g~xBD)8ccs-G7(k^PzMaf9eqS-}o%sZ$4HtdfZIEX5wSv zgSzinJe9`4N0{el_`wp+$eX-~YxxP#jUdki|=xfAvmb z+&b#wyq!6t;`{brQhDfKLYvx~U}dv?8pE>`JQX7?2T#R3AMowl8lC1miWkK5O76Z) z>RJBLWOpBji@Rc1wAQ9Misqg_!Us@1RqxL6@ARxWl;BuQmDpaH?k$lo`ESUT&Bx>i z?IJF3O{tFLy?gYHwf9{o^^!t|Z>(jxuNPsHDHabtk=51xxAtAjQahtho7?Kky& zaQM@%;~RJhk5+!GeLb7cTZb&aNYGnz5~FD)G_8aVS_4i7G;N`NC2f!K{Sl9E@XvyI zqo=3nr~Rov0mrKXdSAzT7ng#bdjGI5uYIkRqjoUUcjIHhm%>a4%d@~bEnrkx2Bs!_B?J2XtNv|2v3!% zqJ8a!lb@*hv-yx`QcipdX*w&g52B^%T{5XP)`R7AEcMN9sE_Iw^RP0VP2GcBXkIKC zRT=eD?Fy#}*f7bep5ITO#qW8(FT&*x|1ABsbzSJ{?u*bF1llOl9>^TQ#m86Mk|Fh9 z>mw(tezL>G5Y%V4et(Fv;$v7={8Q|+8vf~CkJX&{5q?(!GnB_2{LYrgLVWAl(C-3( zgfb~Tx4EqDPadKfb1v|-SmJZO-=brZKB`{}!9z4LS=BpY?D5@gPG9i->6_@G^3#3! z55~N)ScfY=I9H&&?6Tt-^i#ho>2n|PhYI@qiTIpTxAP|`8_LH*`nQO`cK+1IaPP%v z=ILu0zX$w0@niJiMEEP4m9;)rrhN@6C)icK-$`t*i_t>wQ9jRNsMhbPHc9$5Qv(rB%zatg*RdEH`h2_Az z7kD`l=X!XL&(>W|I-7F&{+tnUYxk`|>5Oc{bKobPmLA7)H@SWz{hff{ z>^ELj0Uo*+EkPSKwDEWr9>pGyLdxtx_SR8G@C565%6<}@HX(PSK_#+PkwCY3H$G@* z1o&*GjQXRyr&a*x+rYVwx|P5Zue8V6Wak3(uXCy zmrQ*T-LIvb$r7w8tKzrO<=xtfQQyu?pl(z=-Bs)bazX}!A~ z-0Hw>D(QQhIoq;@J7QR$R!nUdvi=tcC|g(h%Yx{2o~udoXrP5OpL7xFk)-QLk0#ws zdJJjtvjx$Kq-oN(k=C#-Hk0Smc{YD#0c&H)LdLmL;K{m`)=`n`rsRLdrMdDgiCljr%gr7>qS{MbzSPRf6X z@-_}_@UYhLZUyiDlXsHm-(u4%Jq=jn8I)c55AERP{4&~!{p`ua$)loQ-aBOfOm@|d z?rv!UejV`N1^kUcImxZzl5HzDP&R_j=6kM&k8AmRxA==}WO%-U=i)7__mEbRP9dF6 zdNrx>TkGKc-cD-YfYyKeBke_s^;P~rG5CFoIfUA?ZZU zXkcTlz2oKySC^nBCpeIfjUITyG#Mk608 ze++Qn?cs_RTYdb#7FtL*YoJ4omyzN23OM>e+9-Ngr%R%DrQ`Iv&Nk@vf1rElhiSfh zJbiD%rp!p-htURONVoPR93S+%m455@*Moj@E_Hnq{hk566w9?Wvj@&$oW~YD>BidY zcN#56Kuh&KN(Fg0jotgyO)(uYBiDqqQF+5>;FN|g(PDo^LznUfu%iP!50>}zF1YD} zzFQp4ztA@V*q1}!qdc6As&jH&C&Xd2-qUCKRh<8JdYZ>Mk$m)B{bQ^Z_Z3V# zweL`6&Q;scVC0<7_l=@oqrf>2z8Q^KuDV8RI^wTjX#QmWr1l284IewMYiN8mv(dwAWQNY|kshya|-jKMC>Qx4C_VasdPD7X0*>cnEtmWL78JIvGG|t7=D85$A z*qLJNjGn&dlHu@EochlH#rR=Uy08CVjGyRt$^R1kY)sKN*b;mvw+ekv~L@$4d?HPw2YFy0TpZLuzT-bYF)@kh=0+(yR9J6)w7mW?npB=$=j+S|8QiS>vi~mhOC%Z*#xq z1X=HdPi}gFYx8Z^CUsKOK{vX-l3fRUh2Jq&7X0MD*N0+$ z32b!>wt5>f_Z4JLzEXd~S%1`ifwY%9+5au+C*3%(jrF(ti-}1dgTL9*+Vwy=YvMX1 z+BC0gWNJ!gIye=iewvv|s&R6FaY6OB`TASNQGXQmkE4E~6`qamdEf+KTp%B6URO2s zw&JsBzL1ls$kg(FMC#;B4f%qjy#W1DjLViBj}tIC?bvnyGmJg0U+S9wla8(j-U+<+ zMeOxx&(u+ORO-G=+QZnI-_W(s!`KE4H+FTlXC`&BzC*csYm0LIzKt!Ew{lZ_xkiIG z0(b!~HV$gdwg1i^?Y1${e0I@9amv}LyLW0mQ+I&rx8Uy^$Ft}~pOn|{e~WJVy^pek zJ|bfy}bJ) z#&&!q?U&u+=N~qIC}r+yWwn2Imfk1hd585`bCs}Ml5#i3<(!}0LHT*-%( zPttxK_{y%z@7a+P`{Md7IO2oyC6{hL95j7qn7ZP(>N@%HKGXj3-iM;jK|Yh`MFP6j zgKr_}iHuR=ooKs@{T$X`?T@mxBk8-v$~BMI++FvB*u1+KyQ1+}Yds11`O)ItdC`L1 zqN~PW`Ruu#J}zE}+;CRS&GGe~^}MpbeIuq{%#-+j0*$%O){vI^v91I=Bl+mZ&IoUl zpZs5Qx4hXH`dNaFxeUJ{jo(mL!u{|SoR#4l@%7*+n&jZ;_I*C+w4hyF=zRfIfs)3fDpuL!<@t&rEx`wVv_$AW0vl%;UsH^8Qd9L^Ql|KJw zrmjn=BGm}h7nQ%BkMi%0f-lsVq%ZVr8FpXw)|aqdlb?TzN#wRb7Bo&;Um zp|$qDeg#^y4%<61hzHXAIS?OHK7Hpb)d!FUrJNvASc}|gpF`;!_P`Ne$wv-y@4Rd8q3~=3&qC;_{bOnHECx^K4|={PkK2#H9K@I{M^T0=Kbttj!tDy z<^`-#cz)_UnDNum-1BoEdz)W^-m;??!O!rl#YpgagMF%!U*lh;@{WSW7sNb>NH-*7 zzU2W&<;A1@>|Yg+6k}G~9rV3}zCZJq123!1r^RFFZnzNNisP8z#WhYvAMx^&TNUe$ z;+`M&_CjqG`!-C6WIxqj7%NQC-XpZ9`G(>DD`2Q>IBpqT>wm-9X0I7M55c>tL-a#^%^BU%{Y%je zT^Ph0d2B@1{;Yt{G}SAa+Cv^RaN}8C^zYba&SEFxJD8vIaP?kg zHEwnuqP%>jb)5GR{7u8u)4j&SwIlorQ$KNdtskbI)@!3^F@Fns)+T=4HIi?WtW7*g z`lPGlWQM+nI97&rxNmzcezNeoe;D}DAa9oyeF*UR*I~+P zUrb)7e3TCM+>0NJ;Kw3t@gm2|;?!o>?@tU7+Yec69n+lSc~qvdcozJwfxbo~E=K6##po3h*f|A;WVh{Uc_?}N<{`@e(|Gw7eUD`ydl$&oXTV4K+Lt#Ko9N`5_XqLvEnps0?r>$@ z(=Jt%hA)dPYQ;w08rTV|BVR~knd-}yY7Cx>&o({ce)`wAyN^75r$y_V8UuDz zV1F4WG{0A@TH}D$=@u|%SF0Rz(9it+z{T?Af5hBF`^a(`Tk;a|zH7zx8W~@Y#>i_< z)Xwvq}0I z2HO8-G|)UP#5dRDt5}!etGys!1-8y|4u`Muv+*U)a1gEzZ%^m!d5O(A#6O+G?E2ae z&E%tr&hpW<7g68K6a8)x{dgD7`$Bn83_yF@)dzH?OY$cl-T1P}w^2erMHlf$II7>0 z_XMf@CD%TFxYl=d_l9WkPUbI~&*UNBDz7qvD;T1`o>gD_W*xnddG1laSaqB)!aFw~ za(sHywHx+h67p?lJ9Sr#=9i+4;0Z^q<=A{i^$t4c>EoQ0)KlD5d$rXcl~?Lf`(x*ToHFefl?-eBvMT4^o+q#w zb|zsw?SyTYf`ek7!cF^}M5AKOBjG=?Uu3^Er_fo5O5j_bbY9Q8&wms4NpyJ`8KF;I zs;|9GCjaQH+aE3-jn3+;JHsHE;d_t8+>vMBdkp(7o{mN)G_O*=Yz6YUO8lwfw`lT5 zXd)Y`J_#?`5cSJFgU?f}AE~dxvl*XRG8D?d6l7o*1e6VI;@SPP7`B%QwXJU{wIFY@ zpN>zosdW#%vpXrp*JkcHRGz+v5`N$0lbWw1Cu(1G!uN+xG0#4b(Eiwad!~qI;Awh0 z1AN+r(=hmiZI*K9W!R?rcb@e25bb@$w^x(Z-ZI~w=3I((RuJoy&94DY@xC~QdNy>> zcdxf4CwArfvrPjN8@g)fm)>drv(|((Hd&s&6{`I>)x-jA4;J=ty}fsJ(oepre5LlS z+<9T2&PaU=dwD#u!-6@LeaDZ!YR`9NSC9*M-<8|CG?Ul5`Z%#(ha*?%2#sbKu9AcpMDA~J@GU$M~iUut}^_>cID>~??J(|gCO z2lIDRz+cf@`(S=YEK>Dbk3dKHeue7Kp?)dfAm2>=67r-^t?!I&$@9efbSGm(8S(uz zZx`f8$Y+qO;aj+|Q#N1skXap-&!c?!`x10Su(jqW`ystDc^`lVlJQFS4qM~XluzY@ z=zR@o9c@cSGb5JteEx0V$Xy+}M_2E)=kv4PX4F$w{E{tKy<1)%;;Y+3Mw@x5#a%P# z!&cTQ-Tbw)F|+6y%|)NcEPUtao(uf9Ikd((7Je$m{JM`ZKQ-s|zT<9d*sD3L@IUUh zv3rYdo3{6*+u}WJIehzLD($OJvMItt{ZhGN%6;s%Pxl>9eZ6!0;%-~lcl?dh_TGNm z&-#ifa~ki9c|TPCLds7g|6_ee`1eQn`a1W4y)DvOcO&1uSTgR(%*0dTxno{(0_*W_ zUL(pI7mufTtQqC8W*O z-S%i-5jg+h#z*_|S%1kT?j8|$-$HCI7x}sm*;&V)@oLE@{27+7l+(LV2B2N9(X}4> zNY>_~*LtpnhS%3j>-ijKg+7#O==o4;T2B-2@mGrU{C=KqrXQd8@~izX8e8n$2;Ry5 zYh6)X+KUn%7@KGJ5{nhU0}E z4)9HQe0PDb&U!onzUTAa=qVrD^$mPP+g;cu@qRbuH&Fful$W2Y@@r0=#bn28f%>~ZjxQ?UGL-lOx~CB{tfPD{pQk`H@frorvb0+-jNI8 z-BK&VUZk*$`W}Htop1X!U~8Pb1K586_Wtj@`s*vNYrJ%~x1+n0dC^06zR;Zi9OrxZ z)86;Zr1{VD%wH>io%{`Fr$q}}_q_%9;W)g9I=KkD#^Quu2h_f%3%LhJcY<_atMj?T zUoax>@Q?V8z_q~G&%O7Oxen~9WcYXVUwhsq>t?SX^L=;wlSE7S1BT9r(ta4(>#$xP z^8QuoWamA@xTSVv$0~u7?H^d)F6{B!`Ob>&PBL3u7RM4S5V^WC)#eV~k=j z8W%cGi}!mv9f1F~-&4O=@Gji%sXF>I*wRVSYgra-?L*esIK#u1o{1j@TfghGV5^S) zwBBvwA^x_*Rr!iJY$a8kbgdhk?#rxUYqy|cX-H~M!| zc(?X~dwV*)kFIsT75a_7h`zeVRKBp}W|5UccNX#tj@@m?@r@p0#v5Ka@bb*C9`oJB z=)AQnn<|{H@$LTb{D!VZY`_%S&|FA!njfA&jdtQQ(`wu14fwO}-hBNoC8q7?^@Dkm zYuPocv+tC+jOuN}r`bq7?Hkh^UG1n`sqe4)Wxrh+`if1zXE4u~x4TfSynYAe&ro@n zNBOz5Ih|DRr>lR!WX;^c6g_2olpn7#GG2pk08Bl zd~3z8dux6BsX1@-wO@REUz6IQOq1{541UiLY-mE8>#cq%^*3`L{_Js!dveaaZs%g{ zZAQjaR`e9Dw(x!nchKwmwm$|Bjlp_vG!yRDXYO%xbkpwyyjDAj1m9ESx8rFvA>(M$ z8Ou>=W(oUO;6-{#PaWxT+~+R2-8b%;u0)z|!DcvH(#ic#5q3Fomn)l#eAR=G?!vD? zzFZsNpf)5kl56EXT8u9kwK|(4+TM7n{E54Iw&DjB(zFx{j>6#n<*})d6~+a4Q%9j<%RQm6xUI{zB4QRlfRhVUM{}n2Jc(0 z^ZKjt-0APB=sADFN#~fXjnY$N`l-E@7ov;w+vMOs-2R8t`gV+L=$ge?C*O4BoP~YX zZYOx;&dJ*=-y_Tm=b8JG;~k!Zcte)~)?WtO=UaQTJ)N!0ThT)EMaO?{zjXHd+vW3i zT00@H#G@Ac#464f&fwhOBk;(rE787awc68aCA2d9x6r5U>?hH;y0%0=VXxxEuG!Gt zk8O^Z;+gs$_EUVj2|7r2R91`m>+uU?KTvl+$q&?h`no&tCfZQF z&<~u0pWTKZsC^mhf(y`;-Ye@42Fbb!C-c&^!A5aDM;t)wrJ zZYKRBY2t-~=oM1)>nu*4fJTMTztGW{_!juvoZNi9*Y_Um>(vA6Z@>!UE+1e{-o*YO zn=frf4>mJj+D+X}=!V*8V&6_N>HCK9@g{hB$_FeLF35t$DB-0zr+mDyo@D++>SX6l z!G5S6*?RLqbiURLbyK;Iu>g50^KDCJbm!jH+$$;n)9xoU8>u>5 zv5^|5l-FDx#{M-qpVE2kA8Xe=T2ofUkaA?0ae- zu;pt{w&lwPeDY810S6C@p~-$EXhXiV;rD*VCdDUpE)sSzSB&4M!7tlqU5CHyd{+O? zd{*L`+YR1s{9-+S3%k^W4jVp-S!l1l&4bj}B>n%=mt$L@`6c6Q+4cUn<~g>XP5N*1 zoHuxXjg?6rT*mZR_dAQWcS2j~Ya_H}%y;+TN{@9;YYO>Skni?NCl>FVsXc3{2X;2Y z@ApH08>=K2DkE7N$^L2lr8}yrdoH%>^L(eKis!R=p3id&GXHlk^UbNeXd%BfciKi; zyrUpGhsv2H`BCT37(sn3QaYyCzvkGI`{mG9IF4dZ;Zo#c960K1cR%Onr60wV3%{ez zm|g@}7x$OXGN`BEi%;ZY(Bb#%BkrKgWqg-z$VR~j*S9|6yH3AdKjV6uZ@BRtxzcy- zC2QrhRT}XvD$j!iILSVUK3ba*e%7Aac+8iR-ner3Pbw3_)*6XmuY_;?hzyGzo(sdqRS$9Hy4_}uqR&ASP5O!&bjEcol%zkf1P$`_&Pe{TTh)|26b%S_zB8s zUM$#Y%4whaM#^d3!piJYA85nk$y4E_?3#Exi{}r*fOgUkk**~D2x%DmeF(ePTr|2z z%O%;!qaaP?=}f1&!0-3b*@A<&+3Q{UF`>6 z4u7A3&ITjmbjeMs@znhcHuM-{J#~}lvkJ&-ljVW2b?VaX?f?>Qf`&3CERIfrQ z{7BQ*XyjzGr_<(l}evbMuJ9Sdez*!Al=Z_w{w-p%aX>0oaOMWGLtuN%v=!Ij) z?)~fNd-tBnyNfs*d=Y0pUw7|MbFuP_c#-t`#m~lvuBD^>!07k%`F&oreg^)2wta3~ z65Ho`|IY05FY==8eXj<%pVQFQaM84$-$3jC$OCua+8D8oF=7Yrqt3~hwNL9ib5Ca0 zTs*qxyIwBkziVH-))E#&C;8!Pe2iH8(ZjJl!`S{Rv{2da_|&y`Gm_uWF=na1=!uI7j6bPir(`P{$67u#T6IFJ%c0eoUXDadCoAZ>t zhVUK3!@s7qORzz4`oXlFTR8B9KJCEW9oVlgVasK!@_%0teTQeAb*SfgHGjMLdx1QS z1@FK5>Z{eh$v1KHi^H~KJ~w7IJKUehYyx-TZ9EpP@=<1BqvX4MpE05-I;m%z^oKdG z&cYdN?Bf~Isjbw}+;OXixdoUHW$CG4uJtg_Vt%(4n8m>S9Q0|%rhJ3<&FFtlG;!x< z{4+f-;CzkZM-P^>KAD*3@>?wrzit~ck&~fJC*Qz}Htaqs`W}DF`TNrDH^K1bb#5?f zg)i;=HSyQd(LJIGUXaR;X6s56yKEZ=k3}?Y%p#=#Zm#d5Ur=#zR@=AP7ZckcmrC;lP%gY51}K;xV!vzv5rPEIDL zvo)it@ufdfx(=2}W z6QjQAZqcm;_-sDj*5++ytCt;HTRFqWX%ugh+^Q|TOVPHIPwGn^b|4qhHSDYNpRB&? z5AV#!8#sOFdB2{)rGq}`d%l(2>0d~yyZx1~zPOKTORsWjJ|QUd#iBnT%6ejy>e0yWHQj} zd*K+vdxJR~Z-;}q!^7O;Vea-YZ7dX>Iy}EctAE7%?~C`8*SuJ|th%K&^Lsj=QPsKe zngkn6wku!nbq~tcQOLrgAsZ$>h3|LperxdF@y^@6{bvsGbf7Bcsd)GK!{FU#4uf}7 zYTnE{;<sW0D7`R@dBq4FjdgZhY0o24^j(~V#7=J=&39mUw!Kp4czb&e_EzO`d^tV; z9XYXb=lXKzQtltg?XAe|x!7~t%b-~1IgA5>p*gA9o`m#z&Vs$#b8&FqlD*e5KYcg% zY$+W%=d?b}!CLX}bCA`!V~CXyLu%j6{k8C4{+iQgFCz=k@B7Q=bE|$I z#1~fv9a1VA>GrR|ht;00E1|35m;~=t!B{+;Jrx|mkWIxty{Uhv`~E4l{^?#9^tk&1 z{MqHiQKr&A_J;Iotk!)@iWRB<7AtDxJ+Y}?&3UE&nnzo#aHcQYoFGmJtjj1XxtZ?U zxR5q<@7Sf}AG7}e->;8h*HK=vqKn8sf_(H2-P1qzuklakW)m;6!i!_-Vnsa9=I=&+ zZzSbAXi@Z^1yLh^x9Zu)16zm(P9+{VJuy0t(}6GJSJ&gLA?K26Z;sBahOuSFprVbe zt8QdnbtCJlcfQ0oemO7mIBU$>gJLn))vU9MZYwFboN`6ru5qUj+oe?T$LY{U@nC&3 z+uE`3Os`=q&d%%iv19fgm#MDB1NFTg!JV3wr#2MR)0(u-<=Qu`hl9(wuC*QqSNnYx zlhb;U!42~ScQ_cD=PTc0N&1GgVwo?azyGMbeRH~)^8e|}M+vQI0}r2IHn^w{_HAkV zrjOQi>#O56wYn0%rOcY{!|=rF$tP=t53ZoDoE@Dq5zE0!|X1 z>bcCrv~`1!KHnXNKHokJ`e>h)=(CYESJ7wAThQl{Ecz^`&hn$zC2z z(^-AddR0gttzCum$%bR}xgns>uLF7gFg~=_M;|{0{SI_e`TDN4bUw^C{gti`>cd#p z*qPghc;lGbcg6A|due4p0(`g58?^b~g{|@b@k79tZvRj4mR>!02-qQ9^+Psp z*uE*Bd1lyWhZp(otUtP~y>xAUe9(RijT?_5BUVRnWlyy~AUkghbIdRg+wS(dYy7bF zg;vIdFZgj|7tdehIX}Zaf23*BI{qHxxrOvu(&eN!e&}q9?N_(`2Wms(T?;sC?NMXi z%w)V*r0kV=FTci<5br`{G~5Fb$uj$rtS~W_q?b&s{=5c`8UwW! z{b!!-tcZOFOz_lxDf=&EpK`E2YKu|P-Rmxy4bO~(cZk%`U+t^c{ZQ#-Jdc$9D_uS(%p zR^V6u4H_COt^K{JJ>nXpd|cx~VB47m+I{-1%AOsRW&C&dcFM2YfQ%ud-RqFC73kb{ z+PD77zZ3teJpYVVh+Ekr`F_UsyD|{rjBaW6@o0Sae z&YPjTQIN^3dvAU_`;VudTiAc;`;R>9``>)!l}j_|+b~8CQ{u4 zJ)3kKsp*8yPf36Ey?CuXn{G%q)z=7{66O_yo9vA8OpYXHTC@!Nn(izRO0i zOrO(w$NnSRv!Q-y{;mD*TXW;M_Tro)GwpfBnc~hbW!l}nql<_)#%07))e%lAzn^gs zTjYFV!BAV)Zx^2;FSp6x_jDcaBlZllcA;}8QF?MFr}4>*_NDfxpLBJ@KC7=gj99d);^R4tQm@?A<&YJsY9tz=(z}ogbbKZR87wutm?Lr)N2--8Hm+?+`ufpr`J< zn(pbj@$WG`r_+a*UqjclcG$Eylhg7*W`Mq_jlsTSlSCi07rH;lZ1w>2BGJj{sB;sF z^J>4Ei}NO0-Tj9#?c+ONbK$3xBlZC z6rf1^da5})Veit6Gde@Ue7el?rg-~}Z7rM4`e`n9W)}YOEd1k6Y{6FS%Nf9V2XGjl z2A`{&unA3H*qIY`bdUL><7L;H)X%zqw_hSYe7)14t#ZfLghzNUV3yu^hW*# zrpKg2 zXY0qGCH5r!6K`koPP{j|87;N$VtG>wmcai8=4r8SkV{{Vg`PQyj_yxLfBCI(4A!nn z0#9Q~`O(-I#^oEJbqGh#lJTwhcJ&qT_Kf*Er<}If*&dtM3OCk2y5~SQ?WH*#{um7H z(?1k0ld^Ep-DARK>6kcfG67t)UrM&i;Jw%Tj^Tc(@V(koS@B2ysBEOp^k(DQ=y8?o zYwr{LYRW1+-xb5P``fg~%=SJ#&ik=ehP_W=8H;CVKZ#^l_uH&v92?{5Y4YN98d=cT zp!4;HYYVvQ9eRX67yEg{2YYqD9Co8y_scZ{?~U%5Kf}ROdE}7w#F&mVsH447{nUZi z+_4bjru`-5H%$0=9V1B#E*L$ptaYIFXTjP$y-F;QvNbLuOKbi$vQ0Q zOwvb4J4t^)YBsqUTaiK@v~MK^oy}G%-lTH|7FQ&8QKa{p+bgcW4_`xbc-zZx412LQ zA)AkbugaOPtnoOXvDfnIyj@AdOUql~ecDTW8;8O{w#e$p54QW#@a$c_)sw#Oh<-7M z`zgRRp2;>;MSQ~xyj)&_{cpEL4(F(IiNn+2C?-1C-X9Kr=i0aqI$Zxu*N&!NboG!&{{CexR@k=o#V)W z8$J`gth-H=mZpPdT7wYHkL zJ)Yw%3-&Ly`tfKkat>e<5z;b3q%G z9-h82R7x8wX(L~CWqWC(#nUnJw&O(3dFi}_?w!hQ<&M#IzMH(Dp-bgVp4=J-{-N2l zPt!-qzztb=g4cT4zF&E~J`&)y3>}uO*Z9|p4$lWK_7rp{8UG^PzXZIiv*5wb^@i_s zkMEyp+wx?~S{VN{ZZ{zhA3|@PwNK;*V>}}GMx#r zacux(b(bW-wZdHEhso&Qo^)C7(pEz|M%u+Y=!I8P(pv$-#gy~7TM-+a(s;tQdb z!B?A-e~k^_M_!_JGk$j&dS~}&9$0cd?1`cSXE%RPsqH(8UW@g!o;{LN(3O!RVqICp z^Jtzolb%T0PkI)qwP*d|%zoUT);ITO>kY^ce1D(s)1Q5xM$)JLYq96*SLa%yc&KqiYjka$@fq`0msp^X#TQ7_8J>U39jw|cb3UtoJAZ4>g4CzP+&wLq^SHz0FzuD&C`#Ssl*gwFSm!iz2)M?IwD_>i1QEhDWXy@zx)={HGtke2)r|D0!y^C3;E{654~;v~LTa8ge%IBO0P zmZ|q;uEFQ3r%V@RG^emKT5GYnREsY=3wX0A`y6G}f61baIn{yPH~S^KKTC2K&q0d> zTQc2*OuKx>;Mx8?-Ei-vXDwdWXD5gs;kU`xlWxR%z&%`B*x#$WOuR2;_m`M2u6v+5 z--zeG@@4SF%ACI_z5y!_+|5@?0YmqgWXHF4mx$I_RbDY`2b*Wbc&hQ6D)FD>Tc&C2 zTR~gzpsh;!R0I7KkF&K?ja7=r>Hd1nMK>kcubY=kyiR&Kbe3DcSM%XO6 zT43f}9Pf$VH8QVf@cDvcT>sQJYonU6Uu~oq`%{el&(lT~dFo?WPjdc2zmF-YI3Y4E z`8cO7)SWk6|sFR&H{`UB+wd8*bG59|#CR&+F8!2D*5f-U=9J>M?9bJ{o$x-4*L}Te-YZ@!Tc`d> zkCVul&JBh2)W2U-Cp&Kx{gdqIUX7eGc*8lql)v*L=M2VFU`_WiDa8eAd|T`dUDe`o zP&{kIB{5%9K|D+JbUaNj>AAtmCJ!!a_p3)~zE9xC@kYk&TE^RJ;Q86u0c#T-+%A7W zW3v2#Qp#LK8Iy-|Qt#_|9y;*tQfCV#OF3tN7w4qp5UPym^psY-H2EcYb_Vj`1s7_IC1K z`e*l1h5Dg=O45%y+D*{zQM8K=b{~yg?nD22N5tcETXSAidt_cz(OVGZjAT8Ew19LO zX))=3(lMk{UuNz>dN}&O$hWPt&FXXii$n2(sl?eOYe&HELiHVfE0qp>irDH%##qJ4 zEuJ~a!>R|CVh_I$V3{4A%9^^tu)Hbcg*Im?_@4~^@<}%Gd@9c^*dDDbjK}uq+gg&7 z66CWwifxacYZ61xwaGkWqN98LdF=o5WouH$^c)TUm=kTcylV2)2gUVOcKvzNn3uDj z;$=Lnud>=xAz6}cOQ%(K9rGUABCeyo!jjP;`|%QRl&5_OlHU^KwLlPXX{h# zJ#`uOp8EV0@5+e>)^;}REF~>WgLfnGMdRe?fRh+LO`0_P0rdM zo`UG3$XAMVW}q{dBWEcuXSzFKM^0W(MKyB=@DpAx@7!bD7-q0zP9qLvl5pFIJDAt!{c(~Ph*jE zR%<=uPXt$FuFlEiT?f^Z4VBJltoWIie~n$vzeI2T4ZW#g+|FK$IuRUo*1XWav$d#2 zl()8R&5pBH?v66~=vpVzd5_CIPNm=_I>_#6-B)MO-5L`1!Dw(PkV%^HU{*_TJ7C-W#l)CzmM;rJQI42 z1qYSI2Dv+O)Q;}BsU?*^sky?LwG$~fai?_7#rxr@;;tI|ANcEmmrq4ztWUDvTJIW} znBdO)tA8v09`DapzLm>G_x0|btZ#}gY4A6Gq~V9`kNp;Z+?)oS^xYN7PPi8c1)&3|sfwnVgnFVY*%)iuX%pp538CWrV&Wtyu?$2C{i9D4#~B)iVPrEKlI7+3Q_ zm-+HbDX+aH7f{}fX~@u7oTqQ~?{)ulBk#}VeYP&X+8Uo@H~rN&n1qAA!K5{^al#?O zS3q~#X2y4b^lUwFpb^{hJ#35YO*_v&=Xp2jFG)-G7DT@yO_OdVolW{Y={nNik!FwK ziY?^QW*cp4zLrm$@+-`*m!41eg&X}lvxQ0Tb7-6>=TClwbSyW}y`R1jw_|Y%jTzX>_2Qx3>8SXHaMs#} z%Lk5p7vW!C&`{Zrz75i~PrRHwO8VW#jYBqtvUO-4t+=b9XRpfRI+(&L8cTf?~?x7G!3TG1id5c40x*lG*0 z)kfOYz6HsL)l2ZKvn)2Ieb>Vf55+sd8o_uFj;$&$Sy)I}dwv4m2&S_M)PaU&tS9#> zuZA;$)}HKFE_JW=W1!)>&D-X!*yfGc=2O5`b;=co1Rhb+GX2)xZ2jKNJRyAlZg5QV zUb1ZYY0hNJ_Omze9?P@$#rWIG)BgLsxc zQk%oQ;=b<*V7fX6kMYC7^=%a=f;Ln0xQl8swm9s_ncmI3D*u&&=rn#;kX{nVwZ#TY(UDwq#9}4i0`Jfk)==*X z56^Vs6m&xM)1yZBv~#AT8vJBe5GQOg+k=P;vK(7Czb?lhyLPYylVx&sUA=1p6FH>E$Nv`Of9S@+1lXo zmE4K<@(otwCrdXKYn08r&9`Ox`vW>=O7?NuKPg)l-b5Y|w8U^4phsSoOTG_qd7vvaO2uiT+_d z;r9vZWaoVw9A#V7UNg4E+BMtqE%u)BcZuVC%ucQ!6h{%d@8!|)fp5)W5*SIV9gVk=5@-w^xGr0*&teUHm7z~5tR z@boM7`LcVeJBfVF$0kw7+oF5)Y~>A(*)a#x*KMS3BXui>>YmXU)GbNSFXU116+2n& zZHdMQ!7&-=rH|4Zqh}psi0GLy2Fo0#+LZ^0!LHHT$nGrj$ZXf ze*D{?xR<+b+5ZO~s#oLf{2KGQy&i7!KE+nbub}(~DIe16MEX!geWR=VSkbKj84uUl znyGsQbwe9)By~0R+kL@rvVXh}ZRT{3%T#bF1>Oa~i=vM1i?DYaKNH8*G#-4&+ix2W zuH$zzcKu&T7m_X@-AH;Tsm6)BNi(ENNK5+*qOX#g?brSs#oe=G_{Y=spwEs>-6> zEZMj;9XQ_rj`OMg`qZIdL&@)&&1efl|I%Th2FYlYFyQc0_hV@e_?}Yha2?Ll5XdHO!k{2*`q+v2&4 zvn}vV`gL3Qe(-y@it}(+veyaCB{e7hWY8DvhwDo*c2{duqMPX_?~0V4Wbdr({c65H zWBtaDeOk}*!|=(R&C#>$gzTlsc=T#P^rYum1RUUx&{%Nt zJrDI;Ga{*=Cpqco3i-s?M5;<@|H_XS)^|87mZ@(1;A zqVgysSem2hTP^zDmEh~WgF#vO+E1Q8R5mZGtb5OQ3DbLZH(aEN!)v3 z?^RxLnLOVI=}dT@%500R>1oezF7l*3zwPia3UvioiU&0Lc)(a>K{UyZ;}_B1TgCBT z-WJyrEa`^iy=j=(Ko{jg9dC!9fA&0H$Mau#-b4Cdq}C6`SY;!_?`w%hYOC!6#(v~n z_xPLaP3BqmWl4u7p+l3_b@E~0F~9G% zHd#Y*{*Zhw)h(p1&LtNozU;mim5AE8)0Fsmf_tKgO-r}J_>|(}_W)Dp-UUoBLjaOKV!@NSe}izqM5>lu8mIi@F{ds6Bz zWxbj-%rEBmsjM$*y(Wy8|8Q<>D-*m=Fc0+hwy2QjV&<>%yTW?1C10jacHSoZsr;y; z`y~%Aj1l0&l&yPLOq;cD@i^`d@VwS&0G_1*O)FIgIHGA-Pq@}nCp+&}@KrnVwT%XU(|GRv zZRu9N;HQ`Lyf|lQ%ya&0`mUa-;3|1sz#7r)BU%4{o^uK0$^SOEnirqqd>`biS|4uouqA86#s>TFP|yeIGGk8H@+$Vqmvc_z zh?_7^2mh^?-0SA*!e1~o7hml0?+EbU;$c;Qe?@@5=Js_S z|FE9$|1Nd1^LFBkNJeDWTe0gJN7S~?)*qBl{e8YoD^pAvm9_6KFnpA`y4W-Er-Z&p zz7q7Wl>S|cd~LfJ-SP6JdXsqmvhUx*pnuDJTe{z=GU#7D?P=eTbR?{&{@qHQ?7UU< z&*8}2NvUFF;qQ{5pG}-K*1JMqX8IY7+ur_s6P`ALdn5SD{%9U*_Om6x`$mtq?xWJZ zNoFq_RmbBU))U^BQ71cZE_feJeunS2yy)dgv7f2H5lq>mO?>A?^xepJUe*=e+w&6g zGnF=^Q?dcmDL*|Z-{H$Ir~E?7zaV`fugRC2L3+36#nhlr=ledT=u;}_QxmXf_&$a8 z)Tab>vhyydPs9zo^^IDc$IO@y9QS8cvB$3v{C3mte+R#ayk?KzOwuoT{L%q_M|k|I z!LK^NuNnMidi=tA!f*F&2VTz3I|=+mUl)6!%+G}{bZnvB7V?emEqrfAbe~y@u7c+m z!E*+AvwXSPq<48dD+4^cX;biOz*DhPlZ{!xp6&4r>j}@tsgs@eXZed>CRPJ$wTC5{ z$XpoXw&Qr_K!x}ATnBDj$(svKHxYYpB|X{O^F)B#29H}U@M;6x=7L+R$1SWU+`dMg z?7UybJndcsoTokg*73Y9z_H%rSO<;~I9>#fwd5@Xx0}In5$PSCe)|F(m;7Jm-Uq(Q zqssq(@`rL8()L25#*mszNo&+-FErY0i}uo{HcG0oqDH|rT58dvqD74qw*jJ!O4pR) ziWVhWR2ETVMWq!r6|`8XvRm9mq}v<9t!#i8*SJze^L>BjnUhRz1EqfV_xfeDpiQaB_C3jQ6vl{C7^)Kq~*ZVh{ z3E$j1J$DXmxTux2^>ovr2GW-58+=X%pjkLpGG6U|+3LP_#+qjmKbgM%9@4DlU$pnS zY$|;R>ztg|^|WpEwDov*n_QpQM0e@!_HcW>yAL-}jor_D`+U{_o58DjfIpqr-T!KM zQcQdJ!f>pg)UHl2*Qc;IB%)FKK)g_#<}i27WUY-k@eK3$m+f=u`VacNvfe%H-6Ob? zw=ep4cT|tELI3e(ar-;T9D5dgZQy(G(J^~u1NKM@begQL=X;^&$1#snGTiCMqndXy z*fl;(I#KxvUiBgxwGOK=`3=4c`2E56I`*+|K(A;Xy_Gy$y}p5G4;j8=#ETz?$#=K+ zzm5F1F`un=)WaNiiZz1*WY_XBx}F)(s=Ouh9X?$<>5ecr{(Jap>DGA|oud*S`@?lBB<`edF z!nUcbz+2qem8_c)_aJdwX=gp2Z$+%Hh(EOZZx*Gyzo0psMeuTwcuD$Q%oixXUgir< zFkkRp(l44ZJtzE{+gc3lVh`KxVK)G~2iUE^s(yrLjgRXju7|iU5vMwm93JFOLfuVr zEBxL7Z7tx_+}H;h&$p15t7E=7b5gPgeA*kc5E$)^QJmydX_QZ`&*x5^t013Gkk3M7 zsimVR8fF|mlX3X)IYm+G_miXN;cvvO5A99QB-Gm z5LFKI4gz8IxPOm(^0uO=75^gqjqOFzI{Y`|Ux$A?{`cXpUC9|C{QrPIOZYJU1d}_J z{A>Bo;ji=GO}L*1W-;zpaJS=r_A~n3cja#UzalL8T;}-xdvf$g{N=b$;HGi^gxiSw zXWS;-zu+#y{VVPW{}uSx@h?1^Gc35pxVvyC;U2`z{E_{9_-F7RBCmCPUxa@X?jHUx z!LPXu@n3uw|3kj(Jd5O{DOtsuL{;vE@ZH*l^}n5*QBnK0cCNa&Hi-@>v9?P4b^1WM zNnz5H`*0%&+>a$&zG?4S>0d|%Pzao&YY zHWgIm9v4pVncffk4ec{&^YAO6VFmC9P6KcJ`y=_hw`$&@$y%4C%@ons{RKL9QGR#t z5qA{5jypJiv0&rzNeeC;G#-xIXSd*@!Jp3Cc)XZ9Ic@I^bJ7RY|9pdf_&=a+RCgq= z${LQE3uOn`c#i=2)hN_($Syn z?%Nhk&;79|zC%j!oOi2uFY)_1x4KO8?J4!|D)g zK4IO5{ASYVdk)`r-?74UzVW9YjL$6I;m=*Zg*%|$0-rt++|88XZYG2OzK8z^@W!Kh zcqAPb%i{G#n&aQ6`*NWfJy!HG_NR#V%fYF;J@p>-+2C_IkOQ z1rGDm7gmVZnjLgw_DMJuRw+xkqf8yS+f*A!9LUX z@2bCT=-w!atViAbTaX>)uQdV5Dd(vN#Fy)Q8I+&tUgay@TjTSrCeM2Etd-o7PBxxk z^gf_$qBD{k>Q=N$j$}`9+33JtKR5ds+R=XIX3N0;cZ5qmCnKL)LsFk2Ia6BkUwj;u zS?mOb)lgrJ1*}mSKa#7mA&m4@hA~MPrZ%cKfmD`QC;tkP^!vc*dkXHe^II9`W~ zSMQtId1!lob{Kv74)kf}#N_DXxNC4X^WTnt7D9Yj+|6<`|^FiFm5v% zhp4`bXg8YYobp86uG;CJ+a)`co4dod{P+Xb-tNkM%#Yu)q_s8mw;)$L42Job5XQ+C zbM*Gk!glm$4?6PRe~sTHdiRyun|HNa50U;U0GHaf_^JDWM&ptBao_B#R64<}3}~{l z>_743vS6ESS!;Agycr&(v$E8Z|Hll5vV<^J7U|jo_-4BHHp;TNkiFraXMbN9x9tMn z&XSI4@;XLuXi29udL5&=^d`!nv*5Dz)&4Eb{Jb>HL7MqKjrx|tzl-O}ADi{sTu~i* z?Rk6y`yVqnv!i{>LD-`{Yy)x`^33*O|0Z6K?At^9`_XmcrS)UFN8^KpnV#K)ynL=O z*0UYRi@^=^MuGI!uy6Bk6`o9YpY+$S-cOxSU(&xPQVo4Bjy#TD$vq_SKzT^#pCJDZ z&r7Y_h!*MZV&>&7z3K0UJpG*s_|#y0f+pp)@4R{Ooy`8e6X^IB`Q4*6N#%(>F_31= zqPneR%(4L+KSAAE8@d?15Zam-Vrv?m$X12*Q|z5CQC?RfN4h8Q+g{)5K7oDEGBo31 zcs$-Fl3tWP-0to2ZP?|NPWV@=|uD=n@1p|FgT__nxjaKDJ@kgeRiu^gU;XKxa;o#p2tBiyg6wvb3& zeJGSO$)h`SL!JMjZRF^OB^!Z^=?v@KQskF9&GMfNo|Sv}?rl1h9bwiz_e;0TRz7V>)p`)9;HHWYX|J*=dZJHX}Yv-Zo!tiy}!|}^`ZT9!;V+<$ZJ+(~xMN^Fede{$brEURk6Hf|z4s%>yuN5$+#w_u~E$H~JIj$uLlL?xz;Fp6~hmUyi>6cN6~>{NL-j?=dY@zXg9K z?sD8UxT|sZ;eHgi_$24+akU?)691R+tIyLsX?b$>A-Rhn_qV+ ztoQ9{o!SJlAwE#2eG8MEk4Lv{^1KoMP6~#;a1#B;F2(_>``z?o9rR-nyxFThp1fuE zh|hvwhh3zxuIghweuZhyLG|M3)LIHSA3(nBdvct|Ln&{|e+irA2zmTw@5s^bl84~b zUNp!4R_Rh`4(agj1_(X{*B<53>f|| z=i#3c-O~S0(5iB5o56U8eT!@Tx5~jdXp7?$`1gAJ`@p{${NDwC2ly9~b|G&VDxap_ zyK?WMO}qzvA^S`1tBZKir#AVXW$_%UgQXvqn<0(+hIj8Ft-)RotntF-F%{e%uV94t zBz5q657$8*98_M=ja^r=pE%i?s-Ml&-)8ZNxEa`drcX3Swi(+`^M!gxS91-Vof#J| z-gQtn@AtfuZ6dsyd)SX_=XD)^!iHwYRort@m3tR_8qJUKk>Ff@(7TyDrN2d^ z<)2TV^6w%4*IEAP$WeTn(_v+afhQFo0+n_2c4+6pWMP-Rl*Ce z-!d|?MRhAby0K!|=W~L5j*-vB&+bJB+pib46mF~g6jy%Lzf1su=F3%R) zPfc-E?pdUjjA{ONlZV~?r;(B-57%5)m1_p>9N^B{+ufJHuJ8-!>?ED7MdYuIJV^Ws z;%z-8e_iK?#4jV>)`Ifar@l-4V&a!jH(LM9UtjwM@!^_W{+i-fh;JsntwZLom2M=y zk@zN$KYy+G)5K?pxAo=xweU|8Ur)TPpXaaj-%fm*c-tS4zn|eY;%kVn_4$W;Gunu& zB+mA6Vh$Kw>i-*=%hA4{Nyt(X`O`ZEx?|+G z@g2Q=$k;#_KEl0u(jk(m(LHlP9w#I2L<4^rGE_D}9tsn_gTTG$w=pi|5r$vx&+|=` zhn;yygNW{10*Wm-^y-I!}c|_MXyh^koO9!^44@CzaXu0^0rKx@)4|k284P&8&T8@UTAtkF>6(w`;Yhs!)8jc^<8UaL(nUSNeWh`~02)4!hCy z={?v{r-PmOoUy)Nw&dwxKVEg_uop51A=^Ruix2AORYvEZ^4k7gyj5dzC!H@I)Lpu2 zR2Ji~Z2vme!$LUeq%?4<%Tw`GGKsy>_k%QJJ^Veh=JoZ=Sim>u=BW>zMf(nM|IN2_ zAUJoD<`K8g@)UTjZbiSn3niK}ybGnUFLL&^nKG)sFk4FeRGhwtX2ko}?aud!;M4|< z7n}Wk17R7h{icFFrzzT3wd3pExpw+^y>X&(Rf0Prg|CY~Ol?{=dAXlA`sXD3P_Ugp z|8woRi{X^7c-sZPEi6+Q?*Y*rN#)7uxd&emxjl!9S6;%)eHpy*Em@~-fz#cT>gVZG z4k%oA{Ctc2bpG{;hv}<{7i>9kNnG);6nCm*pRgf+&w<*^&)yfeHN72^B2Ka-SF)WV zjo{@Mo>KghG2s%=@3};6W>t2l+7V?_yzH+Wau@GwQN65oa%4K88T)tz|4#nK>lvJl zs!6barJVg9HH-)9^#>O+%oy`P;T0Mm-QmJkT*!H7%G$=*@&}Yv=fFNqxeOlNcA%9p z=)=gve8!+Hz&-I^S4P+V#T(U`?55fp(YMywGG*jhMxH90U~1vLh&khZRTdhVK$*fz3sr-Kz;e+Rtd4YWz_m_Oa3Z{2rWt39xkLAu!X zydFu%^R)xi(_UoqQD0Az$&mlzFKY@VWzZFdtKAkNn?LokDcPzeKgkk(#=sin{deHj zoe|siK-($OpN$Phdvp2+noArG>d3(-k%Nkrhiay=cQ-kCB8SP)dl^mC2k1MPe-bgMI$eZ>$YhSs-)AVBotNbl|mfy#&`wT+( z**;u%B!uCY`tb6+@H!u^H-|&`7yEFxSJcs?v&qwQ-*WghTXbX6kn;&FanOo*H^FOxZ~EAD1qOJ$edSVB9H z-dK#Qcjp%;*+2A4@Wb=3{3XsOOe5GvVC#Xc2ljDbb;i60+N$|WrZtCG6NG6@5%S=+ z0@ikETkW)?^!uY|CExDNd2jEkPY%xrsDBG-5ZznBFIqO^Zibf4$&I;JL5u29bLr9{ zN~b!lyELYYyQSQjQ-%E-b*BEVC-sOsvteO67ctJRNiA^gSombOs*O3D#N!X?5&o>y+WXL&p2ZAT|`Vt=eb9;M6ft=Ib^caPday2n)e4LDQa>=EUg zr5>d_#E;$VAyxh<_Qv+Izf^A@m8c%Up|Yv{IU5UmFF&l&hZ)Wya0=H7aP9@y(hA;Q z@wmeBD&K`Z-_5w2fo%r%*gn>{{*|#c_XXycp&+yNP}F=6_Ul;LPB6C~e(%LLDQE?5 z>3F!IR@uLI=hWYteQ0-SD_rGlq5Vn*XB5Tr&a!PR&g5W5VAK8ECCCBiaqg{Oz}nr9 z9DXMYgy)BehvK(vYojBB42AZm=3Eu0GdD^v9;xr2!+z63`kQ+8psrwV=QR2=&8xRd zo}hP9NR!DXvS@N(@@XE=!2}Zi@mX!LExL(KF>rT!4an3qFT)YA|v=*WxA3hn|yW&X;?f%}H1!Hv^ z@Kd`j!kr%C`-I{|KXs8pFIoESq!%6HrD(1u zy~-)u?ichGqWgcUPF|Q-C(^57eY_*6k8yGP^8pGUks0&)dc)bU# zJC>ggy^PVEj94B{r?jI}y2f;h@*ZlB$3d$5H+s6%*2>W%E1+W>|MPfmZCCpP#s6Z) z2rH6$Tjg%2i)HJsf#*L6%C7qjtscX=R-LW@R(Psahq!8Iu0HWgez&6+)0E5dW&XFq z@>QOqTl-p+?_GsO?p*7dg9`Gxk0sj30br821SiRT&SODJ?z@G$O z^t!uP$VX*c$NDT=rn6C!dw2`EA2w>wwiC8?25a8Q_AKqRZhVp%dzb~Uay8W z&vw$B@M+M6_n+XsO5u>s!2a2y^*yfOk?fb;8hpWvAPd`lZWpHejo2-7t)^;B-drPCWYrq+0DX(Jh*qJ z>3`V=Re)LdS?Og~8{Pi&rNB7c1<(TCsQ3EpHt-gtcV>H(UUwu;VXuXyPj|0IrtZ?5 z*)pGY|BUz@Lyc#JYkz=ihsU)YSl!KjFvOKdOB*mt#Vcfh`gFM1w}p;a-LL$X&{m~; zCPBOMiQ{&Ti(589dd8pQV8ZkX=1@Z1Z9e_XAb-ZA<6w~O0ols7zPk~ob}>HqBJP*? zYn-r&?{DJ&J^#P*-_Q4hxWl;rg_{~-4=JwhM$X`WIl{66S7VFU<4?Vc9Da3nbc4eE z_`}MaJO$0Q$e;1=dH9VN7voQhU(qdYy-4yXxtAQM9gqcgY{jyv?e=>{=I(yM*;t<^L(h9IdRkE+Buc z@n*eEDqg7^Hg>S_#Rg!ySj+8VopwIs1>wCV8hKoJ6ZA>4<=%cc9>Z8#@mG2O6XPt| zXj<#t3y#C!_&ICgE+6cQuE4I?m5l9*Uic8k?<V|`WQ#<1*hf63l*;?`WqpS9O#P}6$-T2?kTXU?;$2`@@rm0hJhuGpnT@Tirss`O}r z^5{&u?w3#E*Lu~RMc5EY?t&n^jIgyc)wjiO5Q+xNL2N^eQ@^U2owNG&ob)RHk1@a;PaN)c@ z=II9(1vm_*sGPAz`O3b+1bY?e7i0&`0q!pPY~9Hz``Fn^luP!p;=*$3K2iN;Q>ngh zhXyOJ?)7g4{yf5UmWuiNfzF>4MQ#4S7}x$S`1AjbgxP-s{vQ6qEg2VX?8*W6JtzKF z8Qph%{7oMgf7iv|x__#Naa<4Q2YWdGcGH8L|0GOfyRXqUHxTwV!mzCd))TfKxK6^} zOc?xf?=uy&UyqHv^U&~WY=>X)z9Dz!>)!l*&{NnNWs8;;aCcB_yKC(0-f>Y}hPdW} zY;pSaS=r5#(hp|U_Z)m~{MJgCM(tYn|7tv`zK}kYw)_Zpf3W8|%Jvsd&Gsu?{qYj| zx}x+0*~Q>e-RnIedoK)KbieLBjbXd8@mn@u$hHesZ`lB^JCbCh6wb=_>-$;yMrI19 za!$wRvCQY8ep`9$o9t+!%{-FL&m(-R=Ah3*x?c67eLl)#Au=I*r5Svoe0&dE=-|(b zqE`N0{L}m!A1;b^@Gs}z{j;KI;hv(X`Jtld1pgHO#k-55hM)2_3$VpJD6R4;FSU;X z-`c6F%C!dDS{|)>%Iv=HdhV=Yz<7e1rUE8M3nk;<%G_+5YE-Yv2|7W?s z?KHGcB=@)E!$bf7S8|W;b29w;)8sjhm;VF$!1C1jj}Z6Vpq#@OF6dLe_AnkWc+IVi zgGUFB@&z3q>yMpo?4fo!<23N<-`ex$+Vmcp(=8L=l1;G;8*v%y84IJ0x$A!%+Z47| z)JWKMgvq8@2;4&8>IiEjOg4q<3z-YoxI*_1#^1~r5NGzT#%Dc@)4u7)WkdLDqeS#w zzL(%$iCMG>_bS}|xYyv?o&viUD+&GtWtSamV+FId%dm}YP3CfJ8s%;K6kY``8w=W9 zN56jz`?N@72If=2DOdBSEtihRt#L>v_rho#(t+E-oiQEU8MBqWAWBorUQW3hQ!FK3 z<3r)lT*WWhgD4wTb6>Z-)4})2##s#gvNI=PXQuJno;JY?wi#ISf9zc&M<3IibV~L; z_ATRpmZIso&;ECePk7!iDJuDgUGeu4pTEX^EzA=x0l&t2jB!gum%?R(E4*d)Do3k> z^=m2@GS=D3`cfhG$7b$c>6*g%YU%&(30Z&*A%%feambM_k9UVV4FnRFB{GO$uHz^h03He`3j4on%-i)fgWexx(tC z!LOY%4^`0s9pO4JwsA>12ot}1{5sbmc)XN#t`^T@lP%SG1NEyiXP{MXgD;z4jE2Jj z4UL`##xfQ2!H;d&-^@B%68stPJ2?V}!;Sk(bdk|-Ykoa}jE}A*`8f}R4d+iKGxspp zp*YQ5J_+VD>p7akn<$;l-^BM5vNm)Y92z^>TvIo+JHMY3-Qd3^_Zn*%ZH+VajhQwaxE+ z0J@yt)A>ICokIUBT>Aqw@1XK`kXCt%&vkiucZeqDD>{Mc^e~Kl@3-WuZ$@Z0K3>n`foQYx!&R~Py3?Rjb3Iu7vc z5pnPn#TTY$#_Q2nbob5Vuk{$U!71og}h zf0S@XC;0A!FCoo$@Gah&9VLFJy*vz#ucu92NV{n{pE6Ot#m|n8YK~B_r^@#`KHqmv zkZ*>3#oMbn$Fhk%&NupejW;T<_9RWF{`79TY=av9lBbVwKJ!U5Len(?&%gG2-rSjh z=fdme{ycvd!{1+f{+@)t2jN$zr~74|?gww}8GI@0w?Dac+hFofzZ+!#$^pUsgNLj0 za1m!}X!DsbXRUG^50F zK_Yq|;kWSDn%W0&8wvX`?kfJe`@D_s4gA;gy&v};!4xK<`*1hl?!+C!WtuW-;L_O5 z{2#-=3->VpL~$beE8l6{a~TZ2@9l42T93aLe;4i^{;$JdJ}D8s8CPSz(sUK~@Qm%L zujG3nYw^Dc*5Q}DoqOiA4(rcMFDZ!cg9hu9o*sTZ|_+1h_d z`%s!j-)C&`_T{K7M`P`Jg|FK->Q?LT-Oy-#;bT>_^UMQ%lh7G@AGR3x&LYbS6%8^;WU3O7hZseRczRYJGS;dH%-aiL;ZtX;;nc zQ|QGN9qwL~TX~7+){EVp9M#}<_)_s&kXys{^ml{nKRqs;@7GyO?S0VNv;3x;SXUXf zt+elDHN3L&3SKfSSsaancIo1M;IMj;eWq{0AESLqPik%2;<4Sg>WWo1ycl zl{v`IXprm+SAkzAU;sZ#Z!Po+Za($u)^VWiu=q_Gda3VTU*E_^g>dMN!~FZCFZt2P z(SJiv&hOPdPOOU*#5!7MQ<}?TzvRTNcc5#91KUwHO+u+y4 zHtU-{Ot2eft>WA@cV2lNto5=^|E>OG33*8Fe#)5Xv(_&F+mhnF1$2Cd{15bU7p>1v z;;XQlyq|Rc_3m!G$KIr-Qu3O%>R6J`% zHk0t->B{C_Wa)9{h>YiwVaet~a5>qe{#%pmn*mPacex#l5ctLyJ=G$uz^>f_n z2R>;(jI~YP%5{BSe&1z$Gn-Fi`5|bK><=faa>MLfxDK9#wp(#QRW7GDDUj_&0q@QB zzQNnM;&rR}y^Xxkf7{$p3*TxZ4i96LfB9XkBb4?v#Ay$M^sLc2?v5#R?Sr?T9PGOM zS%dqaBSoEROuG-aD7q?pExa&0vJJelCHBK(*@>fjeAsVf*$NZ9X8hx%GC@W5DN^?YUl0Ne-;OGzVw+o@&gaIjCyS zCv4b?N@$cGDu>2m^868bsy=NzajLU`mY@0v(WtY4qjKQ;HIqs85mq-ezY0rh^rHvd zSsj|I+vin-ED+}IZ<4>p)BEP2oNn#~y}t;%{hNzonKN33UqoO0D(nGhn}mKUOs>uD zIA3Fmd$QZ9|KIxhFHWt^P9iLd7}q43GoH$xhAQ?n6vTZH@FvV_hfsa<98;9#2qC4o4vgK{)Qku_sS^l z^*-(xaVH5Ih9}kjPKp$+c&d5N^~`%ZepK=tp1g?CE4Oy=TO;HpW2^v5L|ZlKy$`(h?6eS+;o25sJEJa z_QRMr;z!xud54BNzkj51aw59Um)F*QRHhVVs-{de?01l@B%Q&0ruO7LB>yq?iXNNB zpZ0gp53rTlyOID-be7}lTXWu^ekk_e(huL=xz-y9r+q)9wxTq`m&8rrO0FcAhkP4y z`RZOE+Ik^$A4d18T}lp=MmS1wQ|JrTHSc#F(s^T@D=mcv$w-oUymI!B>s$QMo*D5+ zWBZ!a^xW53*ZEu1ZX|6f`HCLVp*>wmzBRvez?}t%%e06xseIyzbhz?!?-kPD$sb`a zSQ&5Pyy%(Iew)z_k90Q1l>=Hre6M{P_-+OtvNT|IM*YQm;lZi7HN!8z>ZT0W>aB?W zQG4C=t@&uhi5LI3_49HoiZ){+n7&vdNIS93C?RR z=6fg;?@?8|k!)JKiT!cA=>%ukZUkHI^EruZAjSR*9tPXIuN^$J-Ts$P?-{&yO3&akvFW8Vot}bE%#oIuAHCrHj2r9Xwa#YT=4$?B ztWh!teLH*16atmAB*m z;O)4n-1m6`dnjVAWa`b+NAL7RZym^DA8Bu$=F;~IhW4ub_so=kRc?2X?pn?~oDZ%Z z4<}hr8N28LtVFx>uRf0K477n;cWh^{%O0T|i#(iYyY^<~ zq4XCIwgl;$eEN3ypf;+ykS&KD)Zgr7pb}T}m)f&goL=SHwc2uVlJiVS$JP^K0a5eL-deGV2QF%(O%DQmY&Q>!X)cTTSOf(55jBEFCqFeIy|AJ2ONBK$L zX?@Z1ov1%a0%QHjGWrvJt3QzrvHnEok}6oIae6Y)S7(}b3TZ>Wj`9(niO1sae)#(W z>Q8kn`&hKMdD?4nMSBf#>I>C>t1k@mZ#xa`N13nxThksSt@?k-gy^V+UbR!xwJR8_ zYhFV4ZHUh=M-OP8`)c&0<_t8KuJ_~jq7RqpTWyTCLwOFk?*(@qUufa-{CD&J0Xls@ z?gO}siErlL!v6?izaXp?KNmwp7ojIK4TgeO%(Y`{Ev3Dxyw0AZ zEoq;R*>=()Iu9OxXY#(SzNFuzf7LIAvJl#Ej9>aFl>3SPL?RItVbH6*Ru+}P?7NBf z_K4S#;c{qJIdsli_cy$ZatOcmf!*Mq=5e1Jz3@K`=Um>2ET%6kt%1Bz}Usk4jzc%JRj=V5#apTU?x_R`!pKx|u0e68UbPtH`195GMetA0U zqu8Yd+%F>AO?QmQX4AXyZQ!XV5BfXCEqb#k&QEQjJSG3C+$bL#zkj53?;VGp!@N+( zM@pg<{2Om8iP~}7sHYR~xD9`rF-9q4jAy~yX5NEuW~`=ue<|zNZj6cjDqhr6o>tbI zWe?BCmEHAP#(i!~OWn=#JaT%PvM9|Y_?N_A7RB>Zf|pF43#|FCrL3jk6W_|c?$O-a zN$d*EaoKzAgt_zoj=#uImUzj~!Dqy8fqZQTcbEogG#^HD8rVgc_|*&CHteH&fU~?8 zx5jr47=41@;NjN;uXXWH1E0q3P#qLR-TmK%cC{6wV;S(GV=3_Sf&T>X8N#yAE*`uM zx~Y@?-%(E5qHAYvJ`{X~@Zv^j$guwP;H`ggb5!alq}Qu2;ymK{KjIwds_c(0>h9ag zSWJ6E)Tb#v5yoHW<5O27)x&)! z3RpWsw@Hu7o^yQxuIvNphjXFT?5ha-st2B`{W!iS$LDi8JSy`s?3-iQH`}pqJ`Wzf z8~CEe$kn0x*^IyIZ5}cw`>K$-)Eg$9z?TaingrJXoaz&OU*YIb{X**opJqL2=96Yh zkY+wQo#HmfU!K@LLSRJ+*0857RWZ>^wx>+ z>#dW+gnh%O?eX|0fxpSaGy>BL%$EZg!+$OKYdu^IaO;8F09?w4RT9=g*r$Ektsehz z=6;Gj%sVM}D=_~Uz!?5Zz@Mf65v>QO#`n{Ac(`4@?Pxwsb*69e=2Pf*CtIZ1#XV{A z_vXRsTd?X=47S(9?#P4Hw_tAzV0U=f-FdM37VNSBcDG<@FPXtOuXbc!{Uqb33WcBJ z!zH_v(~a8}V?(6Xj+62J*qh;lqtmySee_v{ly7fxW3HM$?Mir}KC5)^yhCZ`iWW`6 zCSpyy1y^e)C+VkN>1}~(9yxs#{s!FZaa(a)aDV)*FW&jF&d$yrz8CSGDM>_&aW~;E z$K8+nVcgOwiD+m1O{U_WLxa)DtN)yp%aBFez#%;j;9BwP_ z=W*L`*YN)${>``#;OcIe!cV;Lcs2d%H118)-HPjrxXYLM#QB;V#V*aDN9WVmYR$2k z{#*Tv*=pJ^b*ebobrz@fpo!9j^`U$01aDylpBW`eNp3Boq{Fq2RE@e1J*U*}KN*nocT_{h=u1>BSB z-M1X&8yLGoj`f2QeP<%l;EE5r7fy2Bp6u@5jvOo8_$d0SgK|`}rda6vSUb0=w3eoJ z=IY#j-q_XJrG?r0LLu~h=I_UBOp8x*_sXcBwQ}vf)IHBSuc&dyel~3!=ARhhjG1@k z*Z(BnRnXXgoT=|jc|Uta2WEIbhRMK%-cR`l=0KlprTQxL6>r(L&`%f+wNb^X+$Wi% zXrPT3Bv&8OIlNTw+(W78t%uHk4Ql`kKhih5`6GSP<}#<;ziBx`1NlL{)R*KcoyZZr7ek5#W>k8M?>u>~Hg-u3R9T+IQRuF{@4^J{;X zyHkRDB!;!`hjW{ofz{c{tx6Mh_g_N)0=%QMoBl=Z)6(yqu`;)B*2-~V`w2U!Fz$#} z-dXlZglUcte=LX}Qan5e?M#xcM-;zf@sr_xbu*I#=m5=eZcUUxd2| zw^jJ8O>mynohilD`PGx0oz?l-Vg3>4M|KI8^8eP4lL~5imx{9x(oK&q)HsB?2S(>2 zB&&8V`T59CvZy%97RLW$xb{DRKmT70to^I;*YlT*3b)SBT0VMPvD}9X5xE9gV69}Jk+4SSO%awMOmaPkusMX45_W}ThO+F2FOugP${~5KCjJ=b zHLme8C;geCeF{!{*TM6Ao$U~81Z*#`mjk=ipG~Nf-GI)MPCrbT;Ib9)2)HzG9eHqC z*D<*b^T7tbTxT(kfKN8I@P#sH8)QNO4?K$b^&pli~! z<+}ihvz;_+d>WnCU&?p8|CYXK<$Ia`md&z{xt@HSy@Z80Yf01K(_l^yRPtT#zgZ_4 zDCN7>e;Y4!XS?)-=8hD0^LeoymjE_Oa-S7-sPO6M#^J*k$KhLj_`5uO)`x$Y@cq!W z7a5SFrUyO{lyl!SlW7wSA0@H2)-cwTtV4HUB5*voF&71GKRM z`h%IuAG^+esm_>gxq}kiYu; z6towjD=VLgTrw|}lT4D&WmBdf-*pc6*!uWl;+u$nl=yn`lfE;3dyH?{U`L+8olU^( z3*=h5&fxZVxZS`-z&#Ye$;LIftsZVOaQm6p=?&oQ9a-t+H6E@LxE|oX1KhV|>p*{p z-}}`H-#_{Nk)v;f$7;{5z78JdjavDOk^7~<$gi_Y%kVGtFx)71%ebT)16D)x35ubvKn_AH3o?F#s_ zBH&N>cGn!{q@E3bE`JLCBq-C<<!%r~hV#pJJXv-VC`Cb5w@=&tWPJvfy2Y9BK{x zQsR{Fd&p0DySq?{sJ==) zKOCQgi@FKJbaqE!Cw*NgU07cVxB5EaVZ`ebL4AF}*B7c`pmAnZuJQaT_Tp6KZrT?2 zq3Z8d_o`pL=OsHv`q$0H(4VXRuBl?5rJtiHC*Ejqc13iH_T5&v8&&K7 zw{W*Xht92T(cZLATs)|7jg{Z$!_T_4b5OJ`zjgI-*Vj))-o4D(yqj?94ahcc-`N?` zcFM7cFdNsckPYEsYQ0VJL11)F)r|+S3qMW%pJTuF-Bt4reTFyqWaEI}y)pSIPxI$Q zBV~P;=S7+4MG{`jWn88G5{f^OY9bxHjPP4A-K?!I?&)tA8FiyLr78AlUgFazFNJT-OY;HJXg*bXMDwY} zC+xIQJ~7W-5rmh7^RJF?(5!n79bZz6QQ?a#H{mA<7w!ylZ}L(-gS`gGiGA1dT@!rQ z@tx+o)z=exoOSki%wqe5>d=WP^p!Rt>HJy`cE+2$ogw=}`&zWmMSEOy58%5g_cM@h z;SBe2y@Gv{2Z?|CDdOi5zb`Sb@9$3$e;x5{Y3@k%a{M#ut;NgQF8p1oD%Y>-j_~!P z{#Ci|!2b&NRUXk<*#*xXOfxnco!|F+&2+Zs9NL=hs$R!P|1DN(;OnDYCddGM>Fdxo?F+6&cS9=K71G6lE(R`te zO-0WVVA_DuzH^;N*8GTgtGB0gw&zB|rQ-$LLRbr7t%OORE3ARAM#2{OJak^EH!qK4 zQ<)8L8UH0dt=5B*(5wBwucp3Ve(Pm}^HlG*K6g-Zkpy2Y zX;zTtLZ3!`S~-23_*6~YBI2eK*E);(M(*Qz68!8#Yo5V8GBEAH{P8t1Evm`g~03pX8bx)ygp>>Me0+WeZx9Zz0(m{?3`@B&OsN0Tm6OgGppgB_Vu|i z=IlR@&eQyvof}vBdfzsi&?#-qvB?gR9V&eKR=;p5=SK45>Yc8^hSx9i$&8%#ju*|9 ztNopO(Lek7AK^d5Kbn?^lKji~r})?KPxG(m-@w0_e=Gk*{Fm}y!M~ILdj6aFZ|A?8 z|3Us5(@CFw3jKDB@AbH;^BEK3Hsd~kE1jn^x<~ll&G(wJMD#OUjk|QF*OiU6m<+l~ zb59M)D)wAdQeXc%VDT)h_Iu4>sZRb;ew4crOqS0$A}_P|bf{v}waT#>%@o)v?v5#=oUDtO;j%uA+xkmd++ zx*z-bq|uzPt5eE$lJrU&O-1JxROP-(yw&#w#ZkpZ`r0>2CKyi$pWe}aRzLe@FXXLV zA8vNhs7`gZZG6Vc!DLz65G-<2oSv5JDk{w;aNQlty=$ERuGDJguGYAHb|+G=!v1)b zn-A1k(+>st*!~TtyMufj4EZ?M<+Il1qj8w!BY&9BQ0nLCvY)#&>gQEwvQ2c>SN4X^ z`f8p1c$Mq`)-ZeN8+uE#J6wE$JEy4pejC^$R!)WQkFM&IP7==QT`|rSwvomgR|U3~ z%5)9zjt1813wtAX2C*nzFzyVZY#hmg!Rj1+VQ+h1t;4;tuO`4fW_vx*$6CgKrE&WP zz+dOm6lBvr&DDAIEJ1gxUCZv2EwTh#r1H`#w>RA3a^>pE?s2^MLUuR2nlnNCJ|DlA z_&@kEw!A*J!Tw2h)`E)%wVy4&|8DnTI=@ngj{UvTlBe{y?pslNtfpU?PoMDDZ~8k- zh`0MkUZrzRPX2E=bOaq{vT1XjM~JiY>z{@m*~!O%JEk<0dp3MNiLQJ)^OHxWGM7~_ zJ@<+oWBs<~d1ccYEkoon1f1?+ak3%XBRw~(aN{dCh_ElX|4^fcvAa^#FR0x0Q{#T1 zmvU=P(#?C)PE;nfQ_&#(Eq5?F?1LVO4@qIVkzj0s>@~SzXJ;pCs%y&F?``u5Z8bmTYjbSKM7Pc!o1Xq7xj_w9P)82v-3U88dZ zZVwi)g)@+2=Be1L61d%ciWOu*Iqc_gW$y0W1b`E350?v8YqmQJE z#`M=7N0a)o#O!!IN_|2?b_nyzwm`S$J@1^{#_dXc4*B~fgF)_Q zdHzPTgZexJ{x$-yeza*C^G}6UxnK2+9JRJ#^$?BTeKfbv(U7^iuNdBO?u)vn4~91r zo^R)D{K4~eKJ1?Ce)OpDivBonMvfVZQIf@Ov9!rV9lp{Utoog>vtB!;^KP;D(tu{4)D>PJrjoH^>vcxwenJ zd&pBc`Ac8TM}Pf}HLZ`p3(+B0b0BhKS&HlLi{~-_8ZrM$TRIUn#qq%3P5akeQA=G_ zt^u8=Gl7fn=j#lIHxuiOW3L@+bJ7|4d8s`qpAK+qoW4!C(M3BZz&rWY7Y~NGf0>8- z8p@%zX!;0i{C>etq>l=aGmR%SS8D03y`e3)C|%5xxV;51=#)pYN-vv2>5aeG%q4I1 z>15|TpKq)CR`j}PQ#gIZ_;A{+rDI=`bY=njC@vqDHh|GRBZsj`bl+5#wsfDrv*C%V zSkHYp;7cL=l?;6hx`x0ZS9DDBcQM6$h|94To5cD$!Z{cehwIHKz2Q+qo2ZfOB1;bw ze-7{r4-Sz$7szj*nBgI&w#9XGl!gzhKPcGvN)Xr< zeA55Yr++eR%!i4}r1PgnXRX;o{=Owf;n((jv?N-?e+q+x(r0mZ2JR7To-1%)#b35h znsC`aT6g_z;tg}3O#dTsJp}dko8_Ftdj7KpHLvkxdOdtzy@hF1uM?F=byU1JZZD(z zbJ43Bvst@m@6zSlv2)7(n3^$g$!@+Y{WnAVlKnnj=P;1FiX)0=Pr2-)6mVLTd4^x7 z*zo{%2KI~fUAv|RdpT-izn!OddW?Pvz2^3EESt)m3h0k%zm5Au?VZ(rF4x}s;LmsIQo#EouH1Ui@%7=dl8t#b^n6(Mmn594XdG&9ApjTam8_Iu)mP)xz%~ zzI(yD*^jkSlxr{FoBX%dNs`+}j^0UsCR;}NewTZRP6YR{mtsSRcS+o2b(Oor{@cte z3HI6kty`s6KW}p~Ar7Ve_RG-kq-`#%%1y_1nh!2F&*^y5v;VW=N(^fs%F8MJ5oDe>q{LH3yo&wP)k89dhn z^B^aQe=0otu?Ng%;=YFapAug5`+OJrd|Q0Jqx2JxE^{(>I$rD_#kDrO$l1OQAUhUuJ(YadLDutY*)p}_> z7Q|%@#f=G>-_V|att06SPOEQsD%;|T>N&YGYc`|Fmipu7;#hBQwnzTTA#nV~+mg!9%Br&&Mb^_n()$PM_B3F!{|-7t8C~>^h%k7wKLR@Imq_T4gI}4sO5KbE9}XZ^g@# zeva~luUktq;^$(dBNuzwPyXH0m;J?~vcKBN{_ya~fZfS3m3srOm@?(~5O?hik%#Vw zlOERiNd5hN=uqj>6EE4A>tSzo&oejXUi)p=f4Mugr2muPm#aGkbcep~AJ`*!^sMb? zWFEIfI1|M61aXT}?1`W*wBNdg{7Z>nh%20(z;$?CT~GQpzB`$FkzST9E<3#l+t0bu z&+xRAwA!MFN0{(te0tR% z`!?JjHr2WAD6qQ39w^z$_fzpocW|jLnOC}h#TmfsoU-7VJ9wz+Dd5Hb8o~H_#1=2n zUHeam*Nxy1AC#}^w9)fgJX=4L^XRPA_p-MBE_kN87Hp%3RU5g2_4{AC_1t(Kc_#hD zI`(X<-PC)zrynlSeNPrv5Ow#ftR+-j@Z_g#o>R!#o87wX^0 zpZl^0)V^g~Rx&oqPZQoJP)eGglIA0RudnU|QTtBeDi58%V(#7DRVNzYxASXE!~9E$ z=D*brl}_@mbhO6-)u+P5FNLvo?qtTb3Ht4_r15et@1t^;5ipi7w(-Cc<}yUT*7z5* z?$Qm75xPTnA-Qof?O+OfJDP!O_xvk|KiZ?za zc)!vryo0>fc-pJQ+d$q%%N*Q82LInA{)Tb7GweM0B0gLAIefnc{HhZR8wbOj^AzNB zGIxkgl&9`56OIfxLVaSmrsUBS=B+rDaA-b9auK=T z+j<+BN9bg;>D#z$aS{HnVvuday$^Q}{~`R3;C*Mp3)sFrm>%dHufOpqkRwL!)b4z;97yx8tC_ddj_<)eu()O zD?^fV^XF0Dw=dXueA2BS9le9=1LsCZli=9v`NA4uiR9wL9!~pO5*Koo68yuou`1fw zVfKHvtDPm|Gge1mtv9D*UNaX~F-{MFOZVS}vLso)(c{;>IfB=CK`_!c?oPu6+XolU z+jzW~JNM$WlLmA1whzAHijBvM7IX|Aj^-UYYr!4F-*H^)!qTJS+oBm8bGJd)=Ih}D z{8Cw7X?cO)a9`tb$F#<{d0XwYxDy`to!}M^ijeIZ%8s7tJDi;7Y_%iI(>=j`>ss@) zzC!YXJeBBPtFf@S|4{u%PpL21N8RhLGA9Fa^ZJu6(yOl5B+p?qbRq|s7u~rLhDmmHzeXvvJ@eP&aT_0Nz^S{zDxqNjoFN~#Q~ox}F5UDYT)i*yVO--w z4P~mKOtqA$13W5AJ9u>W+`s%Y_va+J8<4!e&)u)J&|>F!HK(Gr4%r?pq*L9iz8gum zlXTUzfg0c&peu#UAAJpBUM^R?DwfM?FPFL_=C#OWNcUdoUg_!1;=djL5!_prynU(B zZuKD^m9ai7S;mGcSx=tpp=CXI|8X<+GW6|0hZIb4cE|yha|bqf!4Cw-XmuMWbz9p=t^nrt7V9N+O}lN{Ghr=JPx z#O~E&T;Bg|@e2B6AB!Ge#)@vr*q!7Ly|07bBykD!RT=yJOk$Y1^E92KCFW!gsptX$k7*n&DE`J?g5uxvWf0# z_W3oDz8u(JkzQ-0$}6##dq}yPX&d$O@4miz@&5|{e%xQ<4&l~RCZa?5Gq}IOUG(l7 zmp0-b#Bc3VcOgknx^@|~3;R}nkNAGMJTG3~ivO99xA|v zUgv^MFDSjf75^L^} z4gFc^~IFXM#NE1bI4tUY-ie&(nnkc?$2T^2|(-XC}zg`SbErSbm-^EXY%MWk(MG zb-dmv?s;J@qK_*J+K}oZ0;k@+DB_KA`<4v4F!ER3CF$37mv&)l)uAZ$uc|j^4+y@P z^y>Ggv3JencBF2no5NEZEl=M-U&Q_*A8%+p_Tq*mUT^MDN%`iDt z#MuJ!^8A#3jP`!wP;t-a4(sk&(}|&ekc^5R(V+9;%t0M+e%4krua>0G6`!_aS89x+ zI4gtZcog?dAE!DLAFO=J-)yOTzFJwtKfyb?C*aXe_)(B9a`JC5;`=B+&_%B7N$LXn z>1V%*z5RuqZVq|XIQ~SbTMkVI*W^?o_4c;wRbQL|cCwext9*K;OJm;N<#?qzLZe%< zp}Nbj6Y(Rg6Qf^rVpnV#{Z8F5T5r>xO=+(Oz z)$rEpnE8eQ(G=>=L`=&=x_ zOg~?rE^@M}IuU&4aKW$1<0y!e-hkE8_J*Au66Jy4S(vAQ*!n2XAWs~pSomSe9~O5 zT+y}M@$2qfRptiT!t~ru*UlLOQb+4O{YB}9tmZ#OztyYeBlMPoXj5E)$0NV&IK}I& zm^8F@fu|aJstJqdp5wdn#RJvTJ(oD$lHEzYXbv?FFLJU@nM+g$5p|$A@kRDc9k`UY z?vK~r87r64mZt9J>~bM%7TWW1w>!@emy0=DpH94R`6S%^qW7P~9#xpq+(tdlVopA; zgVl%Xm4-EmT4Y2vuK1@roSVg;WUMcA-swvAR*651;avN?nz}Ax{ji*Rbo0mXOMH>O z5noRWn9 z?Oow2lj0hKxEkUlXEjQfV!t!4+V8aU*$X8@BZ-KwvPs4vkfJt1195602z_4X*8N#(WDl|f~xC7sb%3(eEeFZO*svZHw6wDiUU z!HQOkQ~srdx%LR12cgsIhDqfE@(YK`^h@Y8xlmd2`4{R2%?l=IOKJyh&q7!aCI{4q z;v`qAkk8`e-Pi^zvqPzW?bH1MVS5uE(P46E^yZ*f?@dVGNl!{gTDmker@^gw@kHN} zFUeur(X}e;*8bq9;*5Vvlh04Z8!y6o60GvPZ+= zNQQNKRkn^i>O7uFtOJ@Ky|EL%KXOa@w~y1xr~}R#0xMjr(R;=4Su&yfUt}Arjml;g zZ*NJCJf08tUhL~0_RPu<;w%es$@Z9hbCY`eIY#|ogv+N z5pbllbsgvC+ljU4*!(xi)K4qVJCVWC^sk-&)%1aCvueNBg45cq%3wNCdfvjN|18|~ zRnR1v5b%C&e}8I0pJY6Ym;MN0b_6h{<5Umg zqw3*X)Wemk2hTsXW#d!GzaMxyB)=j5dS;C;`wxP$|Me7Q60V+EV>}wdKM{|FJ1kpR zt{(*DGT!8uy=TT4eh~0G=>#i$3KMRlX%1mVtLD07`^o-RA4<4OZ?L9|MSlnz_k*iO@tU2fIeXQe z+t&kb=}GxT+xt@Jd*)D-&(&q|JlsU-T>bez;Jsm*@>8dAb0&FdRECN07pedEu;P!= zr81q4E;mP#mxoJp8oDM0d0AORS16;BGqul~kl2Kps0Wpa_1ke+;bG1zhsYj`KDmq1=bo7Ic*IbFR>Brh(_ zY3k)~l^5~H=u(+ZN7qF1;?kUkuBRg}A3TLlvw5G_KgQdoyel+k>XclDJ6;+%yVAhE zMz!Te)j596f2%K*e~$KxWBbXk^&`zI6(u<{T)>^Ke~#x0)Xytk>uTy-q{kdiU`KHi zE_r5dX-hZv+AF`S!B+sTb=+-#?=>Sw<1|{QTAh2e$lH|YxgzwOYzmE^?kk8&{*Aa| zPs1yrfpZIvo@?<7pJ;3Fv^7Ipp%1&#he;P2+~q!OK4G#=q*voPiWtvqA8-6P(=zVh zj{C5T+s@sX`%qpPH&G7pZI1WxrEzmRH)3cKCU&5E+T<9_X6#T}Agr{>M z=`5_shqV%Bb@tak#dWrbut^^06FzJ)VX_0%FKB`R-uO05C;%OA)s*}Gr_RsdbUIZR@&NP2cLiv%GaJY0? z%{_MapZ_wsS2O3xTe4a&?7f3B-H~enk7Qo-za4%r^kvn2zoRFL>8XUSR>6{9ywQ5& z5NqsO>oXj`A^i>TrJeifB|8hj+g{1KBjuQc>?|ZsI3+Kdr@b(WKCb$b{?s0Gi=(a` zKUe#DTSC0WX{^5lyc+w3Vb`PI4XzU2ODDaj9q^&-`$seZJCB4@*S{{fXe5pECki@n6E-^ilfG+(`A0`bX-D zki}Kl;Jiz~-Sy&~>~GnWhF@i>49cYZRVIb0Orl#hmzC+~(xdQJy2AJ*y2W?hTly)= zc<~KvfMj+%*IB4mfSmg%y#}S z{1e&EN+Y^UNgvym@z^0V!|^kF?}f2#th`iyH=Ya1ueluVS9Wz;1AR>aeKi3eMPDuP zVH>x0UF+-E;`8m)WuCWc>-jj<2A3%xY>HaB)UWVZnKTAcKU0V<3T?{_WtMHJysb}G zdiBYtODi52Z_Flh>FAeFmrgcAema#!_{2BSX)^V7c&c^&?s{Z2iuHo&{%-E7FKs~P z#dF&G1)LQ(J$~mCV}0B|;ebCR%_cYB)&&yeV_i7(D zn=s?eD}7i8cr5H1AJ$2j^?Ml~wuUh4_pb0^U4*Hv*W-r$-laZX{a!J;dS2Dlhw4Q? zJXhbReox;jhtk$l*Ij&Dzi0H!@Oah%V_}s(Y&~H{z8O=KM7ML7E!c_(W+GCjKd3 zjVY|H{0D7Ca&xBr`5N&2PxR;Q;IaPveekw}Z}DfM{=8GL^xvv;_2=hvUP%49;rKo2 zC&9~}y11QmP`=)>xIdptJ5l+{lV@hHY#@*S>GE9;ee3edSMK#v3cA+&@=0Dod&SE4 zJ<|U(<=e~o1oZ>!DBoeuD9B#<)m0-$zet?5ef67{dwV5AU2`AOXq@%=Z;-suwl)!O z=`60<$88ScWXo(K&xvfA5QpG4p90SCso%{|PLpZjwQ-N`hY90^OK}R5?5aE&!c2B| z2YqJ)aj&PWW=lWEmt$*C4%ro3MIZK1ez=uCsdV&-TSX&%q2Qd~+d~W4S3QwE)WDwX z&>m7cqvg+kjQQP@M@x^VB|qF~Ij(fj(&K3nob!8H_Of?xB3ibyw=bl{@Qe2i0q@0& zkoRGnXj7cR#C!3#fiUC!CU`GBnI>GW`VYD4GfbvJJC1q#fo)5QqqcXA{aXUR!iV$X zLs?+T(u$5DIHOWoXc*w8asD}ZwsU8dt;qYosC`XbE^lo zlzk<$lMB#^cjRW_CUFx<;Zc&bK>i*k3p}m--}L zc8C5p58w6J*-;n!1^4kUzko9r-0v_ScLo1m{=@ujULo91(hzm`zaIUjwk(^%m6`U> zdRa*WV}8bXZm;Wafl*vi^+%n`uQ6RI{v`g%d1JGker%>Opx)bv)2_}*$EH*^`1CSA zr=WMNKS#L)BYhN>spdJ*tzWQLTzln|r*u8*5*154KE|vAt@u?AohcGN<)ghkY7g4` za)s3wYFuQ9XXE>3EW zhWpr3>@$sC+gAY1yG1iFyKrSc_mciTXcOE-G%#ka&{+WC*7&i6dMhD6%R}W9ttHf( zg;}|EZs3a^w}S&#`==*`G!iBrXe5SSKZqlqJjqV0MxMAcdJR9gD>{Eob6Ly2} zkdHeD#kr?P{8^Iu4)5yYO}HvUB{VPeG`9pav$wKjGUsvZZaeV5>EMicePzH-x*&en z`9#FMY@A(5k?$ACH$lGlK?`LZ5bP29hz#%ZHSj*4-hZ+DbU(1FVPAPY-l0<$Tdy0bR|cZG^5S z=u&?rx>R?1Q&@EUT>8nkFVQ7FHi1j^W%XC)ziB}S>hRyM@{`Xb&-ZG=zE7CW9xe0r zQ%YDbVLJ2o=g*C=pPPOCY@^R=;ePI|xW+HNA1u0t0=l51L_F>wu9dPZqAYDbO^UK? z^=UpE@K|^Crzp!hANM@3GbP91WBw@Ki@uom%~)3)sNs7tX9xWrs0yWBID>bfC_^P> zc#v{Y{sHZ;(te$11$P)Tht;P$fA<3`y|M$?{lHk6N<9tV3(9nHo(wRX@k-VG#k7H+ z+#1(EI{kojh|$IRb%ppKIozG%3^e(*lV3OaA;V4%b%y9!{w;Eo4V{rV0<7*Q`V_Fz zox)QG>{9r-41T))3;iyBjPS;ebcVfC9QR$^MAVtfNVW4qiaU2yR+Wg}URV%iaCh_H z%YRM^KmTY3@4oQw<==K;BKquK{yNf)o0>^q$A3Hj!~Bo&UtgVwYI&GtuO4UdI@-?T zOevXie3-g<=kiC6Pgy?w_)W_{IC#_Y-wl3X`Nre7b8k~2cRUqxhg0wJKe%^((A95} z?vmc^C0!}*m38sFtK>~{n?=C?X8Csq-y(f;^K%C$`R~aKK00{Kl%B!=K%d6%)c&%IN{;QGq)kCHCFSL!-P=VgOuy>r2Fg?(lD z+XtV!{IbDofxUhCPX-gDn@n7Ox+_nSF5&UkAfLKh>nwO;dCexxgYf4o;1A&nfSV|f zi-Erh*nHZ;w1uQqUd7m3a%Df1Pf*VD&j433xWc|j{_+<#%>VO@volVtqB`$Z%Vp`j z{YrPIXOx;7HPl{fYsa>CcoMSV`kLi;9akMbclmP%^@f4k#{} ze!K?%h3o^AKZUyw5UTx zMIKI}uENCAj!}`#6Kqg4rw4O&c9h!h`MTfNJ>L6>=K20UkH_b6|GVz%zV7S3?(4qp zzi-_!I_=gVUbasvwC0{4j9*FM)41{Bb&(fjQx2Fe*S(3WLbw)yOZU2jD>nhxVF|cy z2A9s!M1fC&fBJ^0_~XwkiZ?y*rI&v{f9`OtuDT~-X^=R>@?#(49+ zd3du5-sFYq6XwnIEeG@F*XP;TZoJ8!hc}raTw&h)Isw-c3AhSEykWljhP)Xz-f%z4 z@y5&gW$cy4*(+u2bZ@7u!VrfiwBPcEV@^GTO(&oL zaq>u5_5x#fP8aE^w5f2PxgUE6xoNRHg2@EN&wEskaMY4sgWc=DQ75}Zchfwcdho2I zKeRxfr}b~NL%2TS{wf*V&Odr0cSM;__Ps47vIBRFztf|iLs~*}Af0#DxKBNI$-b5j zcsnwei+dUT(cQQd*-g~R>x5}*&j`3whR64I?krZ(o@$F!Y~yUrNx&~!RbP1I+F=iN z;X}YoV?X}+L2%*0_B!izAXw@1-qv4pO9n|-(q#fw1Zk7YZ#nTrA)M+Xjm~ZkzSk46_dC@`(oYP=2lbgU{E_BV*C*9C+X#2_SN|Nq&E&&` z-p-)f~HP7wzfp4CxvcBwefMiK@^JYzk{L-zskkbZdV>=jF6^=+3_4>aBm(<-^paiZY)??_5UvzY%;X@Z#AT z;0u8N8Sv6MTFViA7_k{K=ueG&dFs`YgLNGH`oY-5TB~GTwuWR@Yp)7B-N|``IC$vq z86K}}AKl3f&ykO(?mm5B>F+1}T4j&5>5b5*yVYNXzJcYxcW)&N#}g++UWn2EvZ#Y^ z({b8VFtNxg&NDD)BJaP$JHd&Mm87fB$D-Pk=|%@~=llb~U2e&X`pL)1BY9j;d1{mQ zs!ho2$C0k(>5)ccp_P9d|55(Ylb9d!ui}4m!|K{UL8|US6^I!KIt0 z?xsGywC_*5_~sC8?)&90sMl(6JG~pxxtcHY-{^F$-u4%NByT!%FL{$31#8t`CN7XW zo%hgPzb_&mqC;|X33VzDl~Wd?>lvdfEVo*}5Z}K9ovF~758sR5L~gZ)AvxDrES;MT zylB_>IvEY*^|Azc)%uWRGV}i^uc{C42)s^yORY{yXZ`hn{CYWDf^2HdrOIULYtX7W z%Zs!D`hK_MNqt5xQxbFMWUSX@tP45X#J~J=8IhIzTU$~i{e;K(OXjjpo|7}p!NPK; z^>8mkRg_Wv+0;l2|H?br)8L3yR!_}&%hM{*Ehg56O&zxPUODjB>Yky(W7~p+W9G9 z)y`i9Rx*E#`mo77_bFaD3A~CwDir_CP``+qUpO~edprwHtv}rbPR{l54)9#L%~tMj zfT;$i#g)rL| zmU#8GKS9?%_$p#z`wbh;H%=P7)X-DY+>GTm`ift|^8+VX!31Rg+)~>LN#@)H7 z9>b+FHOA>}v&azpz!u-SRP!XhTf30>=zUYSa8IWZT6o!p`+*zX{+0SsW)$6nn~v+p z1ks{1?U_-{i@GOXgx0X_O+U{mN+>_o%8yxmD&@y1KRr~wWbyx{{AUu%=S{qvPLq=q z=-X<2BarWnQ<{58Zjy4lcQqjwfBq8mOJ?8|Yo=RUO>X@AAQPk?*xw1-aBH2xT)_K7 zMc^0%Mz(0B#p}%%&4ZpL-s=`Wc9kJ>+4TJ$rL z)0pV=0CBeyH*0Mtp8hI<=eLsX`8x)jh0g9kT&Pkp0s|D693v$G_-vOR?N z1dDTe!tlHs9=C(%GvE>2QIU1}W>Zd!rR)2C^`w8CbU&uc)>nDIXAF|g^ZN4=aFqgE zuXaI>H4oI9fcmfcs@CjWJhr`MAqyc_crg%h%-< z(}mKlGswd*^6=9y@?Aw_#Fz04bj9)FwblQRmPc#uL*(fs&j5M+dVzTK@8qFR@7`i~ zyiS!&UrPOI$WtU<0Hb+};x8j!HuT=RGHl9HGn~jN*-8$rOF>)x` z?ZV&BKacQUT#b>fccr>#L1c-Pu0T zXzfutw6@EIR*k31-w-pM9wq!1X#F4XCDIz!UnS7`FVLE6_(jjzLH|5xZ(RCI?Jm6~ z`A}O=(#9X4u_Pm2w`HHoeh+c;wfzB`oO)iaI?J}8k5yW`cz&h9FY)5y5R7Xp_@Oq{ zTcgig|8!#w@%~<-@(M$G=ew4sb`zfu*h91zIC92-9fQYX@aI3j zAX>-iJA79(#HURfb4+(guKUO%-SJJ)O}$*-1-EZI=?<+kNQa8glWJ>t&_X_qLwb|y z2UeEOL29n;<$0s&4B>#jt(D}tmi{c8=o^;DjX}^M+F|Jj4yJp!0%@!b0D`_AH> zMGK35);TjYeh9C|j{))|k!L8B=cDk@&7pu%`$)g{lCD11L!MUh?4{n_#0k%J@S=gZ z|0d4s1@Ylp+S%!a5FWjkCOkRRsf0W`$x}m|@1LSiGAX>*gEt$P-vQ(Fht=sN@D?gh z2(QK%;k|)8leEhSd9p%y7l1e2@TM+A_XG1JFf*Zc9s}>V!iLxRDbw>!iZ7x)+uas zMc}R0j0TYv^?%9KcKp1H?8b!|{9nYMej59_xW%|t{3VYKxE=f@mjk%pR6710_`icc z5>JnOA6N2Pgx|}ezHcl$RW{cU^kf^&Sy9#>h--jmPgk70)#QB~JfhFz@b=Uw?cmo> z{5p(mCqKS0$8ffe(xT{m$zp5>;xelZ&ObPCNDf!dzH-TSkK8>`qZ0<{-?Y*>x z`o1quW0nsK|NCisUp{G0Pl(^T^K~(KW$PqSZ|MwgPpQ0f3{PeZ+`vl4e7p6~j{=)5 zkRe~DPd|(@cc@(%V+5yli0y>64k4YU^#i%GKb)K~Cy;(!Q5@(vhg)Tm*Y_XfDOgK2 z*;!_MQym|rJkjspkgTHJKGDhgs%S9yhk$o{piIRl!-He#t6Ee2A7H7&?$Q`%X`;c~ zeAg4FcK13@ZQo7XKW%L<*<8uLZ+&W{kN83U&yzNd8)5#x6Tg=ao%?Eb_19VbIhr&^ zhwJFu-_!g8(pQjF@y8`-NNswXPA*?gI zUGFb`9)4F_SopUZzjZdNY!T~R$j#)LL3`=Uiq7pMv42H7EY=>D`awSVB~vlOBU#dY z_k5+1|3Y-Q+AU$YVYoA5!n2j6WhuJOJ0apR>1(KVf&$zjX z-VO@y!~7grzYh}$WM`Mw_1a9vm6v{-(@eMp{whq{1q5EEf@@h_7PI^}o zcP??YXQA^lS_`DVxif_$Uk&&LpC`K?2Sea(6j{lWVd*_0Uh=n2b(==Xi9f%AYJ$m0m| zcs7Uh+VF43UvN6-P!CUu#M*-W!!F{BiL2$mjlb_#I-gof*<k>z<;H{1+X9W$!=Fpg84=pX z%@2KlQu{nWJE%WNf9q}hD(s%$i3VUL)2Bka+BET7eCSNxHVb{94_R{kiMEs-rSk-N zmREMK#?J`-4eqq`Wd^n_5&+9 z(!9Eh^lv!&nGZq#HOPeKTPjC-UHgBkYx}}rj{Q|&Wh03m=Z5H6Aw5FfeL1qNR8AFn zcSPjyO2HZLYlUqWmo6#?KX0 z*J4Keb&Ih56P8?dUY=H0TN)&Q%1qq;WA4b}A|(HY~NF_*r9 zdnQ(wps(f>S-2T_Tg>>Rb2C4?pFU@0qjTJQF1^Z7^k z7Z9I|TZ*eWcQx)(+(z8val3Fe=N`a)2W~2`qxgMaD-6-8e&zeO%KtF3mg(vf=or^O zu;Wy&&#yO?)i=|4^E=JnpjZ2K{|*c}|9hQC?rhi6IztScY_h(luUc|M0KyS+slow2%6Wj|FF>M^?cTUl-|}5zBid zZM2QNeVo>n1ppp?W`QwpXw>BYS5fcjp4Snxes5?!N?g zZ3x$4Z-A?B9$dwSOJ^=+6AXe&Hm=vx;rdolPt{jCxeovPsB<5#=Hp(!UBY>bRO;Wx z8_^5Sox77!PT%a{4S;s_3F@zPv{t7Gl^IGCr>bCc%=FcIn;IhJ}K zzIke?)p<0PwF%NYtW8<_%^47U$c32PmTy^P)445om% z0%#gpz}_x$(MxIU3c#IDn&(;ef$&_N(uq@@wwxW*sb6lWPU{ougidtdDHRP` zD_7Y%b0OOFPJ!<9>Yg!aTQ#SW9{mmHF9Ln$-fPV%JUO7NYx(-A80D#LH3omX-TEK0 z+D5$kLu(H6rHsu}=hJS;_U?UW&GGa21lg+w@12aXlZVm&jhCamWfGRXKJsreS!3TP zr<<_8O0@~d41BVAt3T&03}%M5xi+^%b=Tb=S<5cIK>1Ukk}qHi6&d8!&Z_TL-y zvF}31FnLNShkm%L6jyDixt?6jgHEP?n)j(Kb?4Z(rD#hPP0*GlH$>Z03AAx9%(aj3 zy$)AQ^$gmFJ-v)KN|&xkMpxugkB#uNGo5vN>Y?^l9<{fdqe5e;;ZuA2dE7Ih_SQU3 zWfm8R&+Dh+yfGkttBfK1YU4rP7>HBG&uBBXaTf8u&-B`u#Vdg|>Bf^y`iig~Z(I|*G{1GmeEl5#sxS5HB~A6~0e_jX~9KQZ8_<;FFH}ZPCH9toqdnx7DvA zp?+25s{(HaczQ^1A-!$KNe314i z2JaKpFJJh8DKfmGH5s05o@ad9%eM+Wz4;0B`fo>$^PR(((fcdttu?&jA8Q2}Bf87{~3nA2K@WDlPKBFH2ji%$;&V1 zu~mizYxF;ufWJ2%U693jVa{A1Yxv8+-=#dzue+6we@6eJ1l!Z~oe=&z67W}ozY_eF z;6KFh$H8Ch@gmg@shtA$-M0YmC+?rTYyVDcY`}`tZ%bLqUaLJcDiIS<0R`ltD(CYxFO_B z>tNV#IUT_04yo*0oej~NP>{aPt=m2H>YN=dIjsS{#^9^*%eJfnzs`tX0{#l(#Ao%* zKPB-UMZ%&%_!) zz~^nfR>Ro>&XwR?08LtBRh=3_IP<{MU^t$qzX+!f&!~(L&SJw^1kM@e#eYK{_Szh~ zF@*DR@->1}b<-ZD>XyR!pF#5cnLK{ZFFsVjhcebU$_#!S8U??DHK=yrp9kKr_cT*R zqv_6N@UmHXXhX*Q49QA|bJy+qA~NI4*ZP|9JwbbEz4IyXu};s~@qoT&OK+k)&GjEA z{j;QNpS_4RO083BZoh*$z4pR1*UvmNJ@Od-8RGZyFCqLh!UOo*@&5<@Ch}#nvD-uV z*Mu`~ONtEP{{#M6+&|)W0HgKW(&F^U%Y++o{}(s<{9%y+{0lL1cHkDC#rhNe7T{a) zPxC*AaOTv7xoUxdF0_hQ`bxR>Cn?Pl;tFW{>Vgo|;n##Q@z z+31SrIoa71&6|ov@}|y4HX_LEO5)XznnLpUH|u|rN1cglGTGGpQuvyX!Nw>$Glx5c ze2cNk`_Z3=eDcnh^vy+o<}ZDDP;TbJJiZ5)H=NBrA~-)^*o`^FEK zzrJsX4*!iXU%wi|_jBp@q`awQ@JYY=^kz$Mp}ZLBuWk&^eERfGOYb7Rl=Q!lF8UdR7_E6|C=8q~uNAWHkBoMoVv^%m&gwYBc%uc1!Ody@T`*8BLn6 zYOL^U_Te>U*^fFmD0?g^(!J|8Xw^E@W^4kj9cN0eqCx(fNJ}RyTz>TBJ z*xN7x-mUGx$c}0wy$XBrCDvEA(s%1DZzFjdE$^4%g=iB^XMsNkOf~7^vFy=G{NnLS z%UecR`{i0g@%CB?xMr~FMk%KVIB&xh;?D+N_S_!q583X{g?_Ci%q$D$rn&TgPXqcj zV;``>p?5&K!24tHZc~2p`S(h`Z0}+89Ed;Ur9Yz2zpyI!wywX&-A}sKT>HRP4z6#4 z>pFPh&$4fVhE1f!Nn1i%q2ZI9NtRoQzt-AMJi@Ns>d&^>$s62PSuj;i zo@YXNSj*h1^EIMhy0w%tRKF7P4v_b=YB3!UyfJ^6HwPHp`h?KLZ0 z%u^EgBGldJSGY7>>`$i~6p&W7R&GH5t{w?u)U;PFa87r@c@33ccDFEhD}% z%Aayhr4ReQ_Tp*M5pz1~UGVF5I*M(yNaX#c+N+dJT7|75J4JhwvQIw&F-^pcV$jW1}0M@cU*tn{vpynM_M=ew(#FU_&58?n>N; zaQBhcf&VM`XYu#qe;EHLZUH#6FG`QF-ssNOSAzdp{9dnq{T~6(#6O)=^Lt|9Zwd^V z4)n6$kDj|MkQ=vm+@o}p(e9AH3x6{4omqU-`w-6m-@`e?6?3qCA%E5xL3}T3@abYX`83+5pRHXd<2Ke>7*Rb*S*jw{{Z}%Hox9b*x~%+ zyN2+uevvmJ23QJQK7!Z!j)#{{7W}Ot zc-^H_9eRx3q>xU7-kbsQi+Anxdu&>_N6}=ke+%IetREu;E zMdv2U*+e;6lyft5x^a;@D}4Z1PiHeQ;@`3mKF_~8E9*OWroBPgNHh3FM<;a>A9XfG zdF~9+&<4H3z-esPeqI4~pR{vJ>!}yC^G@I%^kX~!D*hGd#XnQQhaS}Lsq+Vpz4L`D z27Y}0C$ql&?upyCoIm@Gm!G}#+J^I6fGxf7ZIL$MUj@E_a1;N2!o&Q>_#cIVQooXS zn)r9|AL9QG;_~0cT_yf2@NdIC7q{?Y&iC?Pg?|utJO5GsJNQrXpXNWqe;@zIyE*g1 zKa+nJ|6Kkt{)PN+C;uK?u1`f;%hDrUHHnN7&c1{-0{m@NNs*PktXF&|ITE`zEwYmM zO~m)%X1#}X0{-73+=biAzaRg0+yPv_W>K(;I}7}uAg%`YXSjaNVu0{={?8IuTAm)M z=l?wZ8Qj>V@D_g$?hgL{z+ZV8XWj5`=RXL42KYA+wcbUG}Vz9dtV7CC9X0o=?U`Gu04+*eZ3pm7J*Bk6!gB?wP zodxz#gVlXV(VNR$QS$y&0&KzZJl>@YtNfAhFcO~+YDBEhqa^KUCZ*^e65rH%iL1N z9nz(vG#5k6G6o3yJ?y`?dVR+J3-PiywO2BHT;9|$v~6N;pK=#(XOiZ1;fT@vqf>p} zt9I%9nCeq#X=MpC7QQp6&n9SG1dSzxwXbb7x5p^r zeh-%Q^@&R0DuF8oZU=B}q>DDa4_HUMc>lmV=4j)dUq%etRJYcmfQ~lcn#liN>ZY_# zaB6n<LI2-zE=L$J>!z)k|IdI;8y`P z|8reDceTLjX(=S1>UKl|EpcEIX{ofhD)Mwu)-&_a(f}Pqe%MfMF9UMm{lvi>hbp*#2%1a8BHw&zvtE8VfS6;1^_gdjx|9hkQl>&Qp z-obCKc2izA@mYuFO-+4nu735z`FT*&nL&A)GYk_iett1jzm@P)`oyOlAChVMuuGe< ze8+~;)W?-?kEIjg?pZ4$99A!TK^PEG;dR=F6zEw10%?CM~D3 zI2a>TMh$++r{+;w!&Z6imTxAMPqeigtny*XIN0%mys2^EYtr+k?uVCsz<9l}*YK2u z@Kjj+BS!~y`fVu9*QvnLwujPuo$@X1sZg3|D>y6Qo92MLKjCPrGuR)8V0{^FmiEI? znlGcn()tcg%W04X2{32_4n;V}Fb(z4D` zJs+5wgs!Xfr%;b2*s}}rruIOm-WDk_xN_12C!40i;;Jo9{h`+4WCQv&`bC;+ zF%C2iN*`t|`@c@1-B|B;YiZPdknhs= z8GgP0uCp(!6Pyvi2R}eNXUK#Wy=13r`a^SUF+MHOqj} z9pj^cDKZ$(v&X2r)(m~PNV=Q+evhM_`N?}>q~<4@oAeTXA7Q^nr}rI`sE^iub3=44 z3&sF<_wefTXd`$LKb`f8Wx<+-)~-K$@7y@S8y7j6b89~$3pa5;;xg#e+*{{qeLF$M zy!#QS0PELCYtg?W%LBfsU%#I`LxlZ0E%!NdUieFZTlVM`gw;N(&nDo+%%s+-o6 zT^Y!U(kswk1;})ac*&GxJSrI0)tyWKa(fM&p?Np&8z+(yVR z>`{{i;f^JgJz!}`R<>+8`atlOg_YeY7|QOJ%i5$b^MjOG&fl+L>uyd_jDIwc!%xxf zzI{qgwsj{frxQ97*RUmfTBFu`mwugEYb2U`4(Yt6F~7;o#TZ~6AePR%JL ztTrhjPd@q2BY!#ji1`+;whH&FLTK6mEvlPev(eg58S(doXw*F(?Neq!quyTdvg!MP z<|58-ZPI3RsZCE)o5pnClm0DPk&BGzy%}USIOj|_jj-$wUnjj$ru>i6HrfZ(c;)s0 zt&9Td80Jf^%Cxk^w(FoyzJ4>S-tp($zKuKEs8bDiYsGi!w9jm$#5) zY&3(H%`d%`3a#&;u63#(brDaTU-z7;_heDfR^{k&t%s{_dC0TAi|ywc$8(meocz_; zl^*7kl%qJoUwTr&%NmsdPUnVr>BHjXq2Q`kKInVmeAW%I(F+LIux709trZFfbl=C_ z+?42g7uNdo1IsoLzGEDH>hdUGRS3_HNH67zA4G)jcUb||-*Oz{op?N?`{C?^d z$qDKn=m)i(!d_o^IOe)Jqwfs(BK^cTn=wo{nReW^n0HNCtKE1fb|Y~}#cG~nsmkT12t)8O@duB09Fi3^{z!=~je=eaTP zF2)w>$vMIxj()YPmAJ|fuk;?N>>Is9E1t{l3G-ax?WBcyt~~lzgz$`}!1JMw@zx>U zlaD_<-hvE{76fp8j8occ5#KL@#w~JLj2cx z$x-Sh`%wG9YOhq_#uD6$uH>_VJ0G<7?hfKrzI3Pdb9KK%Hl6HV=|bQ4m>1-T&w@>EsTfb` zczk?z!>S40|JGWF>*wg;z4@yqO2MJ~25Qq{{41mS)_l;9bXKo~xciTr>qp9?|7a?I zd$0ETf}k&}pD9h>K`Mi{Ww?@SXn#z8-+z_Yw-35LLuqmzju`c!+tK27tv2E$(uTp{=$1;2Qg0uOVybLZop zf1md6&`RIRA7^d3fw5#qS}?|R;@^p1=k&Zj*51JY@tIV(>`KiFbD`X-nCFsknMmPH(5AA~R_l9n6II zekyVxJ4H62_Pu2TULHD=@{HPzHuH9s@=&FNY(T+_FI$22{$1!4XQR+Jl?R=ev)Jmu z`ki}wOZ`^woaRP&M~rsS`4*L>da4ehy%w5Q#`x0?x`V$8+I<-rl;_6bXke>dOWn1e z7w{-}TSIhtyZXKGRbziGJeFNENLy(CUG1l{k<}r->#c|~_+HNUrYn~6O=I3^7EPk9 zVxhGGVf8b`7ZC5>aE%7%n$8ckL%X$uKVzYH1>0zcN80DwVHNyo0#5CqKB#uMBGe8y zzJ+!Wyy{y9toPp`-R{bNeLHARar<4+5DWIR%cz&?b5$s=+~Ou_2enrL?Ug@Id!?&y z(astR2Mt!d!Y+4rx#i!YcW$lD5}54836B{?m02& zj~ZW8=UvpfK>d@txbdI6)oQ2Un=X$dQ$gSLvhcBgA`4~MvZee#2V9&tl{2xSF5+IW z?AJK=f{U^aZto{4zZX3}ACJO!ChmhjMRwQmOZ20-`8)ATS8L5eYvS_DW|W;FzurlO z6z5mJ(3m6};YQY}bx!^>8)WzL)-8P2oKJKVGS)^S_fHk&Gw03<&W|}-=p&**b8Pft zj>ad=DO_B9{rEm~g~p5khVOCZbx%wqLu#YsQf#4$hsSkpXr}bZiH}N#@Oyhj_HK-| zo2B5D{VKZC@b?)^??Uu7vVRryQlH(97t}}RzJ;Tc^m@`)k*?xOpp90ZYl04ZZqx< zrQ=FKqwh_RsA2v1rFR4*b6$69j8U7a@5{#AVKk3H^B6Qg1I>PpESTew9i4q#``2Jz zIxWJe*HDw zt(cSLL>r+nlqMTNb2sr@YdyM)@Ktam>KV;E8xF{%X#wb76hBy`=3Y&Fh2%~yT9c$^o{gLB0AH4MLc;n$Vm93oEb zFi2b%ahDR;Nu0<3Ib?P|{HM-?KP>;{;5r*zWx{1LqBg4`tTxjcp8CjgV6s9m^R?eT z+GG)J;@hw6YR-2MZsVe01O6QRTX2uT)msvIxFdv@;re#;-()Gmp73oYS3ECuZM2bh zJh}6Pekpe%W!ENqxu=@cY<#P zUTdbm2ENYX)NhLzqvJ_SrjECBhbm8ekqlM>R}kY*UfJLy@Wa!6k7U_&nIF?nr$BGt zRXjOo->DBq-j^OZQ|Y)RxMlo(+bm{WPi&K5ZIAm<;yG=SNtxdlO?trR+XOMoar$gdzvbMIJ``^y%i^QPt%pNA^lhRxlMSsl z(AruFd~Bo*JdZ0#3-fqgh{vbT!{aN&L$je1d91axYWRB%Fds_bFY`Vp>%sae>(cNw z4qxN&Rqr2--xcsmvVH+HOU5M&(nI2}c;juo9^M&Pw4kVU*-r&ZhJ$M};beY7~l|7Y{z2|Cux_eVm!56@rxx@;@FuLouj_h#H_ z+*@$-tGFYDdmDc*^IuZ`2Di@mCcXn!08O$7#ZP_THa8U3H|kuz(A!`-r>yhJ{;cp7 z%$=^_4J6s>vKcjB@4?0t54J4YJhcUT^;a#jvF6s7UZI^jNSn*^_=J}k-7l3ssK;%@ zrq$hw25`7}3OY|Ra}P38L%L+55?6XKJda&sGLyKMA^Fjpxte?WdF4L%?)}S=nM_xH zU^`rgj12AxWL0Z;YbbXH8TB$%PWogh{6506m3{hEgtdmFI}Sr~qYq47VQ)p{vkvO} zrOu&roP5+vD=1$&M0G@#wsw$5ve>>5eM-N%13rG{xBw<`EjRI9HLVrIN|#KnGkDFF z)TdIBgU8^H=FS>pM633`l8GOH7qTJUIts8U#QmFKEiTF8en8wF$q9Ld$FJuONrsJn z);qUqtP%aA(alqvkoPN~{|NfXu;o=f)8Of#g|Aq!LFfzZwvV~o?Jlw zdchJWJ-7fk-?v(cFQ#vm0$U1f2|C8N>uc~pcDmOoS4lo7$L|GJ(`W9azev}~PP+?# z_{@GG@~*S=`$*Fn`zGM;2WA-eAzbZ)Xg?(L{hYlg{3w6l_cVu0p?_pUpLijf>u z)+c>`R~bH!>}c6R+S75mo%vbdZe)!w;7N>n#;E7)XZFAF3ilbrck#~StA(bO&@&7z zs+Y&TmGQ4F<6yf)I+y(JJiqR%s6Vac?CcKKlcg`+`$EC|(XC;79hyYjRcXzcHm!7P z&l}ykvf8YCG51(luO5Xyon?1;z}seiwY}!P1ukFE-=H!lm$33@FW$@=Hzo%x%aiatfZSv7n7#-P|=MEpCNhDo1xMrK9Bf6iL8agxy<>hLplZd&uJy> zc_5XnjZe zN_ak_Gj8B%0MAa!+F|ipgN_n^1M&UDdENB~=n{;l^Y?_s+Y0#5V{jF$4^<|t59v)C ztr3e)I-BJ4xVZS5op&A!f1?)!b7J_l2Y%hjTv=x()NTRXM)&5Z+IuDR>CNg3p|6~B zEBUtbN?fg5yEhKpdj=;Q_||nP4jajQ4Nh)9JDo z@h73<`lFm-M6Q)jX^WW0BV!Zg;F6Bic^lc1iS*vSjQa;6zH2{B^c~MQm1+5;2VFdJ z`mO{yy^wYhetnBd@OlGUYcg8Pc~?k&gZvx$wql;^chw6y%g0^k`;i~zE1*nR`UK`lV0nwKSFO z#~xo+QX%w5InP1=`3iWW^ck#i<7WN-R-|E*%}(~AbR zF>|Hewe(T(vV?X|;XOFDsbsL6@zKYtE-6dz_*1dPod>KN(}+`@1n1?(`N1*0fB$od zw1`$;H;wCII&?SEmlvWVqc|bH0DY!!9G@3kzcYF1eSebel!bniE_fq4_QE&mblqK? zHox95-(h~)>D$e(?;~$Be;I!1Ddnxf-)sH`{N3hn!ry8B7W{4IufyML{xf%%8==bC>ze#wS#j^P*Iy(}g2Ty~LgBmw(r zWJ@;CztLwtLpUo6j!5?|&Kx-%>T%Rnb1#R#Z#(~XtNMV}xgL-`hz^nLoJc>|Ct6H@ zv1Xp(cusf{S<}7P%$UA+80Z0wS!y@+(_!)@L&rww8iKAbKo@qr8!HrlFY!IZ-%EUt z${;R6eOrmUXlbAy!uZ+@pS~&E0L&fa)11`t+~`YzzA~d%eegc|y83J>Ize@k-Cy9! z+Q7J{`L$@}9$u*YFuyD5i;eu7`1cs!_Te@YF8Sab5A}@zwT0IsBjC%0rfrmw@9kCi z$uO3&d_U(Lj7IVJIO4X1!q_z#P6o-}L4NUAdjR^zjMFo?{{4+c@~Q26;7L6^DS;=R zAFBTq)Zg*LcnigDOdtax{P|KKVKD z9(XYhFD6Yl$wufaV&6b(4SaV}bpRc$|7F?jhp(nG;qJ=jT&D zgPt+MOH~(W72GP|bmy{?GAePaaHWT2`*OzDoyXijyN`h@C9-~8Zx*_-A-;ZGwCFA@ z^Sxb~BP(8dPkH5k8+5*e-a}?~MY33@L5@CV!0-fD+5Eyq{8Amhvt=d2g3reWA2Mn}ZJ{`=3Ebkt@z(KVc;anA z@yGGq($%&F;#+jX&Jk?yVcucW-k0=&a7)f)7f2syUavDS+SB+6a`}nVx_3Pe4;20j z{3@hwg@UC{8?Xr`@vFZ|CJKT1uRQ1`ta#Q~Cy?dmG`6>ZQ*TGrVYk2XN$G?oQx63E zcXK%9?SfyLLySlU1N=F;qL;Ahvjg05%8!Fj>7S$zidUzpo~6i-@#z%L7kDQ=o#?~H zC)sea)r2$I(!z746xwHuF;Mdiy=!nD^g4LzC7CV;=NHMRv%=z|_@}bPKk;!3c?EMb z{L}eP(Vuj5&=(%2&Lz~r&o>I-PYii0C0_=z&g(&LA5h^1YR72zgm5*2OY)$4>s*#k zmp&Fu2{6k{e+p&+FfsV)@}Q$Mw^+dV?R8QrbV)XzCTjdQNX2y%G8;0&8O7<`X*X8dF#l#&f0%*sQsn$ z;*=5SpN;Mt9ilJonBW_I|4knB@^1Q`JG-d1^J^hGpVDyCb&(qyu47@B`=|Uf`DgQ& z?%U6#zY>2n|2qC}BixRwH`s@8hxyO&*SF5HKg?TCxMjG0?0t~+hlaxzOf??O`K*%` zOf{c1XV1Z3_uaFaW7(%WV)N7@)>*613)cCRCVdjN>%C6QGd{U@XN}i~8n66%UpSxY z`zrFE7`GfAHk`$oHr_>6e|wGffkwjP7XBtWT;c5&UdmZAg|}JwZ8vq98;{+s8{ z?jh{@6LP6|&$7J2t@o*u zfl1G0pObf+v%#HYa{L@`F{+P^;fDE=Pu-+@MW^(g)4{mIyz?wv+js-I23%?2>LG3v ze)@Y7jJp|nCt7(%v9sDp7mc!;n+;aFQ*TBKSIZLC$HDcN+vn)&H@d!y+k7iU8syA^w>-2^#?;` zygQ+cy}akVmwfL_C`0c~*IOB5_*IV&Q${(kLkVU4nlf5KWt^H&#wN` z5z4qMRL1PF2e-%Gqu~|#G84*Zql{M1EBxZsD6lbLZ%ZiSZpv5@Dq~kd8QtXTCf_Ts z9o**m;H|YXTCI#0UGW7m)rV02A8Jo`JeOfD{ z#L6fJwumxb{lr}VRz2QE8N7pepdNJzWu%fXl`@(V$|$CcK`Ub#zj!ox6uOc(d?pgg z_#5Ny!=W-RPbj00e0|XN;e;|q&tP5O%ILH*I)JUGjNybbo~MkqP#I?=lu=E-YRb4c zp^OH~7_l-+t&9?2(<$R$6Ux{o`vE>jtUA9qJB{H&!9TljYt9<*>7;gg_2IC?Gaod(lw;on-gGnAfdd0H)Qufa50 z*wayGVP9^wg>#v7X?xdybji)NoFCtxYcdhxQk*;x5C=mNeAKCXL%GPE}rwLaBoW!%NM+|NApIVP#dGTxI=Mk!^K zQpN|~po{_LsOeTlu9a~QWmHhc<5tEPWiVgf-45(Hu-XfQ4~vnZ6#uQh2GV2~jbj%z z0ju)@g4H}M5mx7GWs?eaEwFkAa0jq zpmdu&P61 zeJe@x^ezWh`)OyhuhvXG^gY0L-`M|B^wpPNItlrzBTja5Y$0Rnslho7$=foOL7(~Y zl0Z(&h%2M7XirUjxir3Ns*&^`Sb8P#m84f#I^WZ9_HXh6w+C{vm-`>*2Kcll<@=Oh zpHkbUK~pw;@O{Vt{m9wZelD9hpZhyy?_m8v>%v+S{iS3MJ@40rucQFYY3pB3jeO+R zo7b#fw{G>?HS1Q_uU@zMR{pE`H`cCOebaU8RyPoT8|n4@Z{oj3us2=5_STOytX6m#R(3Z2EdG^*^pku?ns*VnJP?jzT( zuD$6at1BqkG@(70|*!`gMXHr#sMtqt`ze~f$| zTh{@u{{hzkF7xcj4qUA*X#FAeTHbMj-d6sV{G)_<*FI8Q zxjL(!8|BR-?f+J*N5Lree|6_r72!}LRv#peb6=TySduu(jEih-4(1Gt)@;oFI)Yw zpiaW6{it--+*$V=7ah6LU_GygaZR|>q2JLOTkmK~2Vb}!bs8_idv)Qx0j=8zKJriY z^|ZF-c_zB&JAdZ+?a!Kr^QbNp!*FU%d=TDWm+U0|vdQX*uREFkgT0iDj!r}Fo?v}O_aKV-Yp$#|dOxyNWVl6d z59dgV$d>`%_mcLU<*Bj2Kdi{nxiE)QRU%0NSSNjQzrm~qAXETS^nF7&Y zLpe28&R*s$5ojzRZZC7hLfjQbV|IwfXN|^M(rZIB7U0+ZfoKfp6OG>@Ph#98eXWf= zshn|?n;s3^O!YuarnX|5|;eIC)d0r+tE-vS$SS0DLjm<#r*Eb{fSHta3lfyCt!P+doDqj&WGRlyY5}7&%c`wx6*$;0bWhw`-ONmH+ zlhP$q-wpMBwau4Qck1!oVbR? zoC6+)u__Fs_0YD!(6BZ8(r16Ng(HZlTT%jI|9W ze;O->nO{l{wlT-+2Iqs|OsubHQ+-t*jrGzmUF1`n{4csOc>#MeCJ)lh$(&V_Zf*jm z764F+yfDq2OlRzV7otZt|IaNP4bk@_qpykdrVxFl_;v11{WhFW^mURa zG45&TQ(KjRM{QM(yFMfb$tTZ^59<53!`Eg?@y=G#-?l>Dh( zUJK@N^V$!4m?KJ_)kbQYh46nH<6~H!Dr3z3qU>pBW9Oh-)t1}9t$5kQ;+MkGaTU1O z8{=xXGW_CU8uWR7st=W0{3Q1-Jxn!--L84D-rcAef9}OqI}67z+Zh@9JniuKxa_3@Y$cr$ zk_}c!e|Gz7yvZ?|&RZAsuOk;+uqW4@y}t9M4(RmbhinsnHY&n5bM|0Mi@)z@{iX$5 z`U>$^?QOPfhsF-(5}l#APKy)XD0PVnKW*ptr)Qi|!5_i6ded+Hw^Ov<@#<_acg=(!l3(>#`Nf+olU3nPw>ZDXu?2d<^HhKS z6MJWubcyarYJ3+h)iM52{?PLAW;<7f{t?b#Zso?W9DlaY%`3s9{5o%tP5x~1=aPR1 z`6JNg>G%x15gnt@qqdl#ebk2QiTCp}{}z*{ZO!=s?i%!xz8P18UaCRYRe)P^A)53p z63ME<3(nrKb7e8(75t$U+*t`97bmZH_1Az`6M|_^fKi;r&oHk2w2y418T8@AQNfzi zIC_IIeOG$y$*EENvZ)mAWB+-ZrD-lMJQ>h*DQ^sBYW@`a)x;KqN5Ab9yoVVp!fsxs zIaN@Xlg`<&vk90}t^7uyD;cU@guMVv(>ipK(um8{yNl4^)>EmQ z_Ktlx4!rpB=-V0l83(*v`T8r5)+o$o$=QQ$(byoI`_MTf=$x&8<=f-L=OQB`xT&&H zv2hp&4qA)tv$Soj#pTjggM{7K3Qg#?47bk7SSZ>O>-`%)4g$-XeumaLeO|3`Xq_{V z&F+aS(M<>8hz4`UZ@@SDl{<$}DtOwa*yEAx;+LK(T!?Lm?4Ae@HI}K*=0fv0GOhK) zpHN@;vwIv__ja^oM&pjoKZ-wU_aXeUtCc1ib@tKO+3@=$qfv5MX0obve9`UW)GpgY zG&d+ee5ah<{_L&7I``RfonXb1CZZw_CX@g?ka@rC1+TU(bpK>!I1C>Fijz zkIvUWJLsd*v3u#aoGXbGeKb9?6<6z=EAf99zrMX!ga0Yq^m{TQPveIBwAO;)=fq=i z#vEvo{H8npzy7_N_e0ZT@oOeD|JVn;4i?@>)-|3;)~BiC2=xCub#%JP_&iE_I_JK` zcl2Ys{$t5BFitUX7&%be8+3B6!Dv0?1M}Q}`4PHBJl6cBL~RJKrC)OK`@2)&e3JW4 z@+8JR4IhWmY0@vkaV70Fi2j;}mb>yWAcb66w~zGB%?!@+Z6dJ>AD0@ct9JFT+ojKB{{@n9g?C==0?yukf!VUZPA9UA3`G%oU`cwX+ zj|kQ;Jblu&nY8;Dw7EMR!TfovbfwPa%1+XKMXkfhEx=WpWCZ)(?b9hORE~=i9DUxM zl~$PxfK{1_SO4yU{&1P-aF-sG6ToTQ@q2>Tke+2Uso%UG9LSrqQ~kSP)%00!_p~0H z=K6Co6+eQ%`m$_U_4hvDZXoV9{(JD})&<}H+(`I7{*{FHLr~+owQC+FeA7*<*R1BR zJ*ew&>-jg_${%-K!|FA4Y*pO~Y!mr@4tyu>Gq`=Y`Yz}w?r(8tad+ZoU!NX{^RMRr zKZKib^9;stw5yOjwZv5tcRTk=^VM$5%VitY#`D}A2Hg*o9fMpPc!NoIur&7Q zJKr7TJr;bOvAikemrWxbD%_g4d3^oglO8IV2cN#BDSR26$sGq@2KXkyr~I)HzSL!T zQxWLZTyQh=dRbnT%sSs9=uOI-DkDAyzDew`X7VT6V0F+{N4)w4O_?E>ilx9)PBr*z zz+VUc&w)pLZ3kX%XVHSGMa%(pH&g5P`#HZJi3T>+N$BZf!|8Rb#*j8oDsZ{ErIc10F<1SxHeEqn`r~QOBaF4Sdnuzmx;Y8=vt8B}k zj-Ju_g!0sbujx424L!FXJ}TU1;UhT@qj0B%qnx2pc#DOx6I^+?{&9ehAeJ{ ze~Q}x2-?U8iIJEHogyhZw&t{)*^%TNS!^?Q2VhRz8 zi(cuKLh_87|1osdO7gr+9_AVc>NZ2V@c$d>wWR-81 z)m3dhL|J;X%eVD*D?1LIjb{WjH@%%V+@a&=(4n^8Yk7QIry88{ben$*d18m;O+7{) zwRHjUD#y2VF=4f}@{Cv>eT$=#Jl`TuxUDBi7eA&+ZzTO|r2DoWHNHG+?snYFGXuJx zgn#;m$AiEh)GydCmkx>_1x<(MO-;z=rw@rIL$uW><<&x~#;bC}wOsej!Sz{i{g5(h zEdHmIkq(ZQHz=c(GFnL=4wdmJWfYUY-pc4UTt5R>C9rG3^;OE~w|ISPx}Ny!-k^*z z$`~X4UqfYlo-#I(-=7KJYq(CQ9s|Hug6mGoNG}d}^%P}nC;qZGD5EeNdFGoMUksIT z8)Zbu-)i+JGhF4s<^p>TxaujR%Hs9?=_2A+B$mNA5T$*!p3_2leb!uiR8vM`+cko( zVrauo=0CLA1MG+CtOV;K(DAlN@AOfju`!O$)0kO=E8DWK03AfzYwW3D43RxNLmXxA z(jA|3m`B7(*E*2$3RX6)^2@KgcrS7nPcWLRL|AJImnWGi;rkit>!n9yha8qS^}jc< zZcLnHxCVJY5jqa46SO4i^(5$MVty_;mHpJD@^Cv0_u<$lE#Te>?pES-7eU`k*O;rj zv0W$Sxjedyu*J&OU4%Y^6|OPEwd3f#sS$Af5?pRRhVJxZ{E~m{|HU7$FZKYF6hR-o zy#Iv^=+`$vRG%o{+eqho8$;w(pUk!R81XUUA0xh9w8J;jGvt?z@r>qLw2N$-mE`d@ zyI?8^Z`b}OaIY@h|H6f{`*-V`Afp!dSK`hk&d;CSop0K2CW-fm767g?9UWRDZ zUAHk{z5q>b4Hx|XfxQ$yd!+IQ3|{lO9^mgXcx)VZ)}e9H>+@(Xr!w@etiGAh?8`u= zn+Uhlo{8nuTAmv6lmc_J&jVfc^VFln;EREe0>35%ziFO&WEs5P4;+thpCh3j7tT|U z>3C3&$te5r?jQJ`&7tZe!=kFM6oiGA!zm}qTS~GKzf3>;D5dQDupT+$VuJ#;?>(e8T z<8Q$IIc^W`)40>Pzr@vgOyOGQT=?s7e~+tm@LuMGt+*}xy}b29zw9r`bOG|xLs)gbhv2>TUUtzBw2q~*u!;P&#Jxy<-7DWgn$`-B16FHU zf_V)cT1wm`Fjd6)^-SjX?!LKS&-^Pow3u}1D6L6~UwUUu_2{8~bS^hvZnUv)a$cc{}!} zfj)3+!CJ!z!{~c>x_h3gw;5s^#$8@;sE?~0F9)910%$FuT+yX<6927leLE{#>nO-_ z75(R)zs;?&YmRe1_K*q9rj}jh?Ay>?4Xoc6|&jKdPl^5vA+l+tdA=-EH zJs;`nCgec5AN$j*C_9qDSnURp=I_LeRu-tpFSwfy4mwZVAocv+w_N!NT;ew|TN zTV)!oZ;RZUc<=7IwQFwv*xEIv__HZc>jbU%m*DqpmGpboNNKOX^G@ANMn*i$|s!a z6Up$R2%P!gTx2*^&%f<+dgq|`y0$^@sYd%A{IQ$ad;BN#C(^j`O=v7J8jlIlSZFjB zKw|+k{+Ts8;S`O}%tK=wnnc%mMyG!(y9~aTAz$TD-U5mR{XuJq>JLG`9d~OrCCGQN z^^5A%%~L=9D{??SwZHnsYsgC_J~4e4Hmg&@W#3N6;^HKS2Fr2l;d- zdnI{%zwkKJFSPzVf*dB|nE;P}!&%?&Rv$R~E#Ok0@#~LTmnV zQTkhQ>Fx6pXj4Bt4IS_0USopv{Q&wy@crnIe)PxhZVdE&uf_Kezlr!?5wCtNz4B*t zlaOOV4W$RC-wWynXW1P`=-X@>N(KjScc=(Jp=*R~i1j^J;We1MA=QYiif( zo`lAT)z`CD&b^J*YuBx_(OGJh`}Es_F=kzC{~tx0E!8U9K3(=6XQ=T z>z;U$Z%1iuS7Q|Gx4}2K7`xk%!(`;J4f$?Z;L?~On>etM zg7Y-8pLLF=oV;1&^|Vw&i=Ur{l*{A)iCjJ(^z;8Gxs*IeE~TH&2+8HoOfID-UN>&c zBbSR4$zGA1Yn>GR=H>}r z)+HbEtA4U0ghzJAHsoD4l=849bD&ST_J*zjY!cDaznFD~6z2aSdTI+7PpMB9Sv&p? zz6qD)G!0y>mbZnx_2jJ~uhu?QzWSSg1E<#DbVh3>a90B-yCTWXme&?8a`#N2iA z`Z?M4vhoeiB1%787HWq?JPn4Y9y~?hc@{jLX3@aD)z(UbsQ^anOiuwLIwg-CR)(Vs zS(FWVfM#S?{W!(j)=SOCCaktmo<~lTZeKtCq~Pe|{!ZK0BZ6^J^Mf6x=f}|19q8(B zP`+$bmEUdUYg`yKxB=kmfZGfl{cN}F9PvtaP6hD~60iN019VE?!qf10qIuT6zqd5q~!R9^56k1GvZFj^gIwYJ7;^!nefmSL0rQ+l~7!+!5SM zaZ_(ik6ewLi~D}uBHRz+`uVZ{W`$qFn4xZg9$Kfhi42{2Q&@U|JHr{<6=qFb``(hD z5?s|)ywZAt*69Aoo;H1VLV1?af5b2CA9gCQt?7i{`v2D90lfOS8&3lr3Z@qBo<@uBea&F-5_g^O^L`Tkdk7cKBY zZ?TCNQC!&{VLsgWf8aw|h!1yA-@eedw)*(qhPUTr{||)H1`^WxLuvg9X@jA(!GyG{ z_131f@rBguMq59q)7q8A(|2=Hu`O;m|Dv4(%YTKvIn=w(UiNfuTm8qir{-tK zIqQVGG{5>KGN3cOUZy4IqMxEduDr?O95$cvWPbp|p6u89hVdjobw&^L94 z1v8U#Pvfrsne>|t(62UC8g)I81{`B_0GC)N^`|cCnQQTcpR)F3QlVu;{OQ-C;q66Ldw`01rKFRe^fLqnaU9iyz=^N z$m&{f9A~TWr>>*k{QUe@dcPQ*R)S8eKT~|;9N_XLoDE{0h#sYW6D!w6UZ@PEogMlX z_0b=Wyr4IOd_L)QpYOjvL>jnqZQMwsO)~{cI0?GdrZwbO-`dOlJU6~r{0D@Wys=Q; z5rgr0Z@o3h+nkYd(5O|51}GY(t5u42C1}*5UF~PLN`)#_N((NFfEB71s7utU1*>+o2x7q?Rgo=j z6*lgpE|@bSBUe4p3rdEI+w&YU@O&Y3f3&di+)eXZjd z>!EL4*4}6DqrG=#ectx&VeVvmSAlOcbEm!Z`>>xG{^jlMi0w`LiPUj*nK7-&{WS7b zdbKb5AC7i!jVPbDqM?kDfB6!H9;eXp4NfU9Xnr1@sb z`L5!BP_7corEfj{jIw?UWDNLk18y%q)AxaMe)z+tV%$+b-UmO7*{j)hdwDW?e75Xt%HBg+*9u!H+e6tIvFzQkY}h}OmG#-OEtK6z z*>qNLD`l&(?Du2Yu)l}0%vGA2fj<{`WEu4SneeMU@=G{$XW(|`Tkb)*p7LHl2z&=H z-oG074rsic`Q&QgJeSsG|#rIT@fX79G8Z|WWR{23PxEIE;} zk39T3?9iL~8`^K{PcUQO*gJ64+b(WEObHw=rOByU+)Dh}r zkH5aaKhOr(y1Jl!E#vP6)q+Rwgm!s#6!|mA_x=y2>5D-BjA}8@JO2#)<;qZudM^Ia z9R6*L%hUKz=Rbr0EdKNPFXsP-cfY&xhJQS>a>BfMl~p`%;J=6eqy>D-lYcw^4*t{m z&)`3g|9t+7`Cr0+1^<=&*YLlQ|9bw(_sP$~_j?e|z3jWr<=UekbOLLe&RYl0bl!jX z_JP0PYtSCwquD^eMGj#sPd>*+@JoBDBfWE&&WdjV*Y&?2!g0&H9*d(tqp@E3FJ|R$ zu2X);KFWV8E5E)@`G@yW{=%&Mcj}a{d~o#ke=sY5Q=Rh7`zU{AR{q;{%1_!y`O~uU zH`Xa%+ei76v+_68Dc`n_@-NQHf2&UUj(wCrCM$n^o$}N6QU0k}`E_;5ckQG6_^kXl z>y)3pkMjS$dr1GkQK$Tzto&1WZ>td>y%`_`$X-tVS0wDfL!8ot#DasGa%^c_MUCysJu9s1@& z-`Pz3?B|zcv^u|YjuyVHfqgjn^+WLidPJDh=|kMj+-y2@d*2D&?lt-mbmOP&Y$e_K zl`v_3*9wewqYs#}z43jJwa~CM(zBU+LN|l|dQmeXB||2_t>d zJq4PadtE&OO_j3?JJ`zgic#n}aRhoi%P-NBc(k}gk1&Zxlc8B2oihSGGoizEP-V7= z`(&TX{JH+`hGa&Jq{%&Dws#Kgb>GcuJilnKv%x!&UWSHR!DPf^LY4@@`PI)+hhA(7cA-+`fi``73)gt zf={L{)K0sI-j>m$DR!*@M+|A%k*-D@VS{hfL`4_eCYfpUI--?t^oQhgY=kZ;Y6 z(k2%A>b2=6X*^~S`;q$lK?-G1!(`E3$Hnqk!O^-1y z+w-3zwx`nBP8&E=eNax_JSQ2I_3Y-C7kcKrNIm+`#$ukPU4o{j88LlRmOh?z_hAcv zN0!dKeDajz3Y&W)`r{r^?XUfC8trS2Wp2*OSjQfCA+8&;bnCb`rvF=(ZXJEdSKq=q zw2kVEbr4S6>AoLvt;)&}mki$p7tS*7N7WV@54{2&Uj&a|{mx>a@r~@Wcg-3d8y1@& zZqlTTrh#AnUk=~2KaPPuU&b@_?6fcWd@;`ze4Db)EkhQ|`LE!wzV&il#eX&bwft}7 zpWBtPy#52ZJEqKx?R;;kosMhryp?uJpY1dz+XlQgyqR`Rj5PkHjt(_`sGw^i*R!Jz zjifstinJuTzNSRWo=D3iXmRhuF6vb_$CuzX0jF$)YXk1~2=}5A+-l&IyKqOvw)x$e z&m+T?wC!i_;C&EaCPf)KhN#ChBgB_{R)&su%1E1F{A^Zyp3Aeg#qq}cIiY>*(FCXT zeKGdO>F{t;ggG6UdU9SD`M92|a^1jHxla1<$bIp@Vqe?{Zu#_w1Bd#eI(#E^DBESU z|5?zvCc^!;1h))0=@pJOte^uIz^4m*=+&YaP73oBwA1d-(6=-*RqarHwz|mkE0AJMqSkx=5SBe+6&#inZC9Fy8Z#W=989uT>5WQPkeUn_?;)kK7zkGBbK%QPNnQyDT_`I`uewk zvqt(X(o?KxEPhs5e`Z0eeYH_PpZh;Tb1&DVPgC*LTGb!?{Pvpf#&Ok`hlUo$Jp0~Q z=B|E2^JUa!jL;aC*R)SD?5kxhAbo?r9a%kVXR)8sHNU0A3f)(=uWzcmUYGVpg?EoB zo8E)4Z?m2d_C33&Ddo3V5kLx@IB{VWc7K35fTWmp=Cg zx@Tq=`+h8=U2=^@THFKHRxQ4TdlUAt%-sJZ2@izaQ{R5myZIs9=P?u{Q?ZsRC$Ex@$^cPntKMuFSQ8Pj_w zU$gxN;4T;iZVPZ*fcq+N-J`&*2W~xZUjWYYtVe3cO5j!kcO`Jmqrfc!ZV_;w0PbGS zaUKa@H*np+T?ky?C~(t&n+Dvuz4z|9{8Ztrv8-(lBme-Ch7qrhze zZVPZ{09P9YZar}8fjbqrJ)8qQ5`8OyTM682f!j0++#=u>0rx84R*wSL4O};HF9vS$ zC~(t&n+DvmzH{U9UM+YMYdaNB{KJ_^2Rz)b`09^fjB zeI1RQb2-4hWf)^FeKB^N@i&bz9~O?Ws5*6cTwx63&zoObdEj>UQBdx@Qn`7t+-C5m zxw~cVr%c%UL75Mf%Dmk&)V=!Y@DJFtr&b>5rMy1HO43#yh>!U8M;{1#KPdmsQu#N; z^0v$Jm%pSEcxL&zl=r(-6Dco$(p*SP?u4)>$X zjX0iN!Z9)8SQBx46&$M~j$YE{gX0(A7!MBlE$(Hc&nEpY(kswDjdbUWwx7A+5+!S|9_&a{I5j#8%yw0 zBYfijyMW&dY}y-PT^pGH$p33gWuH^3>zr8E@(BOg68tktb-fSxWz@Afu60<~>{#}) zQrRb$>N+ph)f(ZKl;Fpd>N*SfDb(fuIptwpyPR{Q=U@jPtUAMh+~=Nz8dY#C+1cL=fQgqDbo5$TSS{Sp#R2i(3{Ek z+D2s9&$ZaISg ze5JCev9gg3`jfb}bdRsJab5lP@s$}~XOFMU<2sd%{1EOEXE#Tw2ffoaMxK>XaUs=F)71v9+c97o7b;=vZ zS8n7whwDc3xo+WlFW24XzlnH>`COYnN|n=Q`!o@s(*@=Wy*ZpX+S%xz0C# z7VR*f>k6)YGie9c^*!S&w{xw&V|-<^>0RSPT+6uWE}lP0pw8Gq^}NQ)r@1#?(#HMO z+#BPa#r-wh8xvf_{TH}j#q~>EjTvs>{;S;Y=?AJL%*I`cQb4LC$fg0ulc`{vHK*}{EacppT>{+G0MoF~l?uV@|4Wr*>4%FXE8n79h_# z=ybMro9Z^cRIl!Gy~f(DSKg+%y7jhIi+JE)@PD<@#d;)t47%*gi57XfAkxtSUGp;e z%?9Tfa2cDPg*?8DJZ6S^u0U=@thO^gi*qylMkL`I>-KYLFjjgc<)#QnTI%PXh0;NN zCr24t?+l(_%dt3R_2=(asXx|lSz}b##p}E` zz1T-qoZXt}n^?U(zy}(UmoO8tiC)FynS3c8nq-%F)SR_nSoPUDq3z$sw)cKc+2p*l zz2&lvm5*3Y-Cj7P~*M$@V^qXS!;;PDgHD~^{Qr{Ht`>u|0@e4_-eu(ktBjYP;_)q;PV*>x? z^NPRl@MLQnSI;^dTP@xlcu_o4(=!Cb^AP=}8$5d#75}LpF0S*rZsI@nT*fGHc*a_5 z)N8Mk{5~BxZCj`-@&a?+Z_*o_gkHiT=k`Qu~$ zhR5dr()_V8e|?tkJ2B7_%CrA8;9nQ>6K!kb^Q73H())|{D{u}B@~(YJe|znut=6T^ zeLj{8d%1}b%iGop{e^d|2FsvB(;t1rId`TX7k};ie(gdV&z0Iw`4{zwrycMUSSjk2F8MGM-(DMw4abg!_csDx#it$-QxDI>+E`iIKS(<`s^IuxhC_c%xd+?A zRapBT?=l8`vF;~>oAsjtcZsGAhhDQi&A+7S=m?kVl{k#qi@UAFOVuu~Tcv-C!SHB+?=;@2k z&^V|UoN;BIe05Bfre=885&M{J({2gly)x96wDa7+d+P1bp#Diu8~2N1`&uf6Zq=%p zJpk|5vv=j-MWENdm-n62SDYJJ??`=L&dc%7P87*Tn zT3r7r(-P7mE#8kr3+qvV$H|_#wke;~-(;&&p6iyjGx1}9_wV(gY_DbVI^?~2kj81- zKR8xId+YOf>S4XK=uhex&&ah&_zKVJp?-l^{Q`9?*>~?9yYx@2Umo~gLPZ^fE>;K! z+j8n7k4~-!r;n*mkmgzo@w+bJznuSL{DQ0vlp9MOhx#q)vQLr9wJ?6BJ!E6(H&5kU zxvALt2E4qGN{BCB`K|C|ZCFW4sc>?#?vYTGbm387@zJop`&$z#M zJo(8-_MSR|uP*QLb;7+es779nt zLPNS|jJ(5p=e5KDL;Csg{&?y$ua9SRCgk19J$n}jA*C}@3fV&M;CElzQ(mpV@P(oI z*uk{IKbB8D;6j#H=`*SmV>y?_^tZr^v9%@rAsw0fPMztUBf!731aBO#jMHy)<~V8h zAdI@1%U#`;|7H2F%5-O9)B)fI4$0(y2y{M)x{d>+u+OuSQW!%#7mCpFDX7?%W ziC$YCC$84V37_cdpqxC>w|8AT=_L9*sFU`~aS=wnP48E$x9R=qTyK|Vdb?sDdb=2& zPLDi&Gx*kCfDg*QkAFY^+vC;yP5d`!&!+wV?Z1LH^=9o_&Am2A+u~Ij9mv&vMas%C zHP{ZW%1HfJKF(#8O_I?_x9J4`{}*fqMgsK^ltl7$={O)!`i}JJf|Vq@4`0H*YOX0_xB#^ux;AYU>C4; zzOybaagN>3HfcAtX^u@P_Wad?#=1E2J-w6Imyzt3JRJjV*8AMY(iYysSH`wOnA`uS z;L+F`V*;}9`*GJ(F5wgA6lL=`+M+zg-wZ7i!TJ4&uO2V*Iji}r5_l_grK= zugZ50;PnFPoky9Usdtn~dx5ld^GW}YQo3X70_a)_tiJey8Z`3kd14`M!n5-CS>L~r zXW{kL>2JT>LA>Wqe4KlU9oZ38ej0~2IS`x?;qAI12# zJX5y%vtz4#qn+zKo>QC*D}hZY&Rk6})-}Hbb09G4S`Ym9yqvj}!?%A({&W2+?f3b@ z#8YVKVNX@9Qp5_NowxJv=b!rFT;wntIdoU~hBb1S zLtDD3=d2RHQ=DlIH0$Fe-$vQaDReo=!M1m2?Xut5{!R89(rusR=Wy*I&+@;cF8MKw zb_+kd!u~AUsSe#tp3kqz+C7VQPps4Kg`@O|TIMg>x9yWJuh&M>+hcodW7uy7Z|$qL zP1|aFR>tDU$N_{S0*DfU#}f94r+Yig|HTJLd}->=P#b{E^$yX{D9o9FAQ^UYD`?Q`~p9FFWS&HQ*Aw!?P#9=GQytLxJ3yw|=j&awFHrX64KC)obb_uxyK zPpr(w!`$Xs9(z0(qccWp8vTA)nl?4W>bM>qcvZ}+l8sDrjTHA(e;f6Yp&`W!^H^a$ zTbj;VDfH+H`o>Yva<6^ zWsfP9{Z^?g`^*RHJ)>0iX{EAXE|t|MRF-q;6G`sDwvw*R&?gMGBVNmM{p1tTKW+;B zYgk{c?<4KO-cMRGWb;#BaUCQ1NX?mEn$|u@U*V_baLK<^@9eMkhty{}_EArDKkE^0 zw0dUkqn;G2r_WCwtv$kxR?mX0o_gy)+RE&k7U6xxjzQk_2j4izEW+jfc|HCu-;erQ zz!mM;sPzfAulm;QM}4(YeV(_TbXUJOwNJQu^+_u>X+$6X?ont^$F<+aMzr5PUy{}T z4C+t)K-+9x%#s3}vN6qZB=I_@#lD#Jr%>jQ=er-guzNx0!AD;Ud7M2R)^Jk{EZ6&o zX~X`Gug2yg9fRYm@7p-Oc0_;RiH@&R9_RRaW}JsOw~~*x>G6)Qu4CGse0()6A78_K zhdOe<=zn8;-AGI$>FWPUy7D&~U;iF_>WJS~GtSh>^P1(vLHLjR6tO1$i}t z809-R`MCPg(pd4rY@A^%99Gu&US9T}N@b79%D%o-_Q+CM)(wV@=bSq=IKDySuzHE5 z4yzZQ4l8Sn!|`o))<+%R(2XGbztwm)W$EbSSy)pkbaGVtFWhMMzjGh;bPVbKsPzap zT0K2kJtK`XQ?t6XQ6r5r!qv0GxxHGvAN5Tu)i;ve7p`7?xvi}iyXnnnDElP2&9{$* zclz-~^=tL9f^DW=*ZizUo1dpS536VMjaB6~-!j_uWb>Wxtpko}Ilt<`dtR!sz}?3@7JfAT zb|-y7zdB(qr`&)Uuq+a`sH{^Ew1AnX^$J(E=ckQWPwj2X%CSIxEdp5CE z=aTvg+DrX?ZTfQB=kro{t_&_1d}iL+zNFuABE)yG_uB3hACuqXznq7A z>A-AamNSXX&Zp9_N@vZ%vUDHU_GPP*wpRNY?}}A zJ9RvNJGMbN8WSJG_jpFaA-sIE|2z}9_a-?<|HG}2AjlQ;1>1**Vqd@h5f&_ z!!}Ox?5=>1e9p^M$7aAw*SXAF2ro@nhcC&}KS25v^4G{4=-x~;B=3eaPA!n@~FW_o?RdL zE$=xW63^YV&O>YidEz+a2-bl}tHrd9)SuE0C(UxsOHvz;pe?2~W@(4TwB{^rB54!R zO>tF8w~hlNzS@z{6?t|jX}L{Ha;O)3&DXK|`JS53_Iq(vk?o(r<+WX$$k#P4$4b+7 z1o*3mt#NIOIPx{Fhx|?(dYo*+X!M*C=lHKck3)Ux2z9+M_5o#KKTt0Ev=>ExPMhr5 zBEK4|SBDtwb;S794PnQT=lbibBVNnmdrn@-S){G8@0o_3nfy!Zs`hR>2Kbi?&;7Hw ze<}Cc;l&RZIwxLjo_O0!c&CH+DJ8rcGQ3kVyhld7@6GVyZwI`SOL%wjuEySI%M!g` z%kVxq!#g43ot5EDdxh;!mgie?!5?mn&%}$vbBc8`zYfpF8ijv8&qW_#uiSqZGD`N} z{l?l(ZNTbMUX8p$4p-yf7x%{A#L2#yP)993vqlo0&DU<}mx=Rv@KYSZrFp&S&yMNH zdf5JF_ht!yY#ckvKE8R@pXMS*0%w}M5bx(AUU}j3XXCTH@cGL4ES)~zgIp4Q;!WdQ ztzwL=*ncLk)Tc4XEWuiKqh-eCnf-{;lXh%%ns=9Kq@SSIK{`r~^ip>0t<&50Q(Tdk@Vz2`y;k8jpfR`Sf`5XRk5AFv0nmbzW$r`D_qG}w)l3F^YyI)rdanX z+FRTkZwh%;_Hl?u{NPM9B_Bcm;HAjFz*Dp#^^q3Zlz5oz^nCWwRM%d*br$|gCqCBn zD!vsy)(q^gb13_pG>3UqyzipFc2tJe+sDwp>7=>l{tNWLemk|4KO^Sl=iRcKufFwd+lP zcqHt(72o<2zIO2aYYE@X4By%e-&Z5PKmHec4B(siR+sSgb7uI(C4AE|d@D12+8OEF z3O>*F7T@v`zGdJ$yM(Vb!*@}J@4|@h$KY!PUq|5isoTCwf0`Qad2gU8tf#`eZ*w1f zP3~FeYpO+`q2@o)@0rI;us!dT3g$ueeS@ z&gyS7vTdNBwNs`o*7I@dX^;2%0fBa8YX7~+wnu$9hdgE7R$;$amj3=oXX@LDKKoBQ z^y-(N%Qu$1Z-YL+gK-*tRyg&~b$R`yDWtzQ>h4ImEISummT^35<@rQ#NpmY{;t}Tc z$VB|^jnC=vyPo>*ZvlttiMLgF>vOPq^!e@N(=P{1^USvZ+aGat7T4C9OShWd(sMv6~> z)Dvl*TP^T6_r?19i)U%HuGS;{eszKC$>3@R*J%+)Ylt&mUHEd^7<9|;;spHgT-jdA zx2kPpVT17b!nkdjWE+ce<0+Thsq*;WZd>Uy>cyArJ8_IsHlOENx%P5ZF7|!(VI^1n z(W|&BJL&lh_x2(0mvgVLTitZZ*7@w^bL>uWBlk$;`}yW}`i{img}tWCTWlBn2;&8^ z7)RPJY*Nx==$|Y+bCclbSWjAqN_EuRx03b&J&AG$(hp{%tI`qbhQH(BQx#sfvJRv@ zH-0=FUQF!tyQ;n0PC+LVUHaD2^#XJ^$KhBkjvdUS#o^jZD*rE8`Q}*uqx8di<=tar z`I_b7vvZ9iE?Vq`vG08S_w~yzh-IDgISy87uYE?j>_9H{>YE8L3jW+wdDL#KUKKIAx@5JZb@%e`MT;V(b_4b=Q*PDm8Lx+CK z6#P47oM?d`;e0ylWd0mtgT7yFy747rM2^k+c{4|~?5kE=i;+gg>(!4g%9%B{iqkPs! zv7L#}Ymk2*{|)>%@=twp8trYT-VWNU-L}2%C9qwd!{@b?YhfQ*w*zJ^?f1G4+17}G z?4*r(et)%q+x&dKG1Q6u2lhtBKd+nGNb4`sZl}EM-B?>fyhk3w&;G){8CUBaI1-s# zW^Kf`PFa%vJ-!3nQ0W~wA@K~D+at`z63kR!j4!U_>N{le)3Y2MzrGiF))}8~;du)D zZ6)rb4;80r$&$KYeWFAp0y*|O^f_ayXgdk_7}y63`o`4#3VT%UrRmI2p<&Ry}aajGP< zFs5B6o&_16(;}YLB|L|KXJ*86{s?$FGCU`N=Stq6F&3q*xRN-kZJQZ<7xyd|@1)Wf z29_d&kXK#0_4ULCU&k2-Z{X|$&ns9B&+Sihx!SH8bl9I7=ueBQz1yC|+`QbTX~g+# zmvN_{uh8;3&OoR!rueLm+=y+pU%j4m$H0{S4bs(%r)7H46ZK$P)C>EevY*Ox4LMDL zwxk!ENi*+g^g=(j6FSrjZARYj`q7ETi?GG~yZO)Ozn1?x{z+F{fHf^&AWR>WpbCV%oDvlUG4Eu*vvJ_#@EA{ekF|_|y8h<;3%Q%4oOs z7nISB$jED-@}quj%*P)p(^J{%flcw^L~<>1cyT$#rS_d6qQ-}w`FLrbAMCo+~OL-k|t z?}qf_&A`j68I=M~8_`MMl+S;Pd6&ezndI4@&D$OG^!bfp+n00R+OMX;SNm3X#$RLo z_OH~h9N)Y@4ww?U)vhqmg_xw);Bl}6VqEnAE4EIu<{kKnrMzee`e zUPpV{tRGtN-@jkNno$cXe{HyWlv8+Eug`IIA;Y;CBXzqOq)MA-KL zo7*mI$93XfJ!m_4M;`u6C2e}pou! z=k2&4w!{8sze%>#@_sunjqMk}k9ssaF74C?QnnW99)Enn`$YRz@(1fxUyze~YkH8$ z)A@#=@f^P+==zHCyzfEfbNKe_C}*8dp${fmj(;Aq#9wW%6u!X2jveTSe2cVSC++hy zA4y+peieP8UVR&NN8jd9o@Yc~>sj>UdU%}wCLZW(t*wJc->*61nK%NTG#=RY<03CV zM_=$8if7=1RiS4sbm@!D=c+H(8R>mG>#@@7v%XlP{(h;=3-HBkccRt)Ezgtw4)Py6 z-hlodFpAvUGyTN|?NqkTBM)V*S3Ob(|4)>8u8*71#XbC6FJ>IzznK4W{;4mQ*L;;( z($OB;rVeUftS`Rz_grCS4${*f-qBSjavgI&o^*6Yefb=EsNJbSbD)UvdYlis z@8^S2c6^l#yx2V=bJW#wCWYxr&mUzaWVTY z#Q8?wGi~xbw1<&p3vFtlOdE0#N3J(Lzj;z+V=w;n<$P;^f09jkJ+ujYInC{Tf6DwTgR>a?7hDCCf$kF*^H|*u^}Vv zRa(sV>}EsTpVUKS?;fPt)HRc|OUd)O0r{l8N*9q<-mA2fwDMl1Pmm^#?{XDyzE{a_ zB$}4T@6N{x8vj@K z?i^(s(6@Z=&T%m<-@DUBT6yoz3usGu@6K~$TE2J3Z$Xy#?o5jK^1VA#NXvCH=}y^> zm)}AE{X>u4yVFRW&O=TRCo*nCf70HaKL_}$hwa^YIO5Pg=qFmX@;K<(h0SPSoSQ)( z3-vj+fa9d7&uOj@>;wJkqS#lpW7?2>@6Oli?A>`w#H-IS0l&|^J1^7j(l*oR&yydp zF6cN0I!+Oud-v`*PD$G*A0Q2#lLp7_XOkAksenVg?%g@Mg!g9|-WO+hUl{S8o8e7+ zcO0h@ZQ^zB&Os%--_7toGsD{w@t&FC{afzc>DyJ%mgaQ%-kook@@nuY*i_c;pXlD5 zSJG~A)UXBl-kqs29hv-1_wIZn;!S%pz80U;-ks~>bK2Xoi@AEDPhCpmNWHx~ha$5C ztKIySWzq9{xFYXi{?3%lmX^n=^4^^bOEjNfqWL57IgQ81kFs}XCo+`A)V_6D`=oho zd`^3J*6_Rw8Q5QJ%P%d@^-H#CKgP)?x_4&>F(+-W{a<=rBbL5=49#ue{_NdZ8|9Pl z-D#{Av8{aXj(j{j#Up8DXf)oe*^bv?+#~xHnm2Wf8#!vV|nk6vU@YKYs2SDaQm}&=Xm;A zJ7wBpJ)fYS_IQ5@_kng~YTvrjw#PC3T=JB)@w+Rs^bbZlgKlMd=5u?kST{J2IlK31 z@6NmFv(jANyK`>T-H~v)|6?Aw^1VB!f=in7y*qD5CgRWc?uaw(-8mf`iDv;W(j9Cb zcI0OG8uBS4AMWB?ar!Ez?ZCc-y#CmaI^*7&nasDz-`8HA9ew+^m#2c9LOqoN|GxI} z>|l+%yqD*V5l1VrmSS%pz8!lC)WPyzo~vRT{}y|BuBQ*FC*QO$#WAauXZyZ;dA`b3 zS$>6USl{HTfBI#v%2J-M;=Uu^e~x>7@wAu6u{_1D_qCU2j_r!$gRzov;Q2E)CFwQm zQzP!>N#kAlJrUPXwQrwtOpg6Szj7`9>N#28&<}o&<5RSk{~~;}mmlL^d-NYWig>K} z&M)EX0pGhy_@0{K`(TFe0}pF@CO)ff z6!z&I^yx6Zp(k(SK8&~ApRNr6b}?-*MtLK&hbm%jg^ERUv-=_BY%sBgU29q>5* zi!bzX;GNr?16|^4h;7Zs{G^Y<{n@s|?>yhRTtPe6*k2!@{|i_6)J1-Pi}~BK{G$DF z?=1P!dQ514fzRh7c@8{-_CSw#um^!(F;+7k`JITK27JH`)(2eSj}$Q-;idxT-j%P% zcK?=k+rIm`>g#bRlJswTYV14cXo!!bzEtetDQr{CvhqNh(CrqU#l0fp{;ByqZ{jNM zA9HoBRoE0?%Y9?l9Lj4!(vtRLxZb3${-=F1)BOr#yZVsQ)cyC`&(M`)fWJd{?wtev zkb8B%n(6-cO8C|?7OyJddvk{GTNys>jQIB6Z(9rczE;B51HNS?d?#l3KA+*cCgR%x zKKqO9y{d$-3cf`pe9zDDU6$edc*J)%_*@Ij`(NlkwW9wG?~Cefk?z^@6Y0a+)H`_h z$hEW7SC<4`Jv`{`d9jbFvz(JK=zDyK`{a9^&3#AB^@`zp!yTX98~!HsgL>V6Z=d`w zZL+LBq*wjNfD3)oroVXC@P6I{{u*$h?^RhZ1LnQ-w@^0Xu2E%o`-XQl7qI{5{Eod*o{i~G^Yo6x3ff?ttF4spyMOirZN)_HU27AcF?rVleBaOfi{t_K<##OXtL85z ze+u{IcPwf#eb5Y~aK9OvrEg4ZqdJrPl`F~bg04Sev!OeD^Ed6CbMJg%KV195c415O zYwwJ(!S<=w!NwH&ZJi$jXVQ1)B3;$O*Zq6=8S1TJb1BnKx^!w2#bv*ofxgIN?_Z?+ zph=u(#Wrc5Eu(!7ZKSO45ZuYPiybe-Ih8u|ydRL~cogPku|93Ebvy-m$a7=D#-Q#0 zduU%6J0h(9w6?={U!{M!F(c^J2ej_@Jy4`4-{gGitB^13-H|sF`+_;GYa0_oZ3S^|Ah0)upVBJFjHi=_~dbNR#VCcW3h>-(iss%Qi+E zWWO{HEiKx`3y_KW=l%KI%M0&6%Dpyl5m#;Chqy`Jyq_HG_@6bI$@-or!+us)Y6>LJ(ub`7z+tRpEUdNFK&Fr7?Iw4-0 z;#IrgIX;0#_6Z5+y?cg)<$2hip}XWMeMZ_s|7l;+pT4PLTG-D4PI0(bD6J2N^&Z}7 zzxUBcFRr)tRNkw3_r&4nS+RyY+5SD1{TTAqwsb+a`tRAB>d6gHt)LS_{^MBo{{*`c z%e)MD-%U~fo(`5K|a>=8pd7P7WySV z32Xel0}p0n$J^o6Ea;t68#>>38MS;PHMYYoDF)!n8XwT3#tFwttkih5Zz^ zpSAyy+Mo7Zi2uMk?bl99N7}pQyR1nU-FKsoF*XDrG}%e>9g7L@YsTB8WAHfWzP!^ONXs>mN9rPdH=NPQdw|=F8JVB_AM=kU zKg=b#$EF9{o7RejeI&y*SzNTcu$h;Jm{0I6sQ1XC{j@uL?+$jn*_gO5 zUVbIRH5xBJo!JuhTRw)D|C8nCynOQ$1A!6Fwhz(WC1eAC9oy!0J0W4juC2Q?{PQyD9dCp}nExVUORw?U4t5 z{*+=&ww@mFHju8J8hdEvfzx=er4t;EGp0QPY&S5o4}o_)zX}*>@P0FS%3*(b;Qs1w zK29DKafVCbVL|JHzX1=9fCrz>c%UsyJop~zWggsI;(Ic{W#(i7pt?r$C1iz#ILffzGo^+`zGJWUP8yEpaYcES67FQiumHULqp%U z4-{kU(y%Woy{~DS^Xo49zxU3WXK2rEN^ri8+KiE9JOBtWj`=Kw0 z&*}ZpV|lLU|L3v!Ht?tSv>NF1#^)PeUc^_FjrwveeFd9>uQp-(j}9s9hVM7%yL)CG zzD|g-q<890LJq-x!7IuULo3!S3Y{K`filA$H!_d7v|8MpF>rC(}qqR{+&K=(iJp~N~E#aM5cyHdjV&3i1pY~l?_b!`f zJJh=rA24ktY3Usx<$~=Aa`C-$bw&Lb?)ktOr+9bd{di=eO-S+*<``ij|588hLKeG` zL+Zn;k;5{0xxC6BSmj{9m~DU1-=|LFc%g3E?VMlPv~smBW%IMGMSpVc@ZZ%!uhbLu zvkhHL@6vvrKB3%#jg0-kI(Hvcw2d=bcB%{VV+MZv0o0e`%j#kczMaYXY0e+|(sklk z8*vc7+-cqJ8TabDYOblw=lzfweY{7EZZz{=bB(s8_nT|6o)rI5xBJ*5P_LdDv7QSv zS}%guadl{Y`3ST&M;r^laV`8x@!f`(ziaCy@G#JgK1&z#q-{0m`3Z3HJH<`Xer59x zjymK0YT(j%lX%mMT~dCtsn7i&mw-qAP#G#`_w6tC-9`A`-F`m%+rGJ@Qdtt_bD>WA z6tKo}-x_7D9*FBA_$iKw*hKA*@2vQ~aQ?k>`?R()@tOWJkbKrpjKHIZqrPh!r9r<| zn#cC{4$MW42UpfZ<7Loz8F`n%v&(|*?2G-w-O!cyMg0)_Wpk(BY3SXy?GG__S()YB|0?V8{1x@+MDWn31D;urk}y2BNlFAn!i_&it! zxEb(S`Gx(G8U8)bFYt%+4tn9mY;Wo=VC6M zZ0%I!IG2C2weFXxCrA5IJvsh>7<_4N6y*3G>U0co?0uMe^H`+(mM6BqGvj|cXTbN~ zQhwsU@7?Wr-mM`nyZL#X1rho4_KZJ`;M@d%!WI6ABkfroECYXbF-{9p?_KSmWB)^* zvab*mk7t@h|0BdpH)Z`^o;Zfsr}d?eLXT5<&)i7kN;7y$zD&Sn%v62#y*Kqme&D-Y zwGbbCA$bdlmo2PZvwdoRv9`OBe&je~tX}%QODl;u2AhWLq5?u^kW76d?o$7mwvv2y6or1!8@_3^~PiU zA>oYgNoQ{r9|AgFitglkk!-+|_y)bUP@8lnw3h7MWN|f5Dfss1$TwxMAmjI1&Y)0_ zgM11d3*|N+U6jK~-?;!h(lej2aXfuIjpO<#j^k%RpJ&CTJ)Y?8b=JKi;`k*vQa?Vp zsN-Vu7sdRaX8BdVY3f)O`fQE;7uL;KF_UrPv9Z`SJLIKfUI*VJ4>Xp?Jb2TKPGFlj z7ct_kUV#r`9AWraH*+6joqfr1QXBDX>~g&}C*S3>v`M+#@Z`z^xlFa|mj52>dAUrN z(bp57m&0ejQ(ES;=k(UAvorc2i>b3A>H~+Rg?+P*5z_H1p4*X8iktj`=MJ9R;IDm_ zb19m<*VZ_1BhUFsN6gbkd7s-T+q*W7FYKq=X?|lLdu6@$`;7>7 zTz`^stXZ~Kd9HqveF!{PmTh0&iO*3Peh!@e#yE8F$yMfhmEM6nN8taoSg-Hd=QLw0 znx-6g&GsqePr*N$(s0f8=l$l9AREs~m;_DEn>(=w@ND3#_(rzFa}vzc_teic?@KXn z4SAtW=u@go9sJjr-%NgJEBT4H=6{-eadyE2dC#a4#%rHfz>_tVC0o5VSC<4EHUl2{ z?F{Kj_DUVne^a(cAWO$y%ut98n|Er=dwR$lK9)!JfnQR!wh(q&Uf%U6@96xaqcG{wEz1$-(e)&YckyF z?1&@x6ZbRW`M%DM7V7+_WcE>!L!SNPrgGR`S3Z6ou08^9JrG{?ijZ?oQHk*_5t)xT#Lz<&)Vn@#XR+= zmvagxfV){3@Y$c0MH$~b%0LV8jQ+45Axvkj*mEGPePImsZo;=vwzhL3W3lx75dBkj z(v`-}=8UJVNhJTtaU=0kf8H_D{$bhUBCpi1Hy%{@N76Cu{DzSPd8WmZ!TFf|=l_sbj;VZ=wA>GI?k=7A+}*OT0#0AyoWj2feK5>#fpuOX z%u9rc?Mb}ob54-)V#eHzdEJD1%u0*#(IsUPZGRvPlT?6kg-=d$afF4~`e(p=2z^SSIs(uU+Y zd@kFBEbH+rokN193h|;I=uzkFi+ADkSl4I3tF5Ashq>&XG5wRKlmDZ_HyYMYr=yn( z`8zJG;ktwCCa!4?dkMNaA6;Egoq+Gn_n^UV|Fz%Bhp`2w&df3`_ECEtazO_tpoeWrDk)z+jw zp-_3^XMC%X_NPj zUieWD$E=9sB5;&_Y3rDp`G6^ZBwyOtSv_A`+1clI$3E9r>T|8g>&{<2)|Z}FJ7ue7 z^SIHBee^Z$O7f*=(bwv=d0w;!r_g3~_1+1U2W)H7q2xtWhQVgcct+vMTuk6qE?_V%J%A3!^ z8^_!6q#IW@@24^E(;;uT4>;~B{5}42_^;r-{+LNdAg^O8!jR%d`>y#IZQ_spMC0l3vn%-ruuUTrX0dp07EzW*mF?xly;F zuT!Dvc&=%EvR2SU9IS|eTSi;#-c0-P*Pz2ReWwo6+A06{j_>59@jK^@w%PSbbb8w% zuKO@9r@c4gtHeGN#wKJTZ#)~pK9KK!>Sl}>Q(M}v@1ozLU-4?-521gV=AJdvP9Uus z=`-GA+W7%5`^n7%x4IU_0L*91RJ98g2pUb2+i_;j1<;D{m@j0vk@%#w(-RHiT|CjiDdwl+5 zd`@ek{}rEi#QfjI=d>oeljmHg()c<9ozCA+YNgH;BlbSE(Rp)Qg?Fq3F;oQYWo}J4nZ*687|LSO~-wAHl zD#hIg?tI)fj*!MCaf2tUrzhO;{JY8Ode|?qO(M1KaDF@W!(npxL$Fn>fcL_aC6~ zx^i1L052b(O=V+1ZxhCZ$?#p>9}}OS7M_RO|Ap9h*K}{>x{Lqr*FNvJlj8R`a(miNyVO1B8Qv$| z(^huC>nggJk5lTNa7p(<8Fa6>F5TV>P5NqKZ3DX0bNQR=Ra&>WKE2n-u9^+q1Z!UmK?HyOCH^trMN?T)w?FlX>0DPi?RC zutwa2&T^f?wKkJ|Kb@CitKIWR-&#!H>Zfn@4f!&}F9)3SBt8Cj&(VZN>2O>MSL#&m z$SCN{m|9`4IGb#z`JFu9n_y!bHpMpe)5qlDLB_9v?H>VGup^b?yBV{dG}JHDm6_qW z=ogbVow9ZJzudfI;!5_#@V9Ryd%PZd+>1`FVw_xEBc4xv_6g@Z+7aiVU5v-HZ<}q) z*Vr8sg-`uvb@lM=tEl_L(%H`k!UN%GQ?S{gOzn!IEI1#9Ct*ydozw6KQmo@4*7i#F z;W2pn<2>ni;Fwm_F|e@WIp>ab#XDaqmcJOf^8T#-E7P?+i|eO3+R5;;)R!MaM^7xb zG3#fp2T;eww3K!` zmVLRg@<7mqsPDeVB(85DcYU|2GK7cw(jUG@J}CPY@Fm}mk!0VnAF^K)$9d!W_HAkW z0W>;hXxDxI9?z~Vq&e3uJWu6WyDP1(HF&R`O=}IV5tyfs;C-5}!=uSlW1hC$d-FOA zy=f}zO*PWz+O)8)JBZIYnsw@HwAa9aozu5>-NA9pb%zz5`(Ag@*UI|`G==_=Y?t3} zkS1gQp+58`wN-od8T?`MFCjnBMSg0p`JW(P`M!&*ynHsia~vXe9LDxDV)~IGefXH} z9t8Iu%*TGM;IFUW$JKoa_i{~se>wJ=d|&-8V~)l-eHXy<2&s3naYFZ_+{*qAXc=&A z#W_xhy#YHG`})IQ6#d0{&;19+eM7a!&T;ge@;Q!eJ&3k6f@e2<+UvvCjV_sYK+Nll zc^k-6&&+$2dRpjz@=ZU>v{wP2$I6#xmSgd_(m9{9$H_=XIX`ix>T|{|#GJ|yGUzTwHIW{#%nKhFq@AEx+JDAIqd2fBGFL=K?j^peI(Nk}lW)Q8y!`<7 zi6IMl# z%cy%kxaNUVe99~Fb1wW;-;%GD_<78J@N-hc&zTlGea~Kh#yRg8BDbMB4(2r{yNr}Y%m7Lqp9Z;CO(y+G!DOPM0y(d3$JlKYL+pDy~A z{)zFC^t`g>IKE`7SJ&#@o0`_@)zM^s4^|Mbhkw$yuPq7_=C$+i-=ZF24R zXwu4Yz2E-4utATB>p{P;u1$5)*G|T^IxZO3TV?yXSN~UYKPlDI-g_wR9BZ7%eAlPF zcH$#>b*(w*3;lC3dtk~u$zyu@HjZKGeG%(Ov1;$rJS=_hrJplr^H{Zhv~bBrhBAzw z#g*?q!3%AOG1o@qll$QMr}p#AcMY+$99LHyKk;+J__>QVT1LNATc{0sGV&My>hXpA zjpO*dDm?cO(}$VJJ8wr>J&T91C z(du|T8@&>HUbfMfh$2uOI#^utS@MSzZXLO9z)*!nuC-mNa)J}Rn&nwg19mkU{>QJ}R znjCYJ!FZpx3A|U8%en{5c`2<*?0WxVY{?q_jwhSB?&aTp&Cs~A82d68na#t# z%%fe}7X4h;u=R7@Z&5D>rk^2v+0UH|O>L}mV$TPD0F92vv$3i zyfyf8UY!SMFUJ7;YT9PnNe9QgYH{5qOs#idE4WI&@O9FZY?k-E(brlDE#g|!SKx45 z-$g7x?U(662ZrsJ!H-S$EbWz%-|u>?|NWRc+hd*Q#X7Z-?wP4lf5NGayo&SE_eKAX zsMqtr)dr5ye8oQfsFJTZA0EnkeZ?>br42v%Sz(iuVGn$GqWfy}z4Uz!z=rIr9qHYK zm(vdE$$iC<_Sn?(6@4eK>?{7)Sk_;`sSY{*IF{>+?IK-&F!v$vCv8Y4hx?GnppW}% zhdz*Yh90z2-pvJn@&O-UY_;C6Q%CLtZYN#c*%tG^Og{Z%*qA?uKDokwT(iHO``uYT zEys6~|1$wRqs4b#%RUVI;xy>B-FbZH4N(v5=l>+l_C?xM9^=CP{@TzQwr%+o@Z~to z&q&K-4fWzQ?(3I-75caK>|Y~q-Whq~x0f7em79EZ%uIU=PIApE`cpSi&OD!u4S1h? zbLo3M`0Q`moMEvY_Q0k&{#q!>Ud?&C zxu4m}IMsme>DQfY{ooFLo-yla^1OHaa}0YX@p1K3*!KaO?@M&tRDY7siLW@>XZ=b0 ztau#X0zSq!u9_wwO0I!h1e}hwvrW zu|z|8&JyepvTFeTAor6Xd)u=s@=W_BZpL-yXZY*r^`jByCScqTd4A-z{UeqCCV4gB z<(>K@&aaVnP_5WsG9jj2OPYA=(OQ1*X(U;nl*#&C;HxL=Pmpe3Y=s`j);X1Hf)1AD ze+O{;lK&aNl;wXpW4`i#GkN9qbUX(w;*6tlta-Z+q!C^8+^s5o+qjx>A!Bit|8A78 z_=ElS`Z(m<81aYl$c;YQB=6s=e-ryyxi87*d>-pNo6eiUSyR8i<};ohb-}R*R95g` z!@rL|akF514&thvcR&8YF^zo=Ax)h%U)vzOaByO$`KAeP8gu`h@~n;P{AzKA;P2^A zq@m|SwyGuCDs?L9e)|5M{!3wZ>8s%Q2iEg*+r?VRP4*SPhZl6Z1b;>ep1vL6nQsQW z!_d3l=n-_Uva*2>H;ggs|_rr$P@ z_#DPF_*?YFHx2yjqmSOC@2Py`vvN)6S@qyQB;A{h?scMj)2ql3-J3yOe(zY@tzM)( z1L}u+qtNZEf~>hux@Qb{MulBca=5bsI4A&+z2*nRSC16|0!;2Ch-%ePgJKFai^uurL8^(5#Bbxn_Q6J9x& zb?2hY_bmGdA$}I^Q%yXBI?}uCDC!KpQ57An5V!DrQuMAd!g0o9lfIVY&*n-oE;q+K zVhSO)tuL9*PEqeW;io$0JVsc3OZNh*TjKdBc!HgY?KO?PRE!nF{lv9do4PVvJRKR& zsKEp5OBZDiWQ|?BULF(cj?>U1U-ciX_c=dtOnqWyFV-H{DVu!6v_}@dY`}fK)e7HD zkA2ZGGx0I?S7l+^A+^)#-?xQ)?Q}D|G+jA4zX|1{9gvU7w?H1@Oi|}q@}GAy!+t~a z9jmbsA%>iugN&68`ml#{34Kmw%l=RDee_Mn(T<XmZbYpd;apwA<#ON6?z|&S&vX!FSZ}I*Mze5TNK3)+`NlRAwqaW*4y#M)iKuk?G%`j`4Y#W-3#J67p0 z=QKVywG+JB@>+b(aXCgksy?>&9`|T_@6nF+3BP^y1FTP_I<3Pr=LpLBbI4s=uD#Tj zTZi|77SiRLG-;~}m?gmnu}<4|54_9i4fmvfCzElqhvzR|yg2o_PGme48BeQnu0ST^ zkI=rU)TORixAyuO*erEGnRyNNn|x`tpGu3ov|K6^_{g(-FwL^=y%5&t;1`f)+ozd^ z%&}9?6-R;zbw{1nKdo1feE;>Mk2d9an7@pOhyGF*Pb;)&^VF|IZ{p>#j&BvtRM=G5 zI@=%YEp;cG-2op=Q}&kAx94oi(ErN3X(Lbm%7etSHtKd9Gj7m=PE6vtEz%-R^OH<0 z7i`lY%!&Oe=B7>zAp2)vcceqT>8$V`3g>0JZqiPgFxbxPv`0T@95Rjm7S2LWvLJTx z@?;~D{@|Ak&V9B2$v+6|J?Qe4@JCzrtp6BpYtuTTb}Y?JlOCzl(lZ{pBwNe)6!!AV z=i{I&!8}JB9O0kEy}bGd?RqEQcH#cr-ZK`BW#4xV9M++ZvTrcxsQVt|YtYe3@vR-x z!W<~*de|d2u-x$+JksSm(@F2d6?8MlgIy5rK6s9O4t+yjI@UPOdWCD@s(#q#)7nYu zi)sAxeHv+2w!$X9Sf}@iFXm4GXM$l~8)A9FPJR!u{Yjo_j-kD8 z1+FdDFW>D4p5LMlYOn2|^ueZeFT$r|UbdF&`xw{PaDE1D+_vV&zWY?by7oC8TE-G@ z_#Sk&9)Cb(_bZ3~d!`NDo8Dc@zohh>m#ufdz68Ig^jxpZXlWf8t-@VUdLFIp{^V@{ zUn}xipfAzMK40$NAN>k-7d;w_9vpgByYH_|V6O-~1m}p7DdW13WnLvr zgr7DF{8(-t^ zTPi!-=Y5oYZLIf5>{jQwYkzm(xf33gf6s`|PvY6L|6Dgnv55__-S&?%EiaDvW@LPN zZQ@gvzEbHOSQdTtdgYIa<*yNjwn)p$2(LZ2{5X6*Sx-LYSf!n=30$aVgmHVpC~u;=#RprxI#Po5NBM^dzxHl^H2U| zGy5fl%YDJlFAc3>BwKHs(=nxi`FC?|!uE?;3$%@m(B64x;lHN5`B~n}N_p<-O66Wy z%InPXjxFW6zL&~PDdo9;JLNs2l(z@oq`aq<^7^y9gG+hFl~TFJQl4i&ro4ty-ij>m zFB^yRWpS4Ghf?0$EN@pSZ)TRaJ>_9%m0Pms7kr-gviR@eKlKZ|i_d=@|6Tm2t!k`X z!haM0iC;ty_%GtWmVX+DoUg7z7j8QEz~80$h4+oxb^4Az*aa2*d)ljA2>pREfj))a zF~)`KsoM@Etp~VC!l56g=jWvcIL44p>gr+)X+&o~jUGEMnu!kUYdy>7D!w3io=<;N zM~iW2sJ&}H-c;#EPrFV$U~n%}8*Lbj`P0v)#eQb2NIE^6a3WV@MH3mra=IGh*w#q? z80ccp^T3`l*KA({jxg4OuQl4JPGBa9gEjmyCA{ljI2`X>E6kzv6-X$8=N7(ltd(DF%IQ*Ii&zf+}gB9TSGUwadaLx9oN4-e!YfHGd&__1| zyBXMl67DZX*tdfFbZ~crdmZp=fnN)JrG)#+67FTdUIOeT!2UUnTakB{Mc9+TJssR9 zgL^LUbAX=%{BP+`@IS<<<;ju~?&-jG0^14f;FuV3pHqUZ0owv>3$Q;g(J`+CyO%z_ zr^+84-&lfuM+tT_uy+G{H?X&sVBcJV_5HcEz^(=MTP4_2fOTvb^g-#k{vHj#ht7-F zM`tc-sLc6TL*)|2iK*C=h5WTItNCkV?&YtYX~VW?YkK%=Z&vfy=4|1w-D$zzXnPj% z*Z$ndUmG+IyQ3ZI-s3Ws`Gwi=UQ~g&)XQ+ z>+*3-JL9;u_TPi-`L-0X40txjzErZQBhj+9M9YtoKT`gzK=x9r6pR< zjPn~~_pD^LU zq;C@=E!zCMk35C%fnBrxhb4OKMvLv>pEo;l;;qBRVc>A0ZJ=*7y==orYp0`DMrf2l*FK6%tt|(a+HzZ| zEtVgtEeDs{vZuhs`uo1^L0RV?{LyfopH!zUj@6^JWeYeQH_~{ztkf3EkJOee@qI@9 zN5}T>$F@v+0&Tge)RyhasWgV^Z;aNKYfEi8qtq75kJOfHOKrJ4wx#n4wB?^mZTUu> zw&e4;dSl?(rM5h`)Rx@m@~m;^Q2F^juZ#32-~OHA>+Buq#K#P_rA8bT{yMKRUhBNd zxTR;lyPlioI1?G~lRvAgG%z85ARwtAF(V?*DJ0s(Q&7AFb_+6D3!$v zN-{OpEgi1+B-yT_oU)yh@o8VOT`-DlUAww0qfPmKDe}m7hZn=6Z<~*d)3>eURhc*4 zQO2(*$=EiyF6R5i(|}RdXMsP=U$QprOV)|Uwk7qOyzjR_=O?mu7*{!7{}sNbaZ(*m zWgQRfKlS!Aifd5`*Kd~QG{$!irReL-0c-C^;ykZCKqpWE(wZH@6z6PJXcKV)4 zM&o?qJPY^_#w#P8EfLSyActaqmO7KiF)f$cCvWCbX0-TL7w!B-)WebDTduLNj##YY zqYuWYE`;`aF{*XcpZCQ*QIC8teWv$}b&i+zFVCb`hm22YFX!>R(Kr-s`Z2uLPWT?a zdUATrc$Mdewox4^AC}`{*g_7@cCn$!;t{5BlaDP}y|q0fE=+ z&m`KH?Q^y6HSTAne-<~Z--@hTrHNtqpZu%KKNu>1zC|)J;6G5#Cx(Sl`JVd)(#syKc53$~LuO zEo~_Jv9_&P{~H5sZ-!>$k=haIRu`<_n56cgA3TgNVGl&oWA}a7$Mgw}lPCSD*YB3W z?`~r8&xg&%~`Z)aUsc_n{m4{#f>Eh5bL{X2$nV ziCcy>RNF_}9*w$F*mL&;asHC;B6VKs*#iS@@T-dK?ZcqS{RLyFcPW4NCG@+GS$nM9e4Z8QyBr$! z1?#)j()1+cVg0sGd9(Lwr)3-3mxSlh>a$;~2d~1$d3`(e+FtQK8=mI-hi_xe-u71M zA2-MM2`#UD@1dM);_3jpvCVf`<)7)M<#dfo54cV(wa5Mx+D4mPS7(pf%f-F<2kKJv zO*io_AA96nhfDfmzmg``>Dz1U2hL(at(5-=<<*&VK8t5yw~^jWx@V*)6WiBAZ2GV0 zi+U8=uU+UYWG0_ZwQmEL+Acqy1@D3lLGE}AP1<(#zek#D+IIKt4B1cZhWu}dam3y9 zo6X=*N7WtoE$JUypSqEF`YdH_I(pPz(E^n9=u1^X~)CpMZI=B ze}r~iQj%T0x@^x+Xishj+Ue)YZYtL_w&?@Q^Iv9jc5Tza)a_U!4;-T&k{0-+tQ=TCOe8a%-eTAKG^QLy4C0@C7{%{*v>o#GAL) zp~v-%GClK4^n9^I&xIv=K3n3=$t8M{oZ2(qytEE2uBnu1`MVM=OQ=h}Yn#-Yy|q&Y z-jUIx4t%J@pChR^A5)~qKHNc{8cU4Y`$M5;3%H(s>`A+fX(!!reWy%QYl)^aO8hxK zqeneHt;C=Ip{{zmvqPRlJ${J(TGnINn98(_FVXVyI{Y~-qeuQ6SK`kvsJ9+}c2O?z z=ii~H9)Dc#D$~@T#enSRPc8B1zhnKWpC44B=lT+Va(%93Jo-`{9<^lg(`0wnm+1NZ zhR5pAri> z=$SGKJ^z%^^VB-@xHem+=hzZGJtcZNOZ3bx(WBnxeLmOeQ!;vf&$wIe_pT9_={c}O z&$JRfEhTz7O7w)eusRaQ(wv@WXY?4GD%0ayb@I&y*QwFbyPb2_iupv9x%ppXo%VD4 z)S*6i^1Ua{yHDf1K2{i8xu&^9YHK-8-x=wWo*w2)lc>Y{+1y*7_wE<+eUUAtx{sr* z{mc1jJ1||qc<=tP>D)IkUK)crPuid*+5eTN9Z+d}^8uBO%wZm$eL!W(=?6R}9%H@E z!4nOuxaVE1K#z0q)_CvtbvokxCEQPq_ZM)V+OmlImYBYP`&zu8$9-G8_nZW2w%%^; z+vEK#?kC3k8Qf2c_tUxWRGKmjZt>HhlhxYRCeh2$i z`7i#z)V&RyTvd@aeiI-Q18PLr1Mh+ABrmqD1Qwa6_gcRqDDnTi7p5#Dt@elUDg)`KlWX86GK=8R1`&YQUA|V=hW@f zr|<3V3F5wQehl4x&sUwQI#qS*RGmAFIHUB8U?={%;44x3FqWQj#`T+}%sqVj+pQm` z4WINq&BZ5e?hv0B;`ed*B>ape;=a@5v~iDh)q!68=6;m);ePZ~{N{d??^LIMNb}8U zxFzUis$4}`dc+;;Zs-( z;u}HvxA-0K(GCShW^HnKn(}&sZ7=&8*eR|(s<04$lnLtV=N|S&7JtOocFddjx)Ptn z7xyv5*CqJv;;Vq)#24**;wyvS#24{Rd~Lw*naDBZ`_HEXU!)mvudpw5VCUTD*MYqe ze&cxJL3;<;UyCQ6A}tem;_@bVB4v5vF-L|FFZ;hy%M)`Qo_LMN6Av>l4?LuH&i%Ii zRC!{%#ou$q6K@8-e4e=7ft_>TRR?yLCtUohj<_Ctz5_gWHP+mcJxmhbUS<&V%tMr9ZLCyKgx5;R?4;`P~D3%N`1_u-ws_~acv%!!cwIriV`W4hMPoqizDQ+yyv zJ{|grruSU7ccX(p>bTh$zmYFT+QN6F=X%fBHh`_h&!ppg!;xDY=PBT(*>R45JfK|q zDaLW}g8Nev{aZS2uf+*@o8!`l)6}@Y4QcqZubZ{R@%x>1#=gD^cnaWynHIj+<2U2g zMZVETH(P16Y_IF5V}4OP@ZXG|PI0Y%x*IM0iMR$`=nEsfFNb`f-82>_fl_akCenjGf;* z+sK!jIR@!veaEgZ@C1~(`)W+%*#9K2bIn*>^F2VbfosaB+fl|`3F>X;mlE#js7u@j zpd)>cv@MpQU&6+*cz-c@Y8uM}{|)edbqwB#Z{qzyd_D-gKWOp3rVifkxvLiMZ}sqg zt;IWe{Ct$br*HEZjgOx`BH~G9P%B`=8P21EJK&r2%7C|(CJ$mvpC%WhTyebv?Wb8j zOTu~Zg_*%`V{oo#+x^jd=_~Z|&xVh{yo6uwd)H47-TmQ*UWT~f$3Z{U#ba1E7l8*p zFZT@?+vFnf)Xf%WgYc2R3AiQiTcP{16W$NWxx2EMXR(xB zayJ6rNyK)qM7v+2AC+VDyS*FZ%5U)CxA?th=RVrxZk;QfI!6*V;Gcc-+>-V=@5AQ$ zma{zY3TZnH@S~obA^!^J1@7H*kbRQV{xXas{a~%}4nf-2JR5rpXvK4h7eGF8UMlC8 zg8%$^a$dO43d`1z&cwW3pAh8? zbsO&+q%H{?$2t^&Th{-M7wfPUXW^IN&*$gkKub9XLEnUnXKS33yN_i%7bfmE8^t>O zqva{$jPoQu)Gp^Uu75w);kAIB>r3Eq@Noym*Y(A?qf>xG@-br_j^;SvSMWk&ti!eB zUG(>U$RX+-+MT5TIqWBF9P4l|?mFcdZmh#cz<&vkY`TQL8q0g1aNgMYW>s=tUG3$dehtFQ?^r`&)ZuHk~X-L`4dN)xw zU=B%f8oN)M5s*KX$3oDUbaMCYf9J)BY{1ymwXz4toO%Aw@9z0O_wm$iTk-k9&Yk;9 zm^0Sd{tvRP#f8E>lk3JlBi|tAWvs3mM%7_#8MVgaG zxK4Oy!UFIa=gYN5U840kca_;m^QRw$btFHZ>jJR975Au;#$4M+!wzFP`&N7DtAr2b zhuHdegC}@quJsr@!MnJ*H{5~q<)^T&dCq%m*1%K8kFfif#>nObDwLS^Wh%oZ@jj* zIc?9dbUjC4n78$2s_)WKB8Vd6|#*w zmAZ8)K1cDl3V%O(=d^(9*Okw1j1lq0{y0C65i zch=?n^u7e*3bIaOlK4GD{SdgFhBbbfrHSIT4SjO{#5enAY+D2mcu&6fn7!|G%w756 z(5}m+4RH0-b_*+cn01~6AFw}N2c+o&j*BwY35pl@j*3@0@fAG3n};^KA9C0Ie$bEo zE6zB_4)C`=Q+eHndCN1vztzrrriGhrr5^A?nznK*+viNq7vulC7K%$^*{W zY@e|_b(Q5==@)p@zCn@L`Ca7A9a8ZXMY1bn5kGENu#hjS)N z_`m;^#=4-7wl&t_DtxYjJ-rIDfx6%*;6vrU!bN+S{eBmpTGmJ>kS9pX*Wg}k$`R74 z9dtR|tS6Csw99S+tSl!j_S)~P%Xg5Gzp1|@CT-gL5Laku-)L;=D%vOP|2XKBX4@&u zZvz~RKjxe_J9Aci(3WG_Y|1^rt#X07Z3A@M!!YDD)=>G~J-14cUSZ|jN_j*3U#GkQ ze*A*P$q~xGct(5e1)$Zt6<=BgzNG9>{|tR8+P9G@xQo&9J@va=)&+giW6P#_Wvfu8 z?f#&!*|GTUkEOQVlU~_Yr`_XT*$$`O0k7;vlqoDf@yhOU+WpWg`;pV`KCkQvr|cfD z>b;eD?1W*=xf=Ry|Ohv#H|{aO7yF%;F!qm|82dJF>{}AX9{0xX zvSTlUEQ*^qZKp=_{(v|3j)bw-cw>{-Iq(1C7_i5b7K-y5>Wr=NA6uNUDFZJ~7<+*? zHrEozreA`&tp3=SqCRt3{g@%2=I1%%lh@8k82^Rd_*}0X|3=O+X6_g9c>=%b+)s7J z=G+%0jQxb8XUOL}?AX*badThRCROc?(@Z+yxe&V3hTYTVrCG%@$zo3m8W zZJhg)UyfhnpYz71Oyk%q6Xrg>$+`c#Gd|~jf5Q0h^~R^%5ct_&OKr5Z#FUZb>7(5B#b@ajZL}Fd7qFl_O2$zzQh}QbHdn*y|JlRIQGnh zv9D-i?ALo^pO-N9(cak9TO9jmSQ~M?KGMY4^ktC8wt#2pcc85D<4QC>g>fZ3Q)L~- zl`zI+8rsu7`x@>Demlk!8xS@d`y57}go`}agZhj;;rCPVz0ZDUOijQ2&X}52`0mHJcZ8L(s_`C438Nq}2+i>}e;79lyykbW10RH;lju;#Kt$N3d z;9L0XvN5{|d0H{eCO< zS&!iFxKAOT2Y*-MPjxHx+b+zto}ZU@HqFHvB;L4(iSzU7olTzx48VEhr$rl^1H(9D zb7#|W@KNy2ruo!8SPy#k^F^-D*(M+E>s-TtOKnA+%boBawt)`MysP4KQ1wX>wt75jvo7>w2yN5 zX4;J!G5-h)T{95FfO%@jMX&nS!hCEna?7`y7E2S~{ z>{nbDVdJy18^wOW`5R-U9JzQYWXa|Di)6l)Bh8$Zv7NgHnmH@G-^?>nh8_yvkbhS8 z7Qh}S2k*l8N*|t;{hEFEn6t9i^1CC0>F=X&Ixa7G4`c~_lA0I%cFIQJv=43QJ6(lO zo&!)Wx_QA9_T-~p})#uYb{7kn0Q$a5;%Ka^E^zCgO}23mU+Q*-`{**@Sm_oU7BiM@b@sD#)9$;PGdG-$@z70ogaSSV#goPv%t@wjphY^ z40Lnzg3rM3X^bNDGZaIhefQ(||wn!}%Cj z^MdDKT_)uP&vkG{UHN^qOXLD^s4(i;Aj}W*c z+>|fal|*^LuOrN%iBEE40Vy|#e>X4qd+38a%0Aq@VDg6I^mxo)bW=iJ@ZB+R$JPhT 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%0Af~WJ0z8or_#F3f@GxRNmy5| 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

9Zt;jBQ6&_KM-+)F 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≤DmC(i1(mo!aB?A|1|v-*uCP3Q=B9V(1j#O2*d zA>1Z(in}X>h!dB0j~6bph0DA55H53DrX7m?>Ak_);ErXW!N7e1{l!wysPcXjzqI^T z(&k1JzeJ>bAdjRZ563N7?>@+{A0c!3HX1V?3dnA&@pH13Yk5O?@gK}YJD|*{;o4GZ zfQ}|0vaW3u_S;1V*G`hwXe(z3<2qAE3qu&M;}@oA?edeT5&oD*g~I4$T~Z5z*CmeC zSV=6bvZ;%|eXViCd0)uFs-Pt{ENkXP;Hnzp!dZRuGBMay<3#!Emo@X7+Es0&S3)N? z^UK;*#{?!@UQPXUb)cw?%S2I^1d4)>v!Nx#$#Q!oxR}S5NQaf&{>%BDG8s+dtF)0h z!6tjS-POgHrzAyXH~n;H8FJIe?&Z|93Y%e?lr( zJee6cJy#;gxaoP8qI9>2;KA$5xVIiB6QP3M5%GpLqzDNhltQk4-ui89`dCVy6K@4mC#KO zaUHtpAtH3s^9YEE|93Y%H=?xAP0xKGLN`5|Q9$UX=W)b@ZhBq-5xVIicK!!$dWg5s zP0uUHv&&7-kBHjMP0!^>5OXOan;MMx;*o_yF9Z>|>3NtQ#ZAxCc;vM2nY5LbantiQ ziZE_^enhmm`Y~>LD8#tw;pf0j4{@A58goDc{@|vk4F6*8HwblT_BC#LC^qKqjo;i} z5U+g@qRxe>dnuVC1Gv}uQ~>!(F>tSI5nv!*l<9i>v9Jcx1(!X)BFwn#ab09EE_?cd z2wnCRfe2mp)PM+G_OyZsUG~fb0hc`_M9ig<`w=_1rG#*}>?x#Z3ZW7T$w3a{2H?4f zO6ldYrwIZ5mUDuscr-41NCM-shn|Z=KRnMM6~B{*A0B!R{qS6hWT79PHxLv0;UOaQ z!$Zk7B?;2i!w(Nx2&D>?mQ70h#L&C^@a#YkYzn^x%aJ1=%alU?Aw`I59ehXu&noc4 zQ;rldw+6pO6Dcv3DHrMKybSJ8fc#=VJncOoUH!2@3;pm60ue&GNZt_AMP>bWNEb;G zLb^!a&<{@&N(&)f%TZ0}hlj)nAzgI#Y5ed|mJrfKS&SbZ$`L}kC`SnCqL>iUMKPft z9*PMeT@>?uGJteZ3~o;ckS>bC0alzKUE9?aaZ*VhuTs|(-a44nL-XWSMIV<#J9!JS zABtav;CNoErPlIeb{-I2*YIP2==wwQ-$)%Gy4K~9C;_7DkNs$<3=mz{_nV2mcYx?x z&&kU2u#~(p?`vca5M4Jh5g@v5X5wa#PjhbJ9G{@osY%(8NQ1$mCgzeOs}Rt2a2)0v zcn|sqoe1@E0lIp$`q%`wx&d^(kD^2GL8Ryq&_#LH|<$!!}KwBB9Z1lfYtyNBC0ms;>XZl?ml=B;?;|0ESY93nPx1R-raH0SR^x!i>Uh>$3-Kc9;-OSlHG!2 z21{mRv=6alpHck=OGZOX=(y+@7q*oQuw*gh$+eRKmaLd4l?ELj#dOCgr5 z1Vo4>BTPt$B_krllFbJZV#$aIv1Av32(e^Dgjli%L4;Vc4?u)ivTs0ySTY}{Ng9lj1L>W6IRIs!L$Q^H-eeVrfU<`XXaHq2n1_He>bxPK zj5==!D5K6B0?H^R1e8&pcas62j0oFK27of62HD8~P)1a%oeThFL@k6Xt`JZ*8{2(h z>fB&P4FF}7G6a+fQvsk%mL<$54;!k=s4>NdUTxY`u6BJ*B4302>@jzIL4R|P&O8}DdpYPU&;#r zWyD|zC?g^Sl+mac0?LR80cAvlfHERNKp8Ov??uZY%b}>TkbiguL7J510gfR5Fs6^Y3bkr%I_mN~JVT8}CJTn&_`7nt%Q~ zPt-UucQaBIQUB$n2q1tE7f=1z1m@zId7>^XJW-Xl*6v7*(Uc`l*U1h1mMLTc_pVyCcY8Zt1T`jDWA1`0-FA>cDS z7tkhw#)uvjg;`C)pC}=fI*tZ&xn6hgriO}f?7$^XS+>0DqlIWWK za5$-79}fV7{zD@+*7{v+w;eqnA+By~04PRZGZUZ~eJotG#Pliqz_zuX3Q&yxsScnR zJ!=#|G5Y**0LAFvB4r<4J_%P3^t0svuIZ}LSd8i~Cj!2qXHK@Qe)=CspRcb#-U2?zJ7Wh zJS1r!v8DQE@H0r?QEOX+^)m23M1L{^A4by$Abq*cgIpE*s#&&GsgsDU()JkJs@C&o z+g4iNI0fI%)D3$9=dWKv$#r@x_^j8TOvkZJsQ=nC^nEhU*rTIYK{H4Q!I$FdWb%y%re!(faSx08G-|(8E~$BJz&YJHg3#y|Nzu zada6|%=Lz!*ussCJQTT}I?!)$AsZbU89^@C`; zz4SBSe}?`Fvd`4-p><~IV<7KreLuqI=;Fb)HCO+PBj~+#H)`BR({c2^dOCDFPoD&> z?x%m6Yg_y4=>E2KfIbf8&(}YqMGn;UXpw{T1hmKk9YcGy=^E(sV0|2FKSX~HJ`dGL zL;vmiCe*u7pNSSfOgEw4!}VKu?$A@g)e(9`i)|gLr@|_Z(#JR1R;L~h-5#wUfpo{{ z1Hsj?`Yx2&r3azRMY;=mI8HwW?H#X2L3`bL2lRY`9ztOLv9Z%KAf#=ioWR&?^y$agJps=sh8>@u)}3~GukDi zA4832=&`VwGxd+)=PW%5zsvR3LAG_az8S5yLf0bAIeHJ2wo?Cs7C%=vqSemRXFm*{^$x0mV{pvlYhRA!M3i_Rgms#{T$@|y}lUQTdOaCCa=*CBIUKZ4P5;}r%=l}eH?1J zPDh~mKk65d_j)}Pnq04Q(Z6ocA42LI^#Y{1N&g7hZ`R*J8@K2?psPRWLy+cHeFXZ& z2K^vv{ImW&^mdybh&H`le+&)Wp|_*VJN4H{f0wR;p6}Lu(T?|M4K3ZPH$sQ^>3hN3 z{rWCQx>3J?@*mI#qm>`j-{bcoeI5GI!+H&@?h*YMboHnn0j?g?hoF{C`V8pyaeW~q zdP08;4gW>o1Ad;=%fRzfdL*>;SG@`$PwNb7e@3?;{8@b!YJX0D4?B5YUx0F6&>w-5 z7xhrs@JsqTSmxjKX_IVgv%Ugt^s*j{{_=`0gFau?t+1IbxGarv(fhVLn}Ag{^gLN85uAJx7hxnpoZH1zaalG+aH74 zhui)XWFG-`VGtv2|4UfhDBFJ$hH??YQYt^7tLc}Dq%K+I>AzYIb@r~LJ(;d$lP z!(d)e{yK>HqVoTMhI&c)m!chCR{pbShF6q-9W4D-<(~?LZ&Cg;ko`5~{}J`QuKer4 z#8%}$2l?Jmz72bMQ~6_I5}zyo3Gn$J<-ZF}e4+ePU=Lp@|2Htrua*BdwB!!u4*^Tx zD1Q^w`mORGL+#%w{{=L__sTySLjR!r3&8x3%D(}DKPmq$$hTAZ$HF{+R(=H4{-XSU zp{agVeg>Mi9DfFCupR#!6s8=%4cQ&XuL9*d{$%hHas1Cvd(`pYhk9a;Uj`eEJN^b} z!gKtKpqDJiUxL~bjz0(XlI{2jm|`EtKOYLoar_@pZLZ_LGY?ka_{$O3&+*RyOZkp} z5R9R~@jpUfq2u2Gg_Jt}Oz3Qo<2xvNu;ZTuoegpPQ&D@F<3A4u8yvp^ou<+8(`dLR z$Nw0n*zEWNz*39j|A>l)I(|D0Y?$NsNBa$T{O?i9M8{u-z*fh<36?d<@w<_4GS~$* z#qoWZ)E+g+! zj_bGMpX>U^<6p$}e?pR|>n{Zbx1$&0U)=Ry*#`~a`nN#IS+4&8{v}-hYXoMyz5{mq zxPCtV<+%P3{Ojxb6-e^)1b-G(_sax-3R33z*3Gc}Nn=_^Z(z@r=I~ozepZ!Ll;`AMh`c@k?Pg*%^Odv_qea|0)7= zGX5wGd3`hfm-v^P@jpOI=VkmSk-cBW|1A`mpYhwE!Getc8sZ8w{`07>DC4`xP@M5U ztjCa$@n1%-N@e`z(5KG$=b?x9N42PLK*qljgWkZ5zZoh0jDIn@Q%T0Z4((8y@gIhj z56bwPkZ*9tUjxC0fC(6FS;pT3-M1VRc&W(vQD~wv2c!ZG7lLX?RaeLD5bKqNUdjoS?4AOlThP%n z{;#O-RA>;II4$FkfJyx}L~)Me;Zu$~Nz0nDF~ z@ecvJXTsP~!&w>sGFZ=YG%ECYcE-n)X01TA;OrdO3rblD!78u>MPC_=H7MEvZF_#k ze;=Y;kSTc|{T=f>Z3L&_A?DI{Q7#PS42@^7@8b(YIm11Q5FfQUBbXo`we#t$no{v4 zp}yPom3W53UHtMTp+UPY3H8M#q1gZsX}%<+`I3<4OG27232DA0r1_GN=1W5QXYiG! z`I3<4OG27232DA0r1_GN=1W5Qyi%OeYB~exr}>hQ=1W4FF9~VBB&7M0kmgH5nlB0I zmr;Iy&6k8UUlP)MNl5c0Azd~IvT42~r1_GN=1W4FF9~VBB&7M0kmgH5nlA}yz9gji zl91+0LYglLX}%<+`I3<4OG27232DA0r1_GN=1W4FF9~VBB&7M0kRArjkJNlgNb@Bj z&6k8UUlP)MNl5c0An>4W+$Ey7ewyzRX}(LO9R41^9P z8r1tP(LX33-zC!PP)9=ZT_VkQiL~4$8f@+o5ktXIT%MP6;1J||lQf`jgf?vb4weB* zCopR{I)S;w)x#!WEv7%k(kW3B@Tk5W8i>0?`J%CWrrf+s62Y6_V_y`tOdh)qn(fNoYG~+Zq(T5nPuSobu5D|ec*n? z|HpV%`VpjY^jQdT^=0^t=$BYghhR>R>5W*c#PxId_4M2L&C(07G)d?`V;PXGAB3X% z=xNYEj$R1{`{Io+v{|6$#-BuO{OK{ZPl80W_`4)(Uq1eKz4LBI-SdKX0WY+I@BMPN zAWOEDO>hi)WSfgw4waNm%ztVE0g|U`)ck>OA}HdN!Vh%n32In&_^wWhHbZy%QLJAq z{WIjV^>dV7|BA+N^x0SpxcYXi-Xd1&-&mdm*-nqnplrvW;c~4i_CI}wPcP^qPUeER zRmEPY&kRC7pb+*&eO3@M?ZUXR6LqfrM=FZ1xandTo2}`hn+h(vUFe*F=!$H$%6khB za;0r`Y&kV!9#Dms@OyGM5AW13l^5xj;)`yVIkzD_JaqH#3?%0+>Qn^k0=|SUwq>h_ z<0BC!1wSeU%PqIZf(Bq9=q70Z>^_lrnkfX=c=(pvlZ8}9#A*MhXH{3_f&JoayQ(>A z4Mn~7+8Yp=X2;pZ*-0{7f+(l?zEN@ZzI2vOpJlCy7a(IQo8N#Fs@bZ&hC+5EY!Y`~ zQ#ANSdwnH}W0?7lM3Ro?fkIC6)vg_h20En&xHZkU<8~y*LVVC|HRm6U|KBExC^PVR z;u7||L|^I=z%4xcQ@r3VQcJ!lW6N|7ciKnyaDP0JC}4V8&E;6at0xlKq*1V3^EpMx0YqJqwvOVIz+lZouZ zpo@?ZtDDs|Xr);GU~n|5BJM4Gm%=8AGuwO^uNUzinH zV=z1_eQ!= z(tYkpN3C{nYp*>d4OAODi5-a29ndY`?({ZCUsF^Q<7AJhEsf}wY zPBcnQ^QETZ#AvB$Ud^@>P*TeD+wL0f=h`%S(2x5kc;-;6kFDW;KOhm@N$rp}A80fg z+&5`XsEeTwt9A~iF6D}bhJw!xtf4c(XPUl@hA1AdsL6S_auYLYxS}R!mAHw6h0!)) zG-?=KTvLYGM8(1ZrpCpBLB$QD$6K{`U>Z_8vUqU2qbeFWgUzlrt+OL*ptMexw9byK zwCJppb+#jG482k%)|izYS$ha8?c6#$vf6sJ&W^03g4Pk0e47;vb+D6_nAy~pEN-ox zl~D^zKG@0S#0j_7maM=o0x4h53Wm|3Iksj6V^Gi>Z)EjujyJP@bW(^^c$uF(Qd}~%6$xM!ES?F7{g5mYDWnz=J zX3Zg~$vtch&lrEs;)x7et2uHsw0GMsQMiF`&kAOQt~$B`cZVT6YX)JmS9fMH&Vu~r z*06!x6^br>^rbBI0W@gUo>h(HXNpdjmz%-Ft2a75Giy-L1JZmq@64=8^nM-Vym<3o zo>f7KX>hoiRwQR<1%pMmSlSB1c&BuUa||P0(gju;od?6-xkl$5!sz-e)ea3~Y9nYy zZGY;p&BE^6gQC{WuyT8Cc7tKJS=gnkjlyrU@XMOzo99Cf!`;I0FvD<{Fg)Ba+#wU& zh^$~@3kLd;S;53Mv8Ir8H!6$KB?SAgc?Ks~YP6AXam^&GJJlE?VVh`RtcmXw4UCg` zvKBG<@kRyhqJrOMsVQg~tM)|_{yC3(;^_l1PQu9SwFfq06yUab&KpW?I*dX}%Y{p!qsP8!vi6^RK)Q+c)ipMF3PvdOmL}px+D+0 z?_VCWY1JM;!&utm@pMN~)Vi4MX+yK=Olhx_`abR=EfoVxdlL!pgWE_;Yxt^XtyfOT zF%ygV)EV>Xs^Oc+^bYp-GKYJ+K-xzgDhjgo@S)@00OikM5YF_(LmNpEX0jg70z zCz9rdniA&MY@P~wn3xauckYhEjr|1Oal|hB;&vTr;)Cuu$`k<44V$q8RHIFm+XMR= zV}uCo>pV|QM8jc-BG_f^$2iAOU&m#ZDv07Ep43avYcX2=I?h^EtLKSQzmBs~;rT#x z`b|90X)vOG8}B`$eiskAWH6$B9}id7yr}pg9(15KX@ei*z1!fYc+frOOLy66=wQSh z;;D@=h9>d`a5cD&IPP_EY|CgDOEj;-;S+!)s@+V&z9_=3&yNOIyQg8bCsXo^M%XqH z_C+J?d=d6XYvTqWyonVlrA##6lurfOgpwoUpn51I#Gt&?l=#%v}cpfb3!~AYq1F6 zvG&AxFxD;)#@dsN_U6i1yVz*AQ%wJ4li`EF^iMH$&X=L}uW{9ZR+pjm@wl1^!m2IC zA&RONU2jT6@n)Cjso_S~)uLB!cFOM=cM$v6DwALBL&S%Bd z8%52lVZRX#hMp@-9vWA7pv8sB1#z_wgjMU3LHrP7gP7u(O`{rNnEWBe20`7F$B7?e zY!Bq1M7mjw%>hCPlRw3RZrdeH{u~RtDc}76B^I=PM-A`Nel>Jpxt39FFsclRt2NM_ zRedO4_^8if`1WPgTEmmn=#a_JV)bOTw5w)KZ8uC{wOmEJs?8$ch8RlXap=A<@8(!t zkhg)a{cn!71`|t~ul;Y1&F(d`Y>t_0|1z^|j&+-vh3kH$N8Mv^ETtZZ@fi3;G-^G} zd^e9Y!@>ixa#>Y&q%}{6}KJ7}r(97xN#DnU%a% zojnY7|0!m7quvbHdo}8nebS#|DLI9hndVcRKgH^Wo+dK=DK=GPno^?Uf9oXHzfq%Px^F7X5CCL<5IK(NV^LE{+W|6E_>s#j%;P)9Dlgx;SPW7RdtQl2`?0 zrj;~rCNB+jfSH9b%C+nwsa$IOz>Zp9aKB1pe;{L6xztEbhs0*frAoL6S|)XtOPz3R zv`i|kG?jKprB$ZVV9crxtyK0AX+yWk7*=EGVEtEX=wSU<7wf(LtB>ur{%a5o^L)+g zzec0tVExx*n!Zz}tLB)QzNB}b8&elU`&KPGb$TbtHaGIuh_!)Ffr`c$n|mi3tY)w) zVPsPv=Kvn%#-p z%*@#6vBpHtkM>^8o*(VCnms?-do_E0wD)TE{Almh?1j7JU^h<}89uwMT6WxYims{)?5OF~sP_qyp#RZ<+i6C^ZvHUJHOMAQAbR2@P`>3*VYTeQ z>6ZwL9RCjYvUz1;k2fO!5^0f@1y<2)M!!V%l9k1_U}f=3Bsg4sJ6Ks*(Q?X9yMG=E z>|M>nYg!cz=8|^lg^r=;@`UIba;B^#B8HwOD}|`oDV_hyN+BkOij8BiQivN%#Uhw{ zrDqfy^vWz_Af2)}NkoH1X0SNP7WL7{BTI=sro>Nzr9_TVaIlm(KB_K2EA?1*ZeNFG zXUv^QcE!G@{vt~LWJkm*<<9+BsN}zhlF8s%$A1wW!{BA$`-`X;1G!9;PelR^2ZQZj zBSBa1mNmrFkwC-2M&_AFcwEgJnP(%td*=5MwfWm#+;1W7YuRtrmBM%F*AeR(?hglG z-zpvR%H8P(64Bx?N#np!IPHy?%8@4-Y*k!?5LkC9*k3`fn(4JMxPOO?D zJXoIopNO@dr;;ltn!&OuQX*Tw?ZMWsDKbH<_>;g!TMQ?`U^z78M23)Ik=}#l@JP6? z=dIz02;Zn98)uDD~!lUcxNU*+LA6V^}NHCXRFUafLu_nuorLs93 zXLxFp&Ea_A!CC#qQaR3;V3-UB;E9IGU~klFmI*k{p3z>gTFf$>c8K;8hOuB*nr#?st6`s3eImj6V8Ce3Zj7cQY9yK! z-$(K>Q@rSMLr>feCl_ph9!^?<4Vudh9Wwf&%k3O8`ZqW1D0~pP*$p~MFjKtj2F)DI z6tB2JGk4VRviDUtSeYGOLq{xXi=hvz*$RA;dd&?wQkzWguNyj;S*(b982YuU+56RF zuD$VJ&^8a{tq?+b(PM7!W$@!6(^f5ezgpw+@I3c3eEE(Q@JN_20KV@b_apwQI0(sbw@OQbE% zGi|Y0-np$oa-RBrwQ382B~V9;Xg7Zn0a%=7*Q=+%t`?nyx4cKY)x!JwCF#Ecu7`l( zGNPPVyDMPd9tv1TF9*kq0=9i*KHk~1C#;(dY`j=Ey|!cf8t;a$?G{o-Hg0-r z$F@CQY#Z*N$h61rX4>O>nfCbIOnZE2+E(qYwC&yLaDTt-!-$2qS1`=@RB&}6Ucw+M z*aCK?7h`n_I(Qe~nf`)|Y^O6`blTCt9^`g92g+vJfugb~?W$|EK74 z>3Xu+ozB^&N%%eZPUniCP13g)aXjHDkUlrfv)WGQ0V%$qjy%x$ZULe4pflOs40F|! zP_0!PCuq(_5o_89My!95SS-fIU1G7A8=Xe8+ib)t6W7Y1UBk0patj+J@gMz%STZ6%_ z;rL@0QIt>h(2K0}iF6u!nvR$=^yAM>NMv_Ib5*n zx=+M7aitUVYOHqe23A?EadHWPMKAkB>$f|q3Oa7N_RI6U>v{!s2m`S_6Gq)wwiqwK(<#|1=(~R)7h4ao*zelU52|&Ei&sFd=Wo zTbvddUpwm9Y3?v5PI(Dtf*sIsQzJ(I&%yc?h~m*-S+$d%wlb z9#)Oi0~zbWe&-%md&npZbO<{vd_+}J(TsIrhlP)*q071Ltn}G5kvu91sLwx3-SSZr zA5XK(zQ;^_cA8!GJ!ax_)AK0*CKF$fK9vk~lZj8J+1JG5CVoJg-61}1;!D%){O$?m zOS_e&8T9moY7)AtjJ99 zwbzkTmA|UKj)Dcm-afs#Ai!;9jO|meR8F}j#QI`E}$0aH0{$y zy6QCTvzS_>)3i@FwMeIFpDt;GPSZl2(gvNTO*&Z5M;ko{o0oM;WuQs)9J=gXdpW&b zEf=kAd^rLyIHa|%*+y&Ss#&zwAzG_Y4WhO7H20edHASRoOSAjCN;OKfI6uuE8!OdR z(`&d}RjHw(MK{g8s!B}~Eykr4s!e>hv_iFs&lN4ERb!wv(PCN!M@LDr@>-+CZqjM3 z(PCE}Ty@qOEiPug)ET`5TZ{vgnhUG3s@WZCe`U{uHpJf6lQ!6;WPepnE`n&e!Xr_C zHCd$Xw5r)1>MORr^LhBBLqYc1m*$v!uh_MekH;TY!z=clGP(e3$9j0h-k0>mSUc|i zui8zL0_ZgE|F7EPjV9S0>K40MMnN~t?ohYb69Y|>CSJFL{?5Jlbvx+qQ^>krw}bxP zL0Wp<4*Gk$*wa>%vrX)2t4Te-hTWlVwSy619`*4zOzOF$<2Ot_fjiV(rCg{{+@bbS zYWrr=@HX0w-f7#9 zHIyBpuCsYQ%AjnlAJ7Vig=Md^)23{mkJi~kC2zMilpUd7ZS&|cA9<@d??qwWtL>T~ zuT{;CP?y7D?z6@b>V09l<#vflSIdr2XW2Fd%=@eHgnInmrfx7QivGwk47X-Y?kh_n~ zP9reWi%ZmQTfO}ZH4wW*9cSB5K4pBOei<@7&Mq-bvn$eZb_)$8v|?{1zqC`xeJZBr z26#dmXCE9e+%SWvQ-re8>=<>Ny+qh|>)12u1Uo@+c`O&KYIcn}5T8_k(wwS9_BCo7 zX!j=$$T?~Q%_IZuTAE2{Kb&Tk2HF#dC3272FrDJ}CN+?M)CTm7f%c)oqD2h*c2X#} z0b{FgR|pj+hJAayFq|z6`*tuR%|e&ic_cJ#{%N^YZnIY?=%9vOrB)c4wwko@$gypjMOXgCtn;o6^^#-s0{AMQ z`{md*Bn`t?d9cp0$B8Dej^@!i$8IADX&s$rZ>2f*F@Z_du)ES+!|%pr!f&48mmHSz zCrcvIX&S-NeC{-V0Y_rc5xbul^kQomJ2(B&e)|dI-1HM98&2)_BYW7QW*@=MO+U1$ zIZuDwd<9|FJlu-nDf`2GDXwcI+xv%AV&0u2c>(m{7$G}GQK=sX%ZtT)mV7YEh&V6M zX)Hq+h~{N%)N-i&YU2QPBC5qs3FF4y2yO@*pvuB!F^*%%0jf~l*r9@&DpXg4I6xJu z(;96)XiY9kr9dtW4p2>D;sDhYCJsI| zjE~a|l)tx+)5{PPbDyFJJAOFoa_BkqYdT*6&j0IAXq}eWNr(vjnm$H3yl*NHGr}Vx z^lM5)=+~5p(61>GpqH8JO2Z}ro>z5*YpVlG8wQ-cv-++irR14Q80 z^ip~hzoysXk<&h4(pHAPOrJs!Zjs{G_%fvsXJxD`AuP?)4BC8T+GC3CtbXs>d#^Kd=6U{q zpU2^>z4v<8~=bRv!uD)W8m!$TiY)T=7F15HoUIVT(3B~b})U%Kfq#J-&f8mn9 zd;M?l@OQ82I?*h`uYO}8UaF zpCr!aN9qnBCFi!Y(XZVjh`FsVKf{vi!81tL(WK*^@G?SL(U?1D2$4NW7kthrcfI60 znM%^6I}bzaYWd(9hM7G1^vs$h0>zp))B#{Ro|i9Al4i)(BN? znw9-$$%q3s<&%YMMFC}VU*X)_8p6Z0@}tQH5wq`)_9fy0WlK+^MM)-x1>Zd5Vy z+ItxZ!@H2t@FgA-y_%o~3S!y{IP7S^3x5JGU*51m*$S@RD7ubHc9oVMz-WI+mYj)+ zjPh64Vuz(&YaNz$9ipq0vb5_+RduH1$=FD#eREr&1BNE(iG%f8&31g+j;BSdAernC zoqHjE{qozOR1LvV!c@}1TNV3M!EJe+N%rA0q$11X^uW+AB*?Rg^bO$(UW$iX^r>a($ zS%GZx%TA;p-vc5Hb2&Qrgn^YCiA}v1-hAp#B%LOUdbUxzDW_pBHD^l1IB}3t)lA7v z*OvJqe%(j#lk_Hj{=xrb5{1WAt02I&+W#G>l!choxz@<0oAORBuGgmRrSuvqbH6IH zh{|kHWe!rAhh-V_C7KZS43IcCj!zrrAP6w40|a26i*GoYHL`tsnkn;NG;iz`kk^|~ zY0N%3&r`txgwi*MbaUT?f*$%Y&zy&+Qv0PnjBA%^{^wdy+t7`e$E7__NtSsA?t@uY zgP&7V_Y=<5$@m$Q_Ytx!^K<-qf54A#FMg0fJ+;@uSSJ%W6^~pPZc6n(OK`u7aylE5 z`Y#Zc`KPdkg99cWyCiKfY4a(~h0C)q0YKUj zTzfq|taTuQGkPGSGB5_$MsUdIAbKjF1nKEbPgQ_qT7cY$zv<>`cq*|f;|WSMpTTbi ziQ0z?WrLXv@Wk54L8Np^!nKunTq4bKGVzhP z=9v~Aye^6CMyZ@pxHg|G25)WXZ6u`bz;$-=bYaz-(6ie}=@XM+aI0WY8OND8+VYFR<0i(m?MTS>{b9WdB3Ok} zy1rxnp(twvPsY9W{q1i8VFqX5*Y}VANXx5YB)6I+%^cg0JyB~?qwM?OiO0dF9mkS+69 z{F)tg$onloot3OQ#}k)ipevN3n_`*eB%P`jX)p7UcrquF6^ejUQK+8#_zjN6VH=g>orkKJojtjc659Gj}i*}P}wp+2Bu}6 zhwJRY8R!>I5)#?0O~H!{$_qU3p8IE%$?1gz^C5I2X+@DNq~>hKwg0CFkbt)kmLnzg zM-UBZK4CS2%EiZsQ$-S8Vo;@qJ{p&@>q<5JO>Xd&i^w^|HwHvQT6ba1CcZ21`U>HG zQS#+Ar1cc;hjAU~mk5q3p*n^DOTkXWoa`PC(E{Rz2+6Ys!v z5^3!9l9yxx_i6npmE7gq1XR_%E&!H~s=xZ7%A_(5q2B=#wRJDK zOQljiQfV-jN|U=(j+|gw=3Pig*+6&Z+{g$NPB|V6h0OCJBqDMTm{aKW7n$d0(x*7l znd3M6o^D79s3``<1>7%xaUNcSI9HTY*X3`Y1Y)5m&qVcioA^n)94Xn^t=2#g!50bR zlH_0UN6iefXS*}eYAo!&y@%28T2Ve6#+38pLKr z6saC1>l>H24xHg%wopI&KGuzehJWHMDEOVrHC@8fJ+RC){1#i;m$}Hi@-8~| z?5n)!%#D(c3vhk4?{T^wi{T)9mS38Zv<7OFLaH2K1dkj!w~Iwc44=1+MvikKI3aw? ze7ra)yaDQ);h9Tm{MZHYfpF(c*cJZkeEPlgP8vm~Uyf0v>vELDzlOr6Hi5*ynTEL- z7&#kUB+$%Rh3n@dow!cR#&ucF8&(Bo#lf$Vk?~fLd~C>BT+Z7;TCSOLPMGsfkW{mR zDY#EL@1{_#^9Emp_?&~12n0{=0P%j1MyQP7_qZ`Rha{05`~#zZ&IfXx8zoc{*N zw}ru<;qIIdm7{S-mXipRu=VheJ73I&a^Z}L>1t4HZ0VGIEX=0jkPI;9A^*yao}FllZ{(Yd|%M zKvfVRf#=498YTiCg3TK^0?*`)5P>HNB=8ohXWmE=_y<&moQme1p{VRY=0nIkQv^l= zB^02gU)~szIu%r5;F8-wjTfoJR}!FEd)_3GybY?91s=ExdFKdkD@s;i%}yeo?M~my zNF9M|E3Z{pQu_byLe9k^mF~Y0pnbQzOGIi1>L#s2o&|&VKlt&a%Ja$MmPt3p zGC!qKq(w}rJ7Y?@p@cVpADM4t5LP*nbeDY+lt>_vS1pr@dQaiY=y;L$BByZZ*J(>w9&4p(D`%o9$)E>&W<`T+tK z5S0|}vj7Y0FnzEkCA<_>${P+fVdWC;Itx?jaQQ`e30n9Cbg6-G64XfxpN{qz46nu> zPI~yc6?ivV__?d`rrz-0Tk)daa0r-L;rw0bl){-XA}9RN1iYj-JR5yNUihzP@rvH? zJ*Xe~;XK$=5bll&9u7YX%);A@)&xU@b*jaR^IT_mtb18Iez`mpn4T_OQ_210+gK~{IkX&r7WSU z3@K?>g9sGXS|dS7T?+fe=u+4>^Ji>xdJX??CYZ_-swNW5uOR$x42T7l303V#ai5Ey zR1+x`k-5Tp8dRVgrY@2BBC-U=7_$3`ED({`L7B2^iYye7RDfH7G1F1x!SV1qwp`x0c zaZ3G1&|w!;Csai!uu3p#+o%gK8f2YkV-8W6au6ySsxj#~jH1R^%o7-MBP0~mBvf5N zm;{sdAW|}lMq9gW%rUW;O&aq-h%OqNGnI@Fic3L^lZ00(Jzfexi+&Vsw4hf))f4R$|VaZXsXi+{j3$5?9`Q&8i`yJY9L0v-C0>b_lM0U|i>jN8mRjj^u zXnlL4cPY9v=Q8PwlPl2o-IX(!?pbimB;<7F+(6jMF?UN#uu_~$m2pf$Xb=(gv_kFU6Zp+j%{UJ2dNO#fYkc9adzZq1J+Ry(Rik2dB zkzx36!kvx`6uav3uE|I05`iGo-r)f3B{A~C38T4t-frUqr1Gvzfk1j}3` zuc;kib|tT=!kPK=(T|rE-;0#A5U%$X4z-Se2)=|7uJFVF&5;mi#t2n-N^%JItugo# zCR~N5CJz8HEJ!^?;b`v=5F-q;Z9Ay);-dh_xdhjiNm$+ha((hg8juMRS$R@lwSV*{ z6(dAdEP4ls0cDGjC2I;kfTLuyrelvt$nD)8jBy6qi&ThR~d`xVB7^pFubA4_x@@2Z_|;Ec0v1>H-w{&so$> zTc!soviguNoE5>fm-OL;K2!>QxIwDOL|HAt5AzK38swDbgfGH%4(=D?2$x~}%`KUW zgtRAcJ*+fjO~dwe&^Hws1wmRp8o^o6yENjz1{qdxI)H9!r1k2#<(w;IWJGg~%s{J1!CloBjbhNaWELIq-#t>OrCySK?uYLkVADL4Y zG#&?0FL^40|Am7~kCQpoK^Ign9Uyt?g1-{MQ23c%C@HTYpr9&jq5bTSa zQrakqM&+S7P{t>JsNupx=fDZrqb2RhgJfVdUp9MWU(eZOvdl)}21=v~Nv3n+Gn5D$ z46>RN-^8`~>qF3GSILjKPWuJd8Kt|eJaj*@BP{*5pK86DR+R23n2wBqVUC&tp6AND zfl50A*ZWFev4#*2HNw(Y({Dq40`$9&fqK5jX~;=^2iKnf4zNv5?5*-6ZZ z$3X}GZj4f;9?@+PalHT{qcmAq4}%KOM5ojza;bV60h;NQ`bDHaLW3AO&g(>GqNvzO zeCL8#RhlNO{}7+lDqV!BEkeSJn0A(0qUUE+ayN)7yU0brQjuBrgLs)SyzFbp+S`8R#!d$BQaY z82ViSBBOM&uxdc5J8-U0l|tQt^F;Zzlx6s@rFIM-`Q7+Eu#y>`tYcFX*o^jeSZzI3hVbq{f^1Cdd>9)2TySu-{` zYQ}@2;;cfcS6?g=O79hY-yyy}AXb%b7S@l%C-vH5*Ng#}9hcrG`WawM?GGYb4D?$P zM%Ii5e{9Vd?2oM(L;SHdW2is2W;FWknn7YMa|M3=A16WD3!>s;NXz*Mg#Q=nBzB9M z`6T~m5KW~o3G1;4G?g`D57&$fsZzZn{I4U+3gDATM(OK{@&-;J>McbD0%_Di;+Q>^ z%m~avyDNQPQQ3i+q`-%Y3I!Ueb$z0!!oUKO@VTN&0*?^i*TVlTP|5-wgn3luRRnGz zzMm9T9hjO7>NiEz1$Gj3Oi}fL$4DNI8Ivvp0t=}UmJ}6afr?_I9(>bml2Pg~O)MW6DbM?82_;!QXSK92dHd3anwo_f)oC1%a z{ z+6P=c*HMVG`9gGccouXY=KR4 zK)tD`jKFV~gF2u}W(R^df_lf5at}&|R8<>`OP~Ca^Kt5Kq3%|xH0dGhBgv74kaa#K z(%?WRCLafO2UV%G>rwEyi4h2uNjV4#DMPC= zX}zWqswD!{Ja#vjYFzU8Je9?>BehB6iHRP{7rndEwGn*%S`dBY84NRcF%f-}W%vS4 zJqlUoXB|kZcXdbUvH+$F;(=~+NfFnowfIiJb=de5+e-$>lwC+k+Khs0@n44?+$(U6 zpcQ=LzMn9jBfXw2rXp_F0mAqdtyR3JF}|Rbhp<;7_UjbuUXA)Iqb3;c&k5>%n)$t7 zOzCb{2*t|YAWwa)6&y*7{mT#+#eb;|7z=T2nQf#_Z(NqWFclo%l~NJ6tCcWryB#UN zl#}sp*HWaECt#maxP(sHLrM27LQ;T}0{0=cGnTT1LL>&PhutDHG$O=*vmF zC}|COy^)iQbx4|z+A7ZAq$!m2(p)4>;-pSWdb$Nk(>du2O3FG1Ni#XAWId9`p-UHt z@hO9vDCz!nNVN+>fNQXCtYLN9;xWbGzDZOg73-zf&bqXX$peQ_>~pBgslC zz69$Sx9gh+z_{f+Bw@E)G0vn?liO92Vw^1*F&Pj)CTr_EnT&$85 z<5Ef*@F0?g26SCtvI&eGR7FN|(oRasrJisiC;diA3#KFKLQX2%jHDK9tcxo-X$mF1 zvj|CxIH`k@Rxd`<3QpQjNxP_?-or`8LrA&`Q$4YXld36c-9#kq;G||sI(8Y7UgV?= zl=KIw`X(nGp`<&h8GmEAe1fs%${{wDfzQY$6B zaUGHxIcWnWwUd=+q!sOvwfZQMl3szNU91MeFXBi77IkhCaG*VF@) z)JXSdX0EPq%c*v)$7G(8RE6uHq>2lX^kqob)Xm$#xR~5mldl<9QK===(vYMWJ1ME( zdL*?L=v!9z2pHGXV0UGKzGcHG=_=|N=M?Dr+=e81%f4Zg;HyJqQo{z6yP=?zGSEL0 z>v5#U@&bL!Iw)!Ctw_3?llD{6+O!o}K&TepMpA2R5I zV$HaLN|lm(kfazlBdPmzYJ3Yy1Wi*-964;j;4M0Plu-?9D-j(-67_Qw-Kr3kt^N|Z zZ6#BQpvNjL^Lfl>D+AA>^w}k1_uxchzXZ5~N}q~l1(Gi&0uM}-Kd>5fQ%Tw^#a&)Z z6>w}x;Q~pw0gdK}h~Q!u*D`6prf2ybC^Ec6YCH_+vnBdlgx*Y=4=*`~G$-^->dQ_o zsq$ceg>K~(%@L=S$OEGEWJJ%4X!LI=5ps8|fw98XjMY#w)qyo64r^$MoC8U$ChAmr zmk6|AW2B8W-Nx!&a;^ibZyZ*AiJXK7r$QS}acx*s?xnS+Hq3nuvJ0STr4%h_wabEbGO#zg*m^Y@bjer@Sv0 zucA3Gp2$)*zFfRc**LmFPJCZ3ep=Z$vidwcX0oq1gRtO|>Q)TV#d2)|htw>GEcDJs zc@a%V-YCvi#hR%xoC5(pzg>p(WyP}6&BnXn4`CG{I@cnKRIwggG{|b8NsVj8zPZYK z%h{q9619RwwU@V24{}R!I-O3XSL2pXp@dsGVgKCnQ|?DXN3op1qn7sa)+fsEp_~e7 z(u})o{mOSzyY4JbqYV%|308g~bsl#YcTrYPF=~#0`5;-g>q_KoEcVlRbi6L5x)o|| zEDkBMwfr#x+QdLjHTC@x<&)p#SC4?D+loUJ^}=ga0985mc9dOEELQbNG>#xabY|D_ zon8=E>lVh?dF(%n!FD-7= zm0h|2a)xQEzSt1$?96tfr~d+=pd+TkB-`uIY@`9}fK-0-(r6ayNKG~bp(`-TpQLFq zc;5Aq=2Hx#)=!f|jZz1c`4Xj|Hwv%So&PgP!W>8AtAj@4w<>cU?AxLoL?7LC!%wq9HFZRb%RIUqk>t_~Err?y_&7Wm^w)}G%`OYrZ z-6?!)QnyuCET}e)u1Pd7WrJ>G`Y={qaSLJ53%%5B>ce0ys$JA`Ggvc^?pr^;Z>@dk z3m;-cvDgU}YqkRW5bP~fUF_q%C4>ZnRI zAO^S_`vOBfqI9?<_V-B2-j31~y@IN%jU z6|%L9VxO|gte_X9i-ck?1$q-!SgI>>70V|(5>|MXkKOF!HsuvhvD^(nTP~OVej@k9 zEy|r%ahQ~+PLiIf6-o9DmMwA@^6-s8YLN)qV!)T90j0{(pb_(5T=!fu4XVFaB&$NG zCq_f*m940)$}8{3dF2pWt-bOA)7mTlWm>9X0jt5a8FaDrUthsxWJda%q#( zt^oJ4>AC`}W7D;1>x=S9LA00hF@FH>{-OXKK&E%gmuFlLzz0~s=5;l;`P9y|O~}fG%#= zPfuD#O?HJItfr9I7m5n1rJfjcER*)-WL2Z`vZ@U=+`#YCu?eDy(F49LK_L25?L5#oOItRM^9Fg~aG;~q^ zUIty1zki81{rzi1-#hK^-y=?c|G_l;J?l1Le;5&Ky0X7Bk7pYXN3@OF-ycPEoznjP zIHK#6_V*`DYkwb!*mX+!`_qWi-=9TnfA{E$`Mua35!KM6P1dJ@q=#G^#Z*p?`CB3_M9^3+7ld0Q_JXjTHbk~XGM37GS`bn&!!qeaL1hMd zZ*gx#)B$sX#C(b{N6|{`-pIs0SULjJ=-G{i*jpoV$+)4I??dK5l$A@yTO*Uy5~#Uc z@~(_jDBdRNWrYOGpgKr&If06p>5FY+x^!dR%5>FN&P!z+N1OeO(;( zl87FLfxU?Iz9J%r@TEnp_iHxx6_Lpf>?`B2XU1V)6^YJ?_U{7rRHgT^2I5}5=0s$;i^f7ZHs(h3*w{>_<}szm!VsH2x+YYh`pj}U)|Ex{Sl2|2jzUkB*_&X( zUxlJ6gt@A`0n5l#*^vG!JcZZOa&7;2pbB#8E#Udve$O#_F17=g;&q*k_|C$2;YX!3Wux8 zF+p{{pBJX7!LCWo%)Tg0Q-jE8rOQ`^wl1=x|GF@R9ulSL$>&(ii*E{br_f?lW-UU( zYlWgCHxcnrs+9MVT(Q=!a)9Nw#rR}4 z`J{4gC)9YUP?SN9R%!lwwi+)L4(64p?Bn(n>OKzr%w4VE!-gWQJ==14tyMUbSA04P zx25I)va12D)}F|Su04Ar_{}Isvw7`#C!xr-Ckj6UPPgkItv!#SL<7hf$gd#+lN7!7 zM8U-%)Y`KV0;6kBTa&iB5y*hP_}BY0iUlEy?%xhT)gw`Kz64~o_C%KI&#|VA&=d{{ z6Hrc#8Q)SlISy$E5?sG%9?IQaC<;*y12*tV8~K*rQ&_J0#&$K+U0oPbGu<|$cJ(;$ z%_@|A_5?iKy<76GCSK|H+05Hqad07u&MA}+YN4U3xz60ebTyxwpypun3jJz6*QBc5 z0>1sN{hHr{S~nMppCHDl{ficD6P4h7ncSgUC$Qj2EVxzG|H*~vs{XeqK~q?euHWaf zpmrr_TA`>XLG@c7fuM#$(Fh?+q;+k!kioVPW!(_APR}fcvUSZBa%Rz3n4@NLQ&b~7 zp-?_nhS{u9-wV5YqM}fkkkcULY#=$ZQB@Q+@g0y$>WaeiX$PA&Piw!v0ety|auug) zHt(^p=G8NXzr)E?RL;WFXr_#JP7>HR8LVHs&HIhbYt%2N0Ns0Gu^Hu*ew*!b?}f)Y z^m{LSA+7bL58j}*_)1vT!YQ_lgEsFg%nKisOh(zK!=liZ5jcr`@N`&@)a|MRe>yzF z_Q91;fp4So!Qph)Z-mXek$JTbHplp2(H!vJseI4@-VyTt&a`>&w0VvCvuHDXNmz`w zWo)&}EeVfz_+Ux6)%HOW$+$}Sz_Mi=vU#s!UTp5}r%+W%ShT0{?vObQK9IB6lJI0T zi)}S(KVOM*k+AG3w+6U%&qZ0)$01AgaR;c6+ldl%*Tx~ghKOh%hk}cdqxv`#Qn8rS z`)e8=_pxpmGE^T|n>QPhMOe^2@)xOz#vYUxollXLvhNb%;W~&?aRWI_gqx@+)rRA~ zqt)^E1#;VH4|oeDZ};oT=%xa(H-dM3BDp~(_c%r?s1pl>g{1j1DR46T;>3cph#<8s z^YTKBIcL%u2k#87_j90U5|x%ZRTNAl!k*XnEU@SGGSE|15EJO3gJwMs(2i!LK-Sxx zJ`Q|rB}CcoBL!!YwuC4*$BGNOZ3G|mQ8V+Bf@lzBCV4EgKyLLk*jUGGtjvN@4y^1r ztegTpz#~xf3+)i36bSq=c6r$^!%ktnQVN3YP?+g^}&bKE{QqD`ZesZGpK7ie>VfN&aXgPVT zHQXe$^3`e3WlO#omdJskGIUF--fhVr$P-(+Ir(tDGf=dRWx=NHGz;s<7u6oN{RzU6 zI(6iaSGMawQAhrD&bNA)@gtM2CnbBiV0iD^}IF3^_4> znGQ7c&&Sa#4iPP|#rDtd&l7W5MUH0}86q0U&crnRNecGizynRwml`}x|2L3SU`A)r zH2o;DqtkR`M5pO@Y>A?HQEa|!=7Ue?Aq<7p5VoI&uxylQK#6?h*ANjM!cZ_egiS$K zbee8!(pJ}k4Csr0y+5Rh53;Db9r#DR15k8c1Y|W$N0#o)jmqk~RN0v*r>5yEshk{E zGz1Mom2+so$j=wM{`TmQ*#}fkhKTa>&sNb8Jy?Wo&#Dknp*m?fGDKveTp7w?f0dMzRpz(QcsuO#<)tY5Lr9MGhmU!5 zi0Dnb><^)_cG)$wYw==8b`7n6dUS~B)iZ!ALqsoyoFSrrt7EG8YO>m=-NUpFhU{fp z2SZ+FT8D^U3F#44hluutoFSrDL(UM<>mfZ_>EOuzkTW>)M#vc)c{Ah;j=UA({Vq8I zd;r6+`rKjZ?E|9a2+-eflhHkE$am{hCHU!gj}8vKI*R?VKF%Navwhkh4=}C$v4Lsr zj|Z97d-WUHe!Y*qiS5_Hp@-t^-x6p4!*TX+jkA9n`&;!$%KlEWzjEV)Fts@(*5Hjv zDmc_@G@E(8GBX++nyU1U28S--1P%_hZ~_O1rbSJaJ6adADmpkcJ>(1ywX*3tICL?a zu7g9Dgq*>lwoqJf=u#G-gF~08X9?`!(B&a#aHu^L8yvcVbM=PU49?ZTp(~Z=?cmT5 zILqyt0$0`?#rw|=Qp1%SW;cbR!66wIqLQ+b9cQ@7#kI#M_b}UtA zaeyK>cF_sQcpzyFw zIzW-gwBAOrqBhEHgrtx&K!FE+X^@ctp&>MNKb$B1z2gV=_t3Mswttl83$lYKg)Ca`-{A&zl-QV?6XRj3h1Kzy&AfxJ(*p3PJchgSlZt&mI9Iwi= z7mhL@bah@#K!~24s!XJS@AN!z>^q(>yhvzr%WZnzi1$ziXhzS0v{g7EPY!u)D68&3 zqzPr^0&_y1UVQ1$#H73u#j8U>lk=RRpecDg#27t?(qioNJUNH>;#-g29GhcfpPqL* zFA}9*XE3(j*&P*!eJ0abfF{t%)`mQ}038kN4(YM~*w_tuXF0HkFt*-FAIexQYJ$<2 zrx&EazG5PJ;?^FkNEn{n+)G@Z+=;@so4PoV61MDJ&U1wvj zu(6Bs^rBMxyCe>~YaDiI4E9~SfSsCEY}%k`YTs^ONW{M z&UJ>F{>hDond+LL)vLK;`xhP^W@;JFwfxoG{?0JdYYI3TW_q0yILx$PDIN_oy_xF_ zGrg4?7iK!Zrs^=$+qup#(>uAbVWxMP(xd7@Hhpx>$S+k-tVYcvxzR9F<=q=$&8DQF(>kKgcE7uO{$Nx^s{R2U6 zSA{)8Bw7yM)rOmts9bO^bh|fK?EehaLN>wKSe@vIbM6pFfa$(mXMky4t~0=Ne^eJa zP(P6C3@~l5bu%jeoCdi!sZ;lE#g{_)z#J13*d}D ztz0)8;g#9~@T!%o&Z3p87tqy!RskktL|3kFZiu3k0cF0_;7>x40VWiF21)qz4y|14 zP@)0kyU4F0A{&G{+=znFmFsL|Wl)wp+-PgkR<{Tl&=>!DH*8jtX;N|Y6JEJ?n}k~+ z4>y8KjS^Uqen~ww5;{;$tz1v1a&lZlvYMA*yuOY$Wyj`35_dyI_>RRya%@H$gzLRXe zm*eY7+;ESN12TF|Q7so}MG0)Tgb}r7%d0%+Z0fNU1E>9S zL^JF$>TjhPZ!kx+Lda5S-GjD}bX$lDoMy0fdX>?It!q(%)65(lIGvz&akFxC;Izr8 zJxUwU|I3!1-2MUgw!!n*pl`EvYiL%U`!3t*xueXZdMsP4Ii$Y#B0Ar) zg*+n;I2>xeDOYeYCto4>K4Ybsg(q z1GJ}ZV*_+>{=RH66Dk|E->!n7b|t6@f|RG$+k&oOLE2L@;yiUF3(}stiUny;&CeFQ z0c+G(&WE6}+2X)^?5V$OA>(Wz%2VUn8tthGah{qOuPnBeg-{_+P^;>8+`mZd!5{Nz3Z7N&ycb0A;t;$pXWa$>7J!QmsO2l|-C56-f zlO+vjIBNOK4WW79m;aHzW-)9Ti@4N;xJ&1+JmzpXr~gi>s|KL@3x?4 zSdjMAzp|XI?^zb4J@p(5(w^FrB`QczeKoDE*Jg$9S*uazPZbCdy#-eFHYWn<4}Z0)IejIBL2pAFD;Y9SlIM`N-?22?g`|0V;b zC_z8H&7LZ5VI$6ALE2O2#(C;I7Nk9OJ`2*GYRwW;pubT+jP}z`$P#T3qC7Rr7ILC3 zM0x5YwnlquSe&O$j`36tofL{F19lu>PaUzbix^vbs+h60r%KoW?Ws~WKzph-OALd` zM(qO>1~HYOG6+(w@6>7RDK`t!p7O+bDvA>VKKhI(0UkX?QVHw!UxPWKa1oWCLDeZ0)IkGq(2Bo=j)! zdpXnD`rgeHv!Sw4`#7C4+MonYfFR|mD{Vm!vLNlLjhW8Yw}}O5Pd&tfw5PUbiamfe z>U-1ki#KJ8gKw~>UblrTvxTTSbu(L|>(uf%PpydY)Oj>`Pg4dI0$cUy_0!o?7c#c? z)O5zyo@!+Sbe+1G4bXLJR;JhrSflnZZ3vD~f|l=RPp!2Doyvl=r%sFW)afipd+H1p zq&?M?DUJcwsDF_L*_uq@1*KZw@3xR$wh-m1TDC@esxHn`y<}9lt?zI0)HY?n5`4o&)v2X6_9Kj~J@qJKYfo)w1GJ|eX9KjSUg#pSp|VlCaux)2 zC_%sNV^95T3tGv7be*~_&QrIuAnmC;SdjMAnl7Rl`WyA%EPb{hzJ!hF$AYw{j*IhDe-@-Ybvz5wo@(qORsq(i&!xF^eiyOf zW%ksSwvYl_i1JjJt-&%eX-|E`g0!c;$PoJg zYt*0l00cdkAr9?fPp!3uJZ}q8o_c|;(Vp6!;cR^`W<*=xC_0&cw=$pv*sApAXLl`8eJ_pAz#Q7N_03oXNyFl>Tg;G+))`~ z|4V9*5ZkGj!Ao-QZB)i7y!R%LVvNcdkK-4J9^u&UGf#kTs4{#Jct^fv+e-Mv+XL_dc7qhJ2vU z12*q|=G8uUGd+5|x{mz5D_usjCV*Gnqql6{T{f>#KZ*RcC0&GU87b}TgDvSg5~zK! zCH;I`zcXq4yj$sa=moazB%Akc=GFSGj?wRpo#4Gm>9-ubO25l(-kWS*qyB@fC_5`% zjI?EJvdhg%*HJmG->md=ZTs zzl^hajmpz+M%hDYqGKmE?a^RGXMZXuW4(vcPE)a79Gkvv3d;Q_P455if13C99z$8R zbB`>wbH9gn?hm0v18U?7SP9*h_IvIo`42G4+@2PT(+_!^282mXQ%0BJ))KR1@A<6?| zf2MXFUnuoa(cojL(PPsU&j9ONg*DQ~s=bo2zD+&dfps(v>-!k2pT+{~6NPo)3D#?# zjrB?D84j#Z%BOv_hYaUseQbfDjyL!{5TGJ$@`Us zKgwUPrk>`|>-9LS{i*glgeuRx8(2@JiX)GC^gD3|5svipQ>mvaKjT>N(H4|jpDHT0 zV`N6(DLNmw<*iTUhqF{z^Zr!lQRfFzokyKFq&kl}Z?yH32i;W2Znq5C{c;|}-jXVQ zdIVuBDfVw$>@BJK=&64F&8?}eO#k<=9#I>9H*LRrml}Y)qHho?5XBisrISn4Cq{eTcb`j&r6Lt)f}dsmI10Z7!Tu2 zWdeEh&a0u4Jk>lPwQeu8jGk&9$hOE+&4XBoKGoci8h5IBFemV-<{_!}spjR7w;gJo z2x;QwV7sm@c)!&9TDnp?K&<6`o~H%+OIQ_W4O&Qr}}8K|jdC-|Dk7xHB0 zD@R@Nv7&HA5iruFFJ=pvWg9ZOEnI}hVP|$4H8OF zZh|Z~oyy5xAIStEPc=8w5eShgS~s#k9;I^fRI^Au({ZZVWqVegYBsr_gKwA8;_qX> zoJ$_uz^9r=&S9*N{Syc)daC(|Uk?oWRP$$k-7eIr=Fk0nc16AT<`2JUfQmJB55}Hq z{-57I)jS0mTgicPDt*)+d#ZWZI4F0>FHbdZg>q%mA+t$2S!E9So9vUylj*3>JAOG7 zFGblBS$2b6_8q@I>J6i>TY$0;`sHAl{Q#e8zHcsYW!u^4cb;nA7Gz@or&SpK5;CuNRd1RP$E9^HlRTzw=b{qwHgSs(HKLd8+v_zw=b{<9_F<<|q7o z*M@!1KX$laU;{( zA4{3m{8{m18g?TYX_PuV6gjZ-^&DQ5M#Pt*gK<^ZDeCg3U?zwl`r^(osgea_de zeC2b#cI9iP^(otLe7249l1TN@WY}e~t8s9e443;Vwdx=V3O{H0 zJ~`x7pzKm9`By3{*MEb3dd;R!!VdKnDPDbox6#LYqH3T%!N)_4(epL3yqCgWb0=e; zc`al2@{LeyC5>Gdhuu34yH5=EEi-^!tgtg|?1ydaVxL}zY3#0X*rjpU-D0pqwD8GL z*h}tUy;Cr`Q$ERv(>pT`J1Y)5I|h3*?a{e?a{YL06=PpuW4nFB9X?5n!?xnElYI6I zg(|n*1ne)pV!-Vlee(D(!j&hFzx2vi$=ENr`O2&Bn?8B`wbyy__lgF!~y2#G`4zKg%@twAAMrAuKS!R1h;Vpdfc+U0gliBPOee!q?`$V5Sp6hk4 zE$6XM^vUA|tcN~%ypZLplgG*@^T{WbPtcg&?iJft@X6!HY&F`wy&WfyXK<~@VxWX} zXYd|;T3sGn&a0_E!K>DEI*V49Qve-ZT_Pj8x=f+X4_$ONQ0B`UaK#Nkk*iB&snz8! zT3s$di3XH71Nk*XL|2z6h{h}zc}C5CWJOn(8lLtq4T3yyB(;VhBuh@Wcq^IZGj*z^SHt!(j#m@gWdVZqHD?+x6^X+m~-V+@%s=Q-t8MA1| zyHLs4zl>$9uz3ra7cw@}shEIQEJrzI$6mW!z&p$#Bj6ot%h)^{d_u`+v}OEk^9tsL zj1)TX|9y%uZ5fqI*;n7EIPLg8C2Gf8_k-`_6zQuSH?bXKZQhTW7cxd%fU<9{wx!do$$(haGRGj6#eZ6$p`{=7+%dLW&%PDr_0g*t{<=FShT>X$O9Bia2&7D!J_1 zKBaPUr+#tD$vl6lokJ0uYf{v149f17_jvM6T+@-IYWhy9>5V8s8uv$j4H40rj)Kve zz7$y_QBJiv4Nop|<+^KK7!J}Vrau<+Q4Q@^uBbUxZZv%_e1tH3r+M476q%9%JO^|z&a|ahlebmhCzGOXSUUgbBlUN&dY zngA@N*YPXZU&kjmIP@A6ht&|HS8p0YdnvsRFJ!&0x3PL9_i^~EE)J`Aj9$Z^1y(oZ z`(_*KZ5yjw@(>4Bc^p>v7~h{qtAvo!>x%`fSL7D9F~oI3x5t7wtZ2X*YQLM^tG?xFIBxcU@7{AFrY9-;wKH=x2KB`YZzwRB6a)*<| zt~nf4^4`k2A5P+Vk%}sPl;n&meVpWsDt(gVj4FMa#C?z4Xbb_^?P{{8s6@*tYKh?{ zp_S#dq5Eo*STdVKP19^$UQMb}b2J@ldM(KrYTD1TVAJQDf%9mRXq&~Mrr!uh+Wu(L zAZ5D_H9eX%-4SYfH0eq;D(DyJJessDHq`W3Qf#Q{@gzIcv;ndzaKBWj>8YfcP}94# zpL}JKY*HOFIn?A{36*51>B^)snw>>MO;@okGSqZ63(=vbSxIrBrrDgpp{6-WdY#tR ztK=%R?Xj&xhng1InyFCJwMouU)8eFPsOcD(50XtX)O2f-Bh++jk~7rQ!9YzlBd?AP zHQkvM8*17`BkhGrV*8aI>qr<-kR8_3aY&o7&h_^{rY? z|Ep+QWgM#33=TCtZ;Kt5RKe4KSw$u=j0`nRWM^VA&`DumeCLZ61N*Pw#ej7iPfTO5OrwwHN@Go-7!Z57KlB2|L?)G4KwR zlZyc)6NC&kO{NLSfFxm}+zyFXa68J$P}6{?gsYg<5sA(%Hmz5+#O~X*GOd9$*DJQE;R?-MN)HI1^FKJ0~E?I)IC9-V#DlVIr zbc$VeE%m-fEZH?|ZR1dr{|=Oup{7qQXQ=5j%Nc6=oM|0e`GRR3TKSS`9cuc@a)z3| zww$4+Z!Bl1>03+p;5yXwo#hNQ9kraHrtd9hsOblbZ?GHz3Sbz%6lRYAM9UGN!f=z( zJ$KVg<^@Yug8EB1)a1XD{jodFA1|_fI@I(M)7l^ZW?K7W57Ro-w3qGIp{AGFejRGs z7ia&garVC!XaDPQ_U~tZs~$<&zyC4XkVwP~OI%}#BNuY0>GHeS%tgw~XsGF0rFS&c zw3rh()N~yuaHwfX)YQANAQ0EHDmv73gXIi0-N>fvP}5Cpx(+ohvz(!(o2|G|({dJ| zLrp8xSI_NG(=C=W)O4$*M=2R<>fl_x;kJ@qo3srH66!TI`lEXa)v&RXByiy zvvvTx$P)WoIP~$GZDTRps6!tmY@-f+bY+`#=%bWr9s1}NwNZvX$}DH7sT{RSju7~w zLJf6t|0GI(AG?74z36VP?S|zHH3`;5hnf;twhlG9nAZL_qq<0cyV*xN)RY+G?`LR= z{a>X^8+2JJ@BLxuqWt|~qSN1p6P=;Gj}x8#{)B1xJBgOSdlJRYsqF8Td)UUkiOw%7 zyqxF^?Y)xd4DIb>TKoIeM7vH&f4`RK^!MwDw!eFPNr#NrL67r!dty21A-5;iCHCR% ziOLdMo-IohA(UGq{cw=V$q{~8Vt*p+jfv%n_Qr$^HLXaD2{q+mW1@249H1>s6uX*v zzTjC6G`YdQFtPellmVL2^N$Nr?(#&rfl-aJSXrX=ol9lqB=GV?y-}b;HdiEODPA3F znvv)XHC>sgheXWJGH9{itgzRd$Jqa}v6~ZnsRfkAo*IXJK^%5V47U4DV4tP112%Rp z9H;a?D^V{%HTIY|?4~&Eu`$@2&jI#{3VZ&!toIBX`^3b0hu*{DuuqP|J|zbGOL~n$ zUxj`29L9dr#_pRq*n!5qeywEKzQVOaL|(8Mv1{*RoH_5|49WcV}$< zQlTD+O%xd&vY~|LKuay3LchhZ%A&`{YlLv!49ran|p@ zJ``9P1c?LPK_s3zs z5r_SzC)(yWUjyuCmELW@Rz3P7Hukd~XB~Vl4*U5y>=$D6evCHnw<_!-6BygSj`iN^ zsd4E2NF4T~aoF3V*p-tg_|V}ICF2o~!%!(Y*AcD^({^}dK-3P?u4Eh?roGMM4AZV+ zxiU<9hsPPFy_3DAUl4Vd#~G&W^hCq7+bEQ9p-24G#9`X&*F!5=n=bTJIK#Bl6>v06 z+sX+XroBih9u3o8;&Fy)+dOe$+Dq9~9j3j^;|$YY?uiZ4wlk$?jaRVgqidFrjSbT- z@Wg~^UC+SCvpr(p7!K10?&n*2wx<{GGs!UR7>_ed+vIVEX~%k;VcPK?XP9;Z+oQs? zg%E_V%+U%!9=0P|PEI3+o0O>RN#~#jd4w0b?ULQ;-AGn9V34QQ5vFbMIK#AqJ=PZPE%!M0WxBIZbeOh+ z_0VD3N|vj_w8|$Fh|P zHAF;rW>9c3a%h9ZsM&$63{Y{JwAJlGhTfUEjY2WtqDgZFlGV=4Fi6&ifIULZf)J)n zzEqXneSf^;bn&D-M&mU)(|Ua8Qd;{~c}9{?{#r0e67JH6CQ#594qT+qif)0389@;g%!g z>`@+LpX9xRc;)?kl6jje?xe}qQ!H~z#o$?>pLVAlpGZ!fP;nleOWx`Bot8wjeu2+3 zZvEh;2@ zDRLVt(xMc(ofTx5e;MU!w|>wQ2G6EB_()~&nvvXEezaws!Lsy& zo}(CB8+0Zcq#yJ=iw)8bdY2J#P zVL|#q&w3W5JvYcLW&;+@g=PWSZn5-qe$aEfEhNVlq8{|jWoz_8AxZT6-2k64IEBccuA3*flyk(o2_a{^H>eo$S+h$&}FyH4Y_JKE+wV)GjH7d(%$Z<=C0$|?Pp+U4FfE4YozEzLL0!M1)E zP$Pd%>DOTM?y-5FV_vP_3o-h=Cm+&R`u%h&>-Uq*yUpe`>i?n{)M`^~M>(Zm#Y60a z)n>Irzt!dmwtgSd8&{So{n~Bb7Mph&^J@K;$LKeIDtPBA{mN|pI&9v#Hm^}Xo?1ey zDZUuN`t7sJwVHahsPAv9d6KQ)DddCkO1}*@ueF738_&F2zlkyWz1{@g;Yz;=;2k0B zRh`W{+~zgv$5Gg`pD9ANj8?l`KU1$#w0`}}k+y!PEd^hh((ll4wrzvWTgJRvzwR;m zmC+HJY^C3F@GAWd*u2>`FOHtj;RcT>Mxb1yw8Q%_`@m!BaHZDIV>aU81o~?{1pGZk zB7Swrm5gD_sJD54b#bT@`&hL3{<%x+I)&|Mv&((%I?+*wK6j0`ebASVDZS&8ZE(KL zyV>S_hk3OR4!WXkFpKPZ$tBxh9e9-w4%)mg*}O)5zr`rK-6f8m%svPpj-dL8?Jj+U zLi=F5>jc{etErx@S3Xz=UL|9I&AXm?>8KJNJy_`y%_yhZQM+AkrOVllR=NgbZyA2Z z5uY4d{V#UOF*nk;-rmd$I_zXi_;H|ooYtY3p&&dv2+>*sczVEZ644Sc^O$a}dCyvon5Ht#P9 z(Ka)R+VE!yqS=v+G)tI zAtE}fM#06%QM2lU$QlVs&8js##XCb#$BPV1KL%i7Ej|_UNLc3}JiIN}*;|0DX3L+7 zTE~&FPep@$B<#|M!TYw5VZ7PkEd(GQPyd{rCumV~*#4M#n3OtjyI~AC2kBvTR$%mi zb3s(=g9FFQ&y8QGw{co!BStfm~_42VUVYSODk;*zUH zzu_jC{qyPEev{PZ*m1mGxle1;RG{7o(MsAh^-x5M(q^%ybE|k zB;JvJe0aimOj$Q1M@_ez32#V=BBm(3Mz-#Fd>!-zz5nok+`V~lUDti*3B1c6@URm* zz#R)g5+okLRn$i93$c)r94EG7TTPKzxkw=?Th3J55dcz(t7|GrXL@RSVuF;Csjg}# zozDEz0V&ztnZ-$}r_wbELD`wE)O0%O$xJF;B4wwm|C!JCckaFK-3O48VkPBn%EsN! zJ@+iX_51ss^JYTjpQ-%+Xj{c2vyu}JR{kaxAMn9^bbxuPd~oo;-4{=4Wpm~B#@4#1 zabsh1WlPoiPVVXk&mC`8OebP-Kje0hw3I?;pawm z(CwN7CGS1b{ZKc9*jgDi*42f|Dk)RfP*UbFZ`T(;R#DN=%2U19SXp_rRppy{_|!1v z*7wkr=FYO}T1u;{sAyeRd8AH_DSohRCsl5ug7)^x9o?$3!@TvKt(Bz>>*~y0b~jK+ z?c2Z?^2uQ6#>TjprqKz#+*DeIpWtb)*g+q*+~?ofx{fxdt>P`UVVjC*%zD(S(AC~} zB)sB^ioSH{;&=MvzWQ^uWP99$`1t^rR_yqW(xEwQ>;Y2RO5NC%sgN6kKG>Y>Zm(1; z_edlR`2aQ$H1-CSd|{Z8NQdT;?i{5@vLv8edrQYlH};j@2(RrgR6f$}G)G@F9-_)e zyZ+JRK4Awe(2RFnH5`oRD(?89xMTjD08`wowD{p{?U}mor*8EjE&M>CqS%zQC@fnC z)3j7+SRM+e6%`Ms6~ih!y3^7sbR=%HGm#qoNVafUA0Lg|krscfbR_lu<8imr5>NPS z@#M*D&1o~l`}bz_z|QN6EGUw+BER2_(X*^mo1ruavo7J@`h#)Yb_`PE{~++}2Tve2|$ne|I7&NMqqAHZ zfH$PZ2vuq}x)XW2AxMgiMM<$KkrbP=e5U%h1Qy@Q6jW?WSiC1IFM2cb0_oIOE?}}& z4H9L0R*(#oc3;!IBRkz0ZrNE*8@tL0Z7^k%EU97h+@)U3poH9q5^=C5Q57dabP^Tq z>2;}6^5HfOfsD%o3}QlFMV*@vSRlXJM3vShs62{mgR^HO3QJKISU4#uMqs^!M+ETQhnYO*2e z>jFhm<&3f{9eL66+UBw=L_VVu8Dm*D_9qJWKuT2<6&oL7L$1EGF{I)5@@?59fGl$4B{o>93;{XCP(O}v(@EYV~+SVy5?SKl8 z#uMdFG%I6i%efVb`WMH`#aWq>O_~?$zGK-2CQrSpk0Zij_n;`rEGWaOqO__GVycJ% zWEuR&$u%ftz99BiI|oI9=~g<~`-x!d4uWmNY9T0@mh}XL2c&!!djc8nK=Ncui6^kN zRFt?UkoX47KT<9c-My02edThxKd9~PMYTPU$U=btsQ{8p*qIf3Yf8D|3zBk)yxCLQ zMjV5zERjJ_2y%890;(2t&esU(ig<1MF3#6yb=JAk-m?Vj+owlG%+XS>t~8SUUWecQRUOX zOZOI4i9qt^l<>Qx%rl9!dvX9TWM=WEGbGmTDt6WlT50Yx2D5-WNlIlkgOhN1nN$eC7 zu``^Ci+Ix4Hnoa_6!;%=tJM1Xq_||l&~axDq@Kv5(-ST0!}N`wRQ zDCtqr1dACFEJF|mCRVXX%6yvjGH|0zdL-}$!?~*J1m5t8S9hPt3WTJpxFt`POPHrh zSxfIfts6&M54ef?3>T?N($g~emFYyr%t}O2Y{>y>c-J#sh@Xv>K1(ew4NIADOZswm zBdWYJi6=<>WL*$cRw@DT0gDFKs*p;NG4Ce3iggi8N!YU?5nqjo_-ZO`RuEqyq>>d* z3aF%mU02soSMNAxYxZr;LR&h=8D$gCF_J7Hv%mozaSbU;bY|br%wkuO*t>ZzqZ>CQ zOtx_~CC8JwY4wx1S({6rBvTnb+fvR}TT5voDI9-dTim~tyA-~Q<}4CMIUW(L_`BfzS&%Cmy<4q0iy!%$QD3=+_!I+rLDzH_ zb&bPY>fXkz-o{)H=)_t$#D57OSC%eHNDS1XoKDv?vWm0#V@xXd5L7ZY{!ZYlKvSDa zWm%A={GN)`qFYaiNwYQ3&NhT)MNeXw^vZq<*0c&IV3jku47HDehn5CR?umw$&;72r z-Kj(wT$^%Y8MiwYh-q;kuA*{~4j9V5pNg-$NTi~kyGM$_j|=RcF~S%E%~mn;SmYQq zVWM^0OUF0R#m5KX6CYqh@Bua!zYs$ae>)M!K+(OcDY~yn(GD<3GHfrk4#aeyJm>7Y_gR|1|o0S5zr|7v6nRa{YVp!9ws zLEXkXn5FULS**YhmGekq>8DI{II&lfP-Xh$fo#nw_o(Tz@iR_$5ntu~#IW_IuPM<# z1y)N;$lAn&tXoaq_2u?yLp(#?Nm0nh4fK~u_kDmi^Uee1ocmxr1Sx+iGa$o*535CqTl!IK z7lY@eV*F#})|B=-=#bj@mNzWisw?Na`V1*Tstv^t!-R>3YD$s(D&vn4{XW2#a!yG5c7j!N)Yi_A@-E8 zNBj-9u7I?PSviS;<)pzAvGHmfs{|bzC~cIZW1lFaCh+l|te_am$OT%UJeriWWcFo3 zgMnEyh?VGHnRKZ*(v3vFrDVT9OFSy0oST%fL&Zgn#*3@KoJ6Cgl_nZ3y-{SYqMQCe zBG(edm6k?vC1R*djNB7FU;S2Wl|FJ8Q38Bb{7#vc2D~aJ^_4vXYZor|M5*j5DN>qV zN1rkxA3lspkd}aYYLPi7ngfx#TcB$CUcNDYd}b)d9JHn_(UZ9 z=*fEJm}|>w1`R2DaDZ}FQerO6TFH{I;!2(ItrGdyRU8-cS0utVlp_hA#Ucr^DHzm= zu<=@)ueXRvGs0qPsk)AZMWW=Lpv9^^CBhrIOs~Ofo=%7B+-9Ekwas3{kIb* z5A4=cO18m7>djaaOr=Pq&)JR)qJBStn@OUNF-{m zON9orY`mIlGcWMnp@dVEBb0s*Lm&u=5ebRCiICVAtp5Gw0(R|$+M)mu<6t!xb>AvZ zTx~g*j-<0@V7%2?wTWOz%;Z$#KYSOpSY*d?MM`?D*i6G;7KKW=PtS^Kd@Z4}Ito=T zL++L+uezTq7fC4bWFYUQ!ZRbvoUIkZE@g#mX5hyb5n1N?+f?Ffu}u;8O8BRkalcyR zbd>hjmEtNVLEMtjyqSP?ta&$PI1!%NRHAtkb?#K>AK6m;tUPtNO7d@;jDwzX`G;^w zJ?lg?#EQF={(~f0GqEPH=}xifu7pk35CRz?u{ygx%lm67@6zEFJC3C6oYhXE;l1TF zEC(TNf!;0#w0XP4`qbO_;-wm~?CHUB<$5UL|20YeQ^226!5mM}YN9Wl@J}K1nN@79 zG4=UCVXr27uDzU=?wPHT;8;oeSRk^s){qso%#RQLRIYC@R8DdCsAk>I9ohJ7{$}>!>(yvSEODG?$+HC+okjLrP_oy z>Wadmel6|O;PREPG^P{folhiGY|%HBOTYDN7mr7>xm=*Nl#fOjg|5E@&ZUlp-y}&$?He9&w{Ltoh_FvzE9OQG)d)=_keUt}5?yb+$WY zL!2)kvzqcTuPwq7b;(4psUX&x=qQ=!WEl>NkuB*(Rnm*<@;2x1*=99SZ0WQrF03tY zb3%V4=%kA))@*Z8--v#r7~w6B<{IhI$r$9zS*WI*g=&knvw|`)Up`(QqHLV?LSxA} zOsFcPG_*+JsI{#@^LVOZh*KxYh6x+8GIBL&+m%cS?j54PByyctY(WNKosm_MEs>-_ zdkI!~WR0DBE|2!FQYXuavI-|zP~W0#0GV@(bo8hH0kvR|(M=GL=@pLbo z(0|N+J7dyGi|&}h8oY8YO1z+$i!#Z(FjYVXJ|c8d&P7R_(rHahlZ{h~ ztx8NUzASClpY=hLwaDHs5m>TP&npeK7~E7Oi86{4FJ;W7Bs(Q#0Oc;F)}X#1p2^(s z5T~ftQ&E!Y%6PnxU26**S}d5fDP~HXD}Po__Uis>=c=6L5`;7zktDk5)&1AXD?hT@ zDQ6fE#bgrDVK7YhS`4!xQA{7Hc2-mJmPEZ27Ze6IBBEHrnh>HUK7;YY8Rg8RXktp0 zVkTwDq?D~2%91I8Mt78JC@ot}RaW2}6iZS^8965zeJPu))+D9xCKMhk4lUbB8!MhF z=ZH(Xu`yj#gS&(F1iV zy^(RWQpWo)QWa%M=SQ;d0*xo3Gp8Sz>z-x-$!JB&6uAhw$PD$SWa5+L*>n=N6x}B> zsn>#*VyU|=`!t;|F&}Bw#W&07;3E>N@4`R*t5KGa$Nz(f9QOY+^368w1~L=zQHeDif>p>? zEZKZYac4^}RYkIKihngUygPARo@{poKo}@22S>+fIe3YP&PZg@{c7W+G{{kubck8(l+_d! zllq>RpM1nrFGl_v#HRr&ls+k=e9sn1to6jwPwONbOFTq=NRb1Jz%Ruz&DFOK)K!Ex z5(%9t6u13QRuim_9Hp%4Z)e!}{;Um_*<`8M8a@ON7WbQ}Dn5y|SF%@&PosuU8Ce-| zbR=#hCA42kvf9ZjOIt*gsa&pAMD6T>R#6WriNl&CoWFLpGyXnuFS6L&MU7@p1W}2%-!mH<>k4_lNT#0F3g;mJ^$(} z=PwpcGG7%wP=y&y+|p zYsJo}r#wTzoIi2y)GKFS{_4fa3*2DO@RA!ZT{v;(!ijw&FNJ$+Jpby2(noQvv|Hpz zao{*nPddhIK|Y;~L@nn(^V*5?rzxj%Cgv55gU-X4#TREM&&FpL;+a#CvnOWNQ=QV) zldqgoe{?nb%E_4%=gv)PSac0(OucsQqn~59zYqq^pZYXX6eV@y zwLmyfq<@MH7*F^hv60@1Gbc_`ihr-3Iq}s=wVQuuPn}b>{5$!|*|R!t;owFw{&hU&ezwpZIYKK_maNw$E@<0*&eIB zY7bnqh6U>#v$|319reFA^|wFQ-)g(KQLz5ecBEjn zQ&#n^Jvn20@()?-l+}#d=A&k_wt2~-8Ed?4RioCZZ;shc9r%n6lo_{%MLo4K-Kd)9 zZ?w#xb))>LDp*wYZQGRF(`nJd^z_)+b7K=@<6~p|f1H0DCv-J7G0w@v*wrPjhMA;c z8_Ye|W;OXo#%y51?i;lYqgFj?+lFoTsI~cd`-s&>+j2iwUAbrsbK9`hEL;AHHI6a! zxplc``4vH$G-dVgSylcuW}h}#{jAk6SHsqn`&ye-FO3Y3Eb!kY|8H?*(VC(!6s&V_Mj%@61R7t>#!ohl>6@Wc8?;!kK6QB1x^*a6-MDrBOY57vSCKK>_OA7vzITzH z+}~@rfux>p8k97yfzMtM6SXW@bLq*r)&0Eny_u=EKeSGhL+f_Hd5ig-R(sQG#*0O3 zXwjnW)IeL`vG{#%4lG(>%C?Q#_T(l5v;A6JU>iemsaUveQKOOl9wSt@XnntWk0ym8 z1HTp*37sn>o#TWG29iF7r$fn8PF6p?_q&Wh7&Z+>?pRx~?ww=SFs+dtOp0(q_1oWy zo7O(A4mvAN+2$~y`xdP3h7J7NOzZ2YU`$iP3EgY;lWpVHJ#BSR6d3;S;=NB+574gUMNaB+XJE+7Ht?PJv(Wd3D>eWrIV13? z=3|n$G`!`8ZX$P^k;Yeyw+E*5_ZfyF_q1)%h)h|X4x_e@0|LS&=&031NVj?_owkm5 zRSTc#WhlqCKuRE-_azd7^X)U%2f6tqBHSM=*k{7TJjfxpY&$rtW3-+tX;eaM;j^Uz zrj&>z8K@XCWDZ4vVkGJV!M+;;H0!Q zj9B2>p`-vO6iCHj7vln@McX0Ggruo&V^W+Gif>PboVVLhSOt@nP&wt+g~U3d_WM>l zYDdB-#zKFauC7=Ob-b;oHDmd&SbzQ>NtvS+p;^=i6uExEn*5|@5&il7iur|4pgpV6 zp4b}0wm$bIn04BkQNjr7G21xlXxq@)P2pf;-?PT&bbln=7>MtW+wR*~LDy|tZlKk++{7-i zhWvJwge}|VjVa{3bIjW3t!ly=(e88Bb;&SQYR9fx<274Crs*7nW>e)U>QKt6Z{<8UJQAf+uRZvSf8WW;J6= z3s(Kk(mwthSsJ&NagC&GMnu-0aFEDi#u>vVs$otGR=uAJu7wG-usLT|bf_Sr=!y7VnbE9@B93*~oSD1)lt2*|HUVt(utY$>5YNn4&)%3J= zEm%jvdKmb{>(+)UU{1$0r_6M;@Ep8^IXHdX+Gm*foth!c3nmmxtPw)Yp4g)Gyk%7w zgws~QaK>8aqqFbYuKed=i)m}m|Dp|?p0N!x5`27a(^Xk#*xVb=SQKG67sjm%lAvLT z6?Dq9ZJ)Nyv{jZ~Yt)6Ucu75|f~4Wi1?$4NE?CPkZ*$a%GP)K0RRtFAtaVP=#y6}% zZ_=U7%vk<6G-XKJDF2(bCEo)vS?jFjr>$v4*5dj*mS?h8#KS$ql1`2DwvpQ3ZLK<7 zv*yzrL`Be2)iT|IqjJ9ywOfmmS3C#TtdqXU+ghQjYc_btTA9Ep8RlL@A9P0{H^nKE zzozPvVd3K5VB*n{=C}0MZon5KOIE!vymrI#5&a9}80EL0W}vXsy~;&fzo>z70z??; z6GyFi#tKIzkfL8Lep7zQx`O^xUDw~dbo;s=>d)=F{K>RdW&F*x?-nar`}rriyDsnS zRXSGb*Zn)8vPe#s`&&76F0anztKQRJ^{D*M-6~~oSB-&H=^%E(C5e{q{NT>*{A=8b z8y2P*cXz@Wf3p9b=|3cOqk zn!E=*x(ZXwVu^P}9A5=deCfoi+0w5HKZ*gFa-?xJM#i0hFM_BfpcRAqBY=c4N~s#a z^uTI_GftiX1La&*e<~oNU@<_%6lSF2*(rgH!lpp{LQliFBaVQ6jN>UmQamm~gE4d& zzcqPr?yPVuP}z@xw(2|OXln{d@^BTJd-l{pD$iF`l;VY|Wp+A1oSc^+asi2>cyu=vHo|}&0z6xL;%l*lp0J2@LthDOuG9OT< z>!%GJmPdF4PTi2YM9;NPSq~;&!PYx)hVDeAQ(*mywawZlG@zp{>08*!Xa$fpN>W36 zuzy_hmDN@|A;{<$;^>Anc|FSOlGR{SqBjFn1u9pMq=@FNMu%k^)?qTyew8z}?`^Ao z)4GFN9(&j7uSwI@U)SI0`ke*q9Jc7FZJf2OXlZoor}1}v)C*2HZrHmKhQsyb2VyH6|2lY zY=evPmGSOf?#pauI-#ekaTz-z0xUAf3ePc>H7wq0UI(zHIuNxP`2^GnH9>$oamn2LtKSa{}>M$H>~h6RY-?0d198h zD$J9qC}0#X@Pn@5{T4TPqdQi)f~EUQW|(vz0=-&vj3T|7C%gn^6FO(mIKgz?uq0TN zlji)F&CdD(TNd9O;~alZ(>h~Sx72H($LeA0;%f{k&0@eA`C5O57hD&8c|P$3@=+tW z9>azaDq(P2t>-VkLQKt!3J7%VxO#gycc9H$eJm<(%VduhGUMMGk3YEd`U#E30o6Qe zgW({M8qeej9r)ZaX87|O+FDxLIAc38oWE(C@5rF72drJPtxznVhjuXYe<1&!VPK}{ z1hOf1K4V`Iv=5$X8pV%@US775XixsT8vg1$V}x969Jc(joRvJT$;)!8g!I$xsxzXe z#w*q^Yme|L-~o5!X+8|m?(bA%DP7WheHOPq!o!z!Hfl{?XY4h4JC-f>Srt^s;STAr zY=?CivIlU};pJs}5K%T`)ysG^dJ_F?{uhGvj3nX)Lslzh`Vl_?KwiBJUV#@?eMMXh zSQozcdTgvGpNs0I)VocPe{|aCgA=iYX53^?WhoH-;@7k)B=tuBMSnr zfexx@Mt6KRcmU1&gZzYncW_w~{S3qs)#F6*JGH~skDx-XITKYaNj|{`&bvRY3#hpA zZFj71;j;ab_&f-;?gfPbfDYWut$kN@J<2>HY+OjHR;U&{Vbi=dJMnD>rC1WF)y}B& z!5fl8jLa6~3(<~w5n`Vv@DGCzo(!ke&Z_)F2m&u}ltBloXp|dhE-Q@C?vJ_wEJUns znIon{x0o}AhS8Q(oFLyG&dop#yLp>dGYwQ<3dsx zf0R0cqG1TxaN8R3=Ug&IKh#tmV&E8pDsaSU=NLFz0g~%-P!TntS7ql2QpTg6Nt@7c z;`Qh0&NTA_f}puVo71`Ve!p>gks1Bc=<(y@c#s-htBh$8L)P}KmmNe%e0YCzr&`pSlfqX9d3hj!( zAP|&%O-hoKT0ggrygT)Q(Sp+WcQFz;Xs zE-&NBz|xDu0MB&a^#XxPMl2|>oikQ*S=yr+{N}n-EgXal8rQkX7bN3pFqP~D?xPa$ zlPT+-u(~-tYPc3FsQM+v3V|OOUBp`+aEce5PABH9K}3yiQ}d>5eP}GNck^A%#>!lD zF~7~|cflS<&%y+dIkZm?QHTovAl?d`(@KQmlKwIP4KvnF35N^d7k=^kAX;ZBM0>z7 z!iOaMK+X`hHVO*YIHH%}n{5RfWcU%R{_4?j>$on;6won>9^me1NxXTYKP&c z6pU>z1nN{Jm^3wx#GLbCR}Hk`nr*|Low7Z!-V$G}{Jxz+CEajn9cAK}Vix_Rwak@Z zqu70x`5KK=AqL8KIjsA@V>@inGdCH=%{C<9~ z!X`4@(cwrSo!oIe6O_wg!Uhm07oH9IKUYn4c5Lu zcqN%W4_%`CqURvV=sOj-41ji6b7E!D6m{0a&jf-H8w^VgTK6&f1$`7r_ zA`6$(BsbC~II;1L+5s1?`L}jNqcw;Sh_3UC^}jFK_&oLwvyC0d+%Z|wLAL}r zPGDU@c#W5&P4Y-NRP=%eUKRwqVw-WNQnrf?TH6gt&p{-*Dsx%e?DAhTPdAvwS%&6O zMcENA-B*KEk2V;wJj!cq$+pwzMeAk2CEt)jBP$RhBSV}JNYaH~!wroXwgfsEy;HaJ zcXv`v6k+4ARU`V*_5S(lYx%E0G$XnT@g1Xyqbv4oIE?CGEi2Nw69SO~S5_|}-ey3! zm5X3)d-WHS*T!y8(*!DE6dD@WamAhv2P$O9aNdoYR;K-342jR>7Au+a>lkwNi__Dv z8a6X#Xkr90C5A+M~;-Le*R(zG4RjmDiOMx|hc%Z17}&30ScZSly%(MH;fg-YU5FP0tp z$;H4CK9zem5lgjm;?!psZ40^|l}SvDH!$qnBIZMft{Qj=Djd59uc-+>nex;pV1C)4 zm;&-r_=sFS-ptQRc;jyo>cLNa?2=|+%PrXmI9?DKike5k9(nTJ;#0{QTEHW>cb@QR zgn_AAQ-yW9D}2FC--1U=O8_5|b>5eHn{(y(Al)aG>`@aLpeoz}-Nkev)PW7%nA z>batxpwoMSKo)EpLC0yG<1P&hi#f?vfh#s&RR}bWi2LXiOjaI{#-t8c>{%TO^0crl zy02ST!G>wa0zeEwxz6`&(=}DW!)H`7nwd(R|E7pAV8gq%;i@cy&hM#(C^p=!>T6bc z)~a_EqsGT`huW;=x-~;%sZ5DFa7v@o1qV%nn2CtHcSUD2w+P#IMXzLT5t!_XekOB^ zU}jfzA#)2ayeoPwa|=!06@8`j);GBE)zTYpVGW>>NEQ4H4~Br6q2#c_l1u_BO{WG9fIigF-D zurB|5dVsajO=1Jj5Q>yTO;3p$BBsf0B$*=^Cm68#f36o~Ga(>o5U$&p^m&Bu4Z8FF{J3S7ce1d{Du#08>$8Cde#9L(vW^2|A6X) z2Y`cI>pmi3WoUfYZ5a$0ZzzPLD9|YzT(M{Tq>UJMNtw3m$>kkQ1^6h#y$A57piT_k zk(<_#AHZR#{UG>5EQot4iqfPg(@dBi;eh*1K0_0p%e&U}PNv3&1w;&)O1#=L zV>>)+CWwXRISmhzVXI390Gpj}szB$s-ACQP1oO6M+_tz_qzr;jnHWeKkB*N988K=< zAzB`F9gW0rtxR9IiKzRM4nww;0L7?1K18kq!e>S{2j2AI5&KDOn{V2o3Hu2@A(0Bh z>}gdP{cN0uinpsuAVOK~2g#Hm6Yp|>Bcs`y0an3nk2cQR!v*>OKwf*sKHYYslQ*8R zL!{-5UWz|`KKXRiiua=?f9T_ri@yE!L>NZDQ!!6MaK7Jm|4`HRJcKi~V*Quw;p?*N zu&_HPNofM6L7ZuLn_c>?`3=Ny0u@i!mzbZTBE)J(bn;eR^%9vIqm63QP6lSEx&}E_q}g*eB+L7 zAh~T3(y6%$%jWy@ufzPgZ{?^2mV;}$$8GDPpo0G6whxc^irfTw4bM`!)iI@J-uDQ~1J`Yq%NVpKZnAI6P|r)-~x>vm7uvs3osl)Y52N71{OOf+xF85DHpDpGhOV3wS}JKln~?Vz{d z`D_cAgY8)A1Omv4-MVDEmsZz1Hjy-PM2(!xKUS=q0f7vwujG#a61v+Y^MCBNoJ<}1 zgthpiYTVl&1`V--tY1WEF4%LVVc{lI2$u>GALE?R=_684$UA&rcT+ zCk&u*dx~M2vVj$Q!cQurx|@KwuNQD&o|h+6`K~=5)#kpy+J+la3K%7hwi1lXzbTKc zX$owW@w%e58|2eL@Bl$|*x?KW33ca>|uaaA8zz`y=xngcce1X^T`p%hXH5hMDgmA5;J&J9B_wpwGKQ`&;I5` zB(T?wc$kZzr(7Nm@fU;+uA8w3!c7m2;jrn7&~LaY%!n7fO<#4>pd@e%VRk>zhDUoGxHacr}kZOJD|bD0*E4gzM1@92!^Rw#m;~ zn*J#Yq22L<2?;+?Wo$v*bHrS7CL_y^Mqkcfv`rD#Iv8i;F{z-?P0W%f zxr1t5vO^34jy>ay&LH@JtLo@|-2!#P3&Tj*d!GrfCK+}=y(=iH3#DU#2~I0YFWwTH6cHhUQwy0J3OWd- zafrx+6$}!AoZm-`y@+vsHM+QLKM}R$zm-(`+>cW2zoyzRQth?q+_G(rx~UdwIi@PT z&7(SdKhwfhP4=9>q{%>oYOLjddTA+oZP_+N4?t9bq_ORoYZN*9#jt)>q&$I$wo*S| zb#`=VijN@-4;upk3aKeLNP##Ggxm*=#0D{mRcI9dABBZc&dE5TA*GdC+ph|=+Tiec zJUda%6)23*6ZkGJw78N<4~fw7zTdIqkW)jn>83jpI6g3`rWV|Z&n(JJY9^pFqNo>L z!WDPMh-oL1Zlr%2UrN+VY`%kHGIL4~ggT@@w<|jexM9`kvLGE~Y^f zCae#j7(0dDW8n;2&#W$ahe6_!b!U8%@x>;~ewX7__eR(PDsRC(pSG8Hg2ZY3Yq?T&5%U$JHrlV~MG%ys&9a($mLKfaI%; zxyaSySLv@20Qyjkdog!JD5N}DNDg+-|4>FuSJVs(yYt%lq4YKpU;P_LMdb!Eur#Hh1qCY($LRlA`N+~iQk;z3Xx>>%BlJ*5R0`@t`eVy80L^3|1>+EdkdO7Eaplig^$+pLzak_(He>q(k;k;I zJTiprc(gM&S7}Yei?J{mgc2itm02&l0gq3rFV`rdO6$Am)B-k7+V_==UnxY%)`69 zq@V!NM8?r=rvLkES6(=N>=;NIE4e{!apn*$1p}mumO#`r{#1rMeo$d~WrvL~eu({0 zcSm*?_dtiP1;2T|S~Pe4IxPna;=L4v=2uP*VSXu+b;n#;bMTwjfH)ykr5cvL7kmdeL1c>E z?r%DTlhhoi5U?OgCD3U)M3NyoA)*ovB1nQ5YR9&EuLH4pm9y+cH#GueD8Ro{sz%RS+dXau6mcUuZ30 zIg+|KPSYdA<#kNi>MMHvdr0Ax&sh)SS8z)2DAE z^vKhqT?pZ)^l@5A{DD{_1wB-S^Z}v6??!UB@G8xy7eu^X2`;$F>!nGB5&?%qr*Uo~ zK>7M;MrW9?JmSTAIE)oOb)(m90ObmR(Hh`)Y>xWq^SGR~(IDyqO+dy}^dG;*P{7mC zFISG*;VnO>;+@ePAJthD&t&HBk=+m__fix~n?+ z*4OFKdQ2Iqc`4lb+<_Kh_?UroTuy3kcTxjp8k+{7RmDaNc4$E|Z-W~R#H9`~o{W*3 zKMWQw#*2a4$K>Kc^HLP7YGua)kVwV`0NGdPGnfO@*1s&3O2^4Y>|GXA*+ZJ^tjhF2 zziX8tYm1!J$00_rLkRSW5VX3f+1x`NvdZhp0-C`k0B-8^wCB3$p8})SXz^w~n#2Z= z{;Yz$tM`_2$5^TJ2Q1CVRs1C@Dt-)B>U^R_IAN=kliHFbSsLT(LVVAhl6_`M*U-c( zY>J!EWOmeHC`$I%;p!7ChVkuL)}>2VwD={n9}u<0`vK`I@qR!mRlY){bnOYxOV*pL zz0FIyIEBLL>K4vdt!^pZ52gYS#&Z4tCRY*ttBOk71kjrQUEy&^5XX38I|St}wqnJF z4yO`h&9J7ChsYnK0FvDk;II+kWGn&DynZMWNr7BDsx_B{Krmoyk+)Dfp<$)QQDG&3 zVc>IaoipO;!4$41Ugxr?{5^!11Z8elcpZjTEwEVx>!JOzFQmAK?MYdHSPX<@LW$Vi z^~_NhhU#QL%G+;+TY6-a21XI3kJ%%TS<>Oj>!YbA3N z*N32=>kgD11Fd|##6Ao_TJ%Pe5i2RnI8;vL>fz~s-y)u#htSsM=}&20-Kzb{KFPjg z*;R3Ct;k!oY;Vo&y~29B(p}TEquHw`PO`sM_8YIn8-XPoBb}7wNQFZX9VgCf8!pZh ziK0D?KAMl=`z#;xS}7mLa>2LEa`yL~!nDdwdoA%r-8?xnd5Y7b4AI$|tMRD4Y)h}= zolwKQ#ab#d)b7k4W8U4208Q`2P16Q)oJw*fdx)t>aSt(n;Y7T5SSpbCQdS^QtVFcA z^kj8ivRsbkfNM*;QXv*^QAX9pwS`=Sxu=b+%zK5M-?T1|D;DFu{Myu%?~}#(i3{iU zhPyri+&O>YM7VKEn^|e?-szJU?hB`{9~`+qoK4SOEN<5Y0X_8cYqMvxN81r6Cm)k! zB_MsQAgExuZ8s6L{~1Ax>p48_uEp~aSWfief-n4e{#JUr!_(dIt6}|sD=FUN@CzDoYPM{F*WsySUr2tl&0x z$;H7uL>hvL}hYgJo|0p8hu7pg(i!&&y2V z9c35MAmJ0rNNE6K2sj&&Z@xW7t9<*COslxtaV68L`gd%@q=9zH??b{1B*+0G=|DQ# zX?=jmhH^nsg1Mde92oBIW6QV0g#4cV0=kE^J;JHLg;Yd#;1>Ztr|jf4bbpTu1qHf` zST_U#y5jn@{BlK4Lz*?X9Ou9x^!YjFT7WDeZr~Vy6CX+*8D1X{4j!k2HTTgL-yW!D zk|_9Khv)XN+U1&(GNLQDXbG`r$l-bH*H5cPe5fzx{|qnYchxk$MKfj7kf*_5k=zFG zMAnyo_d!ODh&6wKTXH));&zen6T(i|F`vO5{KQSGR%apnw3{?)DE}8k_dQ)(OJwGQ zR~`q+9PE&n>VpYgVqXOo-hI~E2)r=P3?|+Yp&~Y2kXSI5v(-djoeEYDJSw6gd0Z*j zLeT<1Gi-(EFY(84Dr@kLu>D}#1vrNRAdrqb!hUN`y41*k;cE8Kh`>I0io7`i?(e)i zj5s0BlxfGs7}5J$NI-oo`enlL_umk+*041Peh%<1?7Rb@0%)IRvwaCkQ&^peD0n&o z$cj%3aucT#|a?j}uGNGvn6@R8GjB z!`@_-1zE3k3yeelXUSCly_jj5Jk}Ds)w{wBRF5q{*=)VTN1EsKHm#FSs!S$5O1BfM znRxd)xx_${6cYLfug0b#`SyI!4d?kJ?X-@7ORLaOz!w_EfDsS^b@7#va2;#XHVc=z zrMUd(a{F4fE*p&kDq20w5(7Tnd|9nh?mlD2$VBa=Ge_qN*TF0yxHfdn>aUVWm;VyW z{^+zD|G)5a@iI zwU;FLE&ISAqL=KzvXSB1=X&JPWtsVq0ih0!XD(t5WDGboYx(1jXojVpQ8(@hNyxW| z5WlImZP$|H=&a%&M0cU|uKcg-3^rx|L!#1TIz!WE?p0vx+jlF#n6jYp?(YLTP~b2Q z#G6+2^8&qK6WlMNc-BOcAbN_)s^O}ZRTdQe2>_g$>x6H#y^_lU@Dw7{fxFO^3gsXK zq4z1fzhL)~!a}z^00+$6y(_j`!)a|^QsYfre zcc7PNfU3deqMv~f$wH0DH$`~SzD-KdCAfi`SI=@N6fOu7sqX|JX`C)TkW}Pc*oAUe zG@YDqhuPT4y2GosYhEcv1UfKFHsW*QE5#uOI>+3Sr{LZZ%8peCH*BkGJ+kB}jBF(J zpLDzd&e>@WwSYjRa?B3m&78yBtQ2K+Ix|i-1L6P_7{?Mx>eJPE8HMZC&I6cX>{Eco z6G4H?>A;#V9e|8DploB4O$G{IAq+nDFTtd2*CpQ^h(SUh!FB+h%nO21DNzVxOu6v> zcGX7qC6hj#+sRIzLf?Y)n~3`J-?diad9lFIT+1D10;6$~Heq!c3t*09t)mSvLx4kB zb`<3b9bSpPF>Ln}4hHoutL75Ts-uc3z|qhaDr5a>TgjW-aPR|gzLl_#o4MOS3i-kq z>*L4dX%o@GK;e8Vyq%nHA&-$+LGdx@7z?P6F_2Tn9=5$>hx{1}JJ~&efoH9>!vvSi zZbeP`Z{>eW5`#4ppUZzK|6BPl3dkUkKu?a+le(zoru0j*bU96KKHz`%`CqNA@jXY; zEG$7I+zR!u4$=K>vZ|1$fMm|~lSz;JE%K|a%Bk4!@aaEVgzH+ybVeMtvAe6m0JffW+ zLs~o~iRYJ&-r$I`hlo^T6=9rEfDLG?eM15A*2E$Uc1(xp?XQpe7QnDxfJmZ0!SIoL z0(C_7q-iLf8JU7M4pfaLPtU|>yyFW2(1w`#&<61ol7$ny>~Yyy#EaQR7MU4+g~&Av zEJf{dCN3rQA*Bf|BXO}Ru;6&s8T~Bd=5LWIMS?GTm?2R-HXG#PLvz-{ejcReVVm)e z!*bhbQW5@9WCvEM(gCAIhm)a;wBt=d)xqB1`wa;bG#aTs^mznB4ONQ7V$ftzI;F=U z39mC=4HOGaD!Ux7P=YH-ZJ;?IUFcZ&1p~#mLoCJ+Xa-S1f=5KMF+PC4<5jUZv&@wz z0{MD}k>vH`n?Ez|)B@YkTo5{B_r&+PD1V$0~c_E9WOKoH+aHnaOPN z_{RX)W&G8!dwwaZR)VA@r=VST-@yvdB!gZtxh0^yt^yvH*}>`);AHoLt;&%vhoykJ zK59QIfmzAk^#M==EYfCC=@#u-5RqQ2{&4_pG?fVkP-`iOnB9D;6dp_QJc^`<8|SMK zw~yQ$K5XR9t0kLpWp}qySDm`(ApqO?6WZ?IHxkpnS+58_74GI+zjN~H?CVo!1qN~+ zNGH8a(KsHRn)Ut1IGbVvdhMdc>4`5pX>tJ=SEN0z*-fkDCjz*^R{142R{v%|`SX(B z4B$}(^2Tz%xh1NI{s54UC1~w9iGU~`SH%b!yy^?YxZ~S2aOX*u;=dzLw*C!vv_*U) ztXb!}U`OWdDG#C7{-er}fB2>{VJ0bMt4wri2=l4M;~{#zVaNE&v{A)ii{c$hkn zG^qnJREOi?YE0+IaUICSoWQbsQ47>iHDnw;kNa{{A#fx-I1Mrz%>(wp_R~1ISnUer z2E~E6b&C$`5$V1@x2Hp9C{g!*UL`6?f)Y8jKy!G}XgY*DpX4P>NR-`rkhBrju_5h= zY-$4`WdFbn+pv)IF>6QOS8{*jHz+%+6f(5Jdc97)I*i&QIvkbjtUPlzAgN>MN0Kc| z>ew>`SV4>RTL@#z({Vi={mR#I5+`u@X>}OO9Uu|OSbmpE*CMu(U#W8m)Lx2X?8Pbqkz)mT0$6Vhh_6u`f>xZXQSe_FVVIKV>^RZ;zVtaYRO4B%8p9R=cGM++VEa^&m&(mro1rtW zgoTg-u*X_S{{^FA5f@;XCsI5ZHRkqJ3i)Mz+MzGj1>%_Cz!gY525ESYJyd^ADTTO8 z4@}tsyuO0HIB&b&c-uO@Phjkl^}cIEY*%%ig&KW!|8Lv+TlUbjJr@1`*KXQC^e?a% z9{MJizxy?;>}Z+G$D;r3YXsP%8=RAJv7Y48eK;LBk4yIGE!zPFEka0a7QjyZBNYFi zDP9--fxr5jU&DW_iT+P6TEK-#-f*6Y-u@ctv-_hzugpDJS=_X$ADUT+`LB#-obj|j zxhL??1x5_c9bD&@Ot2Z(1uSMAnPV)^z+i%hekcI3Z`ro6y9hotqk{8-skmS}9OuJJ z3|nH!-9oL_TGo z;P#c%(n3JEf}<(@=5vyzPhpOc;FE4&5ZTZJX`=qj14WSE=M zQ*C3nt^IcR+Fu4t8HXc$a!BY_+lRLQx9v%`1iYbDSsiTSfMbkj0<0VA+Ua#U{1`U1 zVjC!5&A4B>-*wwbfT)Q_DGz#BdkfA5Hv8nWHsW^^iOkw*+#-{w<} z=?o5A+s&|ygjMf)@n)b2U&*-Xrghx5t-!xGZR1t_Jp`Ju>Ly!&?RL`bXX%KaD4#JG zy%P2xgs{Y|99py%;lTwvh+}aL=>`sKz!TeTx`j*urgMg1+2Gt{ja1I-ye}y2a zkKW@!UG!Z){}(~R{5}`e{AdAHuZ#W(5@;a$ca{1rmpX7!m{9Sv=r1aScm3n9ka`GH!yankDE(JtYehHPL?5{2miFw>@p)b|;pZ0YMgH-v@`ToQIx zH?>#dd<2?BIRn-}TtkeU+iQxhg!ji8QY4nf^m)Mws4G+v94fc|T{3-->65+4SC?kJ zHw|0{cqH5eg;6vGMAmHz76xmG=7R}0X)TxTYFBk(j6jpbFZ_dz7%F@nV1lgpj!C5G z7nv@<$1o8pV#bg|9So5_um9_iiMe7A0GU1(ePP)ihz4XSyr@tGS(t$0wb5smH7z8g zS64+Nu*tWzu6-mo*clt>K%HCBJN2wz(Gd7N$7ztk1ZJ6sbR{#pm+bjiX0l_8y7`6z z>V`Smm3wG|2+jrELI}}JN-7Ux2rSq`gW^E8+|Ix5YYQtUZTB%D=V<1wg-paczbT3gWTAf+~G=1Aw1ZU8|>1l8-^y@daAWj=%l@gH zA_T(DcdQ4%y`aH*dS0+9iWvIang4>Zba@Bs>gR0`xE(nElI^%^pO21Tv9{;{M&M~K zo>5$e1t3V7u#~7e{~P%uEFBxUy)bfT0Z)Q1MkUv_7f}#3>;cV zwi_(P#(yMrdWySW|CHl(^+L!nrid9a1yg{Z3IzUoY~Kx`%;*kol|pOce$oxJ;~X3+ z>x>XV`aVun1HrIi$oUGBz*ZJur^3eIr1Y%=ycLW%c2Z@VVBmMQh-HqWs$8$*WFK&` zuIj4(e%ncJ9d44Yq&3%3u^V9W2}?3$^$1HzjLrv=NRdnwHq*pzaN^AWKUi2bOj0yH zefu`)0b1Sdjk!&3T>dv~7_z)(4}6Hu`G+za4XF+EJ#V{#&wZT(^k$@1`}zj^z`D-b zrqiO*Hf^Q12}yH_Q}VC#QJpj-1Kahs#FbWIA;LY>T+f%#VZ>Gxn$3-{+fOf=z&n(T zLF&-C(@TU|9mE*-z04Yj8t4C9zX)+x?+z>87tLV;-G}o6q|5#`+H_zmspo%fTUM+< zDhDfeSWY%-&!cj2-pIv;2e8T_aA6!A%;ZPdR}8-X(CYT_A{+7D7Jt-?v0p(U`d>({ zWzPvZ33@BUG=XSP>;mP5S#_*6dch2|)34-fuci7Dgmb9`S2}W;0 zpx?!V?T$JT?$n8bgXABJrcn`E{V9L^vFK$8HtLb&eCY};BTX~5VWQ}>SL}GS503+J zfk4p~a)867);_U14LA`X!OrWbitSt3RRiVG`76SAV~HALzRkYw7wORo{kcbp(b_Yj zwCF+hy71MpFeaSEQhyL-PFVOefHTg{oj~%e^l~o*5TnsIGY^(c7i$Iy*7_s9NeGjE zatB*SJ5V80huIA13mRgi=>Q&H*1?l|A+B(Saqc1UNB7udMh9?lKj;tIqLi<`rF&?c zDvT?)vXYIj|5Oi>4IP(2#>1fekMSPUojeBUAJyTg9mXr51IKLB4;2R|a`}`m1P;B$ zF5u)brq`nqnDea@+7FvO*`F>b6RLhl+a+>;+i`W9-#pB=#r@cuVc{#$vY6|)p*XR= zfN=<-VW=qMoOxy!XgtceFR(U5@xAwj!1Zm%IHEBR^d3 z^-B-p>FHd6|T*UgF^dL}Z-bLxc?#vZGivl^*Y>}t~LhViyt4^X1027)uq(g$ZjPpM-6 zcQ2A4h&#AiM>wgZ#MQV&jBz-fCw2HLU(%UE9Wf=ZRpRrXu!KzJbye5gW(FkjW)TC{ z-QeqUQ=X!}cK=ejuy~2$@s&sgSWhu^YAo_|hTrwDDq@j@GD+64 z_)iBPx4vs49tJzQ#=Ua28IN#~g4qI3dYxCpP7P!mrZtvT)Ebv_>4LP>CFZmaSf}(g zTdj*7X2r+kcz~Eb@~!X*{gtXjh7E=4eKe+Ye%)OozF53K}hKF}ZbVF2j@ z-j4G!uu1Sc*xO3MR!R#v=Ac|u8cN~}UOcQ#HWAArISIR9sf*mi$@N9Bc##wc=QI{m z3rc%%N*mKW4%UUsi7?^jjE%An2~Ap7#I#Y%_8b$BH%_L@tiW5g`^Tja?+4iUK&BXo zUCZf!6p}xx_OV|YnKt3<32BW1(+%?Q9PX?b4 z8!T2C5K;Yc+jUyB%sTow*8c?MS%|kCDxO_ZN*VL9<@=sV_e0z86d?hA({_X6{EZ;( z-<3(Y4Ux~rPlN#o;5U=6(s0@~%ZcK~*KZ1?-L?n%3U)^<#4+vSJ#8hZ(Zx8raJ`yxD}(E%ygH*2_6ETkXS z4+Ik^+RIYtEMQ62Hk{Owfg~vI3sB0}h0x>d0*!mGa4QY0^J@IN z+(et>0JhH7Tv0-`Wh+XGcL=ZY9=|$Y^4m+N!a^Comkn(5(KxUjw_Lw^ueURSZR*a1 z_*J&}OZHr)L2O>l2C;cHMB04;ivQR+WlF>3`dadPO*${GOyS)$_^*1(ep_Af^)y~z z8eU&JYJPUr52+V_Wc|!5ubnzKc_IE~rtA`t3Ru{3&;C<_%^wnMew?IE^35;DyNb)E zA^ILogo!qwny3{>=CH84ih%F1x<2r9D|eFzo;;=$5V^tl{0 zeH?U42TWrf2*Bxp*{uUOybdRWb&Ia&3T4!IT6XOTz^(7g?5YRMbZ89$QH?`}<{e=q zSb$Mrd9-I0lNFCjw$Xp9Ukb1(2B3$Zi^6%D8Y8u@iGdIhgv8Fj&Xz{#Yr24|i-^W< z^TjWukfF4I>ku)u`QJ=-CwLlPdqebZD$q%&cZ`!sC13&Mkoq@t)poefC**=iCRPqW z3?{mpkt6fXck{rB;%V{kyley#wy-7uZyAT$=j4YT@_a^6NH!3yy{tuiKCydU(UadK zGlQ}7+01WF=CX25FOmvId8sVMS|b>t!MtReo5g7EV(rXA{__kxMi6mZ#-k2vg(30{ zpB_asfEd?6LO33b5LggUJOEh3QCq*4Pq2D}H}7M!LiVZgVcJMx3>Xrh*~fqY32W5x z(n$fnE_|+!h^otQj5UmcJk|zJK~vad5$aJUGAP$9J3f&gNG}MEqh;ApGSI+59oTDU z$&N1R7jX9QGdtjD#I$hzP!t}3WVs8!{Hjb~(#kZq5;0Vk-qLL(+zb1nMz+ur3B*I9Lgai=SZs3LJ*W zLdizrvUunOhu_i8cI)|RlWTHdIbnaHuq5j5LET5Kz}^n?wx4l_KA{1E#B_oX9)pi% zc7UBLf$7|SernprrtB#IT1QUgqS0yafc`nFo531qRyPl8wbcl(c(5Pgu*VTgkWkoR z`jNWN;XmnW3)tgavp9gs3QjJSuw zGgXNN;Kul9i0FKiTg4iyhOBWv;ht6y@Cla-tCiAYQyVk0V#HT05UdP4vjTh4bZ z(M0xAOHOOfh{;>D$ub$N$Z`l3NDX85-sc$E$jp`zqw*9)1Tp@iBrdx&B4@4!=a~H@Bo}=D zoMsZNa4HB2;f1j10UQkmr~`y9mf;7|%6{)aoGQ#$sVcxM1gr77(bWKwEFVZ}0!btN zS-tl-S{s zC8X>X_Ub2@32*qQ9U776@&uVaB!iR6MHF#7I#04xM2sm8-^O3%%?+B2>qKSampRoyWED zJ@HQ_6QG>vHCv19m{4L_(*gnCcM_Y*p$X)cClL^#&~`oi+%#a!8`kvZ5xeAAI9iG*$U|2_3XiC$-wZ=~4 zH_Q383#J^`@C}03*d4!OkL^<^e)pX*dye15V_g!U%BerL4HGJc3rd37zBxfBWHaDt zgVI8YeWY2Ur#{qX%fvDJc(kGgE<~*k6WZ#zBnW+D0W^qAyA62$s8w=yuoIvgh8e9l zN`}cF=L#F*&K;hW3hagqJ8x)J2ed$JdX=blzZN`^BqZ~V?+v2@RPO!6~hR=C+&pLZ#-s)w8E2hduTnWTSCK4*Yp1Y9QJ?7?%tenj)JGmOtPJ{*Vg5Ulg_Mn|u|4dH^s|OJpOx zLGEepaI5vdPlWgOif!iipU83}%!<`6qYOI(3oerWtD5!I?Z_TppGW#Ute=P{FaRlH zt%exR!Q5aQfqQ=XjYN-mXam`b0SgKjCo_C8s$+?ur|4GuI*BTHA$&+?A0x_!OVxml zj5WV(0$syHZwL!ycx8Abb~V$gD}oDdW{1_YQT}{lk5?gG86yxs2u&*hITnbVQC}=5bz=nk}z&6uhs!dj4O{XWpk-=eTy;ad{aOs#^Q(;!-ejE=V5HG)g9LH zwelMs3ZEclwjI+xM2DOefK4`>vV3l57iNO0t`hp|zd{c7+rz3)b)P^J4abnfrYzWV zZg8`UeBL<-T|}lP0st!F4&2(zPM|lG4^1uszF=PRofxf$>lBuzlTr(GQd)w8yMGAe zdpvU^d{5H--ft*p3#~-EaHrrAPNWnONB5}YUeB^0$iF3rNcuPA+!p6W$q<_KcwTn+ z<@t~%8T+hQg5U7h0jECVb>Yh;)7t%3h@f(}vg!l==5mj#(aeRIXI&LpVy;<uV4^he6wW#X7$Wm1K+dbSN4 zAG>86*@oM9mGQU|n*;}La%q$Pa<6oebbVe&7={?K+Y7K>6Dg3O0Za!xLmF_y_;Gb(J(9{- zqIKj3+STq3n!#TdRL1AA9UGAm;oEHE$?yx60hORkBk50$Q0xyREQN;zY~;X}778!2 zr3I!4A!#6W35hTmRO*p4Ppo48hXZTlUDPN?#{2 z@+E#h=%zh0ZwD}aKygs~;2o$QmU@y-mk?6_qHjhEP1K!sFval?RCM4H2o8ei2e$5A zR08H2cbnc67a`(!3Gcj@vfGGAopbIucAe$I-(eVtMg9VZb<6rwHK)Iq^oPio4!od4 zoQ8o#0J&dX;Rh*6D|PNCM1ng2hp#?S@(wHTNbO{9Jjb748i)kBV;~azCd(Qa ztY8m+Ur?u34oSN+{FoRIqn;oDR>F4wks_zVgqN3XC;8apc4XNK-;^obB6~W1hu@>e zq25VoY+Bj!kapX13jj&18CS;b?->EpUH*Fo;e^qFF%C~!r}vE2Ip9zD8M~-tMhwz= z)E?#+Zam`J`iD0Dz4ZM3B}K9IA6wqy{cm)o+dg(X-^U*@Dvc`xZM>j#7i{U^oMRmJ;0^;4qSRRMPc{%a*$|D?;8vC>%_@nJ=q?U|`;*hj zLZ}haD(EMJh{O7x?+p6)t@WVDmn<-CquR*6LUY#60s?Aa3y%EnOSqFayHOXvYY&sN zPby%<9)O#{-PeQlUBaSSj~+V!uSHm+ExBVGe3x&ChafhourM@{RZO7!aLTV}FTw8o zFNe8Ypi@7z_RH!E%BzdT3up(;YDf#=pe`UC(-kGwWZ2;Wm$CP23xh%Zoqrii8q=5-?ah-6T@kf{xtspI>Q9Q04D(437eyM(6{Kk zK}H@vBYA-z@%AnN$>J76({1kuoRqKudu5FWpo8TH8j5-8@Rk=OQG{!99p9Gk$Iv*p zykSFci!Dc&*{^=9br1ok^-4cz33$Tup;^sy3Thw>iBdP`9%$9EOS>~O4i2vmy8}!7 z!mn`o_y|8bt}jP_=qV;H@X(}SEdcm#xyPSns`R~6$@g~tnlj)el3B5&{sOAN2ZzBD#%-Ve zvL@nLYkv;VVzAk^9l%R(&y7^7^Ae^c-@++3q^1G6@ax+(U%Np+kUkL;1obu_LqX!5 zLV*Zn@PY7dy9&d3AV{c+{?kN?;99H*wmV}E)t2z{E*vno(4hmE=_EBS(W()SN=LHqjv zpqpoWbQ!B0-Sjxy1yN{1^*ooE3WbWV*Ce#iVDsdkr-ovLGqJP3>3I9KP?*eC4dEWc) z*=w&o8<-ge0YxP`;2@~L42TIDVi1YpG9nmaj3Fv=nHd<6;;1CYlOPO85}VqDwzegL zN>cx+J+-ZEsi3h5X>HK|OAw8bdDoO1rpZ5R#J@0zgthM$HMQzS$NSw9T zdfxlxd7t}xP4ENM0T{auGgn0j1}F{wA`5pkBIA!lfWbt0HN1Nv)RiHi@$ewdF%~x2 zbSh3}((qTptjr5uLJT;l3apg zP>YS;gWMw=@t`L21ZyM?RmH-GKvH2jruhG@P+CUnnUW=*c!?%(9g0j*pwZ4DWIv+* z=F;WbAR?1Xms)s+7LIq|oa>zs$*Ty)v#=BP94jmgkyxx2gbEo85@veOAQI%Q>3&#e zFeO7z*0G9;0lFsa2*f7m5~nis-DIR@s}+|H@nD;-bmYUL`y|g|r8J7E8Kc0g>BKw2 zw)4{ouLPY!Xil?plAUWjD6>olA>2Qp2Sfy0ib83?l{$P0vS%2{WYjI(k82u2f+pV% z%X*GHVj3Zx@MADKmTL{UgSWj`bVTEwCi{h~ohM`KP$sIwe;iTMaV=~sx_^4AU08{|Y*J508#1%3X}5x1#Epp*4;$|+4HY*EvfZ4>gVOC-;G5IHM9vol zw6}n@tN`@*;R3J_QY}^xa$Dwp!T!?!RJGza)k*GGN4YPHc_K)5=G%#e@txt=$K=Li z+ECd_=8fC5R1N5wHg>6IcGF?DEvdATjfGjhREZLiwkW~Gq&#l2>^f2wC6TqsdIjR_ zUysen;0GTGnlFRq9XaMM%&+`FAs5U-8ECWX1Px3dGaLOjCEbaCM@Q3u{XmcQ$vDny z3+3p3Oz8?iV|d9;!B@wAOy`D14pQ(Wrgip%yfg6^K#RUmSLi{%aLuG`1kfhSg6nZc zN zxM-WLeGnQdekecmA|SMB#E-Vvk`sEv*6lhn&_nILeZU!S|eryG8^3^ z2XFkn5JEsr!0mzgkad9D(=v<;hGJR>?G2wpwtRrtC{TV~s)9ymfljRv&+9NLB1eSoV+EQw z#%!I)EC9k6u8~k39Ax6o=}tzw#gS)D`VO&X0$BP$*}<#s4We0ZQ|+t>f=?n=a5aZ| zEI|G-Ju29E>S@+LiW;U|gtE{BryKWX1>^3jH?H{ABlrYOHgU8=(;+Bt zfgfT8+QNgPLjlY2z*gws9NU7JL5*^vcyuzn6xc>;2p0v~D;fliB|@DgdN?rx52;KL z7K^9BcYb(q8+2Z~ z4Z+`{37v(uyC$g zg<*DY6ZC)>(j&Mf-WFl7LN^T}1e?`YrDMc%pjU{Q==0El@O{wq#z&zKW_^H>*-evS zYai3=6tr`!0!KLHdu<)i7R^7kYbEBL;tz4CwbQLxpClUn)utPSo2!+?V5sy|W>iC* z{W5*5U4td5U4!WegVpgou(^jO zYS-Y_&)UTL`Iy!Z?)xhdvct@q-uBdK8U##?*v1n&br`TBbiOi_aAuA;#I+J34;vp# zBrdI+7LK_yJZ+lfBshN|P2k6Q9byJSNsuh;Kv)mV@c4VibctXg5;$NQkz;Xwb5d;y z&w=X(BXvro&8=l;fEF<+VtLYc(4zJn_H$Y|9`*f^M}62X68nNfL=!?z`!wHYe9j)<$D03&!Xe30#Ek-#jg<~Bp2v`AWn}D_}X!71s6nOTSm_nV`-+D+d5_2N0>vw zoEXf1jIJnSTm{3J%eviJsm9aqa9Z7y^pF1V+IzBkZH7TwC7J z=GtPyJvPvM)s19A59f2jBbgEpSHV`hlsIVDPiajK`dP@N8$Orr#4-xtuSAv&3|ixCee!n^dU7iKe!= z#XZ%DisDs$=3OUQz|gx+d|RC8&YA4jQR8P)JEyZ#I;@I2r}2psO*#BIVG6gyZ1XaN z%ohvZwidU&>kX-E>Z84Zb@cI72Xj7nyxEq>ZbJJAKjKPB?##qK;W94Un^rm$v=T=W zJ_x8p1Gwi`Zy>hB$B3YWNYzL7X0nkdQB1`xUZX!76Z@Y1%}zujV^j>6QXwEIeS2EI z6Gdsf7qIc0hP>`N3BH|BfS?gbMmPvNpA|=^pa6KwbmWJ^(tjQHH=p~_Pd7`Sibxsz zxH$KUw}H{mI(yW=z$D@6QO^+uDM7{&hA$~+6=o#itm58W07Rq`)g4zT#z&K^?^}}N zpbhgq>k&`wMNT4`qp~MLL<2ZuPD#{t0UEmP zQ4OM{5NX59;tnFx>T3vSFZb132gEE;y{Q;rw7trl;Pe=BGzFhTZa7^cSFXKiSd+h$ zU3|Lj;vp=7pJKsW__EO?Jdb_9-Dh;@3k&Vw1A zs^jc3oRYEkm>Mo6REjko!gR<2m^^U`>=a~o7NZ~6vrPYjuF^*l&v)lfVRKxI-(5bp z2;^CLbDWrO=UXJ(KA9NKDLJ<;A?xM8^v9G}sG3|!a^4MK%Jiw79Z}EIMZ{43r~&F& zxoX44RV$YFuUc6x)y+4pS+$}cvG|Jq-rIWnKXJplo7eaD-@MVqa8CK9T43bCTN5*z zMUIvID_5>s?F8>WCs~_y(L+(^>=amIqjt%Vt&Y(KJRLq$>Adt>Ag;!$%M5 zDC(05gAS)!3~@gQo!sr`fXK<+KX>c$jce(Mey`fNkqLIc-RMRy>sGCce%;d7dy67A zEBU(RhTZg&9w{7i1n9kaMbGl}>sR&pO#K^I-@1N(a){M% zv)u#rY9U>l@DzF6w#5t>OK0mZ~(6~XhZdLkCoBLMps^}`ALplT!&#POJ>fM@@Oi(C6PmeU z#B6CL{E>WE*G3=`?F`2rQg9dH(?L=q`F(4+Uq`M!$&KBZ&|1_K}RYL!#v2&#Z zq3lqqf40Pj=55P+dZ)Nc6EhV#NMG-&^&A0malc`?K2<{ggP7f;^9Z%GCEpm9_Gn4C zGsU-oKvU-f6(r6Fy}gNBI5YTNr;JD5`t2IvDZU7WuM+C3j?T&V4xsbpD_3&Ug0LXC zoSyd?LgqI!5q%vzsaDt=k`~mu;nthL-qHP)9@mpEI!JF{-oHXXzVzLE^V(aZdogq2 zN^s@CW^m;p*v;UGb|?1wV~9qCS8I3xW2b4MqRN3Hv7CiOMf1h}C(N{LKG}*8t8k9! zP#2ai-HtWI5PS`Ty`~N)nzApE@y_Z0avhNT}6?;VKyul?PC)7qBg-CY_!-_U|I1%?Cud&mXYp11K z03SSGtv}V&RoEk=EmnG!$hr-H-?Ofne2Rr~xqGx>Oco!4u&SdE;8UY@}ax zy369>X|!UR!X&|c>vnq6VTBlp4C(G6jw1?vZcW0a45P{+U9W}V4tmdHfd1^Bb~yOR z)Bpie17!v>QCuh+H8cR|GtH1;47~thauU!ot6hO!(A&WDE@DdP6m~e^T7q`PDbQn& zyHncbtCJ~kJYW%^-M$#RPYftg^x>|A<{K$2j&CT|Q6UouiL5BAu{>jqxab0$NvO43 z>4e@fUwKU)B`(?^FwBJ1eoAh)yVf`eBC8nI6jtk+OX3CKzk;U~raG8zgm_EcuQ?2X zoZ{ZiJV)z{z^p#0#>VZ!i6+ zB=>qpR8qQ~R=p4oIX`UJ4gvB5*;X@azq2f2ivqU&t z9q7CHCc8w1gYo8D`#0RGQ;6Rlh4WM7z57^xaBWy2;@u_c-*~f#a$eT;ZnE2s`FYE# zpNqb^8(ye#kipSNB%=EySqVT1Nk@Eo^4a(T$gBEXinF!4_m?c0ZvV<~?A{=A!_)ESyj1zoB|K9q=GI94Yf9;B;;wpsVqv zTX6p5i29oDl|Fkv{u zgUZ2_{3U$IYci9^@Rh?0lrd&M(*GqwM57cSJwA`{@&!|U7#CEq;j>~3h#gR}GBJ(i zXllTh8mBxvdT1FYDV$$4bOhp4|LdbFqceE7;;IlHKx8?01{*2Oq29b!UE&958#h$! ze;T`=dyP~7c4z<@vlE+gl&%g0(=|OFwwU)-Nh6ADn;!opNpEzlV5iDSg#kf?vK3Iq zeIfE)R0b!$%&lC3ycDnUwxK4jMI>e?u2hjs+C}ccz|&&vnkK_-Fx=R>+BJO1kIa6F zQw1G6>=545Xk>=KQ6Ld!3W#ok{(C5H=p00xjFg9y1!6aFs;)1PwhFuXk~kBb@EW`6 ze~VhTDAPI!lQG=Y(HYluU5J|+k^GR}>AW0|ikIHO=KJi@#6*&)x2Ysb5RdKI7K@kT zmeF#qVP}`lwE_~K&fyd0*v`(c$!^^xXaJ0{^Ga??PuR!Ovp0k$$JJ?G*%&MoXvVST zfH>a~018jsT)H7zYMjetZ&2bH2tqr?hF=2+|^Uw7m&gh%KnG7Zp0$ae%P>3U-#L&soB;DdP z4tr44?s#nlqhHmwA?>s;$@=|tsGvUd@`vdWLG}otoWF5YI1bto-3^9{Fw>7qT5{?- zb^1(v;wWn(Haok+Id((zG=hqOaqgF7cO1qY2g3RIC*P;YS7TYj5L(!}Kn`N0%5}NT zotp3VMG`xMj2NHxwb5@dT>Fm1_chUP_IbPit$`wD6*v-?EBgT@I0m{YQ3#gWq?zc` z@PjlH6KPx_wG*I&NR}tF3&<7;0`h(=w+ISPO7H&{@r-P1Y_5ZzfeyLUhqyzXO8Lw!ybhIRv6HNJg`FZfl_B@xw4l&plzS+{jje#j-=@8PCrO=4&W#&`(To@ScF&1*RjHK`iBF>rc zhLMM2-8iYCS1wo3BMXMXgv!X0YXpq=l2Hf^JkLE?)~L#wW`tw$a)OOC!|}O~OA=62 zxNR~wL*Q@@K!3^d1cDP&ambYpSAd=Sm_c17A5$>;<(SAbq(gV8()Mn%<^~BmrVVOl z-1foW7ri8@emnaC8c zScF8>)6Q-JF$BjrNm>{q8}{=gMfhu3Cyc)9L`xw+%PJXbkF(oFipN+vnD8^vSTC|- zw^BYQFns?|*y)aNLNKEAup}wX+cC_QS-hD6VAs1wc;r<(mC(4y%8F_F5TXi>!IAYL`vdROK^h+k2e~m=01_~#u<)4zK1VB9uI2mKSCO%;k*mXen`Uvj|rO|@Z^IZCb+zHAF z)~HhIcS#3QZ7J^A}A4+TD_HjHuAxo*{EXNR}U(F!ZzCx+E62`+POF~bul%w$X< z?WA@W$_E@z?!&ymAqtZei;$ywMt5L4Q5OAVF%C5@V(6E`_GyyM%_bQD8G-U3rdH#& zJ-GLNMYS6$A>y`R^JN_*k*x7FL%3Fc3^B0hQ2+-3hD2Px0{X*h2W|&zIILZv@Yyzm ztmq}b53--X?i3qf-NT_NfU%bz490$%YQf0CJx=Ap&b33GU~LeyUHHu2A_rX9X9{k4 zO;l!c&tb^qqt1;REfT>gxEy}*VTd3uU%{{`_?z77d;vk(hmm~eb6=_|D0|}~u!_>M zL77@D$y=FTr;_sLgtSIpc9yc?@x}(ZRHWh+QlcV3aylIp9bP5FnE0QFchZtDU@7fP znItS}GdE@VMpH7Vz>2}90VLBSnBvRI-i>IY-0zHl3ur9tLza~m_5nNrVV^``DPbSk zl5#HKaRc~0s9YJ8lBozUB~ztdlf?Igi0s70VyRi_(X6anc@h0@V5+MN7k`MSQj;9FN&8@~1t{X8XL@!E1duohwsIiiGQsp?=5Aqev>1kH7aSD{*v z1RYP}y(W$~{37JGkc~N6!T>OyR2upeP9EU8w1vIOxeT!aVI-!`m&yP%+Gxz8L&aqN zwwx&&DINNyfvXULk*E%a>=hG+C*;By*Z>G|nt^x@(`MLrH0L-cAj~RlQ0>D-v^CTJj$pB5dPJ8& znXbT|)ZY8PMq9WzrE}34&oNqB(j2r7bpq@D_B7qZMCA*3EhFI4m*f zCxu(t$V5tKB?d>AVp&4MXAtEek?MA(!t@O%cxK6Q8I;i2#njjO#Hn!tsfZQ)=2BrE zaV*(_x1qX#frMt&gyg^4FvenSFT{nAz0PCkb7ud}ZW6ByR**G3_d6q$gRC5~fB!-* zOeF9S!H*F@a(v@?&w^bPj+iVBY&>>Qm2A9JXP`REKEr0Vh?fmM133`}f1ksQC^!{ySi{!*%MB0~KWq_U(ZI=B z*^2QUB`i*!W=IhdmBT#EYy+9PufVM^z5qQQ_WVv;Q(`shomI=n8K$O`nSJ=7XXq z2F(X~i|HMjR7US)$qi8;W@CEJy4YPBzm(p_k$4=VIA&*l#F&*l+v8iyjQQjT$q4JZKO&g7n5Kk_jQtkv>C! z5EF(tb`Tj@u)u%g=|7I3lkj73ii8P9=jKup@fkNdR57H3a2OeIqXXW`BVo-#;{L%? zOPvRG@_wvIR&NhSAJ!!Ytq))&!8OGcgBCz6=HRg@tgB;lsGiU|L(-?J)LH^dU5Qk8 zvU@~tm!{stb8$V|C_ND3I@sx4r^67dLUDQAL?}DJLUD(Ae8WQtqk2|~lOA#Za1PQI z|Fb1{%lg9Rm8E}xnjoqVp}UY84qtE>vGNy0PU6yZ3IO6j z5nJlC$0Q!Z!sF~Ux)_6afWsxqiig13bHDzU-e=!4S9k6aC~7D;S8v3Pvg=hx(M&@4 z5c>=hm+?}`aVhp_+A*>8#c&esK;H%o;%VfAQb%B_IgY79;YRGZvpNauGERY+Py01< zVTD|iToSLA(ZO;T(U(sngNcZU@Yshmg21Cx9DPh( zZ6sRU=sY)~4kN4_Y5-^tIUp}MZ8cB9IlUW4BdYI9n;}5+Gnc9?y za89lXTEGmKR%pYW8hH*&ibdE!7>M!22{KbDga90z9}cc#p$3z%dZE$Ka_iJl^fyrA zP}cNIw>J_~`pz{7d|qQi9uBX_PeyXHpUxK%{#&IucDlMHVP?PL(}^4g`CRuv*!WpQ zP?m84_Am?^E#*cr#3<6yH?j&KvcrWBQb4>C-QEH9l=up@0X|^HFLvAkGKGuzoKMjs zS_7g-BvFPQk-WpxBkSGAZML-NowjFjzR4Sk1oqaOCa+i;r@urV*W)V|N*L7EtTzT` zdU(cnWzrT&eNUA)D+7z9kUchjk?v+EtSkE74-%3PTOkDd=sFOoARMI#CIpe-x$qWP zGNY*qhOK6Vh};g!J)2o|N(A17YRt>!+53w`y>$wASwUjfgWE55S1@j6|1ED(+ zl>vbwrn9wCt#Ta?KEqXCI}ZmxH+AviaO8#QGJdQ)7p05vujILXYIzn{EwZa*wN+jR ztHm>RcX&;QZX`IB8w`lheat?6r2JEbRMX>B96`tMgWatg4bYOGN-PV4OTZFw$=#22 zXfZNZk(zjfTf-?U-S%eNtPzuVDKkM-95;$gIME?5;M1uDaqkX4U<&GyKMtmX#Dd4* z&(InS51wvVK>Xmx_eNF;s1dk5*a%~GFLT5h$KU4Q**jQp!S4_ifLjlp9eD{gJ@RVs z4bpAX^p^UqnDMmRL6D3Fi+{IMBw&A2jsNPDV0+rLZkK6JlmQG(vFr$ZkS|iMkmG zFXQ|Qct@LO@9%z2vDt*DPVwyk;}5n2%{`1it>9LhR=g9~=GfOFLI(48;&Dy1CH~?d zY`>?yDzWgpEUpjMEvE{X63%Wi2oBvp%Bz9!dVX-{P^Z3grlPW-@acTSt!(gP_kKkr z8F8OQ8oR`@;8U+;3j=gkBZDVC%`#yz27d~_!$39$TzO-n6U=Fak=Xi3P&g$yPgy<; z3fMrJ0)V)YPm|_t*!PjAFt5a>;5=tS5d@6rVV|Q{XNl9*`%J1tyzJG}HT;NmaB^(| zO%bqkA|>TZpZAMh+#z^yq6}s(CO1`oKWsi`ZvK3SJw_YEy%J9SIS%_D!fvoGq#Z^{ zJR0H+5&;{l*VrxWb7llV3ZfnYXo3e(Bhv;6Z7kjbwkNXfJxe!sw;=Z52-M+_4?gK) z|5D=+6%0`zb0M@GXR_zU1vfs%;DPK|Z{vdNkt+r5XrTas8Bre)d~RIuQBXxN6NG4s z7XJAZDibRVje3P1h`G^+P2&N|*O8Gg-R;i-oha0L$vWNU1W~8kvbYclRnWL#KlhRh zXU4dcNZldk??E z;3fOL>(gW!8vKA4ywc^*`W<+ed~PYk{!cHnThReeg69oWJ751S&4Lg4+-U{BL+{(wPP-ne6_gH`Zl2J ze@+8`OV7F~jt{;S_XgNSjlS`~DgIISBR(|;{_&y)(buEQ&MJx-Z#zVKRc#1oBpMnw zRD6stX!i7XzYwV-z%Tx>FhCF`?*|$?Vwr*w3J;Q+Q-oc@U(gYMW%p<1*3e%NqF^d^ zH!@+UCGgYvc4Z7dpoB2D=f?z2PW#bmv5tsKe%e||U`N4RabLMgfo6m-GU>g$-sG%f z!ub=a35!k;&KgWLFAhgv6(v<30^O4b`p;MiyqyrSfi9pw(}O`OcT?POulIeAUBP}j z9$@Yo3eJB*2yn82@{s#sDZ-Y9(8jKJc`bkz=6+Xiz00GaEU&1OpHkM1 zDkFMwNn#JKHzSBkC<#FXNg>>uL5Y->(f;wJf}judz-o-qk$!t|nff#+F5@jmRHRBH zDu7vWnWSVV=WSaq57UI5od500<^9VMKGJJ>$eyG(?rqCA_AX!F9|`4DVDu#_o>g%2 z-nuLLZoUOPPLcB6jVDSk&a*s%WW9R3dv9K~{?>KE{~#US{A$*l)_Uymd&}BYeXII! zRyD!)z+dKRJ)%;}Wd+AAIgP1t>sxIBbnGr#XNrt^38*dS-15ekIJXaph51 z4-{>j6qtd-F~1LrbbdH|t;z_G?CIQX{|krT?>-#n(v#9S{wTIF!hpyQ$%HP@P!Pu0 z>{`~to!$07V=AFb0&52*JHV zt;V3_Z)AFWKV5O=ke^u2arWu7=#vc(k}3?+VEo^ezXVHaj$t}X2E?gX=jWUX!*ke# z2=G5kA3&DlUlwgoynHD6(v^H!oP4=5`EqsgC6y=jlB(y!$vjJvFR2z>hdCN@<=51B z$ES3JA9~Il!r&=R=9CjbWBFh?i zjqD?>B~*vG7Pau#)pB((@>N+@I?J?==;FEfZLzK!iwTCS)(jQ`k47O17i&E9M;Qe) z(=^qD_&Dux@dS31d&2kf7jXgNxQ_4|I+RU?9bHJgZP(6^kpP<=`ibA4xH>QC#QlLc zByyMQac4NLOZ+bkelwRBx^x%lu_csvn{*43>9F%6oz^dA?lfGg?qQE1-LbtE4!ux~ zyBY~HbrbL$%ExXP4!1oJG4D*IKo~SytWw!4rGE?-e8lbF-K-C(sww#6u2b?b+@fn? zU2c91X4<1n%7Q;ZN4O|4TX3OlCt7#E4E7p9Pj>w>QVCkXWS0LRAhs#NB#8+3K+(#fc*k>XSDjHV;&D6+Q!B6D5qOCg9=>tnYDAL_}}~a_Ws2GDy4DXqErf#-NBxoAD)Y1=T)>p$by1b!Cc@a9&oz!WeS+_ zRV*^5fLIexOsJcuYsAiF(n!oLB*>7Wmvh;!b@e2)N!%kovFssncl^aj+Phm5{B!nu+t^X+;a+m%#uYEILg0*|@ zm%=4&Kd9rYfEBF-F@^4MN&ck%IbbOz^cz=f=k;a`)j3rnbpF)=m+j=?T9{z7;L ztOSur{nutRQP|%)#^j7+(tr4Mhl+y5faeF&uSs$!u{d@gGl{`0_`NdrVo-~0O2rj& zzK_(4`n78LN^@f5caH9a`M%pY+Rteai^pbY*iGnbA=zFHGu#p3RFTyomhb3R+rW$=h=XZwPu8Po-dw)?jAbL_@i?IZ@P+K@;&_W;qN4}^#7PysC zNr*S1nAjxH+uCsK`Tlorcs8;cI8K7b;<-q;YXsqhK}W2EZRs>=dbo4vJVK9@_I@UiQNb z?wE6+rS7rVJ$m&ZCX{=dp@-Q;dn=D|e34cKo|^8x*Co5oJtAXI$<(N1Vwe2-rma?l z^eGIcGtCDZ$perbJWr4N%#542xq6Fkn~0*fPIa*P9z~t8UBiQ}i@zUS5PsO=R52E3 z98hZ*)aGg3%=9#rp4{?^wAqFq<2sv%0qng1pEFG=Z2aU;*^gLwKz^FwcPTx3E5UG@ zLd@AZ%~GrjN1TTkmrvVp)6m5l!zEbd85yWc$HXYJ&nQ-x?GRJTnu}I*yxpJ1iXIm1 z_%zVqI5&+CLshLq1OH3hIavN%^Cbjwf2NI$F8MQkT;RS~+%5cQ-JL%bEdIpYA z&(HUSGTab2|8p1lv8J2kS1pi!+_|MTWGOt6;Y;V^Ee99 zQV5v#a`${B4QWxOWiyeikJ!SOjmy`sym{SCpCH~8Od_0p;=>KMD2BlF&Kumf3h+0Yt%6B{Dee0LhXKEO)bo`=&!)5C?D_CQ1M8=pd9 zWXC1@wY0=+Okk0;dI)S3;J(JG3x%g6v=ha+Zgi7ikl$mZWkHDyH0Z*M9An=&1!oq& zhw9jB7$DnoTcPpzxPNVU#`Dtia6$Em`#QR@b{Pf-EzBW2qr8I z9nXhnJg4N+cM1`bop!ZqO;e6d=@<)XKc-LI}1;o z+kh&c+nmNT#ZHOOM%c-qi%<-(z-SGFEwF_wV-{;e$J%hj3wEIP?8iHrh%M3O z7TjFGL4t#d{NQ-l+h0YFIok$y>mRl3)|roEEETNYs`tL|OwQrSG#dQQ$!BaZ;tKQ7 zbWsF?`zW#?`?OFxQqE`oz9Enaf;1*lnZg!Cfw5%w2KO?a2VuiyqQ*mFbUJmed!TI` zZJsKl?}L#snTh#0M*?=Ni!9hUMi%5DdIsDCQbz>M3kXaD)ucGuMh7wGgm@qkClKcr z_GRaAron+APeHtog`%4v1l5`#n70A(!(^aV{e^}xMk`p*#q zDH|3p35R~q8s7sE_nKHJZ1Lcw_>0{Ke*-D2t4zdO2{NCGF!I@k%rff{TBF*r9pS;e zNn!*)t#!xkQ*t%s0xdIYgnr|mU~EGW6%cuPx{W)8S7anwS|h?j`B)34{lLD&go%?t z%?q}1NDz6&u25}UuoZgsd@BI&wYi95kw#E{m#*raX5SZ~bXkCGvfbWhnrA7w6=TjO z2f>V-i{^)|TrOvFnlMzFXiQH^1f@CGTsZpj}Tp}8%3NJIBJNbu>; zih6{SgsN1$8q9NeVJ5k3C;h_hqA|d7xap>Lq+|S*5Uru}b;@i{mdO%1u`+%6ETK%9 z{}N6>BS7UG&8$0?ewQOSS35_a)h4%Pd77wxO*nzoj|r!36jB2y|0^nf0>SjwhKpIg zEMg3s*gVkLs=P-t#OhYJ;15VRe2t`(^z?n&4%>9k>=N4;h_ALX*vm z1;)9%C3V5BAH*0eOqTdSRP_jpJ9Sv`4E^xGuhI*K__%wWxmE%pYM5mfiAB8Z;*1Dq zxGp1Ai>9Lkx8w=?!(gqnZE#5JZ%MPU*dizMN3G`=>Y<0h0+OKXuzXKgwl(1}upKzT zCq7q+_w?#MY1$zz#(j#=PBC%BLYH%`te2BH+Z~2Wf1G5+g=5xg)l(o&%#0X6PC=L+ zttbsYehhs!FV1%xVVHaYkb;?n{xMUK;LaE+kplGqnLN`c)y-Yux$>g;T59=#O}YOC zWWWzrQ^dtY1-KOkP6JE8dw~mSlGHw^8SG$^?uAAOJ)jYgF%H*2Npcu`H#CEP;vDkE zFh^}^U*guy-~g}D@<8}@_Vc=68$l?npqov2JNgdi>t5kX!0vD#pe3BJFk7cF@jN}C zVk}BmG7pz2k{Z!3=Jx#PFx-zwEm(kuX_qcvPgZE#zT&!j59^LShciUQrXlfCThM%| zvmFS}&t(~+0KlbxfJI@gfQMx}L~Vw4Ajz0s#Vw;@xY25+HA_ago^qB5*XI8V3D*;( zQcYHrvT>;(zN#urx}oRhb*t9CQ*>&A7A>Pz-@H(1(48ttjV47+kgTF*r}X#TM!m6^ zw(!T5N!5=IQmUBL67`33#Awxs_FK)`%nP|gxCxiD&yut&ub_Z)A)8)9PnIBL?|O?# z)b&3jQQrm7tgdMaPN$xFLZH)puyV)G)~XjFVa1EDTW4 zd(1`!3O-mvxS=Loz~9`!bjM?}Y=Ht=!W#y`a2p#Ba#TblU2uG^F{z(`Nqx5l^E6DD zuX;`Y)a18lGx~)>ktoTc)zYZ9h|$k-iXsHz6m8j{tmX~k#`s3myS!nZ6!DE3L`cX< zJXkPN^I~&+2@h4?^AG zbLbIW#WRWAERH%8_o4>{1q=riP+H5Ee(p@q@1CqVd$cMjW& zpl$j@bd14I>-q4B`Equt;>qEQkkV!{#WEVz0)sA<}g>xPx3Ek}Yi|Jn15@6OYC(4z!#oaie^XHC?%0X(x{cOR4BdA@uD1tFe4U#pVQA!TeGRZhv)~da@q`-Cy!MG(5IbnNQ ziJuD+L~A4x9=cf?jR6udl}vw&cQEQ*kQ9fbl@;YgNCtK9|&8}3flM}Q&6*PCPq-S$tSXtO!%01 z?P4{9AQ=;$32GsBV8_CD6W14{Arl){5VOW;Q5JHzBJ7*{I-F3}X1PX%7Md_Zc+s8B z*f7ydu*_mT7KESV!J)fTOA=}A4tx4B)B4I!jqAIRG?)X}oh})f6T+vdZqD#4wjYSv z=?!ZMautaedmm{<&|_<;F}Q{_!4$VKqK$VHL`y^g@Howyqt<05Rc6GMl~jShNPk(S z^ooa3RkDRJ1<4jPf=9$ivXTb`lm&we?dybHMr4qdyE^%+jf=4`^T}emKWxXu-#QJp z-ymhoEmE|HhbKb>2nFEFco6@?RstxA|iN!%X zyTcBa>a$z9amGtmw`WgW)*E>%t;FPMs#Qk624qaAwS#YBuS#hQd{;R>!Yd(h7KFW6!Wh0AwSdkL zt73k5G4`PI!&CC-pQ1Go<90R+r0a!dr^%QlNtQeyyENH%$`k_VtA<^z^Q3uEG+iU; zoz2@2Is)pa!=tQA{1}ax4km;+$n++8h6pc1g5z46I7-|w$|~EHN5QT&h;B8oW3;@oHP?^|LkylqJa%+UX<8rhV6e5Qv$+Az8sd z@oYT6uqd!=HZj3;2MRPKgb$pZPMZ0gG`B^7{B#rG8Sb*x8c{&AMKY*=P#r{7$t-@6 z5Qgjt^gUxic(vmNAWVD0_dOS4-aWY^T+c_LM`BhG_TKQO8=yC`PIT)yHk_%|QX<^G zqunwK1InPrR&_*>GARf5Y&)WPY@ zR1nf>rP{0EIBrkzYtELU=aV_}7+Z_I-nN|NPhR<|CA(O^FBNiQtV?1Jto{ z)rO6$RxIydwX#|&7uum3lyD{Wj6B?hquFQW zchk{t7lH}X30~rG6v{jpTSLiQYz_U2F1puGf9bs?aF^ceCxJzxOYilTy!2l0=`b6K zFRl&Ui7*=i;-u8C*i&*g>B! zF*o$vB5Opy;|>cD$KQxk>3&kxs}H?#{0$tY67e_mwqp7E6|4ICmfzH;O7ulxH#Tg% zdDABn?}R#8ziLIlYrIcH*p7`itDf@jt2VA)zG77(oCkU0m@S$iqI_)VzhTAA7ET14 z?n<#f9M*i~VzB0W@wZ}oNe}|L2_q9}F3@I3Z$*eZZCDITxgHXU6>bP7Xaol$q&yRk zBP@6^KGinIkkq9$Is+?LWE;eHNClML~mO-j6m^dS%-Gg+u+GO zj%dR7x+IH>c9Q?Xu{gOc490Ums*H;wRo{L5wCm0+25O_o!lwMBQ;I)Oa z!N~mN2B{Y_|Mf@j*=L+CY$#n9wB)aDkQ>|unBn8m(yicsGO-8IZnMZ~-F} z^svKLgu{vCu{#*KB^>VnrQb_K2#Xr!XHC5n;C0Y$wk%e{1M({zevwKzoe~_N^b92= zAh1V7%xE~EO611*6VghJ-B2t^8Xc@IKD(tftM6sD@|O8D8v zp8UC^!-l@FaWeOzR41>3Rw({g-g!_r;Y$NJ1S3hY3p(TKwv3I>V1) z<^)HENP{RK(0=fWutDhIcZV~!h0{S4;6})q;8w&G1WnK;L`R8fHAqh66S7YUg_%J~ zn}a*O0?Y6!YCi^BJ{PA??F-LlT)w7z1PR7LnesNp&yZ4w{;F>A3-Fwvc?-fv8al%% z*nlVrVaY9tIMw{GdI@%fv>TQ@q4CB@2M+*w&mvdpa&O|>4AzvkH<`*MXY<#95OG{17{RH*g-XtNUTZ{=~NKBaF=o;8;twNA#U!I zn3Y$Y9EKq1=nR)H2(L%0V_ei5+QtMop$dyS6hI}o-Iw0f(#vHw^=c+^VKD4f8z?Z- zPZV<8Zr#&JoySOFXJRM|!whb&#s*B`c4#wrL#FRRQ#8soGjoYSyRgy_R0(IH!Cehr zGNE2nfR1wrK|9r*8tsYjLIgwzRE+AovNODvpdYve7;gy)v6dL!kA>fWK;-cm7(V1< zEg+%cJq!1TOX+A{ST3@Vxq^53x99=S7RXgS*g)aBhqGbp3sT&{+#%rwW5Kl|+o5Wr zSRrZR_1(tJjx{KjG=|aFoW!w0ZSr?PH>3&aaQ0+Ts1&#Kus_neLV0d&rg!W-L`(<+ znI0NNm{V&bv1ZWhv-kK&Kogn3%#4nzFKbStctO!=b%vv7*_DXDaAO-`N!h35Zs;~^ zpfjFSA%_*ljQMirG~87?c1<`QE5NDJ8F8{tWV<4J+Y-JPr5ze|EfYL#qHF$wez!ZA zoTB1&jad`UE{zXIL)x0FpE*WyT&ahI{?X%)oYP_HZj&oEaQ?1dxE;)aFqs^I?}*Fs_$O4bOf|p}g3|7uoTR)bsO5cD7OTSNQK?{@ceo{u0)6 zxKy!>5YoJdsvg5ZPqm2D_(?d5>sAPY2=->sOmK@dV_1jg9P$7`4BxTAlF}86tPr8iyEv=j) z*^bI7;B4cT{s*UkMj+Pv+1TYuV}gT3bUO66FX!JEX7kv*4444rF(Sfyr()>`p$q(ic2{^=29+;P=A)CSo79% zDT77ETuSdxAD43e334e1-Uye%c$YHVMnubO4}n$XVBQYciBjrVd$1Fs5cmti6*>MU zsp^xV#yfvTjSsTewWx6oR84}q7ByZs^BbeaJ5hl@5xze`jeoQ=T$!fEZ#QcEJB}K^ zKSPb*Xp|V#_y>inu|d?M7Bxfh0AzT)Hs@Z6C8axWLn+`HU4}^!~Szq z<8Yd9BQ;)SK5A0qUx6C`5>)sT{CAN5o?#vT{yRgBi+I+GFya}x32s7J(l`cJC4AqR z6!l5L^5bY2$5BM?{Sa|XhTJY!P>DZd@GK~h8 zE;)@x4c&wj;J>vgoq8fJb5Nh+H48DQM#HKWvNbqaVLsCPlTrr!EdS5O?_`uo|IzD5 z$}EJ=D8o#dRaG6vK1(XI5{j}|nT0~wX|>+TA1B3ImHuCR)a^&U#aNpp)TPr~iy&Ju!||1NEjk*1DPB7{`i(3LNX41jo9pGwe=t zte-TF^}CK^{Yr*oUEw)aQxUEc4@94ocGjU4L-V~&++d1D+a4g1f{u}*(m zIo2xkQIlhR7>@P9h+F*@&u3Z3``;OkHC8Q&b=A@^))h4cX8BpxH3_b))j8=DXj9@c zsxrv+Q`Bc}D;{3MN?gF|)EU3RG-UYOh`k+iqUwy6Efvl2U`)Riw7{Sa-5Or*`O-no zaBF097K7??hH=WbypYpkTd|WSk57tHoe@*2S5{G;-1gQ}s)I$wleie6%*=)wdkx~en8@+fJ_w6(!#(eF80^ba$%XrHG=KP0uu>hO9!YH3fX@*Si_ zsqJl~MHk1kDD(2hXi*yWpPLrF3bV2w6&!ep4|NhRzVnc2WE%8`<>gC>30X8{m>r=o+QOKN@wt>>N z#_h$s!X1Ur4EC(h)YRNY+U%I?TR)TW;)@Nrg0(p@9{HbWIIQhT^U~in3+uJ*!M5=k2Hz$^ZOF@H~nivZfvIhjhXr<{Tp+c zDdSHl!;j%ncK-|3zQX4k<}?q>uWoLh^X--VFST1pHn$P8Qd^nrOo##e8lF2$PI+hL1=bAF`{sg>dSU$Vf)&q{m@euH9(|uT-gimhD=N?VN^Gg%uzX;sT zQ;d`ILR@|x4-i!TXeR%S3HaWYeD388d_w|$&+vTip$vRc0zR{~tbfX$Ya@@=@m}DW zIS7iGF+OlgJ~t{ehMN<0JTo$%yC>6zIlz4#H;!U0;x(E+GZq9|Og>Eyw?geR92k#N z@G$tVM7e&f+Q#K`JFOnK<~O+|AD8hc@J$9!tXX=?l8wulJU*YhJJXkk5^ddgPCmD6 zNVct$3dy#Pn!=hWr1p34VrIW%Nq>%ixw;W`rHzRaGU&mGT<$D^6? zm^HKZ+G)R-{gbZ4=H9nqC-8wZd{eTGV=gIQTSBxx|C7Moe8heHR5Jh4_m;swnS>t& zz9U_KYK%6_%IA)y;dvcMzW$Z(s|~;Q(#r5Q%GBELdVfB*GJ{`EiRRI#dE~Nut|yab zR3^=sxvbqxnoBe5;h8H~GnsX_Gtrkx^YYP|=GJysW^FG9zBhAb?8>y`*us2nbEX|_ znKb>2*o#(9VvhvKQeat{V^d~rZ1@Z~C)>xMC$%>EAntn7b)?Tr;FHsE9Y-2$kvvmLC{=NM9+Ih$PLiN0bU*sIKdfnL1jYi(DrLn+kwQC;mXEN>bV>jlDz-(ON z^&U)(#?FuZ(O=5vUP<>m?$?oDE}!$SGbp}ocifvVqgRu7?#5ru=VqqcmQUi2dw(sT z>&(EnB=*TuzYboo^D42|f;^|m;Q2dm5A(|PjOl&yt<<^q$XD4*RmQ>hW%h67a}TG- zAwM_KmY0E#A-qJoo};O@6dq*{WX`_Lq)5^NoBk-DYfsmc;sJI6@5}aw^uC>?gZW$` zGiE|^9^Uj#+S!z8C;9o5;=S8=zhihV*Zvpa%(A^RJ>EM?^YC9X52R1<1tLXl-3mVW z5c8ad$FP;aGW#xuJzW8&eyGeDe^rL7V;aEP1U!RpY(HoFsq$9BjJ7@N=@z%ou8a0r zX9Vv7?#}e4JA*CxtqpTpMtv^)d;wuZE1exR=g+_0(lqBAU;pElT+Zit?EKZ_;(ojWd|qbn{&HgO zXTC)Ft_^Ml#-g3i6+R!#X_*u*9vUtg!jf#V@lu>x%_%M}E@^3FIsfUO zeDh#Si6za7&KXUw@-`oZHn%nwvs!Lru-? zuC%efw8ETks4&f?%If)t_}r`4pZs@gw9U`+(wEEWdL7apejNA_>Q3OPycU{=ANt>{ zqs;igPv=g*ga;GZg=hi%b#?0J>FZRIDF(3vjyQ~y#w^rK~>sL z_1z785^YSZXFEFudlcA=49u5%6j-A(e=r(y$AT@bl(>z;Rou#kJhQb zX?Rd3n`B<%{yhRbGcVS^wrETb0q@D|-!aktRl79?vrY+e1IAZYT<5_e9@)dkFZE%>Mp9`I{&+ zt8dzHMlScrS$=)T^vb7vdjy|N(8LYt_84ucHdub(j;|>npTr?u$lpD9sHrc?d$i5> zC9eBw)!i1s+s09Mf`@2n4Hp-}C4ttvFXNtb%l5C8p?OywPXphTk_EPm3Kxgr5>c}S zH*ZSU(^)nzv93VyeZWVh=_Z?VtK-5T_hM$ukI)gVA;BNMD9Cl5qB*ZUM!w%Kd@#t( zHvTFemmtSF^YoX>9J?qeGvt^LaQB$&xY z@Cb!Ge!O2;!k$T#+ZUDV$9wM8{#+=>XW*VOKHbg_1-X?rX65PjCeppWl(A!;U_mnH z(aZ69r2b6$C})7pPg!}vc6BAuhh@vk)^wjn*U#6}z+YgnE6o>+Cg_2qZcCK+(5fIS zv$3_KG-}(+z;Cp^4vw`@_I(izpfp|GejlaNd^S2Gm1dx)@*4GXa|rk&S$Zka&H)r= zPxGF@J@Hwj@)xi3_%J)x<87=phDx{ZW^jhhNxVMe^)atM$Q{Vkv4Fw#c?-7&5luaq zw=F97<=enr8GL*VH7D2o!B14)@4o!pCxfiaJ#O3aPX}4v>N=I~$8X-|+VGuq;4jvJ zPrkEu`I~^FT2IZ-pVL6!zFAv?+*pIVJ)Pg~HlK%II}k+rzIbd+AL!w2+k)J_O!)^X z-;e*1?LltLITiLA^W)2S<^N;f@}5{jws$;y=~rvR_W_@q&L8uyT3g3~cctOR|2FY0 zdwz9}gID`a@PnmyW3o7L9tyq*xZ`u8F?(Qe%mjb>;ULltI{3;={`TJrA{~%}e<}li z1bAO&Op$=wn5z7-zwPmCJSRbNO)JeSzXOetDdRvi?}GRLp6_E^hMjR5bHVolA4rec z9b~sT6MWL6v@s2jAMcH&#k;@PHu98>dR~YCTqate&y$-@NY3pm$%_NmFdv-sC@^49|A#?pCPlG#MAh6 z_Z}skwX5t55e+t`Eh?|?htRcYS|Iprgr_D&Y3_YFfG<&D8>)Oixjq3%}7^<%yGc#wN(X63m{(r;~1TV`BT z$Q`n|iPvH`zYw{H z@10Y~y-s@*I4b7bZ9Fe8R7h0kh_EO68(yGy@DSB?)GuCuOejV%iqye zh~&Bsen$eADSzvgg^K!1pMNIsHXFCWafd#M`*`gl`e^5OVqDGI!|5ng$F9YN+{!T3 zhuB8Ik5%rPLZkzA?SvMMTAIuGG@DW0A1bJvD9ugzvNTISUdUa`Vo!`$Fsej@8AIQv zhu0Ny`_f~Y7jmt%4)#ohPgq~b>dW(<=M?-n@az}~8Ean(K6XPPH(>2f@5^G|?aNB_ zjsdCc=`ZBQrfH-6E0n8#2!7x;zbB%(j}9^q-+8-V8-sHv9G|VPjq-L--n>oJSGGs! z2^oBvm7l1dM)j(k&7Wj0Gi&CdsNI6+?kHsWVWY{$MEMs3Uz*-GHh0Rq_tS-lzjuB1 zGH9RoDd4jxCo%sfGg98dX4;j3WyeB&*m)jwH)$tl#{M{cXp7*FY$;TgBlt4LY%7%U zh|ea>zQ|VA(E>O&NEU~TPG@G?&k|-@`)#a zYs>^MK-i=HP1a#!CfGe+V1JV?Ii9}WL%`b7`Qr7dyvKGHD#~iIW8Oyjr3=8Rzf{OQ znl3NZSEcE?7hIBV&q}&tVz;%Xt9N~2kzK$1xJANaHuV!(QJ3CmFvG3Olx!LJD z;__by_H-H+pJU2F`HyK-8w=oBngE*1XiUL(J_DY%bE7g%_z-;K?9%H)3lxS#vIFVRoho0z+}?fZbuGnm7l zUPjc$H;qB=@7dEf2FbN?qwNph{RaI$XOOM`ayxNe7| zaX46M#*PWgXujErF?a;{u5=xrOxDqLdYC(ureT6768KSdw7n~gS81Qzdm-}lIbp6n)6XrmvyC;L( z)q%G!ty4enhtu;T`vTjenxEDWm#wQ|m zXxr%5-$M|vwd$Yw(J;%$#b<=d?*~3RU4IJxDDciSJf=~U|0Uo%)9_UPH(dw*PQy)a z;@jW#u`u^c2CfEp_}FD(?vV`c@HC+6fBNHLRX+p4uf0BuY%OBk65Ean{?H9!nQupb zqW%LnRzE|Pzjb*S*>Mk+?`dT#|E91^MzSDL{*5arKhwYafVVLw%Ks<=pT%}ICC}J{ zMSsRBO#x%k4e2qIxsu^N!TW(fnTDI3#*gugTPv?2opH*45cohke_Zyw+raGxOUn@| zodtU@hSnqf>~vW%tta@yz@JRR%~w!uwfwhNris)B!S@0`VEJ3k@=~s0SDSQ;cfR{_ zZrl`B^lNI1%Gn3J+-GMK*x%7Ms_V&5S8hwCzVEpNI+*kYJ7?WkX9M4q&VO67{(ZoA zWcJ!0kRZ3KFB;m3LB7wV65&lhWt^FiRX#`$I7V{9)Ej&nZ2 zmuM~uzg%aW>c9^HuQiTyf2H;~Jq3JkW}I~P_;KpH7rNWviS@{i5pMBgeUvoI(q*L9 z>bckE`NnN$d*i{AY>v$UY0QIGKRaRB9 z9gBaxewsry()hMsyUX`EUK=SqbOiW~)|MD>fr~0&$B+d^|dTIX+6B@H)uoV z?AS^9?c`DYvmYYA$(XYCKfChMleb0bcKnv-7ZZC9?CaN|%6geJ?wm@b$vVzI|Dc>V*MZ^NarYlWi)F?h6cWRC?qfe?;Itx!AN!-~cu@7<`Ucr$&A_YS1;L*JUJWk@e*D{vQAND4H^vJ}xAZS+;{~PJ zM;csHQfq8|ga@?_m1gguFo&r^D$QBcR06JbrZle{@pZ-HZuFloW9f6O3u|)>52Pbf z89vRU-}ks9v1g#>wN`ySv;L+!zo2&94LrL(Jnt{~>=!+bi`$X^U7FEO9fCjex6u1G z9%*^xj7V*4F1!Z9RG{EYo-YpF~?q{|voS&>Bv(#Wb^y_f}h8 z{MRzMd*0I=f*(3wE?4nqg5Z<>EzGrM#zO<<$9(3$BNxob50{F$ zdo%fU=Ctum`R5KPMzZ(hSW5%lWc-iPJUOfw+3&}0d^-kxn?6O_u{mGfp_XC}b#rF! zt_FJyj>+DU#fWZ**Q$J;;x->2Rm7JnJ!VG}Z9aBtF>6B|%!u%l@Atr&#WMSWAamTxgxf{#+G1mIw)4VXQn0qL*AHE)~k+>Zbie>V>Bost!`+ z&jY?RGnbN&wo|6yyC(bfm&W1mW*l2dHK&tomRU)lcra0ZzURQ z=_2d~Nk1#yf0LJ~KEdy;13v`(g>?Qn{*m%Oa$&KejfyY-IPg2t`9=EL_$&XW8O1Ul zUuTEUKlY+xBtvjC+J*#Qqx^RResDB&YSoRi-kpAkxP{F2IeQR_kQJKhW4D`?%O*UhVm^`3aQq3TbM!W5$B|?buA3O6~At{$v$+ z8*Nu#a|`R&(M6hC?Y!p%%6Ne^wc0tht1{fTa~be@^ZHPgGW=K^tpfLTOuDju9sQ)K z)sIK3zl-tbZl&&FTDhR=4p@ z`CkHFK31I8i9YQ5a4{_;KKs)}NpA zyRWaEf6NWVtbKyXmVN$*f#1#iBy%1`SBdg13t;%Zj62GCf~*lz;~AI)dQ)$TIl!qX%N>7 zf%-li0KP2Uh7^7rb8Btxli=w3c>m zf*&)y7WXmsDQzR4t+)SDoj>tucHBW<%f|Db)ObGmU+IsfiR)6Rkr-ER`m~pGB=%M! zcDA+UPS#DjKN4fa^L`Zg9cj3YsUMG(TWZ5!tOH-RwQSB}`IOrC0`OY(Upr7c{}JG| z@-N+1JO3fzwem0CUK{>&9r(PvYUe*#2R?fT{9$^2V%sZ?U+ZUTv4QF_@O{AHfZiCRX zwOeKEJApC^pRZlUT;PtjtKN=#>Xh-!36wGR7iza-8Sr}Tc&JJle$0;oUzQ%P6rP=X zPi;JV2k_15{3$$p9C$rE`{*y$rbFAlP@4{I|01-ZwIQ``Q*`J{q^U=THvLk4I&{{r zP?xRI#M~>c9Q3Ro)8qG+o$)C;Z^y6JhL8HS+VDMf;FEv7a{e~P$o2qmH|L2yWaz9> zyK2)}j{@IdWA12xC?tM72JK&}O$QwTo}qbikoH@WuOT=YP5meBK|` zrX$8YR(tR706yF1D}|3^-0&3eHiJ9dk(HAl1%A}v@tTt5YRnrvv%fMOp)!Tvb{+ta zL+Yety1~=2K22#3e!c$L+4>FNkGLI1OZauUxC-32;~wB$){n$m$j~b2zQNf%XY#je*K+`Py?Xkeu3yjVq^UJ-JN|q9dS?H5{dx|PrdB;mzf-@Srf2Hca}Q~1 z)zkJD_3Jr8ntJPC*>`K#^D^*y^RxFa>(?{sQ0;p50k2ojhQF#`&oR=}8n=OG>(?{u zaQ%9oBu%Y)79Xi!PwBb(_1sOGTJ?;5zJ5K2NKHJ+^&ACWZ+`Z?P`{q`zph=+ zqrmIc)AxN(fB*lCy*rR&+j`#z%PJj}Ex*by*_O-4Z@SD&&h*TgJJ%L%q5*Ud?lc-W z0J>-97OS?<0J5M`2D|ct+m(Q-P6M=@}6nzz1QPg-}|*P{^oA~#NYlg?>q*#-};8)5M2e|y}2@XP=Ey){<+6Ti>%$KpW0{eL_@{;&U|$H)Ieo`Q}mo#Q2~5=Bsg_ zCx7mnFXpMmGk^8XH(!kd{U(LPkHvvr{!?F`C(_2?pZ@02IMCnW`Cm2Ks=_pW-~J1a zKmXf2|IO<6l{n9@|1*z&|6hIO`OAOyn@8gyfBv6~^FPcRev9Y7ZtMGk|AOb=+Vd~? zFL?e~9OQ5P^WQZ7z`jrLGo06$!ukEb@XZ5sLj)JSC%*IN|HW^U(HF*uXZ`KJ^!OZqh38+h$^Xl*jPVcu6l1*nmmgk7T<51RJdd#WeV%`9 zoqzUUd3cUEMw{m@_cFN!W^-&w=lw(8c}(a1jej-bMm9!G=l%9CepAmAmP_H~~5OaJ;ekLDKr4$mLcss8%E@y%Cts<(gX zo3F-ne)He_=Bv3yf9v03uCM4+n}7S8dOV>L)AonQUt z(cGCo|MZ(jb7y{;=f7kyblFkQof-4|YdX_EIEwUFt}&14{O+@e3+Z`<{{MvMkJ110 z=a19>@A3R=I@9vI$91N^{^D_+>979y>pIih|K!WMOg!_=fBJQu>2Ln!$91N^@}GSZ z@3;FeQZ3S-ac6DB-QW1nAJ>`w9?$=(oqIYH8t*7tj`^MdTKbcMT6Z`va zcf)u$``gC+(_(My*e}-XayQ?O4x0@l%+4nJ`D*&PQJjv}hwXGe*e*WsQ)4&ZADCqS zsWICwuJ?_}=BU`t@7DWyZA#mGIq!TmxmwO=+HctHZa>*CcKgM2#{fSZ=DR(Qi_z_5 zvE8h<`;iV}G%5z|LD4yHcbn%8AbZ{)7tPCN_pEt*2EdBLVzqx+>>HyoJNvY4AZVmX@s(AaQV{?o~JeK#I8 zZkCIyf)%c}#Rt$ur@ftT=97IPhzTb@Z$S6c)*941tah9Eba4$D@EBaZE2fEXWu)1BH{C8a4V^$?cssqFPv1A@+b#16c^k!UakE)3myMf<{WtTy zUUdW#X3geThnpL~x4T_`w6BWEY6dV@^K>@x9GhpS{l@eTAWrwdMVr{}8+#jKIseeu zEGM6@Ce!!D-Fh}}d<01v)5&tVo;Hffc6!U&*Vos85Q+xPbfQeU1pSO+uYiF_X?s|$ zI8>YHijUvxkI7~++cmBiTh4gH6!X1rdA}|+Yk>6R=NY807oQSA^{=lXp~mg}69`cv zUGw}M$kZKon`hmhHOJllc`;~|r@#j0+v~}6URSgbaOeQ^Z~n9boVf(D$Yu*p75mW{~sge|klbORcv zljsjRLtAI_jFl!H2rpv;orZ`QXfW`H1*_)6sQ_5OVeclkm- zaMfOOT#SyM7ss76&&8-y44dceey?aZJH7sSdNP7~$Ia8T&bb|-VdMI`STZTJZu<+| zrf~FJ?HZFMWOX2)r zEk}sPbFozzZ5Ylpc9Gfr$}}9tR+PXA*;1DLA(n6>E(f29P#$)KD#hsCsMS0>D_UpG z-oWU{m_Tt2$rn4Z)D@s#9qz7J$&LdDKkhbbm^^H9v26@TMgP3hK$z{prP=%%N&~ef zpBMq~?_g%zfa2lwBCn|Y37WuCpexbC{#GYfLTCFKz#8HC`7^m8#9=l0Fj*`m6B@I{ zjt`+qLD>#$v4U4EVWV>(2erkGcg~HAJM}NuyTg{L7+$2FUK0O)-FYVrxNHq{o~zkp z3vFv?js(JX)*Bf4+E}0ev_9-132|9)Wxw9q5ncng0~|!+`QsLTZt{7>IF7r5hAri8 zvgJgVjgQ*RY<*KW9derc1*8`--ifv0GJL=NEW!{+`iOJ^n5)$srnX)ci%sD5aL>;> z?GqY4%vJ~%mWT3T=Nm|WcGU=|D47ll?RG}z#-VnI?X3=pAtT}zV#%-~I73AJIMgv* z`VOgeGX)c1_Os>Z#zG3l9gH8L&DxAUi(>x^`^SF0hQa`hGepS-={sBf!*&<9?g(_f zoS@Z!)2}$1C7T8Vgb6hQ-8tgFcpgBtntue#izBDAle2zv{Ng!6(&S+Lb5yDv60PFr zjp9AX;l$AaF_KA`0>dP7h^gikCUVG@{`c*+wH0qwLrnj(u zuuSNdkWy@^b=)di$l}Gdfo}oHeQK;Xh_0K)X&Zdou0Nlz_lxV#a3nFoz-lTKwFIps zQH|Ehy>1p8G%O}8PyoefBfqby8`7*e-RbROIV%iLP5s+N2_otTmB2T#M=3J!u*uS> zGGO}-MR18Gx@~M&d<#8u{t1$q?-#4J*HJMT)A6tL=I+ zo5GhHL5%M9sMHd%uGT$$!6$)b^MPrz&O4)k3QSa5$NbZLdN9nirNY#W7fSn)K6E{- z33~L!tI2MGh8dJ7W*eo`ifYEIHz$pQuU*PZaSbmj3L#CSx!4X6aF;=unz2Bjiq>O` zf{!#luO_Zlfe^@@2;r^NK*6{~H89-Rn5)ca+-HKjb0o1g3FVzkfPJx);Q)KIT}}@B zwZjti5!F1v&u4X3=7vFLtuXy;eV3_=AmsIO{V_l|%qo;L>|lGo-|A|D@nZ)8WSl1? zHA)MNtlLpUDkXN*eD-Zc>uF!~22m#;jhieba{8Jsbi&`GSDki+#th#Uy8?7XzhKE- z5YY9ntHo-9=#l;%g?M1M2~mGM$@Poo#kij)Hwyp_vpDaZw}!^|15IrwAK96UFeA61 zLYYM(x%rB?W;qyvp?b zU%g%5&3|>X_|>Gx{K(Abn%;JXEsWV>OkXUcxm-+kefT4z`f(W^nR=@en=UrDfKoPQ zP(&81k4W&SmPBKyzK`|J1FGo-@%6zHKian2x z`)?atB#KMzqPThLLk4cYejk=I*siDM3FxlIu(-}QuK*36CgQjC^ci0Cq zQzQ!G7bf;vy#i(Ibo?rl%~vR#sD3E;qj7&QfU<>sLGcMHt3eVFAP}`Q6EjipVj7y9 z6%W8{joyh;MKk%7TY=6M^MwtDos-UR*l8ET^V4GdZqNbj>l^TW$H8*|l9O(A6}sxj1vZrF*(Sbo_A)}eV7@%alP3}UVN13}#zCsh=CYv@ zUu|qKGk{Uh+!B;enuxPO|Lm-xQ`NbNH|?VEjP_0dt8B-{%@p=H-E-!5$h4@QLETrT z(KN2dYtW1p34MTvyTP0sPvr&yv1U2`N5Cfk7gnKEOb{+U5;9zNhR6L;cl@pa<3@ko ze%{FKGnswRTr;>BW@b=U-L}slnvTQ#TtOuWT6~3!ijTMRRqzcoPRC$p%TQZL28K&w z9OkB7+=xdWMMxR%fLwsV_3j?8u8?hcD2yrbe6s@YZwl<~T&=pBNLahS4M(9!6(ee!R&wX(&Ir(=JBRH)Ju&8DR$}IzZzKY|~wSNGXI% z>FH>l^)utWbJ=Z`X1y3pFy7n8c=SHwDY!#;;Q5j|iVL*WKR*#64u`k`F4@DMn+uG;Yk;-@SKVY?S8amzBIM6~aUo*Z9d0GCLUrxEkc3+U`+-5@u`N z$7io)a++Bs1=(Gr0oD(D_AF9X&=%m6%gm>XwD$8a^yqYgJK_}=weRyP}rODCdS=!UfT z#kPfsNyMkSG*HK`qfI;oLy&|S_6(ZCQ7327-SbOaSMvGZtZpWEjWsHL7zr74g$i$* z41hNc%h_AltHed%MebNK?aT2K-(c{O3(`=8LI-BO=lYAD8JPDdA^UV{Vw>Yv!%nk} z1u4x%-XMF!WR0nzZjI(YQI(p1y#( zt|#Dj7%j(!(X%57Y-4Q(VFujJrH@II=I|7aIlM57?PUYq^!*&GJrM!iaY5{*=7WoS zifme{Xd$Jv3e_mM5)`i{+xPO91YpHsgc!jII_kjgPr7K@g@jwi#PE;crUA2lA#@J% zFCZvZ*+PIc!D5&R*U24k=`?U0nu;9-j$U?^Z*JOeG_p(&zsrD?+;9l!N{Gqa7!%;Z=*jpfrq?%#-OD15F}hof z=It7**Mth{9qR49!>Y0@?4XYbE!fs#f?m2a9XUiA@N;SZvhkf8I5OT7&y8khBu`|b z7ku1(YnUJJc`8R0|K}7@H>`w-?auMVX`xq3D_(LD z+8uUSUedA6O$KQY3Xtv-A- zz|DVPy*K7#$qWehG(rK0hsiWj1u#Mhj_Og+8RGgpz+lnRiY#H33Mv?-%AZFtEC!aU zGA_mm&EFnk(j1Oha141WVp+IJm=>8Mpqu=MW#dKLJiq)|l}AS$#l@f$b--qF>xqv3YBioD}-8MS8@ z93bxoCp1!Hy_I*P>e9F;|DiEfnl@FM+@tR4xyS1(;ph~gKS34Rg|i3@Luzm^Dg*zl zy39!$M!V$6FSfsA%zRAKKru#<>!CyCo>ha(`Y$zG232G*mg7)q1Ww}Qh`(G5Z zT?heNBs@?(S`?y1%A#9pu)AoU-mugFzTi$%2pBnpDZa(yvW53KK2=T+#^GZ_O2LQi zYFSiBO`2$P)5h(`;DjwcLQH%a=eF&j#UR+kQA~ia98~W8_AupaYg6L#Fvt-TdXFG+ zx2tE4>=bW2dQwzFDp58ErUMq-#qdZF*JAn69=M$#khgo5@#WN%Ndqs%W`Dfc?_QVx z?3Mo{2O?P%N$$jik5-7QIbKCDY!-yBmz#)I?vBBfS8T>I9X!OSAX%34Q1Vn5;(Uvx zTo`kOQ%6PEWR{D5`=>HrKmv=ql|V;<^oUV%9f&BUV9f6>$L15ew+eI)IB;g`vKk9V z2sxz}Pe1G>-QdPHV)UzM_Yv{ZjJB-Oi7R_%msC}fE!Kxk3qJws)9zG%oWUDGC+X5~u<1gojK+{S zC{D+$^b(etWf?X09L|1|7z+Bd!l$^hnk2-ujk`LiSKv}~Y*~=PWU-uZ8r)(x$`k>8 zr0Xux=7<_g%@uqebOIoe0(+NwX9_uV8dH7paG8M&e{}$@jhNvEsTC;Si?#Apm@G4( z8qq(9vsfCO)Q3pc!T1PEaM~+<^t(MW6vYcg4}N4iq5Chl2g8L1E{36awXbIwHvu*d{J2@ z*XRJUd;*oQ0P~w^S-}_z{N3=TfQjP2#*o|-`-8}!CC*GyGyJ`SCdnJ^d&<9`ymq@; z!z79n*May+L|WpdjP~`exLNap2Qti9%~2^>+``Fl@*V?|iLqE$7;%jOL*oz&I@-O1 zKkL8Y+;2Gn7J<=3h6JL-^5R1HG)hH_Xpv&pAHKu0t%w#rqw{{Av{uQ)DA_Jb8~AvC z?u+3m>hnD$GV2zIuurxbRzVzOi?hC|_h}tY>z$YbdAZZn9B63F#I78EQvZEL#8V_-pgUJ6U_&rU;DT@kE(sbTD~RMc zr;c#S#?a+z%o&W)2eQJ&g1F=Zu276FKCapj4+L`?09TpxTDSW1E)dG{Zlzg6MTeWJF^I(KdKYCAiMLJcmSF&uGivIR_=YQ5UxEtjSgKV

L{%*z{0bRNF%u3FCZdh=6gO1y&nmPlM!w zz54*G*_Y8p7x4YFcxd1QfIX4{7*Xsqx1{bB?d%_sMBM57sR zM`uW)VWBuj_x!Yx;?;!x-Q0K0aW>gVb#PpW1{g6K{8E8qD#oDGGKW;;9wE)FSDaon zhwX$wid4G6b7cTm-kP()6bcQ_fyQzv7{{5V9Fsn{n=Te1a2sbjX(~<=OjZ&;b%buu z7-NjlPTU8e=PkLmgc&h96s(9c-V77+H!*^g4(Hz;h~_AM>}jF1V-6)xuEkqID3O#L zNy(&I1*8sPLjEIfpT&ngsA-9h@BtE#O1=?x1=ITUlwDqYGQ7_cl-d|0s=5tc6C4+xsecDuP+6SSPHYAFxy^bu)(YyVSm z&Hy$!i7@PI*6cRIP__6~b!<>5wWG(SAS(kxX#m2&7rko1g2{1KL}sY69>6LIK9bb2 za8s^GA|vk7$I^ATrllMN)&M@;Z5joM@zL7}QJM?M<3JkO z)`Ld!KB?yx=!uaPso19VpEchZr1o0xh+P(h3o-ulFf9griN$Y0{iL;Kpm@ z6GjSYopqYS2OZCWgMWX~ZM_cc)l7U8o(T*F0~R>J$|6qyn?p3p3}PcB*vtzr;6dTF z3}YORB(7GpBju$(J1g_ugQnxoujEMvNZ1Q9&@F5x25IgFwhRb!T5Tv#J$LCeLj6tq zaCi5)hZUv-<4iqMW{KwCY|a(i59W#MA3?@dlA%0&tQ1Zr=PVE#@^U*Ccm$9nedT?~ zoKAi-*I$E0jSF~rkV!}WndB$wAln43$*Me{QRnPLY%E%0CN_g|RcfU%B`xxdAhl34 z8t7w+JMEGv)sOM`M;D_3iMcU>(2IIJ)#B&yr}SRyZwpCO28g{oXCn=U{?z(?AdqB~W!&u)E8RI~h_5 zp|4Du^RN6EU%KrOQnpUdFTQO({qeUif92V)Jo)xVcVuWz_CK0jEq+ZARQ<3y{Mv`> z4gcCNcRxa@QeM!n-K-A$GTGkQ|4wgzjemZ4@gM8Gn{R*AXQb=Z`bY9opyX;x;^x&t zBze^ra;T}o47EXrBg?L8!+x?Pv)Z^-@Ym&NYAELr7Vd@FiQXdt1S*MdaZ4AsenY_Vl+)>Ed7(~eyC zmdkN+l15QQi3ge`;RpTxS)=%1S$2{4Wv6xVlB^=bv_bT9Bu{jxda|-5`w

F*<3+ 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 F{y!l6t#beX literal 0 HcmV?d00001 diff --git a/shairport.c b/shairport.c index f799f8a7..b6d03087 100644 --- a/shairport.c +++ b/shairport.c @@ -111,8 +111,8 @@ #include #endif -#ifdef CONFIG_LIBDAEMON pid_t pid; +#ifdef CONFIG_LIBDAEMON int this_is_the_daemon_process = 0; #endif @@ -289,8 +289,8 @@ void usage(char *progname) { #ifdef CONFIG_METADATA printf(" -M, --metadata-enable ask for metadata from the source and process it.\n"); printf(" --metadata-pipename=PIPE send metadata to PIPE, e.g. " - "--metadata-pipename=/tmp/shairport-sync-metadata.\n"); - printf(" The default is /tmp/shairport-sync-metadata.\n"); + "--metadata-pipename=/tmp/%s-metadata.\n", config.appName); + printf(" The default is /tmp/%s-metadata.\n", config.appName); printf(" -g, --get-coverart send cover art through the metadata pipe.\n"); #endif printf(" -u, --use-stderr log messages through STDERR rather than the system log.\n"); @@ -1256,8 +1256,14 @@ int parse_options(int argc, char **argv) { #endif #ifdef CONFIG_METADATA - if ((config.metadata_enabled == 1) && (config.metadata_pipename == NULL)) - config.metadata_pipename = strdup("/tmp/shairport-sync-metadata"); + if ((config.metadata_enabled == 1) && (config.metadata_pipename == NULL)) { + char temp_metadata_pipe_name[4096]; + strcpy(temp_metadata_pipe_name, "/tmp/"); + strcat(temp_metadata_pipe_name, config.appName); + strcat(temp_metadata_pipe_name, "-metadata"); + config.metadata_pipename = strdup(temp_metadata_pipe_name); + debug(1,"default metadata_pipename is \"%s\".", temp_metadata_pipe_name); + } #endif /* if the regtype hasn't been set, do it now */ @@ -1311,7 +1317,11 @@ int parse_options(int argc, char **argv) { #ifdef DEFINED_CUSTOM_PID_DIR char *use_this_pid_dir = PIDDIR; #else - char *use_this_pid_dir = "/var/run/shairport-sync"; + char temp_pid_dir[4096]; + strcpy(temp_pid_dir, "/var/run/"); + strcat(temp_pid_dir, config.appName); + debug(1,"default pid filename is \"%s\".", temp_pid_dir); + char *use_this_pid_dir = temp_pid_dir; #endif // debug(1,"config.piddir \"%s\".",config.piddir); if (config.piddir) @@ -1508,9 +1518,7 @@ int main(int argc, char **argv) { log_to_syslog(); -#ifdef CONFIG_LIBDAEMON pid = getpid(); -#endif config.log_fd = -1; conns = NULL; // no connections active memset((void *)&main_thread_id, 0, sizeof(main_thread_id)); @@ -1539,6 +1547,8 @@ int main(int argc, char **argv) { // get a device id -- the first non-local MAC address get_device_id((uint8_t *)&config.hw_addr, 6); + // hack add the PID to get something unique + config.hw_addr[5] += pid & 0xff; // get the endianness union { @@ -1822,19 +1832,23 @@ int main(int argc, char **argv) { // Set to NULL to work with transient pairing config.airplay_pin = NULL; - // use the config.hw_addr to generated an airplay_device_id - { - char obf[256] = {0}; - char *obfp = obf; - int i; - for (i = 0; i < 6; i++) { - snprintf(obfp, 4, "%02X:", config.hw_addr[i]); - obfp += 3; - } - obfp -= 1; - *obfp = 0; - config.airplay_device_id = strdup(obf); + // use the start of the config.hw_addr and the PID to generate the airplay_device_id + uint64_t apid = nctoh64(config.hw_addr); + apid = apid >> 16; // we only use the first 6 bytes but have imported 8. + apid = apid + pid; + char apids[6*2+5+1]; // six pairs of digits, 5 colons and a NUL + apids[6*2+5] = 0; // NUL termination + int i; + char hexchar[] = "0123456789abcdef"; + for (i = 5; i >= 0; i--) { + apids[i*3+1] = hexchar[apid & 0xF]; + apid = apid >> 4; + apids[i*3] = hexchar[apid & 0xF]; + apid = apid >> 4; + if (i != 0) + apids[i*3-1] = ':'; } + config.airplay_device_id = strdup(apids); // now generate a UUID // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c From 598ddc319c1141674f223da8e377134ad51d40bf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:20:46 +0000 Subject: [PATCH 506/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 79c87738..98590abb 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-130-gb413a29d +==== +### Minor Internal Changes +* A small number of internal chages made. + Version-4.1-dev-127-g4d0460b9 ==== #### Enhancement From a9c8de50b150ca12506d1adcbaffb69a6bfc09e6 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 23 Dec 2021 19:07:09 -0700 Subject: [PATCH 507/878] some MQTT metadata improvements document some more of the fields that can come through, and add a few more "parsed" messages: * active_remote_id * dacp_id * server_ip --- MQTT.md | 45 ++++++++++++++++++++++++++------------------- mqtt.c | 50 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/MQTT.md b/MQTT.md index 3dee3f22..ec41a0b1 100644 --- a/MQTT.md +++ b/MQTT.md @@ -95,31 +95,35 @@ The first 4-character code, called the `type`, is either: * For `ssnc` metadata, the second 4-character code is used to distinguish the messages. Cover art, coming from the source, is not tagged in the same way as other metadata, it seems, so is sent as an `ssnc` type metadata message with the code `PICT`. Progress information, similarly, is not tagged like other source-originated metadata, so it is sent as an `ssnc` type with the code `prgr`. Here are some of the `core` codes commonly passed from the source: - * `asar` -- "artist" - * `asal` -- "album" - * `minm` -- "title" - * `asgn` -- "genre" - * `asfm` -- "format" - * `asal` -- "songalbum' - * `pvol` -- "volume" - * `clip` -- "client_ip" + * `asal` -- album + * `asar` -- artist + * `ascp` -- composer + * `asgn` -- genre + * `astm` -- song time + * `caps` -- play status (stopped, paused, playing) + * `minm` -- title + * `mper` -- track persistent id Here are the 'ssnc' codes defined so far: * `PICT` -- the payload is a picture, either a JPEG or a PNG. Check the first few bytes to see which. - * `clip` -- the payload is the IP number of the client, i.e. the sender of audio. Can be an IPv4 or an IPv6 number. + * `acre` -- Active Remote + * `clip` -- the payload is the IP address of the client, i.e. the sender of audio. Can be an IPv4 or an IPv6 address. + * `daid` -- DACP ID + * `dapo` -- DACP Port + * `mden` -- a sequence of metadata has ended. The RTP timestamp associated with the metadata sequence is included as data, if available. + * `mdst` -- a sequence of metadata is about to start. The RTP timestamp associated with the metadata sequence is included as data, if available. * `pbeg` -- play stream begin. No arguments + * `pcen` -- a picture has been sent. The RTP timestamp associated with it is included as data, if available. + * `pcst` -- a picture is about to be sent. The RTP timestamp associated with it is included as data, if available. * `pend` -- play stream end. No arguments * `pfls` -- play stream flush. No arguments * `prsm` -- play stream resume. No arguments - * `pvol` -- play volume. The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. The "airplay_volume" is what's sent by the source (e.g. iTunes) to the player, and is from 0.00 down to -30.00, with -144.00 meaning "mute". This is linear on the volume control slider of iTunes or iOS AirPlay. If the volume setting is being ignored by Shairport Sync itself, the volume, lowest_volume and highest_volume values are zero. * `prgr` -- progress -- this is metadata from AirPlay consisting of RTP timestamps for the start of the current play sequence, the current play point and the end of the play sequence. - * `mdst` -- a sequence of metadata is about to start. The RTP timestamp associated with the metadata sequence is included as data, if available. - * `mden` -- a sequence of metadata has ended. The RTP timestamp associated with the metadata sequence is included as data, if available. - * `pcst` -- a picture is about to be sent. The RTP timestamp associated with it is included as data, if available. - * `pcen` -- a picture has been sent. The RTP timestamp associated with it is included as data, if available. + * `pvol` -- play volume. The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. The "airplay_volume" is what's sent by the source (e.g. iTunes) to the player, and is from 0.00 down to -30.00, with -144.00 meaning "mute". This is linear on the volume control slider of iTunes or iOS AirPlay. If the volume setting is being ignored by Shairport Sync itself, the volume, lowest_volume and highest_volume values are zero. * `snam` -- a device e.g. "Joe's iPhone" has started a play session. Specifically, it's the "X-Apple-Client-Name" string. * `snua` -- a "user agent" e.g. "iTunes/12..." has started a play session. Specifically, it's the "User-Agent" string. * `stal` -- this is an error message meaning that reception of a large piece of metadata, usually a large picture, has stalled; bad things may happen. + * `svip` -- the payload is the IP address of the server, i.e. shairport-sync. Can be an IPv4 or an IPv6 address. Parsed Messages @@ -127,14 +131,17 @@ Parsed Messages The MQTT service can parse the above raw messages into a subset of human-readable topics that include, +* `active_remote_id` -- Active Remote ID * `artist` -- text of artist name * `album` -- text of album name -* `title` -- text of song title -* `genre` -- text of genre -* `format` -- ?? -* `songalbum` -- -* `volume` -- The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. (see above) * `client_ip` -- IP address of the connected client +* `dacp_id` -- DACP ID +* `format` -- ?? +* `genre` -- text of genre +* `server_ip` -- IP address of shairport-sync that the client is connected to +* `songalbum` -- +* `title` -- text of song title +* `volume` -- The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. (see above) and empty messages at the following topics are published. diff --git a/mqtt.c b/mqtt.c index ad5a4c40..912d618c 100644 --- a/mqtt.c +++ b/mqtt.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -11,6 +12,7 @@ #include "rtp.h" #include "dacp.h" +#include "metadata_hub.h" #include "mqtt.h" #include @@ -129,6 +131,10 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le } if (config.mqtt_publish_parsed) { if (type == 'core') { + int32_t r; + uint64_t trackid; + char trackidstring[32]; + switch (code) { case 'asar': mqtt_publish("artist", data, length); @@ -136,32 +142,39 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'asal': mqtt_publish("album", data, length); break; - case 'minm': - mqtt_publish("title", data, length); + case 'asfm': + mqtt_publish("format", data, length); break; case 'asgn': mqtt_publish("genre", data, length); break; - case 'asfm': - mqtt_publish("format", data, length); + case 'minm': + mqtt_publish("title", data, length); break; + case 'mper': + trackid = *(uint64_t *)(data); + r = snprintf(trackidstring, sizeof(trackidstring), "%" PRIX64 "", trackid); + mqtt_publish("track_id", trackidstring, r); } } else if (type == 'ssnc') { switch (code) { + case 'abeg': + mqtt_publish("active_start", data, length); + break; + case 'acre': + mqtt_publish("active_remote_id", data, length); + break; + case 'aend': + mqtt_publish("active_end", data, length); + break; case 'asal': mqtt_publish("songalbum", data, length); break; - case 'pvol': - mqtt_publish("volume", data, length); - break; case 'clip': mqtt_publish("client_ip", data, length); break; - case 'abeg': - mqtt_publish("active_start", data, length); - break; - case 'aend': - mqtt_publish("active_end", data, length); + case 'daid': + mqtt_publish("dacp_id", data, length); break; case 'pbeg': mqtt_publish("play_start", data, length); @@ -172,14 +185,21 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'pfls': mqtt_publish("play_flush", data, length); break; - case 'prsm': - mqtt_publish("play_resume", data, length); - break; case 'PICT': if (config.mqtt_publish_cover) { mqtt_publish("cover", data, length); } break; + case 'prsm': + mqtt_publish("play_resume", data, length); + break; + case 'pvol': + mqtt_publish("volume", data, length); + break; + case 'svip': + mqtt_publish("server_ip", data, length); + break; + } } } From 1e588a2e2c820b26b70ced5a77047323566b7464 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 10:13:25 +0000 Subject: [PATCH 508/878] Stop generating an airplay_device_id dynamically. Base it on a fixed MAC address (not necessarily the active one). --- shairport.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shairport.c b/shairport.c index b6d03087..d866f46a 100644 --- a/shairport.c +++ b/shairport.c @@ -1547,8 +1547,6 @@ int main(int argc, char **argv) { // get a device id -- the first non-local MAC address get_device_id((uint8_t *)&config.hw_addr, 6); - // hack add the PID to get something unique - config.hw_addr[5] += pid & 0xff; // get the endianness union { @@ -1835,7 +1833,7 @@ int main(int argc, char **argv) { // use the start of the config.hw_addr and the PID to generate the airplay_device_id uint64_t apid = nctoh64(config.hw_addr); apid = apid >> 16; // we only use the first 6 bytes but have imported 8. - apid = apid + pid; + // apid = apid + pid; char apids[6*2+5+1]; // six pairs of digits, 5 colons and a NUL apids[6*2+5] = 0; // NUL termination int i; From 7a68bb0ceed080cc1015c3ba1776331795058eef Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 10:15:56 +0000 Subject: [PATCH 509/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 98590abb..0c4bd273 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,4 @@ -Version-4.1-dev-130-gb413a29d +Version-4.1-dev-133-g9caa0e6a ==== ### Minor Internal Changes * A small number of internal chages made. From 1997f16fba74b3ea0062045b2af42eafa3a97d6e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 11:03:19 +0000 Subject: [PATCH 510/878] Allow the airplay_device_id to be varied either by adding an offset or by replacing it completely. --- scripts/shairport-sync.conf | 12 +++++++----- shairport.c | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index f377c221..f0176a3f 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -14,15 +14,17 @@ general = // %v for the version number, e.g. 3.0 and // %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc // Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H". -// password = "secret"; // leave this commented out if you don't want to require a password +// password = "secret"; // (AirPlay 1 only) leave this commented out if you don't want to require a password // interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support. // output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one. // mdns_backend = "avahi"; // Run "shairport-sync -h" to get a list of all mdns_backends. The default is the first one. // interface = "name"; // Use this advanced setting to specify the interface on which Shairport Sync should provide its service. Leave it commented out to get the default, which is to select the interface(s) automatically. -// port = 5000; // Listen for service requests on this port -// udp_port_base = 6001; // start allocating UDP ports from this port number when needed -// udp_port_range = 10; // look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state. -// regtype = "_raop._tcp"; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp". +// port = ; // Listen for service requests on this port. 5000 for AirPlay 1, 7000 for AirPlay 2 +// udp_port_base = 6001; // (AirPlay 1 only) start allocating UDP ports from this port number when needed +// udp_port_range = 10; // (AirPlay 1 only) look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state. +// airplay_device_id_offset = 0; // (AirPlay 2 only) add this to the default airplay_device_id calculated from one of the device's MAC address +// airplay_device_id = <6-byte_hexadecimal_number>; // (AirPlay 2 only) use this as the airplay_device_id +// regtype = ""; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp" for AirPlay 1, "_airplay._tcp" for AirPlay 2. // drift_tolerance_in_seconds = 0.002; // allow a timing error of this number of seconds of drift away from exact synchronisation before attempting to correct it // resync_threshold_in_seconds = 0.050; // a synchronisation error greater than this number of seconds will cause resynchronisation; 0 disables it diff --git a/shairport.c b/shairport.c index d866f46a..88db7924 100644 --- a/shairport.c +++ b/shairport.c @@ -1830,10 +1830,22 @@ int main(int argc, char **argv) { // Set to NULL to work with transient pairing config.airplay_pin = NULL; - // use the start of the config.hw_addr and the PID to generate the airplay_device_id + // use the start of the config.hw_addr and the PID to generate the default airplay_device_id uint64_t apid = nctoh64(config.hw_addr); apid = apid >> 16; // we only use the first 6 bytes but have imported 8. - // apid = apid + pid; + + int64_t aid; + + // add the airplay_device_id_offset if provided + if (config_lookup_int64(config.cfg, "general.airplay_device_id_offset", &aid)) { + apid += aid; + } + + // replace the airplay_device_id with this, if provided + if (config_lookup_int64(config.cfg, "general.airplay_device_id", &aid)) { + apid = aid; + } + char apids[6*2+5+1]; // six pairs of digits, 5 colons and a NUL apids[6*2+5] = 0; // NUL termination int i; @@ -1846,6 +1858,7 @@ int main(int argc, char **argv) { if (i != 0) apids[i*3-1] = ':'; } + config.airplay_device_id = strdup(apids); // now generate a UUID From 5617f40edd35f771f02c05cd00fe24ed1802f700 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 11:22:27 +0000 Subject: [PATCH 511/878] Update the documentation a little. --- scripts/shairport-sync.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index f0176a3f..ee96fe6f 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -23,7 +23,7 @@ general = // udp_port_base = 6001; // (AirPlay 1 only) start allocating UDP ports from this port number when needed // udp_port_range = 10; // (AirPlay 1 only) look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state. // airplay_device_id_offset = 0; // (AirPlay 2 only) add this to the default airplay_device_id calculated from one of the device's MAC address -// airplay_device_id = <6-byte_hexadecimal_number>; // (AirPlay 2 only) use this as the airplay_device_id +// airplay_device_id = 0xL; // (AirPlay 2 only) use this as the airplay_device_id e.g. 0xDCA632D4E8F3L -- remember the "L" at the end as it's a 64-bit quantity! // regtype = ""; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp" for AirPlay 1, "_airplay._tcp" for AirPlay 2. // drift_tolerance_in_seconds = 0.002; // allow a timing error of this number of seconds of drift away from exact synchronisation before attempting to correct it From e275a87d734ee8ca7131eaa0adb6e60b2ee28272 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 12:33:28 +0000 Subject: [PATCH 512/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 0c4bd273..c9ca93cf 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,4 @@ -Version-4.1-dev-133-g9caa0e6a +Version-4.1-dev-138-g5617f40e ==== ### Minor Internal Changes * A small number of internal chages made. From b33e8ad301879eee84b5eccbd23867ee307ed070 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Dec 2021 12:45:00 +0000 Subject: [PATCH 513/878] Brown bag --- airplayd | Bin 1204288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 airplayd diff --git a/airplayd b/airplayd deleted file mode 100755 index b22fe8c961bc9e475fd34bb9dc1f03feaca5705e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1204288 zcmb@v3tW`d+BW_SprB%ohDk~}9x^J-QOU5ZP*Ev0sW2(60bvl5!w7?7(hd(5r4=O= zl_izC*kwB^D=KSaTCvlL%3V}epibd2Dl00>@4B96Jyk@B-UKr&oIJ_Zt;f8=z+Z9#xUmll3xusl3$H5 zLj>d)!c^!EWZ8H~GPqEH!OjmwUMk|LXqVw+#2J{6WHSI1gACeY=%&&rlk$2vLJu>< z4>63&cMXH>vUCXkPeXnh@)>GTfPwk84nD+vXqzDk=?r$>WdVN{qJR3Zyc6Max`t@9$s{^->^5{b;TOxVW;(UPAHEWYV<>< zb3?a8m!sS``H}Jw6^U+FlG{;Z+!nej(HL^tWkyZJDq|5eaaY}bncGs%WPg)Un*TzkW zMg!DKdZb54RC(C&5ypen!$Zo$X3t%jwz+cHl_!?BhIC6bLMGm{z3<4xzD9^6BjKEg zyQUf;lgq=JTw$(=>JZmS+b+L5#5nV&p|?Jg722(@5jO8?|07AG!$L3u`c+Mw-Xp9h z2Grd>%8Q~_ln%)WgFho`BN7KX(hcWLH9Z{@4h=cO(Pzj5M%eXmRriU;m}J99H=+_< z4JlWT1|1p}!m$vJzz^pk$HWPY;Davz*B_3%{s_?soR9nrw7?hyJVkNBse-$}hawCU zX~Pvi4R|`j83<=0oQ1$ooCU^6;3xz)0=i><#^8UhALm$6V;nF+aT~_l1b8081cVC^ zE<(^hiAb1)Fc~2UVG4r%nF=yZMPOOuVuVW&W~j88KujL9Ko8kR@xju8p2fw zS0l_t;O80ykIAh6G63@tya+i6xd_)G@Usvh&t$>>@VnT{Rj^rtV7`EVGE4) zz()`sLwFqF2?YJ~50$VH_#DFX2rnSKgiw$0GQz6}{Je(nI>NsY8W7$<;Aa~`Bf@qB zEb+#B2>iT{@BzX{2p=OfBYcjq3xS_6EinIY!T-Av_9A?P(1x%NVL!qlgl`er5%~EY z;Rll$#u4C82)`iwhHw<&cLej}z#5H!Rm%uRh(PFtz)x@B2?%`KwD-gKn^D|rFl?vwouTuPK z;9P`xDt;}{qj)+n17SWwCW03s8({%L4ni)%bqM_A0SguP0T(H50Eh_#7aI50mfNv_!GXDmD2VuKN+X4PA!g~lGAbg0x&qu(W ziZlK(_$Mms; z5C$MbBMd~aKZ69f(@sI!A1i9l+jZ~0=g-MHe_QV%+dI0yxoY)>ISF^&v1;G*gA#At zKY#4wsWsMEd2@b= zOMCqv8BH(rS~TaeIT!9rd;Gz*G5@;x+Y`S1=*h&KTg#WfS-fKNb$1PTYGqpQf4=+f zs{FbK2Yf5fS`1aiRU;Z&D|I~LcihO3!DWz{6xZt6(q5Y0+ z`gupMfzvL!chBcP8`pk1`s;_1hi&h8?ABR*Uro>a?a-HNW8(JQ-sgkcPdph-FM@hk3wI3<({=Mk2n$sT(SC&8)yCW`H=Mw z-2dBaY57-mTRn8wO;6u`qhoEkW5^{3dcQJ0WZUe?X>&4Tx_y!|uxQ+l!l59r@}uZe_vKBPJ^G2!9lnstrhCUc+<8sffY)oUxqVCEp0bO6Ikx#{_r@`c3-|cO zA5I#bx%ZOEgLBG1E0|DKHtNy~e!A++bJuU2^v$uyw>^Dp|IH)5>KQrWKgGzq=egPU zjo5MXu^&zdEMJ`|l4sVe`J{x6l6Vxj(AjdGD7Er;eUH1Qko<6Hi`FhXOZw*ShdhdyIUtKk#;x4(3B&XV-IKOg+v*5bCE zHD8|k%*L0CX6{bjJbXn~_^K#xY`-_}dm`(W_m)*YF!{F;%S!)|nsj00Gq<$gTlUTR zH6M?^dyntl`3aBj8T0GIzwZAacIN%N3UA22@93P**TyWpXW4e=vUO$MmONDY%CEh? z=`nd<%q2@6==0($eSVzrVEWCoS2rcLgbo<@@aZr8x@A_*;a|`0_xTNn`t}*S?dN{? zEIs_v!<9LnC!XwmZo-;zk6ngx4Sm-*Z!0+PME>QEzPsVZajX= z;M?!)bNbN!vqsmi=$-VzgV(O^5$=qPy*vMb7w=wi!=M%Cgmin{7HXa~n=L{k%;BDj_@Y;fh|fK7XhzVVwJ&q7e^zZW!?K^E2n%I`^_?xBhn0 z{>6W6JTdNc$20M5ITLCR&b@Z(fWGV2+NMBAzb<~@$rtYb;MVi^R(F57U&EKzpA^09z}h=wvie-z=WieEp0;h1G-Z@PQy%I71R!zbK-O?zIS z<-PZ2&fYjGXW;seU;Cl^>(9JAyt?YvvWzPp`s##1m-TIk`1#{!_h&ACYQ-PVKDp`G z!BtTgU;F;;?_PLTPVPlp-6Q7CIrRQjg&|L84h{QYN8SxLtg5KX9AC2dBG>kg^Qs?y z^MgOyzg@DUx!Wgu>o+~}=nq*VHzi#D$ea5gjeq9mBj=yqUi0OfBif!E{mx4hXFgQ+ zZh}93@{vgnt`-W+@Dt}Tl^wP!6_y&ZmVPrB{c z{0lc_o$<+(sflYG-v6|{*X=I&!ZY}?MNv~CpSfXX;Ebfx-n>0#>Eqij>mC2rv<=Ih zSABcm(vE+38}nw$+`8wv$Nu%%oI&4wI>vMO zkF1XStG?K|deVzizxwy$p69(V?Y(P0JlgcxQ~iHjyY$3a1H9G4?}0tO+jwEf`!g4R z^xpp8d&hL2E=fpV`1J1+2A-R`vTXiSEBY+z{`w31uKq5+ zXRnl!i`R`hkkk72b6fWOGAeX*Ov($j&WMe_+&ST?J8pdKaOFF1A9$x@$vH1KeR4`% zVgBM}n+GJt?TI@h=ELd>CvEyq>E5)L&sy03p)14h`o(i(#y>~gairv}-KWpIV@tapyZPU9l#1$oRfvpDlQ|>d>p3ev0aL<$0l7FZt}*n_hq9+@J4_DZlvXie7i+ zti1KrJ$IKqbmZW#|5$SCE??6Hy%z`WEqj0UxjADtTowELqd)fx+_7VJa`v1xBfIs> zDCu|biOMG;7jGy_b!~s>)^YEjk$w5rJ8n6)>8$%te*1)$3x2!y?PJY5PrN4Y<-Z-B zTY1Lh;a85!np-ht%$xh~$RBjd=|9}OFfg}QpBMgq#kY^WJg$F#Z{`JGZNL7v+ddp| z;)Bn;SyXW#bjVpb-z^;XM*KxTBrN&l!LszR*?sOgV^;dE*zSF!_bku*K50q%r;E~J zdY`%KP!G@Q+C!tB`ugRS8<*ez)0jm=8a6*Tz2WtwVcFN8lJ`~4%t6l|t1O2kU=VlYd65Z%41L_4!yxeMrk(cFgME`I>y` zu&((#hjxvx(DVxo@0x%8NnPX5xVpxF)9Q0;<>zVnb2NKz6Aom5o<5~({rfcg|E|UF z*V>z=_2+rb-f?}p)?cgTPwd?_KUA~#2Ce-^H2XfQ)jx-Uz;_1f6={b``>iy z8Xv38$9bCk=dP~u+cf+2)ciG5EB~fe{%p-2H*5AgC{`HuH%_Z>rM6z2HlnM3_i63T z)Z}l~{2hN{*YY>Ubd9^U@wZ>|-xAH=TLyM5f3D`=Yc&78s^veb^&dCmJ3q8Xl{Wsi zY5H!~#>1~eI*Xg*?E}sJkFiqqAuWisW7hcWtM$Jc>qXU9JUX+qL=noTmQ_Eq}J= zFR!NGzqRtuYURgj^YKB=Kj&%tORHx83pM`@($?=6wf1h*@&nrXH%^Ps)9gPN^;Kd# zhrz5I&nIi+zf#lhJ-()*`KREo zNZzQ$hyA--^Iwjp&uN;y_$<}_u>4}JzprZcN!Qk&yR`8&i~~h|Sf5W@A9`x?8@2Uk zw>G|R(c0gu*((J5bL8KG{l81I->0@RKUo`*RtHp&qss{%zX%utgiM zb98%XdE_Nc-*?$5^&#D^*}sQ2p5D~#JxJ5NIy z@iai2FIQ^$?`ZZb*ZOy#tt^oG4%77gQk(DhY3rZ7pkn!Wtv|n_KWl{kq1yN@)5h09 zE&j1K-d@-G_lY*1hiK#B7cKuK&HmlB@jp?s|EX9X?GO9s(DeIWv*+J6{d#Eo>t;

Ty|@gKT0(J{aaffZq)3V6D)4}bFWtZ+#x{$_UG?fe-pI*{YtI>K5hNW)YkK9 ztWU zaQ`1*jMMa)rM2HjvtOKM?_ae2?~Y(|=6HNvTR-taSLcW0r9q3I-@P;2EMKFQ@1yy5 zhi1RUntw-V^Q~61=PYeKxJ2vkY^}W&+W5ax(|@Ek9}=|ju`;Y{{|;;Yov+pB)AT!E zTc0n}^j)C&vs&AKPtxo;R?9z4v)^!SJqvSm)qjpQzFyPTpEcTg@PpP~Pi_2MB&HT#{e)gP*j-#xCb{rP2B*LaTRFRx~gxoFS7G^pR{+WcwJ z{Bwn7pWn6d5uug;Olz-)rvEO@{&m`ZyjWY`?$rF7tL-oIwedDqv&R%o{};6Uh1z(& zP}6_4w*C*&#>Y0zzazByoutiQ`Q8J^$J0Ugnd`@sT755Q^J9oMUixbG{ZX4QU!(qJ zzF2_|+ncBL|32+{tX}iaVy(Z+hMHBG|FORQn*CqV`rlt0k2#t@-_+KJIhLxO_0QMF z+kCCRQ?&S0mszy4{5##d#y^}QJVf^wD#ZD>c3XA zhfgbivSyzvwedPelivXQ$o>2&ZGOF>U2lhJ_M55MXQJlM3$^|IA#Hq|rRn3->~%zI z|9P$b!`B|jxch_qE_(nSqJ+Jxu8Lj?gZ9I?B#{0Kg{SIxu__}o+e}&q3 zouSRYP_4dmH2dUg_NdXu%OTAl)tdijYvb<=&3^yV`Wvam2W$F1qP2HZNZ0Y!W zwf$v<7H`n}f0H&|+qL}J+VxYdwqCB+`d6v#zt3p%`4Y{4i?#Z$(dOS!%|8b;`Ij|) zbG7yPG;O?PX!=do=Fef44S1 zKGn`^Cu;T!*ZNnf`R6gs|9iFdr49C%>-YWI`uT-s?=^Ii`j7_n_BDSOYyC@wJtA6y z_j7O8)=Q7pzpu6R)}`6=u-3n)HTgF+dw->sKSNvZUeWB`UCY0to85bBytZrO@0ZZ7 z`J**^+^4mFv9_MIYv;L>wDsePVD;vFS)Ss3NNev3%|2VT@itp)?=-ExW=+3| z+I;J;>9bnv|IJ#y{mv@8#qsosW{(TB`8h(Xed4&bu)QtIgUc=*AkY8BfDfVX+`8^&>^tgho&a=Q<;4RAV zdp$)N1zCmphM8DW;L9i~_ImOPmw1cVa`BQPZ*fMxFVAazp$c(NZb7c!AjvE&SnMsb zct&1cVWydyUF7w$XvX5~!lL{PKMS6dV5MXhW_dGJ!UB)CsHm{WW6Ce^6wbfSo9XwI z6cl;0jf}+}QxVlz#$vOe$L@^>twF|0EcAH`#&pI^J*b)ATk0PxDl+Q}%4Oyi7JEBO z6?+S^&|>C7ufH>`$eZcSUF?N^8Tpj7HL^=8dFq0m+uCI3q9$|_9WAm7u~Ms(rcp}# zxp}4q#xdKhyt4{Z%*XUXpWjoAa+d8q+4Yf!aw26c)pDay6G$+3VcJF|jvsvyQW$B!kT(z;k3)3B&8 z&s<0fvvN!FIq=7fHOx|l7_18lFjxE;uv%7^MEK2@ky~V^%`YtF*pNvXFp(!S2Sqc+ zxP66rOGl4&pX15(`m;IvOs!-q-mEd_oHKg7r+A^qU+D2Iv}-n1DE4}@^!kf4GxG37 z(vmzZZ>H+jvM1|=|8ujK20MWH&A}QD=TaP)JJjI12MG$9iF)a%*AFzmU_0P!n6#KlHwyt(Lt4F06!kxQVW?n{d zv8NCtq$oEFEf-X@4}78F^Nr4;nRv?!pSYD47OeZALHQ0T{CGj+8j*(xd< zJH|}QgZ@lWW?bOO&&VynHeTrSEGofv9qjh}lH5G(P1eNjG=`mO?(GYt2`~@x=3{3> z6{bD0AfR0wG74zYqTI#is>p`1-q83MG)kGQ!V;cKI=eyzg()~NIA1(CjnLTUK*=t_ z*1o`FrnBoFl@P4e9IBmlcG@M_VGJkKAp8Y0ApsgL_F(B-;LkxUSRV(YmrK4UzXXQ{ zYo619+}GV6X4pdpbFK)xV=1eHgW*!F|Cn3F#Tg5{9&f1+`;zT9OAF;hYnXAd$;k6~ z^K<>!EtLwHg+A$4)5hj5f;9<3!8o=$4yw$IVm5E-T2cTVtwBwr(~3%0hzJV=``D!` z=D9$P6?J6djPUq#xC3Ej%g*2e*x4sI5RqJLAod8cdZ{{=k(pV76*I$+Q=PvE`sCnf znP<+6MI{+USbA~5=p3Q8Cic+7Tu0@0s@0S-pPHBGoq1$5#q2@G;V>?~OH$ATc5SBP z?6GZm!P49ou$9QfP@og-bT1U=cr@Kmz=L2g&o0x&1~6j*&#~tEssV6Ib~VZI`q@iB9Yp}A<1H3fYs#zyCiw1)?$zwJ`%OdV7lIXPGain!}{_S6E+jdk|UjnQ#Y#x%uN?=V!iAC;2j#ZW8c+$!W!#o9eM#^G2Fw^(BU>*K-_ALG`NZ`(jcGjLMP z!T^`daRaB z{mpZ-^ms<`(t=E01Yni6wts6JTOF`#_zQEWMmR5c`@p*DvvjxG#FyY!zW*e-S)oHg=}*D|yH&>eRG za5Ki+SwYv%1@&QrxI)Rrg5}S}A%_i0o3lP&UdB>h2zat4@rKo(WHNAV-6^ve7kF8d zI@|5Eai$;VUwYBnIfY%V=FD3Rc6V_z$GU}Rt?qVmSG6_kcg>V{#aO(vdFw8RU8C|c ztw0*ISFko(z*45OQ$a_IZg6;*mFITK1uJ0#fAU%|n{8Nb=OL8JNgtR#5mq}$G#SIo(= z25Oh)Fy)w)+*jw_rIGmCP4+@3p| z>FN(Lg0Mp>x3GIWq2i3w*_b)1JJnFfEi+f@X*$PtA|?sE!h4X)kvI!k7dZC7>}r56 zlOqfF6h!svtZEm;&1>F$I8LXif$CJ}RPP)U!jU*BFZjy})LLQQ#IlO^bhQd+_i?h!R;=ZhdUPFV)&dt?0@?R2BcZbiv*yAQzpsSrSXOSK zM~sS2A7hMK_cp9YG8s9!BQp#0u^n0WEi)D)Mco0wEePCN&BH+lHyNyqj64kO0vu@Z zWCS-r7U$y9%3R%#SFYHPF=}O4$Nb=Z9hGd|X|UUYOOKmwj+^5(pzePhFAJW*!SHxF zyawr1%9?LhR5T;ig}k+dSa&v$SEYH6jpr5`Z)9d(F0Or~Ic-bLFDhIDZrz}?DuN{L ziYzE9#7R{L^jP*oG`lWd$L4!0%$Y=S?jtf~`Q2zdjfI@`K!B1=xcrKV=2_90 zbnz@)mtmhIq3-6Y!H9M8Aj26DyDW>CoiimpIQn{VJBz9I<*)rb!>-D#lYtsN*2L8kvvFiGedoh-Rm-bnQ{k7%){|m` z@o8SajHd8W_{5?Gi?6WmkWge&M%FCrxWHJlH_vB9i_HokXssuzwo7rV82MJ7*FOu- z5sNH&mZW)n)&i(>QukepmTKe2&4d&%emAfD#(>xq`tmS|^fq65dv$S=;izkUJQ`Lfu zg2LiFuNUh+E{o82oJ1CPN-H&0x0Yn&`h7St+O1w!h^IMFhc{3=6U_$9K6mMe)vi5Q z>>A9zu-dN0tqqJ1`Td2aLVS>+G!Pc!o7C(?uhU zXiLyKSKD<5`+3|>>50njMN3Vq2UXM+4Ca&+EaAHe=Hm;yQEw?sg<)7^&Wh0nt`9Nr zuxeUI!<>BUqyY(>Co<1Ei{o@03z$!G#~HjD+eu<$S@2bbk}7$(>t@d%o+V#8a?VO*?= zFUZKn0n0wC`-=+j=*z%*0Q(xgT->|E`53RZ7#Ki!mWQRsyzXXgxdm9y3V19+`2x0v z!&KBlCe)jS zd)j70KIl#>nvbeH*%`UGYWEkRfWd1iqj+gC@6PhBCg0t`>W^kQ57ZT(*N^OcT-S3U zFwYqnyXdDEPn~d67Z2{UvS3f#G{R2AENE&Q615q4IGe>RS_kn0>V;A;lmUu6J2@r( zEZk!=78e&N%X)av4pm)e7Qr1Nztt1AWa+-pQ)Jc&)A8jEn8{CXvQo5x)yXKaRA!{u z%eHAe^JXqpr!^s*QM3T^co2$em%`rpMt(^Fjl#V+H=FI@b{gJmQIz_#@+>wQpTMk~ z!eYPK-4biT%eJ<5JpRH9O#Z@Lm?Jyenn=(kr^v{*hmWPQS(eRpn$a4MXv!Qu=xBCk zfgf)=!DDcl7l(A**0<*_#s>znAA6J!gCsi_ZlD3sUk{Gi*!s;Auz6SCz_4LMY|$Dz z7=k%@2LCUHcxGt^E)~qh04mV?hR16@!SZUf>&)BoiF=o%c zILR{_&+liYOu6)mD`t2`X%iEWjEGqeJF30vHDSef+v=n2ndLXu z%eGHI>vlVq6o0p#USUf;@DJa0HXE_Dj5IZ3KbRYCmSwgj$@H$C3V{SY&oxO@9omOE_87!+(yAab7 z%(3jicBrLgJCk;d_lgOtbC5dU~3_=1w27l%<~o67%uiFumnMh|0}{qJ4~wBM`xd`D_1_ ze?pi1M#$Oa>pMJ9v$yf0D8EePuN?gkrVd+W{NQOiSq04JqUxZ=%8oiLuv?w#)%{W=bm!7@^>Vz4mca{$^CJ(b@ z-H@-i>t0muG~9N1(;uN-`g4iuFT{=Zo~hv+m*=bck{f#%{b6DL&P3F^*6%e$eQN#g zOw{ex?VEVi4hJXf%rUrOP_cYwjtt2pp%H_IE-ZCQx0PW@d1=uLDOjYN#Z9mF+;(L}_LTkj-}$M;Z)lO4t` zB7V%dg@_++-b+lu@6HqPquB?EXJH%=$KrS9iTHu>4kCVF`Y3TahTIk)egryf#1B{} z5R2e{;#&?Qk%;d`B@y4p@4XW-W@ivzhX09A8%8SeF8mz?A{Lf;M0_VVornc3i+BB7PXGp7?M4jydsC_@6iz{wF5G|HOOX ze_{asC*u1kJBj$-L^JUM_@9{TFj|On9mZZFR-HB?ejNQE@k98Z_zL__Og4-T;#&Bh zI3Eke8^8waGQ@WHpO}G_nD{OHPh1ZF6CZ&8iBXvUM0~NuMLY%mCwk$3;+Oc%JmPx% zT>#?anEyol=z0S2a`>NkKjuF%8~!I^{Yob8g8zvt;eTQh=0EWS%zxrz4r3niP0WAd zlMW+`*cbjM?!o*gJ`evBf5-eM{s#XOyE%+9;s*GiI0*hH_JaS3uVMZZUxfdOUt|6g zKZE~?qwu#Xh*+3wi1-EL;D2H}{7+m1{}b~Z#suO&;eX;w@IMiMd?1;4G5l|)!~eu@F#n0~!T-ed@ITQD z{}UtOf8sOv8x6z{9Y!9pH`agR+nE2vfWs&y-UZQ#1Gq75V6OvA>xlB zR1$xO|A{s5Kk;MupZGZZPy7o0CtiW|pLhoRPrMWJpO^#x6X(GH#Ax`RSONbNFMljy;Dg}BvWL=*R8{U@Gm7%t)p%zxqt%zq;O zutFU1TCD%XyRrTgXW?(`5MRUmC*qGGBoc?f|HL}Xe_{drPuvOr6HmwdC;kHe6TgH1 ziTA<(#BK0D@d3<#;xCy0#3$i@;vD#&_$uZ{tgYX80$ar6ZoHqKj5&Fh#yvKCSHyCPaFmR6VHVIiTI08ZA5&(^B^(BVYCxx z!vDm-Vf`l_g8zw`27XKyI29*P;y>YkVl4bm{2u2&;#crL@hZ%J;tcqoxCs6y_JIG1 zxtRaNA(;QfM);qY4gV7x;D6#3nE%8TnE%93{LLg{Dds=%XZW9(2>%oDM<`N>h1maz zE8&0QBiR3mBeDMz@kcRoh%xX#@jviC@nNk0#B{3qg%cC--jgBE*<-Ld`?AIAJA)?@w? zAI17lycPZ@;s*(j5|6?EZvnr9|A|>x|A`afe_|fif8r}x|B2f%|A|Af{u9SK3>Wbo z_@CH>`A^(}`A=L5{}XS5|A~Fze_{pZKM{X~B$0SC{7-aZ{u6J)`JY${{}VsQ{3m{l z`A@t6=Re{%@IUb?%zxrZSpSJj;eTQ=_J3j%{7*ar{}a>Ue9NjWhV=YQgj@IUbj z%zxtf@IP?|{7<|O=YL|H!&pc3!T-d0@ISE@^Pl)S=0EW*%zxr0od1X?!T-denE%9E z;eX<_@IUco_@B5K{wKzS7)`{#!T-cotpCIdG5?8enE%8tF#m}^!vDkrnE%B8VEz*q z!2d)){7*cF`A_^e*8jJGhv0wW0QjHy9{f+d81tWaa)=Q{OoRW42QmMN^WlHu+1USy z1K@vR_YfnF_%`-`Vm|hN;xz34#BrGa#CWX#L?gsVGUM<+u~&#OgBTKGq!7P^|A~h& z|B02D|HLx*pI8q66Q{%f#J=!9@jA?Z;+60}@hSM9ScUbUh(B;sPCOCwpLh+{f8qwr zf8wj~KXC#4Ph1556Zc{LCteBv6G!9vkLbtzCr*O@i32hJiIwm_@f@uG#MPMp#0l^} z@gn%2n2PnExCQG!aTn%4u?+h^@jCdQI1=-p_$d5OY=i%aE!h8wKVtq9hhhE`Tj77= z_3%IOIryK*Km3FH0w*no|B26F{U;XS`j40Z{}VsO{3mvY|B2%<|A{`V|HQ+X|HOsx zKd}VsKXC&5Py9RPKk+m8pI8U~6Q9HSPy7}0pEwomCyv}VvAb~~qWrbCu$Ga5u$J|K z6I$+UiD-X4P(M8a>(!mRSG}`lcl~dh12(S{yjJiU!K($Y6ud(4a=}Xl_X(aOc)H+e zf~N?cEO?^e34*%?j}_b{c(mY7!6O7W1n>AYXrFe$+XQbByjk!j!5al{5WHUSI>BoN zuMxaj@Jhie1TPo7RB)f*IfADPo+fyT;K_m~3Z5XiTku%HU4lmo?i4&ia6|BpUxfVy zZxg&l@MgiA1aB0)LGXIP>jbYAyhiY9!7Bx?5WHOQQo((K=LnuIc$(lTf+q`}D0qV4 zZoy*(cL^RXxKr>5!41JXI)wcNZxg&l@MgiA1aB0)LGXIP>jbYAyhiY9!7Bx?5WHOQ zQo((K=LnuIc$(lTf+q`}D0qV4Zoy*(cL^RXxKr>5!41JXeirr@yiM>H!J7qd61-9H z2EpqEuM@mh@EXCZ1+NsmLhy3IO9l4{o+Eg=;Aw)V2%ao>qTmUFy9JLG+$DIl;7-9K z1UCfl_(|Ab@HW9)1aB6+N$^I&8w9TxyiV|1!D|Gs7Q9mM3cIJW=oj!QFz#3hoj-T5zY}5rP|ncYH7GFL;~aErK@--XwUV;0=P; z3tlI9t>86+R|{S#c!l8Qf|m;J6Ff)obivaEPZ2y>@I=8A1a}J_E4WMWXu+L=M+j~R z-tnEVzu;|xw+P-Wc$46bf;R|WFL<5cwSw0OUM+Z~;1z z7CcsPm*CNYI|Yvr+z`Cu4`F}7+XQbByjk!j!5al{5WHUSI>BoNuMxaj@Jhie1TPo7 zRB)f*IfADPo+fyT;K_m~3Z5XiTku%HU4lmo?i4&ia6|Bp--Z1JZxg&l@MgiA1aB0) zLGXIP>jbYAyhiY9!7Bx?5WHOQQo((K=LnuIc$(lTf+q`}D0qV4Zoy*(cL^RXxKr>5 z!41JXjtct=-X?g9;LU#P&A$YmqrGone&k;Ob@HD|w z1Wy(`QSb!8-Gavo?h-s&aHrrAf*XQ&sC&`o`Y(8!;4OkT3*IDnqu>pK*9%@Jc&*?y zf>#S(DR_n8<${+A?h`ym@N~h`1Wyq>nY^xMkI_=IC(QZK-rla7)-V^IYZxN~&W81Y zu!fO=NMn5<(!DPXb0EJ<7E{5bD0OCB(HBZ^B%@=hS1P z9FP5eZ2zV)5k@i|iA1A(G~^89idN=1EpE!4cx+TFe>>Rz{ovt{9qxpzb7ITHzBR3n z3|`){#aZ{imUH5{Omrg3xp%UiJfUdKh0HJGS3Q9=R{9 z?)KdgO*41<)A20F=oN@?4>HSz;U(P&V^H8a^p#i#MB0Ap?{L-Zxzky*#|a(py3)W? zOxUVv`fj7*wYFWbMF&DV>W)I)m8iP{^`3NL%^sJ#SHOk(SGsFj9q!4yhl_Y9>M&5J zfw~Rox_nGcYt02KTg!*mwAy9NyKyB`CtLjOuJO>brYqmg#_m;y`det318UF6ytJk6akZn}?Z!i1(aOHUe z4yU0F*1r;SBo>(F925xYyYd?|jlOlQ+^wfmN9q>i8f1=vP~-B}{~SB^N(gi=Lpre( zNIPsBvp#U-bjuETOhaA{kmJDY!?OCrxEdeU=Pf_ zn${yDrnhojaE-uY^`6nNALe!I3d}c$Yf?+d*qYW5_v9ANBiOkm6mfezoji9C#>-8I zlgx2(?qPFWoH^jIbM*Q^%#f9VaE!kQaIO;{Ibm1F`oJx-jRWmm?~q2Bz2GsU*P6B` z4?+6Jkll$jr3|v$kQR-6^lMKK!x*@U@;(*SrRnu^ih$9D4Q4Q}twtdK`xFP7dOD%Vs~*IUdr1)V~FFC!+2-s2l6to~wU@ z4M*kR4J)kYBa>2p8`2|@yu2a+Y&dr8*jE$Z-NDbni;VJRNNc|+;RbwfCdpbCX8^Mh zS`gY0+7aYBY_7rZMGkCMi?$-b)4~4^-V^N`@L?+AVZan%2yh0F>B+!9l-y6i35fp+ zOaOKO%J8=CESQZ$U*QU)F3n? zBu<5&5E7<^8$N^$2w}c?3j3(zJgi;VH#did z25jDNVyL-xC98FE1~GnpV8pVuhmLCNb_(+LVvk8e-j7;dD)M$9FL`XQz+!jJo}{tU z&3Y3N=h`?yr6;I(yh1my8s*}E3n3S)$_+<+71CWwE=FOr!YE)4@}0mel#5j5A`nkS zdYFCpj9K>iWnbtu<9#?td3;=_^NrsVc2Y*Dxi7=ip|U?$4#L^+;`nh-yN z^?ZktYgD*RVFR!k`I}X|UbVLgaqd%fDt)8ET7?^cwaBkg@pYdPVG1w``7?kS zkWWTA+9e6`WcT#liArvQ!UTo!z!t3WZeStg<5am=#CITlxRP@zj8Pa3tV4bja1zQn zRk=vS*C9PZ$%QF26doPjD^QO74&W4&J2KkRvmNm~q#p!woVF?4tFQ%_g8W^;CCG0^ zIm+!sd;-#&l-v%5jS9B`UC3_$=0kq7YOfygNThF4a&-zfDy#)^pW6VOfpRq{M?b7X zd>7KIm0Xp=N`-5HT+1qeS3rK1YOfse8Sq1yN-qUcf4@Q>kb7Ppun6f%*u(7oh-*sR zW#L9VLNw0&6M!+mB%lj851940o<&t^ytfMqG|-C4@DI4+mBP6M!|qDqR00gQp?PL&(6! zyb73nV^5HxDZH?gKU02AnEA3W(QK_ebHZ%^Ha$`f%{{&iy?U z=WyDB=TO+Vm2Ek34&?c>_D7u8a6QfIE4G6(@WA+C7Sd8Nh4*`#&8sw{;@U5aU_)yY=^FAuV=2-lJB*5Zq4tZ!qfrvD{zP zog(U+AoyR&X9<}OyjS0XAZ1IDpArvSz+d#myy4_40>jb&moGIA;8pMac}UBP8XQOn z8yxu0<;DSgt!sZN(n^sw18Gf2i$>Ziq^&|)I?}cy4cAZmE0N|IjCC1lZz1gjq}3p; z267ch+k!M)$L!yTw2eqxhqTQ|3qx8x(&~{`hqRZF20ypbf0N*Aj;Av)hUqu@`;KGB zo+Nh|#e49zsV5yyoVg%-^e4zB*0eH@WAP8hITkU7FmGC;P8%GE>W|QEaG)Q?2+N#? z@sWaYWM9|qoxA=}w`DgUjxo=VD-WfLyf0K9&z4EcRvzlWS&G+3Ik=WPcp|Ppz|)Y{ zK732-E?~;A^?_{|b4@CbJJ_Vz6o{ILdFHY5xfyRO35^V^Xy3Zo#02&RyXiwaQ17)SZnP{AtIe=^M>^bq#>Ra zvOcgCer!aUI9`XsU-Zvr*xrqK%=;DeD{c=C++fSVXFRiopsu)n>jUdh$4l@T{{HNK zQy%%zDt{@)Tp9A8LB88LC~ySVUXEXa>ri8Nqo*6bZs@>0K(xIR`O(lP0p%NqtPdox z|KJIj7axt9zI$6=+{1)^@7d!Ax?wHrIb-Y~oCncAA-hh=ZWXe4Q{9wpM%|k*znXAX zvg`IK`In9(Ukmw-kgrkl=bSx#caoCdsOACfgn8971-g)J2HlZQ5B)w>z!AR{dYp-{8WT5aoEyn<(mULwYIdZ*g1wrT*(w{bi#5 z1f@UkPe8ZNZ2djSZ}muLX; z9z`QCDh+2y1fCHafIAU*woJMz+(<>Z9A$aNVqaXY!GSW^Ar@yL+wbX4YkwSn9KWYP zeg@>zg!~#MKT^nJoi@kS29>v2#cfw)*^2$%B({f7s_l>WvJt3Rpyf;!8M{MbZkPICge4t41QnW0CltXg~Mv? zcgI_6e=N}M^M(_xIkZ))uM%rE*Y1u`tYcbzpQ-v=VwtL z_Q;-}Tr_=mlWS}1M&vuOXGCGos1fDjRJnbk9BkZK&X4?#Gq$wmsWP<1(SBB2RiX_2 z)rR%|0oZQ>bZ>%>X$#uc_Ft&*Ujt-$j^I8{IqoByP_Kcq2`J0G!nRe7YOhqZC)cy% z)t|1_&v-iOsR_Zo71X~@)z7tihN|Bq#vkT_Id52oc0DZeIB({mJm<|W>;v{(NKmqS zG+FLRoC}oYTxh|X`Uvb41zmo}8On)#_LaIcs+J% znrt*=sS9Okmt9!HcVP|Z{m+-6ONZJMOuMK(;U_T$@bxRxN1IjNOl90~vStslg1G^+a#_T)RU#`9om0VUr}ORKkgq^pz;HI_=?t`e1oq(lR=fZ8O5PCi_&E?$zDDKUDe}zzqddoU8`{SC&$M-^l3k_A zHjlRSq%6)trfiCmU8>1$glvea7d|j$W0h=yCd+%OxZhyvVEe#OvYDD}DrCcyEPY_F zb9}yZ1AGvVeA^z4YJJ))d@xa+<}ddrQaxv zc;`BYZ$>AUVXgTWoYfAD^x*u85Q8ur!Cs$|!1?!`tAHC3nh@F$+7UVsBGOR~!G-Vx zSHL+A^mQ~`$o_X3X~M9vR4#xfna5uqNT0bwH25;Mb% z$v}IJtVBEuYf%=~9IlbEu&KR9`jB3#)}oa-()XJ2Jon-(y$)yFbvWDlaJKb8Prpj1K3qRn zA_YppK=b7;o z=!@ZGUJKp}j{8KP@me4T_cbfTwLt37!GToNlYsktkIXO*VCa}2}XzN@C zS@!XBr5o)sOpS{wm3N)!59%9OiSo2XEb8LDF0TKZm8?g|;@;E1&5#`q{X*QKyID8N z_iREM#|6vtn&t>}K7#TGp|b(oQ)dJBD-GPEG(ral3ek@++@qw<+;1b1PMv=icEZmc zblGoVA9KImCiYvLzdQF^-gDx;BHnMa_gda_YR3CB9mcz@+;{h44d2@j_g1j)egYkM z&&k%I3F*|~4H3yxl{#&*BohVOTa!~(N)XzECrt0~dkj2k{4dlBs)P=H$ci9Jd?l&5W66>+A z%z~oHS$MYr;cD;*FV5o#UhoEBJHjIHiflaZL#RXOK!{opZrq0S9AFtj6~ab@w%>6d z33w18GAG=45;Ez)H3*v!4kEa6@$Lgc1Hw@R*LC5>e;`)|tV6(tV8krMwFSZ<@D0Fi z2=*Sm6Z{lRZ0?T{dDeAAHTJ=3>^a;Y(_!lv>_fJ#HzJ+$cD=AQ)>U&KOhUOdoY`gw z`DP`*_Bir>$j^g(nUd#N3{!hR1@;;HT9Ng0EN*~&I^;J({)SO_{tkcG*AXl`L6u!9 z%Jx!aZ|D=ef0>TG3ik$EL)|sc;{MMg@ZkO%E~?oRhV(4d&pp!aZ-uHe^Emp)L%tFB zl_m)JO-g=_khkYWnacZG!VeN0k zzWd`P#(_rU+q%aioxX|}b=baYhwcZ_*A5|{uH<8nBhPl)As;&?SpOO&A9Eb}6v!Wi ze43EApW!)=BVP`A1GcPC^4zEPs`_UNe@4Nd^12ZIz1rS?wn4u8Da*}wIPGb!b{%BtbIQ`^2cg43$Zp2oUJ9RYM!xNHBg*pmOUKdWsH%Ue zR{v3yr!Emfm*J|O^EKJncuN<`QWyJqUUJyrz-;J}jC@;{a#cr_(1rIK@*ucw>;vcF}c!f%6X zOgFVoM4UD_umS#co)gqD2Hfsbk}B&FeX@N$PvspJdGz%>Ro4b#-&IO>k0x6IS@wgn z>_;v1sfBE9x50tmFE$R;BA>c&e$=ZvZW8_Ad6)C!?PWh7-U-?3uHSk{&KHdTcNlLM zX9=9KJLeG3Z5wefg7<*q!|=`_+DwV(yEfK)qGzH%iHO_wsDK>Tm_ec+yno_H9lRgh zj&s-hr~`iApM$fP3%c`tQ_S0ee3luFIGl5e~~C3 zi}V=hTJ!lK%SCJD_#EGfG$+cHqTI7+w+dx!U60~SL|xm3y=>p{oXWf#ML*~}>wXO0 zmlnR;tYrO~EcK)BC`;c}^)n z`t!RFyN2LiYhS!?hJMULKX`AM&$j1Tb~L|pP>cTZx%f=H_izTUQ}EuyV@PZN7k*c8 z%b&yWhCinrPwu}>GyWgyt5v#H1MPR+UAQL3Pj>B}HyUFeV~p2ayoR2EIDN}Ci_bUh zwdOj=x)Hb6v*C(!9m+&rl#=B+)`NH$;;E|Mxu|yy&b4ctcus8Kc{gl}pJ&@Y1AZ+- z8pq=t;q$SO=R4bMi_gj~fvkD{G_Goh!`baLOzsL`4Z@k=d{!BqA8w2SPX>+w`hk@Q z)d)2R7b1=Cf*l2456*YNq6=^j1zbL(Ohekgk;eDE-UIR-H@@FiiTBB>kQZHuvnN6v z!gt6wpH+g-L*RXHA21KWk5GnCj$q!iMql%sgU!Cih`w^}@%bj#0ru6rmkQY`CEHh% zO@%CdLRmC#+Hf<@i<==|Weg7dGdcLq$D0SObzi>oG3zhh`MB{gWD|q$d@N_2&jHmt zAAD9Bjk6k`Ro;U(I4^1;6N_^d@42XJ_JQ#tj%)UwiHsX-&9kQYzO}2SH3jv&0o@!% z&7RB9Ki0vq!u1l*v&g{p77=976L_-?{p=*D!ev7yHEXa~>ZoGSu#?vGmQ-EVY}XI*ED zy4tBX#=oqS_KU|^k3LJlIgIyP=3xBuevF&b*{=?f z&N|HN<9@hD1Ha#dXN3v4hu1K6ZXn%SHy$9r9vtKOHgXqyRqX!u>xQw@O&wU?g>zW~ z%9o)01eA}%`7RD=6OiUZS|ZW{c>bIOOmYnhBq1*mdAZ2LT7dU&@Gd3nOC1JdPhwrC z09l^e@BF?raV2_=Ez3FYV&3A5?Hs@5_A4A$3Nb7gdI51q4#eKD&(Mm1~ zWev#HQ*R|h9nzu0QLg2jZ%VJ(NIQr$*5B3_Yqn#3;7OFL`3&C=Qt2n*9z`9}Hz0k^ zJG>sVw)okCW*|X%I7M1|lL8ea4cj~-G#UoW3*O4++ z51(o98Ntn{CrikU#djXlmCQWI@cShYkiQXep4I4I=C4z6=5L37 zYY^vJY3G-zIP)`5z5;QsC##%z&WEu$8U5tkEmi4$q;rg3VA-36 z&%}wje6rey=!XcbGaU2O-g)2#9MG|Dxh+?T3c`}C3fV(`70yT6*b zJ0S$`2lvBwM#f-Wf$wmQ{@Da@?nCU`nrQ1RGhFnCV^_-LDH(Ykfi-*J*}Trfyto}@ z?fP-Ax=a1>s%+%{P=Az?2^BKv%0TKs9XWyJuj4Bj^|4oP`=&R+PV(N_iL&LSodpX+uVcb z>o|;qIIPo?Fy75~7s2PMJnj|q&chslE~&)eTo{-PeUedcGTJSk%6HlZ1=e8=K8F5` z$J~g{?Xy<1Cy?G9O#a9&Ix{7D!vmtetlq(>!U!+IQDIAfO{6-#U?hN zfjsvcfh6=F@6QLu0KYW-hVtrrgad0(jy9VBoAG^K=GTh+a^yQy zelL~3M&zd>pWpLfd8`ln{UdOVc@oakJ=O=tq0jVPLoJ?>V*K0B(YT+dLM|0@agd9H z9LApc42{pWj=?UW&YISQ?&||f4q(lPFXPZA-;<0*xmdIPwQc5k2j6$a9{~wCA^QVl z=`Z@kpbTt-KgQ5`M&h}s?*H)iK5&&6_5S~dKP$*mo?TIqB{{p|iZ0e!MMXu$7L$~^ zq*Rntl&Dx-BgG=yy6i63_8Jx{B`PWv>bh-8$t^N0%(ck7eIuph>Rvr899`MfwxZf< zYroIed}d%cvi|A*zQ6N$%wy&=^Pcy-=RNOv&wKtJ?4m8DVLPde@kMtfzCl}#;!k?{ zhffakZ#`@-zidO5IRKsk@H9YIH*m$Qk<}6wpSD9|l?{&*E=R`OkHG<_b67FbT7lDg zMj>gFI>kHiF%YY*YcKvz)sLi|4tNE4%ay=LWmRk+T@MV!)Jh z=AwdgVq1YLxp&5*sO7a;>pMx@fJw;rTw=(%Iup;CS}M@LvDc^kDkU$MsI8oXWi@?){p(zvc0X zwCec8i)V9hoPM2JpUz$2Me#v!q?T>ivA2HB#1_JCzE_|z&8Ce<*G!~k+CDkFexl+h z+*^okn9yCYRts|@YdoXDPuRm*Gtod?qor52K#hkd_24-?RTfVrcw!djW)DxkmrrXw z9d^w^>p0oS^{3(2UfMC8ei5wT+&N*!oXJeWGuTPT&=xbP)E}pK@iSV)xK*@%VtFZd zo>yEjvBlX@^o^gK-D_Y|hdbkS(3TeadR+J8Zn0+#1uq=$M_*(@%i3XV>Ui(OA;FJ! z@(*h}gV>$4)3(GSV2aR`M%TxK z=o73Pfj71;UKL%Fm`A(rV!svIc?&_b%)>pL8m}?@OpEIdUve43UxQq75B?36;qH}4 z?viop&4zsNN}rq#e?_n06|efcybm*9ZRGBf#;qr(=X6mSa-2?Eb=b61k>6wB)qNI5 z=gXB=842ezmlK~!f2pL5>=V}~_R$9EC(eg7S_TL*FKmn!>Y z`d%Gyx?80*$%J%WDrHup>#j(GKMr`Um-GRzGzYKtECW6Tyl*1>3Fwl&sk@IypT)kA zov%y*v*g6y$t~npyvx6YJjJ9TOB1@cF^=As{`ec?XbS%48T0s_2+sdddHJI$K^$9V z3bzM0gNrcFkj)msp3BAUBku2ucnh9z$z%Md0e;0vE0(NSQgPCf6(>~`eJUe3nuVb{ z{8jkH9OWnkM>pTSxISEXe*C_(_ucoqC*Jk3`m<}71;I(?_vqQ5!icHZ!hhoccOCh4 zcTj67<9rugl@Y9Dx>WuJ?*0)zk8mw-pmgH){fu|vp{0>{w*P_-e6j?6_$~NaYuQHl zGlZw`XK-r?=U+`Z_!ap7f!~DRf!~FH6MjE_7ybzTDDE4C_u)T;-wfg*{L{Fk_3Uwr zf-$~-!gs|rtnJ{ca6jd{3IB2YG5r6+-;1B~>5Sl4gp2W?z^}l662A_A48Ils*Z6(- zzri2He+qvm{y2V=iu^Zz9sbk!UHH%7Z^Pe?KZE~2_?fNPruaHfuXFWUyDB7Z2XPhn zllaZ}Q}`YD3H$;4=kZ7JUr;(_?jgLBaQbI6f*0`%@ps{u;lG4mi~n2vKKwu6XI`5T z{1Lw%|4;aB_^B zqwvS@3-I^hN02w3(J^zHDB+U`YmQLG1y9Xsy0@m7`B3&_{AUVb&CNfFexVOe^qg2= zY~()qcAPS6DSS(Z3@l|Wi#4)mv8$KTAHTPA|E_+t#@i1W~QHI!swp3&8I!U z4)TqXuPRuUcme&PIo)2HzK`@Rq>qrUGcKlV@@ErPxsgK)65B{0COsYl7TTn@PPqqr z5xQrH>&KqnC*RVI?U|=NV8MzmtsUjheh1U_;I2O!2kxSjg2XLruun)={!Cn{Y!!`1 z!8vi`FYld@{gN787uWim=HyMlmjSE!5^I}W842IcPF=$B1A zV&k_F-$(rQ#4jR#+{SMweu(&X;-&9?ZuP$K0=IcDaN>*h0;hnx7C7-Uvm}hq&SBq{ z_!i=G(hCyru(p-*BMwHVp5x-+b1C^s3vuKtNu`b9g~kSrIo0T1g;x@O7vXx^Y}oP? zL|KjGYb0M2`JSM>X1<&GUd#9Y;algV96bAfS-tjK_W-qj58by7*f`w-ba7jtX$#-F z12_V%w?Wf(;No_@kiCWIq64}UPF3|P0zLIpJ#2Vp7dp;w~*dy(@Si6Df!z- zKa}(?(%0JbDw|$SdN1jVNgp6xcCgBCu<4DY50joo`WWd(j!9o@)7waolm0jOGDG@q z<)?im=jqfj{N^4G@WQ*r=IbY4ZhArDCGr&mx5vv@Hv7$aIh$|0%@-$MHTj++UjuOI zY6tMn*z~=mHFuOv+Vt#GLViXLNB&9wCFy;n@Ab+ZnEj?L!6&?BHeWgUM#%RF z`L+YM&&#)a_M1Hv;FYh*=4&Qju&5yMAo+5H)5?wbQ#kw0`DmN3$L8xLUn%+SCZF2s zpfl%WXVAh70aFJ|4=~NZ40trP&3*^dUBHW`oi^WY@^z8#%jDA@u;~}12d9R5BK-(( zk$xTN;)CAxQJXTvWR2Tz=^E`D7Ap_3AH!E4D3K4YQhfD+vLpCDs337QxN<1p-Ic1g z={2NBN&f`tC8UoZ_ozAV*J#t5NG~V-64Gl(?^PYa-)7U>Nv|jUV$xekH~Va)ciZ$H z(%VSCi1aSfmz*`n#{mm72uvR^=K~|&R(X6Jnf>PcH1H}rZu3o%Z#((Q$hQZ$HcQ)H zo4$|qeWaI=9wDyX;zjpmMfu)n;iIJ?uL^Su62%#e1Hem<#%z2E@fF0cAih@ltV~zh z^eWODNY5j^mGlmqUT@PINN*>7De2Hy@I%1Q0RN}Avpzyxmz5u*EBQ9MQa*X6@FgcZbE$V`LE=U7 zlo99hWFcc!e0PIGW9>W7Vobm;SKi#y!o2xMQh)M3O`0b=G^7><&g=q(7H!y?11jJqF>H8iF(+f=G z;DW>hz!Va9xF?T;7G?;T5@7BDrjocuXij(EIt+5^di!SN#8)a$}Y0$Wj4K>^f>9C zBVA=~vFSB7y_WPKi~dh~4skl)ZO8vCm0`XqZ>@!I13m`)<-n^CblCV#;wy>&DDic~ z`FFG5i~U!P?Weo(!p-@PtTqc%jqT*lf{a>RW8mrn*So=0jU9L?b|>vVJ>c*{&y0oL z3+ynkYk(ai&aA(Pe`i?!Eu;RVpFz6vm_1eT&!!iV9xN_MoJ4vKadq^a{8i{I?~F>N zg{=a%7}z3U%ZY2S{jlDqH;`UMdX#ju?3n)&n3N$IGwXmTXxy>?q&3C z(j%nz68G_IpM2?sYrp?;@VOga+G1h)f$0Y(0~q?*0_jSk@S^`VAtdKhG|naZHlxu()Qs0ur_7a3}pqX8+g^HZ+hO$wM{sG z^nx9GzQi6PKlh$iA^Q+f+P5&Gb(8S!zRs!?s=dLZcRF-_MQzW0yF9%scxQ-HZ{eAB zjkWgMhgE;-SbbMQ`sXLm(w=fx;^*g3H)KJ$_2!4x>diVyIcJ5yyV2rFi?zqyyNQBP z`MLukn18b{S4V&Qyt&^Hebl6j7L_lUPdhx!8=?`)BJL&R7@az)x}63Mez_la@G4hz z!uD65Yp}_4;Ek)l>NFe+>-06$sJBeqMGkMv|A&l84gWuWI zU1vgySu4vHE$}0VJ(5YV<wGrQ9 zFRZP1m;_bSnazx$+colm^^JZU%beyuqdz#4gkwwgMcyBmkH zAF*Wp3x51I;&t~UIevinp#|c5h&N{hlkl_?Z|#4KEPCo%qqEudMG@;sf|5J~)|}$0yRa*?es$hP>82cFD(3Dmsh) z=EL`p)@R|9+q2W=>mZ-{sh_Wbv=*CBXBq<5ZX1b1C^p;Padh#z_{<=>O&>>zE(!W+FrKa-81 zaMtM?c9fmMSQP4#d2z}ooVugl3{LvybYRPCwrNeIU6E8?K4~>c<+0u}i@W#Ku&=XJ zo8|Lu3OyFJ`BavlHbPov5>9O6S)8s8(jR`lPV$YR!;<@0EAbTz#McuaP-i2H0eAcu zk7Jyj>3ibfU;rIfnwAv|$!FboD}F2A zTxlw>_>~H$X9YF*t@us&gZQoZ8*!buZd^ZZocKZfT-Mfy@kel@I5$S*+Vcw8k;Mt= z9gR`xvD;_c)`YQ#rWhYn86Q5tS$@4)y6x=H)*K^Vw!kQS*-HFRiI=W7{D2p~p`G=e zi@tYpS51X+m0sMx+PDZ~&RT2(<;f=8X~P0&r#?qiIBe;wSnB$esjhMGOpx5x;zT1A-v+vqbp2E4iCA`(S zJRJX3PWkE(?^nTFX~Vh?Tb^R}AtzNQ^X>r+O zI!mo|#mmm9Si;*ylorRQ z`uuX`xqj)+V`c;2DV^l;R5iu=ql)}7@;BNtlvYL_@jFUdA!(O`+oyjD9xs70 zl8vSK$vi)dIK7+a>R5zaXe}u`yC0Un8~vj4ccOE4qH`*(42sW^AL$(V>Syw$zZ?&S z$SXSg@!eZu1K=3pe2d}?A8Ai?QeN*0>VaO;IZw%z@Rrh!8!epv2Sfc@W4KFiCQtKH z=B41gwJdyJP&R<@w%fH-?Pt3BlmQ>mzvW!NE&*P;>S)G!>C_1I6Moevm-1cSV1c~* z&~xI=UgjR#&~qu|9VbrZ3uim`B7{pkaX1HUy4D}lkhOQCH^eXTX@InL=0}>>b%5^= z@JTvOGJh`h=q6pdd^vPE9&a@GZb($&SChAzyt=#pN%AVr@fdxQS4Mg=k6qb1lb%U> zHu?3R2xH`THr*of77_PjXw^9K(KFWUIR30P#-?!N3S#uMgMSMBuCUhS^|pq_GleDR zv;3sKUU^E@kz2pAD`pBcmu3>+7E0!u=fD#^n)u4+rX6NbcA1)v)6B7 zOpj4{?li(@=HK&7pd)l-Wb(Q_BvF282l^*+qX!Z0`6L@^v`F+?Hm9m|YW${jS zvii?^JX_#L@o?OhTsxVfUFX>Xf|pF*4y+50VXF$CWOyesa27H!g{*9nexgnP!h1Di zeD~S!KhpMUZ>=4a+qHu|z$otf*__o3+RWKdjW@mIlic^<_aJ+JrM{1_moC^N?S75c z=aTa(&mCv8?n0i1C3hu`LsqqxlNP&4|HG5@57u<`ojP0{uX#lJ=#jYIKtIo(AAN6u z&U-#Gaqn~RB+5B9>@KbQW!rkl&d_*N2d96r6z*bD!f!Lir!Sm#oe z?)LID9_m~8dUR*Y!s@%4GS#O2%wK44gI9EwMOn8aO?}~!(}P`K)PHCJ7d(C&PjB!> z6a#fz`%z{s<0@oV^45Xe_2Jg>9sE1%6}amawsSZq(@rj$8$+nmbQy2~cA)8xyiXpkHHUIWF|&G~@U&nwuaUfGDj#)n<0LSUi z>}lzo=5Lm)4*+DpE`sSezi-h&EI0=m7@9O6D|D{ zdUqcn-R#H1n-hR(IRK3KZ1!3h-;V~)=_kgEtnvfok$x#fzc~Akxno|=0bpI2OR!mx)ol8zFKa_dvNp(j_l{4zg)`lGApG#)9n?o_vJ3Ii@##!p z%(%S}uRT6-M|mEfau0wvnNP>Sr`?jl1@MbcK_r}Cw4fWMw-$m^dx=--BjT0D8<~?daDJVwxG9Kmf(;Dogr~;eI9MyZ~&f57nIRH zvZ-ECSL)>2TKCMO2Y_)rPA6Tq!1oGzC#R@uBRqb!_RD6UE;VBI@+Q(~L+y7iM1#Y> ziFCDLAG~Y_e>M0!4vjU zv`^$&)(NPmD+jwiOZ_kf{;MC#zIg7maNeM|k`rmuzd3-xEZHy_ft zC3$NGR|B}VfvXz-=d2I;a95NsNFKpV5yJjaK_q37egRVDqUJ?R(4C+aG` zsJ`{kBAFKtYM>{Gt~0ttu-0la28T+875#o8sqaSQl4+(HtdU{ZDc%Iwi*?v`Ypu;nSx6tTvoS8~k&0 z)xf{iN&Jf<%enMp)j{QqY8=4dhQEz5M{^o6n)~q)`MMJCAY0)wzd@%f&YhoxU50LbEB~XyPrCZ>)v^80Ykuo&{1oU+DGk?*w5MhG=qYpxJE@yq9hOYa1>D2AsdlS-MYsB!WJCSW=^eF^_UsH_ ziPtyv$uoV2Fl(>I-jWW`e2{E|ad+ zKBZ({Vd2&IuCVfI{8G4(aJCJnM{h@ehw;);IfPXgjR(S`F;?|cTc|EMw%$&^@K3l| zexLSv=VVXwtaiKJwTSwR(2mj>Tak~g$jr60h+8r*=5Y<}>yIHk#8xuAeY|WeI-= zdQtS+eq`_%I?4ZfXcis9bF0UTV=e9nh|@U@^<^`^p>8?0ZqmDlP=?BOb^FW#>ZZP> zKIH0_Ve=)|&GjACP59qS-IUkWO<~ndW6*)>Cf($0Oxem?kX@sHR6e?d`Q76Vr_Se? zustiPd6B+FYb!F>imcyy3S%I&h!_4`Mr|ePH=ZX zVivX-SdIPbfOUG@$*t}Q>)l`Jr{gWAS~t^XsX0NXIUfe56I!$Mr4Us5&!Y zG<|6{9eGM!rEx6OeRHtA69;*1sQ4ff_6K8=MMK%V#mSt(-#c-I{Z@NQH=2CVBH!er zJn3I&&wtUv&(dS*l}=trXU^`oM&|4{Sn-hfFC3B^)syzTK5UcF{#!#i*&6#86Vx7` zqHY>X4GjI<_%45BJ?D=-e6hF3W%rzq&Y2eXw#>`ZE` zcQxxp%hl%$k4Q6pfIhEr{~YL5S?VXUGk+wRGj*HOX|DZKp61LmvYzrd_wR?RcH{)v ziIgnDLHUl{NRUX$BOE!0_X<)H`p?dE{=C17sE+6cI0brzeRIjJuotJSyWqfYF#G8=wch)q~!; z=w!vkC!_F7I!7|iddjr&M(JC!lXTw9$ZzZ%bLPy+`W@r*?Qi(I~E3z{rf9@QsXmUJJ8*0Db#i`HBp4fCsxW=Qp zDD9s74LdeP))}3taXR0&f!3~+uD+x(z{$$r=*!}nPnTQIS;_y|XG2r#!HW}XkHnoO zy{S0{`j4_@N2Ht{j=4>zU~k!ex(GV!_-?e{8YlJUjmBo>(_BvDmm807{_l_m|9l}_ z$!k@4;K*5dSK@qZI=z|nz7p1~z`>c&>2%7_`9PgXlx_bt-ty2}mg?iurK;1Z7Iz8h zCBTV~;LcL}Yo4gHi+(=kSxTPdxOdWiqP++_lBZ&o2Tw&~yFHJAEMz&FCA*C$hx46= z!{E&#)-R!5Z6>~*3jIaUVCD|!bjhK<)2u#kJ(#iGwp}UkrIvQenB|Mn5w?s>%Gkr5 zj9Vx}YyB$YD9Uj8xL36K%gD3ZT0BjM4~)`zmr{p~Ui@O>8K35r(a#+c^{Fj6*bJ2M z1JV)rOKWx^|XnPx@&YW#Yf}E@N!?^NGwUBXhQ~quI1AIvgE3 zky{w9haSm1M>b$4_%t3%hsu6Z*sL+oXT?Xyi&gMq9_~VHabV}I+0&0^>9y&}YxWv9 z!8y@|Uex^k&&-`(4;`75m9p6ElRjQ&$A?<>-lG73NVi!93UDPd=x?Sw`uPVxxy}iku%t#A3C=e_ufU}d}ur2J-B_ij}lkZkr7;hUya|0yOMAh zemCw5gty|~gufI27X0LM6Y7uVzo7X)3AIJ_^3%;ZqMsRLUmCrbmydD;mqZhy<+0(f%;roTP z{Y`se^A=L3`oGS@>s;d<@Kic=gt$J=MH*UI=g~XtamxE1MEi3~t-)yh*X@Ob`uJw_ z;0?SD65=kVZxquvt`lzh-hJNx2%bv2D^Cu0ZZ$6Gtcb(0FNz%wU3;MG&V-O;>_eA`TYcmv_jMyAdB_z-cjY4i5Z-^0_}r#j0cz0f^2e{L?F za%O}vleE*YncUumXjMOUI$6B^4r>CMXPbFH@K-{Y+S}Eq0=g;;UEJ-WKBBplILBuv zyZhktkmYmEm$@TFxEj9}*NAJuwc(y8{!u2uP6n!Ii*m}Zh;W`ciXR0(Z|kPQGucRb zWB<9oxITW-4yJ7zjFu6k$M@x7d|=6)K3 zTwmRGYS{K8M{qVWgZ)3+UgNFi(-pvXqGRMs=N8(1hBkDP_78i=BYr-NuJQK=scv37 zc%=i{fN2Kr@8IK=%ty6;sCkgaU$u>3>n&^@uob{Q59}qt2DGQ(Ut$c9>=xNIeaYrm zz)__7SR92GM-&_ZIDQR|3+)`+%#%r1|H&jh&N{*`NLL+1|E1WT!liJWuF);aF})>fd4MUJ zx6$Hh2TwWacY~+I%hOEUU{W5v->b4~$)k67yOZ)%5cjR5JW-pcm^^w*`b$Z9a>;XN zQXakEt8(-9={$qz2z%=OmBR2gU6Rk(YQ1T_mH)Xi@@Ci z?%%(S`%C4^*dOJbk@^|9)Grt#CeqL)>IaJ3hb^5>+*jy>f}aA9d*cM%XzZ$8VgEGn zdP79;8!Y?)@Orz#;dgnS_44%EJfF6C+Q?(-2cADle_C4BTDT?)R|}lpn~;v!<-y7R z7Y!e?a53Pj(HVl9@Zd@;TrF@LSSQh1jp`VMKTmmZdOt+?-VL0_z#(*NF@7a{>x^vJ zA-@BCt~yQw6Mlzdvh>Kfwc*FG;rB9@-z?sX=ipogu9ywqL)hJ0s|5d>{sgWlZ|Rc{ zzDLR9eou0W>AI<7c7W z@mhC&Rp%wd34XV5Ezpi?3ztXde3U2M=F#0V*Nz)Je(Jwb1ov0!E8IH6?%MIQ9$cq| z+hgGxfphKnNe`~c!aWb%653JoM&YfczL$D%RTgeLa6031zVPBN)Q+vHgKd|e$lhO$ z{X>4W%e{XL+a-r@wab-B?c&x%MzAx+aBa2#yw98?FP_jqAbn;RbL+ zxH{l=<411H2%@+$oLj4@#vw1^wj=5O^G5nC{b{nD zzSmBF{OMbHix7C}0P%bYJjbr$OdN0Ca`(!Gb+5{Wb)U+GXO0fnNHvcrqYc0K=g^Lg zfls#Ur;rohhNG$Sa-kuWGSvSAXt*01^d5lL%Y-N2+77bwwjeLM17_ZnRo^1t6n#i{ zTW%rWcET#>{YmBM%u*(DW^JY{-FwkkGXT8a1=>hn?C9xD#0#h15^5oSJ@GmtS?JO5 zC1`NCq<7X@T&?8My6!dPac@aAT6_(p>Hq((B(09HXnboDjpBvw`iZV`U}or79|Jy% z^7WpV>@e}*6XF3p7Y~en0X`S_t-!wzc)cqm{D&`qfA8uL{|tRfYXRq5{3Ra#i@-ld zI|{$fuZkbrfNuc)Ea25&zrmfqICuWkMw+Mf11DRv9DmdD@SN*bow2Yy7oU_*^E~9u zjDdaZ->DuhzGvlzgvxc_-F&<7M!PPdb#NEgVdJ!B>dGNKPx9elx`fj!qtnAFI+U*i zIvl*>RHuzL&ec=|=}yf>@yPU|1i zE80&@)+;5{8D386Tv&`c_fu!V>x_MB!2b&T2|WN0bkE(5sg4Jgl>a>CiwCWQ|H%B& z(UQDRES&k^OabTf7H4DuoIOc+zRTU(?o{5PTe>*WyJB&o`^0d5*s~(6QxETrC)Y{; z*P=QVV+R*w^L~yx_3)o6@1|Uh&1UU{I!PW*Vcwbzyw-3fV_IXr+q<_r`l~RHzwb-` z&&n1oeOG(4f=@#>z74DkKTn??#J1E~nWLTzacCb{^K_N_6>!KFR{G&KUGrJZ_pF@e zIXinTaI$ybNj#aTYtDTGw1LS{; z%4dJgjXCH`qfgAf8+o<@cVk)**BtuB)RcHiq%E%h!M!QEf%VIEar*b{9^FDXM0eW) zIGU1hXg_caIz(qG`-VgI|5*n4&y5FYJM9^2Y;?L>_Oa?w3~v3WEynGxLc-$1w>&=Z zpHN2EoAbfgL}Ro=Dl}_Oy_fN!_7gY1B;HGx7Gf)Qo}TgY<)0gTIpcFHU;f$W{{C|3 z=|{Y*dAHv_Jnw=!(!253|L>NWmQzXz$GXmC2zjTIW>q)T;{U2iw&PQ+=fOX1?NawP&NX5%n9vtKU2~x__4o=hAOPr{i0%#s$8mHzX&)aB6&u z{jZu$wBNUE`|0jEeS>?jl!eX{-Lj)q&yFRG>5Pe=rJlm`=gzRcZj4iXHO66kO#At3 zZT=eaPoW#HAU|4q@`a@QvbmJs)!|s` zp|9yjlOyO--3zOLCZ}@_rd(eyF_usFS@>SyHKvsU-wTfx8q>C+d;ehddG4Fjg7hx_ z*92FL`#bTq_$d(7ihmG(H~uEvV#2z&zK!tVglF)z@3{{@{i_*45pl=iG=A*_Z$5D+ z@Lh(#8ov(zEc`b7^YGmm^3RNC8>z3_yq)@L&tM<#s%h+5%eTg}^O&oMN4h7WwSUQ9 zDKe*VK{6`)M{AE7E7i9iVy#nloy#M+^6%A&r<0VSG4fLGza@{6oz&^w)JbFH5aAKr zDDEP!o*FAmZVqW9; z>dMpotOoj&`otG3els>wZ^^ILn9E5YXM7Uwa(EVnw|XB+xObv=wC3;bx7M@ot~;fM z4(|2QS7yuKO`R8_!Q6F)ez)gn_)l4?Q!}`9KTvna8a&=;e?w&?*PFb?zEr%!Hv=ym zU$n=scB*H;$>fJ#jo}^ApB}9~oyljQ#TUtd?lY<%)qt18# zlXYH}ROcP+;ki2RJOeFF-Tu!4bsqYET<2q6N1Yr0$vQvFf35oMe79HU8tObs-JV;Z z&U?H%C-09Y%XTjErT*n)+wGspeoIAHxx0v`QU}4x{^_@KbI$PO>HLA#Cnf);ACN~j zm-@g1W9SF^!)!k6ve~f3q4Rtl;E-O_8(OW{T=|w?pSNS<8v4a-zLn6b{xKZ-+P{m? z7qjV;qCscNU!YuHpVF^3&*rCGjYDIi4Oo?{HOPQ+|8ZQ{AN8h|`r|Q4z^7?!k@c5#0;%+Y2jy@56x<6KrzWojKVd+du zryWR7$mURfjayadj4G=$F0isx&bMrurO1--xIUD5OZeYyvN>djmGb>ZWa}Te-=ITk z`Oc)VW_1bQPM7?%GN$!#$=FW#uXn2Mq<*fQwpc!(!1J_DmAUlh#0Y+}tVzz!d}2O7 zw|M+?coveg^|rkilCv$ZL(Z=Modta}W3lF$I`69a7#dXn}F&y2e?$>wZYw0A;dv;AMStnJ9A`l|MC#*t+%FRR_n8n=Ae6Aq8`mybv5 zH^TE)Wc|xYu+kk4*7oZ>%`>DYZ(?38`Ez)ESiRkc4luBKTkwmo0!!JmutmVGeHB>o z+i4rgj?Ul7&Qe(VRCbnoV@-Qn z|Iq%grz6w5S--WiTQz!cupQU*EB2Rg9lR%>Px{wA*>(CcS#EQr$5z}td+tLz?3!P* z?gcN@o-;AlShP<_+1FYAq#jctcDyz|^t6ju9?cd6G`?t#bS>WYwg+`yJ z_rcTBsP@dmai|%)&gZcid*EXQt{&HfYsI;Ju};GNIXLml-769v|Gb*xe>Hj|`CL+z zwK zzuU%r1cNVJs}0xxB>P?P&#m458!!rYS{zZ!Z{csH-w40b0?*R0Edvy(J` z5AK7cX`XmGW%*~B*yA=dsoz!6cg=hXxDm?LTuAkoe#A~Oa-_XKosaO(D(!iE?yS;@ z;M4w&=*KYSL3k*B*8`>FY^n z{?EJeVSE|!M*k5nI#sszpF)~BzX6_1;&rav=&NYB=AruB&$(xnv5!8bGWH_h+tD>w z*fMt8_?^UWCH@lPE5UW7J$ImW86ykSW5mMV#MvLcT~Q0HlM~d7krVOa6VN02dw^HH zyTPq@QmTNLya;9rc@SM~7N^#GIFC>TQ8DiTC^C7W&%|ZYwU1+ledX9zy1= zIGC?-Je%-h!iD&U;FsYaf!~6khu?)C#~njBxQ+dF{384l^lje5+-9sL|r{Z`Z`BR?Xy-J>2$uoNgoVlCE-2?42R>eZOt^{u-viY@@;kvA| zEhl~pZItKcFqOzu4Xy##jBCSn;JR^rxPIIa?mXm9a=VAHJG-j;++~atIs>kIefdFn z7e?V|WG-A7oeSsd4l;GnIr(he@8*uN>Y}^P+1z`Ma$h@npHb%tQt7MG<*n?ae3Ux5 zGNaK$6OF`cT-r?CTzn1i+0ixTY`y4_PG0PNOJ7QtM(q7^Lo>8#+;TXge9K;P-x2UA z-=pZ|xwu13`)U00$BIkQIU)YV3EsLi=jc_2?qIuhU5zgqHykcUQ;tVdj><&GYcJ>5 zl;L27OLCLY7(u_@|JF-*%LqQ&wotnEQv|cZ%a>Dr=xlio$JndjNT+P!SmNVQ8=~jY zBj)_S@Z=CDxI=-H9-6&Jt>bJQ`#i$+&4=e?M{S|FMh|}>aZZP-pSUws!R_oX*gg;=T+7=qP7iBM*0-tD zKedLCicBs-CN=&^<~81GJs6rNwJxbQ*6u$h+|zJn7Dm}aBTe?ls@?PZY6a!i;J#?- zXd=87=jh0^bf`RcHeYKRvMDcKYsRR_rPnk6Azi$0vwL5lGi&EAPCWTKJaKSNF56C> zzt`aOfP3pt?Y_|cg*&uvf{eQR3;O@ERCutKe!G_b{I8Y=D!odw2fXqe?oo%^&YK1QY2E-&$3E(hJsI+dHS$(lW(#HNJYk_% zrtXDku6lvSaZ9`V?=Q8bA16BAxL-@^G%NJnnAF%A~g4hJ2@jPh(*(^8Kg6VAn8V`qJc&3PM?5O#aGJ+J!nedR?68 zO||qY&hf3XG;9-*AF65C8Sb_UuD$RgZID2>X-k)7$7fUk8cCwY`MDNw}yd zBe)O02LFD1&8^x9|4?E6uguB3{*Tn(|Ik{nY~fVunfY=!&vAXa2wutt(LTf}{MWtg z>wkX3A4_-_Gb`8jE%`qGf2y@2uW!i?7XP=xbJ@&agZ~;k z{)i2seX|qbkHKYSyU)V+0^bGv^}tirY2`VHy(u47_ONiv9+u7gZ@`M)TyV;EcJ-C* z?B~;dR0{c|6Lt3KQ{)qTd35fqlWUVL@Olt8jN67A!;M3b+GvLGcEZs+Si{G!!haF} zQPx_W4qQkV>-?Tv#k^KScd8);?@7fUW-NFkU-`N=IV5i$O7g4^fe0WQ~<4nGA?^Q7Fe^s=e zee2O-A8LUPt+%T_uJ5@1j%a31D$(z|I4fQS+-byzXN`&on1Y<;kWA9MY_&f{+7Bp8%e&_AlpdO zd8<3&t@@XdBiey8r&*1pNiOq$HoxyICe7XVdYHcB_bb^bYCqW{;>F3VW6kz0#^YRQ zl1zVtd85|m9)~9N)0g=F2lXrQw8-L6n78OceC6R=R1WWk5#Zsi)=D+bxp`@-`X=!M z$c$v-JBLUX*pIMq^8J0F4&d}A?!CZ?ck1iSDqE}w&EzxcOAkz!jxafe_Fj`H*!JSz68$-@$1QRq@B~} z11tWoV(cs;P4pVRrw4gH?!9M*xM$wN{~3Y14%|u?znqMhGRBRQE__FTuNZvA7T^2Q zLi@Z6nd{Q}Nu+n87`T1NPnU(M07l_%a7cDG(vO@>Xx-VTr`6Ka0&D<1rAhQiE<}${ zqv9$R2T$~0WJ){kf(<)b%iDLD@EL@y-0ni=XXOi8BX8pWOc|G5o=sk!O*YT7NqOSg z{9hn>-jA%PUSs)Ty)N|XML(NYuYs~qclQHZ^k#UTR4-)g74?!#t6rM-sGL??PN`SU zDM{tjQjYpz4Y0%1ZRZO9*N3p$ZWD5%J&c%bmje2M%J^#Rf&|XAT@1J{{dE7Xuf{Gm z@}l>_#2Y7z=@x$g&U)~Vf?w%QUbMHN_R(9De!j6Y!upRcqyFUk3Hh+N=fMnGm;qqY zf%$=l+u3RdrET2N#YuT(LIZ0 zz*O*EVPRf^My**t1|KSkue9;c65l79;n7#Xp?c_S@~C(dp|0eqMsBOfqqI$=ZL)Q& zB5e=&^nZfY#LKTyI{B)>ue-L=MOn1-f2%K|kEO>Ok^O9NoPYM39a+?|pL4N&z-#>b z5as3&&X#=!?E9ck-x2%GTA2CP`w=Ozv>k$Zmlv05<0>h`;nBQ8@B6Ru;%X_&;jJUA zx@pf(zV-m4&`}%3wxewArKv7K^vOhB#Ko-(<6=)T&ZNaZMB7)t0r(FC{~=`K!}M3J z1!GcVNgs*^m8o%6bSSJh;~D#|H*3~}d9yX^z3@5;?}A7;w>uX8)&iRu*|6iiZzBy{ z;`^V77p&Shux%h+UryV7i?$OFq$9+869)gCoPj!@`l}slNY~sI5el^K|r}y^lP=-QpqcZC?)Fc+!1S7+L?&cgi}IvUBRJJ7N_|G>A?QEpz&xNF+h#lMUBy7puD&>m^S zy%VOKW#LD_yZ*K-Db{SJ3tNc=~~O=KgFDYVb&M+K~@FHSVU zBlT<`7~c0-T%Dw5Hf4z^LMm~kyUuLje8Pmr~gnKbP3OkUV6VxUuWArUQ5p7cAZf79$U%xKKS9<)0gvR;Kav4;HrVE0#0)Nhplt6HkkOgf{FfSUpi6k(=B1C=^q-L~+)mEX zt&>CicbhMR$kMEy?LyZ%S=!3nS3DR7R~2iji{U{RZ8E@m)c|p&#C@K6DNZ_D^X}`@ z=5T3_skXMHR5pE}(xrx0$+?NSAT=CHctqD}}eGA{6 z_;=$E zb%C*!WD~Vf#(+H+)DG9Hd@+cI~zI~%Qt%ZYYTLxfxBPjr4}Tf z!G02)zaDCA8ZS&unrt~Cu1YKpoWCQb`3(&=qs;QHsRdj8&K! zP`=>{`8p{7t6upPcHhP4i{ey%pXJL%3-G1F^5vbc#h09dL^Qo1@tFnqQc@D~r4*P# zU_QA3T|QqNU0+y$FBPm`&rmSz)81kuRFOobay* zU&9+sPQKKBW0uZw=-dXK=R&8(rzyVGw>n>#8_!)kRcIX#8IcXAz8=n(|9A6-Mz62e zy-Hs%#^zk8uXi3(kmx)Nw+Olq(APz$-`59E4*U8L`6|eFkZmLN^%~pP$G|JS*MTh5 z!w-csy}4BfdDp`CUfXUyPgGX2eg7BWoPEE?=DpG8O{E{TamKXblu)KC4@U<6}^{ju5W1;UdDiKWEyCaDxrMnQ)^GtIQ@F)_*)T+puu9c;QwX z-m;SV+OXa|mrOWW$XyYZwKgff(8f0<#pm1jx}^B1jTbNTnZLMoyy%LX5*f(GHEJ>8x(x%7EJlTuRV8dC+=v zD!S*Xt3!WrOQ>U&U-guJY%E!y_->L8+-~v3!Pf!47VxPL?dDs3=*Q2`??b|q+=sG| zP4QgoDy~j%bng9euMgE9pbx2@7w66IL;l>`jGv-BzSUkEh=+%t#hRPup;mt=E;tPxnG~nB_WY!{2ysy_?v2QX>iy+) zVY_}r?MwOU=e4$zyrXHx9nl@GHC!}#yf^RHShVx>PlqVQJmVH$0luKIJmnrvrOQw5yHo z_UOuAM*pMiGl^So)9>)oOKtke#J%w2{kuBB(*gaR;HXFMOz+TM;j+Yj z>R3-3Z6jXwet>!-1GDF|+7K|~fG?J=9ALB__xJ*I`FwFU>3>6) zqt)?6Ht8tqdg9BK@MZR#0d3AkO_tuN(HWfGKbn8G;R(`oenDp&ZUC=z^~Dp}$Y|!U{a=sZh6FM` z(Rnm``-k3@C{N?R9T=b4ADI>ojki9o*>WPi3zlKlozVM?pA!|}tN^FpoB1g?XzI3qa zydJr9bSjVDrZ@cX=v2Hr`zJZ_;RSo%A^N`!?g_JQCZ2SxxGRyuIiYvap66*_i#&C# zK^7r%cjvnwP zsCC@mUALn@@~erIIP`h(=`Ox}e!O6F;df5?4T(q`{z1F>Uy5Ps5iq}69qNM7CE*)t zFVPMwT-##zisysC$R5=?r0$(e0fU@{?QdavEKD~r`M^j_Rw5@1)ul~=#=F!;zFxxrLRf2jV}xr7=YBgQD8!ZEDsWd5H;VsR{L1@zp9Z%D zH-Ni|xO^Us_!@pK{;l{O_&xYr@$bTS>$E|CMsP3RW%%F5cjLJ373lm=afEXp(eT`k z-jmRtqR#N>4!ibjFM$W{U5}IF^YyCEl&Or`t|OG@yQ1prqyqSBNI8(*)okK z+wC}`zAal!c{N8?f4-VFEB{gUo- zxza=G`{;zp=t=7noQat>X9#3L06dy2GSDfyn$=AKw50I|At-`H*$jm7;hUdM-J!@8sj(MkPp^E&WRV1mb&`p7gVO& z^?AyR2{--OzxVpB&6_t@@b+yx&YdOJ`%TUCW%plpI*U`!x7KDP(--lr`;qnb`(OB` zPfcpy*U zS6{kC^~)en3S}(E&YJ>8dO$qX8&2uCOk6h3odwtW$tK{;niBPXpY8jK)Bl%t;HwV$ zzj^Vc;o-2)3$NbO_%8Vy@f(0q`erX(@v58di@Um^+p^3XM9>~{`M@Q+=uFC{?$dgk z=Na~Pqv(!jj?j2Tp8~hLm!`e2F4`jVJDf{JDM?2R^S|<=xFX_Q8|nQc*FO4h2i<=Y z9cly7df6ko6C1|qduB*)_RrDEf1zgPd+C#EGyOj*u<{)pD%X{%dp6>WD_7_9TllWE z{AsiNkqvL|ydrPzobbgqO*+TOG5u8ZDX(~oiu zpT#oU$BspJ>OEn>Nq5CmryqtgW$tqWXU=j_wtU&Yg45Z?ZLb4P<&@Izb&ulf@WZ^H zh^!eu!u!tP z!|Q;PouF~99~pDJk{u@f3r}zB_Tzz5nrt}L^#tlFJ5A^QG`5K^4U$#b*Z8!r(sI4_ ztyVg^QEj*%`uy=h<9&$h?Q!v1v`Kfvm9R8cIkpGY@3GE4$v&EAy%G&(ff9&V&&#-+}eh)Ht&YSfHQ~8dy zC81r_g0Ffs<7*z5vaEOVN%&E(eC0PM&awY#Dm`|&WDeah8;2}%er4hh4`~iDSI>6J zX%$||yn;HbEQK#4?DA&XvV`~hl($E6m{i^c2Pp5Ur1Cyac`8Hgrg9|*=m~QcQhi+g z{WQwQ{=V%r+Dma&_)&Q0=+XaIz2mLn{owovIFO5L{~q=+(a!oAXQRTgQ)fnM=$m!) z%~H{={!_>ugQLRx#H!Q%NiNC+-w$;W1(8YXn+TqvTABVph{51>UpE>~k<|O>^&B!}- zv2q{!&L)@c@0Yye(!a}l{Tv+9xEm#0gmdGc>>?*)ZhUj+lPSa8C5@ni#FKshv)|0O z)hBhXRqclkFfjTS%t5k8@ZH$&%5nEu-$~!e#!jf_9d;kCdr{UbO?R^H+nBmI(Q9Pj zhQ#O3W^RWL?Lt1fkdHs{e)2|SNbjbJX6Xv);9FTkkzFD>-ez^1t;bE@BU61h*0H!M_vVtrbWH{5f5+ zyzoY?k(aZQq24v-L)ZfWUi6<+X3m%WG z^SG+X#4;B{y9ngwK|=hpC@ zTIq~4-olwr;6CBWMl4A-zK_nBp}pg@mD(%+#!xo)68Dgmi?RXUa3$P=@7n(D*l9J` zPfln48+uan1UFYZ%<^%ex!N9RP`~%*YMC}~IyRHWgipXvn zuKlAEEiTz7ONi4rsky+`<=oRI&sN&}$ei##bsDfrll=7~11a!GcWQfqSpkoP|5)~^ zWK(pLF1x=|KDhP2pvPk$i5}6exPvI~7|L7X;;^Mt&Itd6s2D$~!aA zpmjeV*6l?UTDqdpl?h$@p#3j{woIE>uzp_EN9z}RqU`@68>6)OrQ}n67KL-DN!e2x zkH<-C@pveHjUZRzh1Q(9NPmvJ)NxX4-^!!=wU;O5>9cuszg@EUgw4}s^GKdQn3PBN z+*OY5xfhfE7eT%(=uL-?*lw)dd_`uRST?|RS98HNtEspM)EJQ?6S6e~`Vf4Qy{WM3a>>)7Jt`RSP;!+Xxt(@qkDj|O>NCo# z|5(b7-k#7LU-Qy8qxUnRLw2n4aevj|R2X~R*t7c2_oehb>DN|xr}bI=uc+*v$S`-f z;^d9rnaGK=mOjkBSdwifJ*&2KdiGe_Qgk?5`nYGbzri{+_*E9HJ78H0;aB?ze_14a zhv5+L`|}&K#te^EV4tcC@yp5i9@?tOwpGpdu+a#+Hqu+R(tUd8J~t}fh4!`VY^^am zInx^{Y9q53h%f!1xt8>T>}}1xbmy`aIsO7;b;$46hOYwtsy6_yHOI?l;pf*=GANr= z_J?m5=?^)WjKP@G`2W}|#s zGxGB|n^SoPZ63w>dE8uuaVAgeC@Iij^fbPBD;>#LF)96bBlTVkZGxLEE6UtAn9Ha7 z*nHfH=k70gREAkYr!JyXVcGOr)7h#ud{0&`z3P*f97r}KW8$0o%}L0EzQwytzTYPL zZMu6)?d#t|XGGN}!+a|57~oVkf6&DLOE8WUK0150OYs4jZbwASL%fArKv!~eg`gfCZFzGJJH{TID?ir$#kzC#LSl}6Zqhfilv zmiXlM+kKvFqR&XS`tg5Y`%#?m4DQE#yLF}H_cV;HCVtV5`VUOmOS;<$(!$~1+^`uuo+uo(&J0(hoS9w*Wi&vGT7m~imOJ~p9tgWOZy{U1( z+6;L_*&Z)z;Z^C0++nqS|8vf6 zwX*8V$kTt`zf1p%;`*V+D0kLRwwKyMwpX?KKe)B_k`nYz9!6hd7BZVp{Z}@Z;5P!V zG3af;7bWQnw-%TG{fywJR!``EDBOE2WyIIwULdUl|3&{`%~oH06>TRKa0kCXQ~_)i=69)so|C#K4NoXb~f^Hq?qlzhJc{2Wr$~;|n<75my`>>P&td);=N|eqqDSSz<2H13E8(l$e+5}xDBK!z z=DpY&Hu9NoJWNxYplcO3H}JiewC#?pJEGl`T}s;|B*h|{%@v*i!A_`0i5b2d{ak+ z<*r-+PWZ$Ny&FDk;qn)N)A>5_N_QmmKkur?(gomjrcXQ+T&so4JOG?{Tu$A^S7(cB zja~eZuDh4>B1Yd6E+W48)Nouc0`ClGTHWAF&cEtBy&uOoV$Re1 zaqc|5i_>}fX4+D3rYv;V!olgTMa0sMeP!N{zKyxMdxJ#dQiam-o1jJGxZWx#qAn*; z7h?xgSGJn6w68g2`6in~W#rg8=4f01N6g~TncpeSw&sJw=qQUrbCz!<;b70q)IZna znBnZJ=-9*A*IsZe28Zk{@&6!X)7h2S0(qiA{i+xo+CR{@c&~4Ti-nVXrO@AK?_No_ z_M+n|(I<_-L`X*`lFnJEV;Dy$w}^5ZB7#A$LhoC-izR$9WJ&jSDkGtNek^H%`5WVe z!!NsKBlz_;OCxYioRiQvR7L!+iLWMJ;g`wN1WuK&d8)#hl%GlYn>h1ZLHX6dO#v4K zy%TS;H0ZyiL_@Wu;UH)b+|$4{fJ60d0-xxW%_%t?ED3o!z&YJ2aDNZnO0O3l%PV(J zN%7*T?kOqk?kOo;M0~xC*SRj;Q&POUr=+mT)IBAI-906Rg;V#GTv+#%6y9?*whCeC zwix}6bFGs#_+~AyT=swa4#n3}N23Rr`%3ng60h@B+9TtA74~5-*fEanr8&tX1Kj<9 z&$TKSe#mard7T=*WvlOGZlJO?2XwM5J+FIFig)*-6jr?MMJe2r1h0Ehig)*-9K7yD zx$qn5tIE@6W%d~4I~(3)Q>Sihv|-*}b9%o7ekd%xUrJbaJxllw&w+*O2l^gc!CiK2 zvRd-@lfMKW{sZ2?Yjc*n1fGb0!l(ZTSNfl9dRnZ)oO2TXQgCTpOzhwP_$Me!_GJsO zPqSuS2cPRyHtG7W9MwT%vviAz$FH^aq25W=Tcw91e|6-4MrEN(oSmjRNsq{uu8SgX zz!yRL!TYf7Y?%)ym8rfhT>s_hwz`S47N!l7X^CTdNk+G0d+7}CAK;5@P2pJeAZtq0 zr{1g2+akz3Y1^2K_-UG_3C^9NxkmlemYr?O&Q@DepH^FD3*~IX#`+Cqs=V7@qyCk^ zYwzyof8)*$Ww|+=`i8!Xmxp&m9M4r&K4nP<>im}G)<2{y!Mbz9qT7A%j=~H6uS9FX zK90Dx3AbYt=7VE1IAl}h+5gIO_TUh9S|N6t*2=~RA4@p>2b{;nRpDxJ^|&V7T3iRN z2iFXo?$B=|{AI#>@o&b@eTX-{@k{aV#c#y_F21>=kKe<0FD_1a6n`iFIR0+j@Bc4z z?*pH8RrmjY_6Ih`)C-+D$IOeII(6!$O`W>(0%k6U{U%|NP))_!tz=92775)ASV@Yx zfQpQ6r&1DOVmBo>lFg(f!=xf5B=2jCB~6&qR#G>{_xU=X^WF}Z!?=HUzu)8L@fhcG zozFS%^FHr?=Y8Ji&xhxaUwiYd@;UXL`Rd2GBa*ziq;34=vk~js|8w)LKP4u*g5L># z50D;5YH@*m_?i9ie;~m(T$9|#h0G1m?<6u8o}2h0^lHUUtwA@|VBc5!v8A7~bC44| z=Uc+>5sY8q{8r=E0DK#OKLhY~Jbvab>MI7IGzIS>WKMSMYskl1c&9Ofv2Xh(>gkS+ z4bO!oFi$zM~K`+lh_ol+e=kD%{-({j6`5#{B-%HK@z?j+v9&Zm>Kp}gJjVpqi4 zxqc_c_WTt)qq`h;Q&#;^ykQb~+QVH*o*x^^=As9Rt7|R!_eVr8E5=Ys*=K)n@coSP z1*=p~JOWqE$?B=MkFqxqixvER)KT5PkRn;V8oMoCxn%>g%iknqPCBEH`%UIqbE9&e zZ{fL))Z|fn8YBza2Yr_K0N#$q^cy)^)ePRvw6A+_9|rG)h7uDii+PlENP zB=UtHH%ay{=8^kG$j9(u#0$xb@)d)5l6Z&grprfmCQzmgA7~GFO@OAM-laJErg=s) z@1Bl7Udt}x8$VM%S0*d znD2Wm&*sA(&9nLcNAdi2=p`My%;O=s)ttogFC{;ewS4M0x%2X)a^=)NKd3J~wET0j z>i-2EX4jWbeM(SYda3%#KPjvJ?qTXnW{(Z(YtE6xNb_J^`h1@-M40e7lCb|DQ8x!|gDk5bNXy^{BP;QLbId@hBDDzkPNSn8MR3HN=B z4Jvmld7{cWaj644zlOhc{B`oTnZIrPW%ygo7`BV_JQf%=o_(2) z=Rb~5$e)d2TB9=mNB)Dx8M7HR^q~;E-FSfSx`laaE;6b6^foh3EoMy9_;eF@n<%|q zX;6N(pVzLW{Bp`iD$je(*>;^jIL5HPHv#`MzAz zk87;6Z8to%j+5kD!xo>7JkI;EWxH7?_zg0lvZ^asudW&DV|Lxm*gEV&_a^GDrtU^x z*PX`zhMk#IJ;jEuID7uik@!|?lE3cC?S!x7+gZxO1Z6l&c_n8lNBT39iG)8F$vUuG zvtTW_i8ga-b2@F-&?e{N9KMylugI^XuYL>vb?Bq(J9R3(FZgZFiETtqqeRBZ*EP?@ zWt6`z$e-f##XtM*=Er|9#8Z8*R&Aso9m>;pR+aaSbwhb}=dk8A#%JZJT)W3lZOF#T zM(N&R$ybsyaVqc5p$6ZA2=fHva~?)`Cz8%ttDN?a${#Vh=(pCR9+u{;31FQVz{+@7qYe{Rp@*e8!ba?75wP~-yGr)=VYjrWWQ2#m zj(Z$LziGhMyj5j1Z(T&0{i9=ks*H`d--I@uzT5`N$>)E~mwPne$BV`<@e|t=?t@5C zhY8J~oG-*SYaF&Va%F?T$$aM;>|vd^k*z#8@Vtri1Ed3_A0$owxFGrv=?u~i{@Qrn zMS44FNoPTH2WcJYBGT!kUm~4F+D^KN^sA(1b8U~ZWcrC;AB>Cn>mT|3l{?F^%{l(t zOq&aazK6B>!ej8MM)%yw+*SS?<3iV@1oz=%EBcd9bWNhKMl0*{51_yDZ|2Op(n+Ud zj~UA=p@n`+J|zcM{yUUcEVPk#l6S^K&Pnmi`kl*z=DD<62%JLJXbM@Q`4sne87#@C z!TRc9!rD~H{Fyb14Xn#tAHb5{7_189<8bh516CWbT7cCUz>+>2td_&XD-EnPuquG{ zPGHs4#QU_>SFMG7nb~2aqf34WbXPKYn@m}+k*?ufn{S&a=@;jQzZ}#|< zGbd|djYM-2mDSwh8sC>x(3em7zD(!6#S+yQ&Aa4Vt1n?a_2nw+WallUFOz>2<1rUl zT3@LoolD#Dk>2myE)ClLfN#5r_e}vED|xQ+ZHM*L_IcFF&ifbIo=cs!Q)6FEw9}YA zJ3%}-8T)24d>n5E>m!15hsRYkpGtY@f#&IgGnG2EzTA5$r@R!fQ$FuKPR%d$mUlt25Ib5>^E~ha7oAEd!~3V9aM~PDX`?L<$`M;{WCba z>svCSx+Wv~9*7_J$^;`n=)2@YJY*fo!Pl5*INCe;!K$ZqJ>lZ;pqya-j&-+c;l*>U z?}Nt0+0bYy{*8@|<@7^+D`lRqdsEo9=VG50C$I4^@XNRBIj6|QkGAJr%zhc#RG+o3 zTFdhUFDq%DMW;Ga&ChBDgE`o`3V4|4=uXqOyP=KpH4jtWG|$A@m~+N={VQMf>%dp- zY7Jt~fdlJR4*Tk2N!FgmnkIZi^d{ZPj*PU!JNv;tMb0gd*$o?Y4Y8=Up~*5dAYK0)am}2Qq{*+$QJ5Z z^2fQ_L0E#Lx^sNqBFd@lg``HCMTek`Vja4hb(V*FBke6bFQ$#wS4vs;*gPtNena@; z6FU8s9O#Ug$-UNBa-pBrS2Flsk5kWZJ5-%2Uq@>!tEuw{b&|CCE%3UWHpOevtIoHt z`@fmjltt8=Y5gNMFcW$Dm+!^40pF`@W7Y<+XFuN z#P~d%+v`o86maee-=C%Zt`i3Jd!hFW+wcqb;e)k-Gv6cW6%O^nf%esw^h^6Xl|LcK zcXOH~`s3l$0Y|>%Ex;KK9Pv!FZ4PJ~?kR7h?8hi;bMtzYhb~35Wp$+k`@DZSfPbky zIM+}|?@h+_Y`S^o-wzaJkIBbDg9K%49FL-o?)Nh0?D|Viv;tpm6~3P4+uL}4i08s5 z3Zh3y8%fua=C93-X8%WS^k?Sdi^yBb-wOVgvG02|Df{4}YKod z0fck^i{Oj)&P#5!cV2#eLbSzhsK0BTAtne7ijki`=dmB3XVIGbmfSZv3du*FdJD9lF|w3{Zz#S9 zN7XBoY>}FupZFrWO^EMJl7e4uw9Ll1I#@tpfRSScaMMGKspOse`?SV$^U-= zPqcP>=hH*qms>}9^>1w{do!8Sl+i!!S3KbHpNoveV{5!#V$W_r2l6Ak*Mn?m?MLsF zuRV^^Z~0JKV=N(GW1ybZhjRKU`P8%ccr`wwWL9`Sgggq4^7~kWiBg&FO3GgE&#r4; zrLm5_bZedMwX%hLut&y~u-%6#&?c8syeL#~U?qSTqSD>Eo zRiC@yRhqK$(M2oyx6^|DTDjvrPp9yHit5uZWT>cGb$~0sJ*+3#ufg~1ywe$>qq)U!{XdC26VxVt<0I(#eNUbjuYKgorem*kPf*Qq$2b}}+JaMFsx{;E zIl6V{Nqk$1d!|_PZJL||O%mJ-l$V&-**BQOy)DIuJt+~eB=v)L{TzlkoIOR8DBkjM<&eQ(eR^F}j?Y5yuE5�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%0Aq@VDg6I^mxo)bW=iJ@ZB+R$JPhT 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%0Af~WJ0z8or_#F3f@GxRNmy5| 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

9Zt;jBQ6&_KM-+)F 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≤DmC(i1(mo!aB?A|1|v-*uCP3Q=B9V(1j#O2*d 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

L{%*z{0bRNF%u3FCZdh=6gO1y&nmPlM!w zz54*G*_Y8p7x4YFcxd1QfIX4{7*Xsqx1{bB?d%_sMBM57sR zM`uW)VWBuj_x!Yx;?;!x-Q0K0aW>gVb#PpW1{g6K{8E8qD#oDGGKW;;9wE)FSDaon zhwX$wid4G6b7cTm-kP()6bcQ_fyQzv7{{5V9Fsn{n=Te1a2sbjX(~<=OjZ&;b%buu z7-NjlPTU8e=PkLmgc&h96s(9c-V77+H!*^g4(Hz;h~_AM>}jF1V-6)xuEkqID3O#L zNy(&I1*8sPLjEIfpT&ngsA-9h@BtE#O1=?x1=ITUlwDqYGQ7_cl-d|0s=5tc6C4+xsecDuP+6SSPHYAFxy^bu)(YyVSm z&Hy$!i7@PI*6cRIP__6~b!<>5wWG(SAS(kxX#m2&7rko1g2{1KL}sY69>6LIK9bb2 za8s^GA|vk7$I^ATrllMN)&M@;Z5joM@zL7}QJM?M<3JkO z)`Ld!KB?yx=!uaPso19VpEchZr1o0xh+P(h3o-ulFf9griN$Y0{iL;Kpm@ z6GjSYopqYS2OZCWgMWX~ZM_cc)l7U8o(T*F0~R>J$|6qyn?p3p3}PcB*vtzr;6dTF z3}YORB(7GpBju$(J1g_ugQnxoujEMvNZ1Q9&@F5x25IgFwhRb!T5Tv#J$LCeLj6tq zaCi5)hZUv-<4iqMW{KwCY|a(i59W#MA3?@dlA%0&tQ1Zr=PVE#@^U*Ccm$9nedT?~ zoKAi-*I$E0jSF~rkV!}WndB$wAln43$*Me{QRnPLY%E%0CN_g|RcfU%B`xxdAhl34 z8t7w+JMEGv)sOM`M;D_3iMcU>(2IIJ)#B&yr}SRyZwpCO28g{oXCn=U{?z(?AdqB~W!&u)E8RI~h_5 zp|4Du^RN6EU%KrOQnpUdFTQO({qeUif92V)Jo)xVcVuWz_CK0jEq+ZARQ<3y{Mv`> z4gcCNcRxa@QeM!n-K-A$GTGkQ|4wgzjemZ4@gM8Gn{R*AXQb=Z`bY9opyX;x;^x&t zBze^ra;T}o47EXrBg?L8!+x?Pv)Z^-@Ym&NYAELr7Vd@FiQXdt1S*MdaZ4AsenY_Vl+)>Ed7(~eyC zmdkN+l15QQi3ge`;RpTxS)=%1S$2{4Wv6xVlB^=bv_bT9Bu{jxda|-5`w

F*<3+ zjyN(44B2W25wH$Nhys40dI#fxG6pbyok1X{;TrNCq(~90%Zw%o zA0qikuy(68Xmn3bNM7ytx}z4fwZFaP>1=VMV98;-X;|f!cbagAp)XEa7waQs29;8n}K5Q;mnsS)ZD~?9xd2ZL4j9+ z++K%TQ}AquKMJP=pDJ#y6autj_$5a*i0}iIfrj3T$WM9I{gVbC0v;@g+Q6*wOpunn zx8;=%ajjr<)}#=aQwv#Q!G(8CG2(o){7flKEabgCTsf{cmYkJ*%ABmBOaOpc(#T5^ z0F>PwB4+tR+qbgoh85P}wA#HVaSU@*NqTTNRd;n~ zBTJus3=+yL4@(k)7Sru+oA9kSI2(yiKnZ&bY|pVqi|G}JazlcHw9U!pX%X}C!fb|M zoOTW&ENFZXgP1?nJa2W*ir`1^RBcn$0#w+hb6urbl>bWW>JGY22;p=n4@dI>NAP;E z2_b@6q_ODER^(iS%ui*wlB>nMCh?B4c zVLYS;x-%TbuPZAzS~VY?{s(T>Fz1qRGYk%YL0{m&;!*FifppUDe_F&)lra~xI^?|C z>@jCKVU?WQ6UA2I03q&e+apEuSUwtT_`rvJ*X>^P4{V5!;R;ejDY9W_9=Q8Cj97MD zS~W3VXgq~OGSegSsb2G~9jXiy89-D9BNUv4o(`LYKIOO<&LSlPd)VOeCbE%nyH=l= z_6Byca-Q4A-_#%IfB8O~q7ZSZ+GGlr%>`#&0UCp5cSt>ER|zjC!wU*QU6wp)><(AE z1*DHI>HLP+%zPO5Lem6|QDXdQV8pp0jw7nVc>|k~lDfBZEv&E^5gbW?;DiS&D@kL) z!4ridwjGFV+#o0byP@S(wP2m3BKJMG1ZAf1YK`MXO1nNNcyBzQMq6%qSrqw|;%L$nU9le3&DvJ=GR zehsZAxsElBiW@ZA=&}<+nnXA|>s_3UyUr_Ep;BQJNd^r;Lc+CwDFmL#m(?sEkYe>jq zJE1gg(3V`E+2=~xXihf3eY%IhDO4M8DzmWA@J6Up#Jfrf{=v%t^HSc9GLZnv7I zWA@GXPNTjSi1O}WxN$Kcdj46G6xDh~oLvq;yr!;c6Lihi=lge4!dx?fTBIGL31Ui= zV#$~*x?ps*K{Jc+dcbl6m`e@pjok!Ck+=Z6(rUC}0~>-ke87;Beo6&U?9u>5p`J8T z%aKm3IPj-BRxa*y-{auHPPpML8+T^3j6(8U6hjoH+Tli_DM4$wq0aOqZuXy)9i?P6 zR0xwEddbW2DrF{6Kb8TA6*qW2pU=$kWqM4A3!uIj8{53!TOy`TJ$`_Op|&JgETb_= zq&-(WO_E`)2#i`MCOjTRafc{ffMOSyrpvs@S30Ky(;Fx_aEa~NgBmTWtA;U_c-mM+ zY{Y{ob1J?2NhW>sjwf%OW{q3vZ3ApxpiSnHNj(w#(i2$JlqUF7i6gK zn&Il8BI~R5>gQw%1Pvxfo>IXq=_4IHC!r`|V{?5cCp~T-%uEgyIb(x=c_m7TzcW8- zFiQMH22BfCnETcU5M*E^TKI5+L0d9@XtkdHM(lPF%e2z!Y8ag?XXhQtUy&&W5Y%KS z*f5!@Cj?wvA522X6Pd;})js{@%6l*Y<1m;)!5A|T2pvdQu%v|ap$v=rX1$a@f=i^s zMSOL#fGY%wmda_8CR%5WTbSLhp^6J44QLSN1+>hq#^xex3e{>imBbhaB1a_lOW@eB zRfKukoLL)69>I?Zk4#=L+zjHYa(M^+khM&ZCjvnL7t$g%-jlTCjWnqC6lkq`A+Joaj5>2P`9Jh|F%2 z-YKm#iU}hrgiuLUz$O?}x&9o~2#g(z$S{~2o#MpCd6WWTJ~Cu_%-KGyP!%9Ls|-l^ zP_oZl)e-*Cctx{YZL&yNP8GP*!8lHskz-e4wah1p%t0TH+c|UB1dfRZX896%MAp`P z1*M^8SDY82`n4P{S@@1uDB(R)kG}f4Vc>BYDufl() z@~0lG6@=A(S(A|HYOVXagT_7$wy7~D*CSlTW|PaiFN#BzBn83?KBS~d zl(-Q=))`4-#F_Clb-9!eYbeFCGeXM6WXE_pm9k$dHKXk%K6t6H61ivB$`C zj4_gd7C1#|!+g5K9+aZs?xm_M!+}O85$eq(8>xWoyzR6+yYu3LnzK@&z*!Ui;1q1X zB|ey)5H;qe6ZhZ_26(cwjG5(74k<8+{;Y~52NqJKkqn;kLFeuY?85pNh4$K&^?q^>< zKW!Nsf=b8_rhT~5Zl${-U<04nqp{jXG_ddqypJA5cxoc5<4$KhcqL95!QIh~G1_`)~@w zDo2*dfi(AqE_RM`&iVHqosa+4%z6PK`yy>>)B;{&tY`iH;213hzP4CJ6WC(ZltCK< zGCvj=hzmK`>o#3P8~%{LC;SPw4!-|Wst4I)fJt3#NQy#SrM+PM`JNwNVNx<;)MH19 zI8Hwzo^#CN;Ae29RTeVdQW+XuPq*cf~L2 zwv};NkbH2K0dyeqV#nVk#ESJ%mPTT3DDRc8(gRFEJ1L6DOmt|bX2mUaTR0m~7}2?d zl@WF#HfthY4%yPaH${a}q!I@;B{H~@YSOtTE@%wIEFjC%qyrgVGyCTfy-N&onQ3kw5X-2r5NBiXx;1)f9Y|Cc zNW;IAKC)do$;R91I{CEdAOEz|8tdm^`H~YW%kj9nw9~CKrvloanR$uPOCz_Bu$)kshkbJrpj` z)P@GE0{XGq>5VC)jDTCg_ugw|I85#gLMcv@J5rpyu@hYp57o#lks@Oo_~M+#uESCD ztQ=hlbbP^n_!okeGbBEIgi}Xd7ME)6DbpCe#FCl&ZDESJPSQG)JlPI%Sz0RTpo%r!jUO}F`7`BA$&1W`$JFMmSy z!W$-5V3TytNnxT(WT!2_+xfPJXX$(`&(ykn*`wx|1312*gpZdY3HARWmv(iS;$!oQ zVL8$9wAtWF0zKvEdASfaPVq`9Q=wT5NV&H&xAQI$)R$pY6hxrHmc|ca=Y;Tw-0d#p zQTslbdk@jyIPZ+}0?M+@ z`u3^HvE8sklXi6bY+Nb7jriJj<=|5~prf%(5Y>v(N7j(M4m>oZW(M|4L%$i?<=uNV zX*3iup0T7#_Taa#hgV?}nGE?i&9a!f0N_SNSyG-{<9K?>I-x`yHQKCWO z8>&ejD;5H*?u{lrYX_p42%mmPLnb^C8oYRJoN>RIUDx=Oax61mMd<*i)*u^?&~Vs! zfP?8YM~*eh>P#%fh#aJW{6tSOn*&>6YG{IXx%!WkN4$~y_3h3Fx$KU*6i<~KMo`Z8 zCuFa}od<;6a@V(0tW3cyHMw6%V=GR3UrOV4LtHtU8>CD1(;1Q`sMC z?KpdW@0afq>)Lk@f=d$ja82~EBl!lPTTwxpFqOu>DnH5Nk{}Tg1UOj23U#r zLx)kAfh3|);HJ`K=yBlr*RRRB<70|UKN|*;FQAh2AVrl~;3>0|zq#v8rV5pMq_3#A zhcptMkAW_v!C?pJ43W6O&FP!eL2+%eR)sZ(dH!`@jsGKbaz{oHMaV=Rb|X zamN^y1IC4-#me@0zN2iq7*mYz8%rz%2Gneo%V~E{l`PEiobPZ#?9xp+)4{@C;{mRI zZo`*AgGMP>;`<#gU&}-RFXQqdRIzjxdz4lyybY45nzk3%yjWcG;;n|f&mzd6KG75L zDyw!xL$tGn1JQ<2j8`3D}{ zl1qhZ;vLhgTuDYI94|6+WADafX2!vtGlA%u*cpzKszcf~ny5+dpnX(ykKjGi(rjJE znVvSTz)nd#*;wQz49Jxoci!50(gYVyEUDISc4%Iw;Y_T-rCUZ_7C;Zg#gCW9Gev$| zw%ZuF?pA&WM?L7N9x^TMInjLGId0p#bQQ`e%tPhJ7sQD=y~csWFqQveAUYKw|GR@k z_?cp*Lus^MndZI|APh9ey+KK8PA$SKIGJ|`g?kO@ z)B1Mr94moo2QLJ@wOP`YiYC>B#Qf>~-Sc)Euh#ytyD$H&UL>qaSwng>w>FInKMK%I7 z7t;Z4mLM<6gElQsE&A>8`2`KD*`|(ly~50jfL}pMRhnZIPgNYYF zO756JLM?BCeu%YgYQp4dN9hZecec{O@$2?U$-z*0G?45OvpgHkOVpKir&V;@_?+o2 z#t(h$9yJD}?8m#E>&HR&ZE@108HFJb#^BsD`DGh0!zM#_cf&BB}Dx4Mwao3d{BI-iYq#tXL=3K0d3gpeg7U-Cnck=>J;P^}V_{Ibm=M z@6s%hYBe<_uysKm*1IpbjH0MaPOp5MZ;!j@I;Blxbwyn+(JJ{ZP)e#xX_D5Nf7ZKf z*|{W~(Z}zY1xjB;+Nfahf5VaY*am&%Vu-X}8iumQMflE?4{Ft=?s2 ztWj$)bX~a8`_!%Xd{W`qg+Ziwd2HS@9piq1Ru!TOF4p+wM4k#K!&8b4rT6Llji<$2 zxJGX?zv--|Ed^Ogl$w07CKmE6uBT$$`t{j8lyn{wOd=S_5*5*l!Ys3s-LIGH?Fg{q zdyqY&wh@&!tNEVB;~w2c@%AYZ1>*Qn0nJa*LcHgnIitH`N>fW7I<;ct9K~=1|&})qwKhOZy5)LGb6ik2sLi7mmwcul5PP=r?*-$B)B(tMuIxcdM3Qg;Sqh{h5wyPY?k zp(|exs6fu3GKmnKX=k*d8B_F!a-6T61m}0Q&1_-k$N3vcQ>yX4-rzY7Y637b7yF5; zUJhPV$%7vR>ykEnrz7r3$Y3}ebe|Vhh`0KEsghACsYuuXD3sXYsEg=8qGP(nU>njw zzQSw*ZA9oIEhAJ8hlsaZk2=nL%&JOpI_ed@ksUG|R*h}coXLf0HVG7c^gLVhS^u;c zeLt-7AauLe#xbbT(UXb_arC^VLJUr;oaAy~tU2PIsaa-VOByOqDO-*)voUpPYE#ee z>5y6{uM8jKDDjk!Od2>@gq2C(31Xy;^Xd{8N~@+3k1oaDv(dZf$dK2uL~ld3vDyp- zJtBV-zBI&bfy2`IF3%E|C{eO9`1kEzb+(`?8Ga6Ys*&|qQ^8L@8DF*T`iWl3Q+}#TGJ9P$>AJ6AWzU-6Br$npT7+f+D$xz<@3Q$YJ#;rYJ)+?cI{B(pI;6%*zs6ImG`8MvFG4$9@CI{J@QE~abF`5W3 zsWabw=WHte*1Bj{nbApih|@C;NTGx`nX(>P(6#K(*bq^wthoXZ#wItl>o~CMPGPcW zr3{*9oiQ0ZTIYk%32@2x(Y{2oH7f^y@$T{+tjdOo&V|e4kh0;6lBZVDa5Xemt)mQ0 z@Aegd&YIN>HfR&~v`QLOI16eZQ^wsfwywN~l_?#$FR^r!Z`uOX`Re^08N=<@&siZM z@#vAyUu(7Bo(0p+ls-?JNWpq7Y&n>R33?O9Nva)VI+ZOBiBpMp#dm9nYCg=U>lZSqM@xU0PH<+>J z=NQ8FN@WvExby>&l)ATXBO>~VBMS4qlxQ%lIj{!Kpdv&EPl8Qb3Jd@CU9;DGB~s1E z-VA+fi+4k5K28*0q{V0Q**g|seTv|fU&}pWs&IiYNU+dClSUl!66DUcZnX+P5rC)7 z5tp4PoSBbnntt)_sHUh%P3TuZAJMAL@`ugh=;cfFZWX{Og-2JIVL&N7smtB&8LwJu z(CXwU!Qw=SSPF@ncVtiFi~^X7L&xe-oUgryQ?l|y0MaufEckVSafJqa=HWIokbCr6@O1Hlb8 zqF+kBfYzv0*ZohzfDU_-mp!#H}hLp8a&tp$85}r4~;i?wC(`*$O5?luRK) zT2OsfwB7GHGo-0u z_8ccyFt$JL{Cwp-c6NkCv6#X4C(E0))3jq?#w$mIS16-gWy#k;=`$K29Hs`<_{j57 z2b3Sz^z6|X1CNUca7zTgnthMEHE`^x8W4$50+-hf<%6E!T&>K~d23vuCi-b26#Om; zcwO2BaM!8fh@JUrpv;t@nGH)H7NnGfa>tzqwDfGQDg)u4x z7w7nYTiuf`9YgbBUC`jPr{b56wr`sax{cDBC|?xl9~1=RHX4#7wwcje~N}tDLM#sFUE1BK)2}}}?E=taVmM}7i0B>x^H$X-X2BYdTzjkgNlJkx*ca^_c(dXUCD{KX%Cp z=pWtx$tF)j*;~)=zx7?^r}p_yVzrekuP7o=`9jNO} z$s=P~073OumT?Zo_Nd!J4G1BQgdkWr&tDqziSuPqkMtJKBCjscIaoBnylI*sgCz3K zNIY&eNEr*i@n*)??@K@pm47FF{0Tskl0lxpO1*P%mQHCOidKa!6rJkfz}%Ks-;2!W z3!6{2Uj-PiIyI)=8<|E{+VBkOXJh;DM260xuH?f2s$qe1cn z0~GGf#%aoPufLM8Sqi+%fG00annmcK0;0s5ouymdV@Rd48A`3u zOKUElpEg@WG(%9zx@hM6@8Uj1u|%V+j5O*J)6U-;w`*w${lSI&e6kiQwn6-(<{)+j zy!InoE@PRwiid~do{s`4TvQ$04iZPI2&VSi%jQvWc~(W%%cG}Ha%%zoy~@<_$da6q zZ}?Ta2MV2X>sWB6Ri3t+&9kcF(!@{W9YV~us9>afF_YZY8Rxq#c}j5Jn!hJX==hs) znj~;~T~!GsP~y%RLrlc8OS-_glwH$ z{rT=$;S#O_Mx=BVbtBA!IF7UHXX!_^w=dgVppZprG18GUp<#ANq)=Bs*Dpds813Aw z3JKdPp^oexM;~A)q)VAEAzGSk<$F?5lp$`JDLK9zd&<`F&{MVqkO$2o4<)#9u7q3v)hywOr}LOWvPwfHL@@Wz%gxnY}BC`&4)fSY9gs5vH2Z*||#8zG-a2ROX8CvyM2(tsgmt_BXT4*&h%)ymsy8L=KXhlG1lG#u+o0ScF_O-+*jvA30OoeF9vFC@EMOG1ZF3;iRojO&T=Ekt zs~L~)doqbs2P#e{>&g)hjJHiYw+FP(UeiTWI>9f*|Fa_-y(S;qHJU0m93#~3g+vU@ z_qh%Ud@R_(UqVnJ&oe@vfup#6YJnWJ6wA+}_f{um$C<_8EfFDYAL{P5;Bfa|>sE$p zo&D5Rx;&a+)f6$co|)?p?8&CH?MHk<23RD2u=q4zhMXnVmjNy@xGcrSy(fDrJEX+m z>93;Q@kw>9exifXTW2_dx8cLY7(s|d5mMQoM=f?A7W!Z>6s0`?JPthDc>JUbq?fXH4#8}Qz2<`7^B6wCA#dzZ};rA-deBV|}jeI?R#G_-7gq|^9dQ#nI{tZDbyKaC` zt~E}#MBXHe(khcez?P)2aFk1H!=xwyR&6Dts3IWmC7hW0g}XLnj&jLg1O4dodxLVq27VAwv{a>JSyECa{2c$UxJ0+a+S&n;iEuua z8`yKRQkrXZFWfUqAT=I(nw}JkCb`tmuw1ZrSCx7=C0_**>O8V>7cQx$;aHDxF<)C^JmxHQjm-MsB;av{g6(0lImxG@e#e|?Nm!(u*A6{Z% zg~+pqErZ35Lq)Y$As5X4({PnKrsNX8+E>OR-@fWmXi_0!Od&sM9jTJka$X@;7}eV^ zsLQSmxVpTfIRGvCSxC5wN^TzWTE&EseDLArk?C zykSF#4t*(EwXy{&x%79MM7r4+li8ZS5i+Q}JS2&t@>U;;Tu8iIpI=S@aKDLIXVpuwQ~K!m}} z$k}0HC}dT&g3tPXVj@F4i3wu~K+%~%^Qci*vGK~Q1IgeK5kYo?j>r4@7~= zZ<<~p6n7CAGgUNI+UT9Bvm&Ksl-DZSmt#%4jej>k5UywGoVAs3Oz)J@h)lr{Ml|Q6 zXeB0TmZp7W#NKFBl>z)v6`6uR89hSG#6Uedb3Z5(Z3&YcLrarl2{{J7I(qS}=p9=z zqCE(y7pA>%oIhMG58KKLT`)IKs&U`p;Ad5R{`F~1wP~N|g~4`h6`Vn+nD56>qNrsw z;)kJ`FzirSlei7JD^|ITDYApL5;3+G2Ji(2XK9XmqgNqGGak9gXEGN&xxtc~qPsDD z2hAOYGmM#~>6wv&1rI2SlZHVJJ;_;_oN>0V?qV6eKy^pYjS-}gdV|`jJ41iKx}k>t zfI+(__u4!b#DOXmSZn{8EnOFXx@D^+U_ouFA>&C81l-?Kz(WE%aN!hvhkZZ8|HWJ&Rj(Z_8ma|e!* zmC+&f9-*aFF_WQ}=_U#k|DdX&Ai>*nzo1Jie9-IT#~u6T9*O3mfOnF$CVapi%X$?eWqHN*K*-6- z8!jr~R-LgeCEDI-02gkB8wFK1X?0tQ_UecufGZvg^VHR=UFdC-`N& z)+Y3VvVf?yl69EtSfVzHYr?FE__-ak>-`bB?^^wq`${4}l-AWBy>1rPIKSC$o4P=5 z)by~&RAh^huX%t#lw{X{DaXkZo1(kI6reP@fnVO9N*Pw++!Habe6F<|pP}TxtNctwspDgQ z@~r_^h^*FV?NHtfk*{2a{DqRZ`>!c@DUXmQhcS&^cVF2NrZsU}UXzQco%kc*e5l^I zX@1A_ZhKHA8!eyM)tjILsErEFAg`LpS9Xb;zp_;2TpAnn&hg$ErX(6ESe5rjujr53 zIgi}OLoKlF9i4=Ly6QShSs7)sRP$IHraOf56_v2UT{`O2l9JmL#Jnr2OIFwqU1$!; z%#Ru$*W33KyV?{qA!*lVcOhAGPABN4sNQ#;svqkIEN&zTCZ?K4dh-Rp`o4JO+H7z|td2=Hh;ffz{x8zpf7oAwX_Ke!y*W zS$@2wQ=YZo{)%-+G%>U3FD$+$6DI{jx=Q!lu5<4KZiftfNig*ttx zu!8k0!1B~8)mb%pHmhm`oF|x+W#VOm3L{rmOxFQbMNTa&NgoL-Md6HQTw|7>&I!5! z*BDs&gXhMSd5K;FCS7xq-6J1`r|mz=isw(B_`c!~?!fV6+m;pFoW*(@{5P>-vn#d6 zeo(_z=V@nMg8A}3H<5MyTsaTYgRF5xl`UNMxZ5t@qGFlWHzHi;MXH-AonBv(zu6l+ zV3Zw6Mp?op=x!JF*1d*eCTDL?+~A>#l?%EodIGpB4b!q%uWPxuQRvsmVxs^&9zXvv zC6|@ukKc69FFi@7s`B5{Ueve-W7Jj3Ws<;A=3TraGxFW@r&XJT9vlK!TtTfKPKu5+ z(~V*URXFzv?Qpr-)V&t2Uk1tHtN8N@ML{be zCBds0vP2K>PT`}wj#z4o{>)7!-q6{k;@zVsFHOS4MVlHGoNDp6(i{CUU%r&TH?P!q z*%)sf>Gtc*!CM>Px35HPS+N~klon=B#=s!}5s*{r#>kT-AgLUGnjk}qSjgw~@ z`snFVuWWviGc3G#cEX9OdT&TjDiPhYI3xaaJFOQ_fAZo*abzLF2Yx0=??8*|oh~x8 zdk^!;T*lS}?LP*U%tp)BF?q^r=_ zD3mu0@!Jy5F!S4aR28Mp-G^2C@whAyjQx$$BqLQ3y67=dnJl>%OZ=TYuf)o-B|_&C zlVDtahiKh%7pO|11{4g>b2Ta&SazZ@S{uE}Xn)oj;$5#-JxSJ^YJEJVu7|rtGFyXV zvLv+x6w7e4{&H#T9Nh%73yVExh$^?*cWN&p?}8g!)NIVr-YTd%l;Z7p;U)aa^&vq? zDcaUBq`E(`g;D?F+?2@sE`;bZtFMkc&9juFoYdyvQEkvH`y) zijhfaWe>4Boe{2AxGEI^G^E7wnFZ@D4}PrH6L>{iT&aM;%%w^@(8KB}yx`D6D4e~O z6_f~*gz+QMLllj|665lPj!x@-KJ6pvz9(n>CS5mFSD;HC=y+fGm_|h(w_HjD?nZMh zoxZZJC9}l~J(%wbSXjFTiulZ-d+MBLLRiLaWg=zOSU@g*;bM4rdU|{>>NUDJA2x#x z5tR#N4SV%TW-$yqXjxLg%Ce`Fr;v8>oMdPWlR+8 zC9kCi*fRmAlxFm!iUKVo{}2OCHF)I+tv33};8nNQOY9B(Ip6gAWHq0+tE$|Sr$<%0 zkgW3)OlW$fbVp{_CM+6u)Y0fvc|n5EgXP(;&U?>Os!tSncn#nr<92cU;>Y#khb+-j z!qd3FD)w?rFaXMTL=StfA&w``6Ln+a5%9k6EhSiv$Mf{U_feWk9%u8jPxu!7Z>Er4 zsRPLTn#v;Yq-5$yz9vf`Fob9TipiE|Q9SCkgevD9q71!04HX_%mGUv)zWdoz+vdk@ zY^PN#5DH$$5cLwynQw_!z0;ty+k6v}u`a!MOWuQSlDBwe|Tz` zsFhkfV4At83GsXJf374|{7YO+p$Lm;B}6QPFJa1)yjsdMEUJ#w4xk^q0mQY+g1iq? zZ}}M~14dPF^oPe?DjyCTH7JcGe4&HjduScIGy9{EJhK>d_iw7VO%UQ zj7ZZ!Z`>q5Rb|`qmOXXwI;ti0J&vyK8c*vT!FrcgsgOgZbUcr|j3I;1I~u!$lG`{e z>c1Ix3D*97dt9$as2Z@NafQDsnb9a%@-L8#X{#~wf)0H=$EE0MW}QJ*xb72SaZrq^ zi9PPx$N}s!zJ^;R2)?(0)80nVWN1wrDTef0Zl=ir*rurv*LGFF7HYEZvoG<@%Ga%* zJk#F~$hB8gKPnS1r4H0*P})_9GzJd?U?9(e;C|*VF}7%6RYT{z9a5xSe210gn||OR z%K4iYEk-;Lcd7SW7+qFfoUUe;O3Jv_Thv^>BW~8{(4FN0L;E3@v>o8XX@x<_h%2P% zTkzMopqA{PG@gMY3#ZmXeiXz3n_x57op`9q!Go7cT$Lu-g`btPedEnkvn0@Di=y=e z1G6r+qKFZ2bs--G9XYm<%UeaD-l+K$o6M{Sj@R=`^htz+*bFi=1{gE_KGU?T@Bb8U z@*vkJ%G9iURAng#1WY8u@N{Af>4(VxrRfI=c&o+k)@|uHN)-I;)nuo2sS~q%fbXUa zNiX1o;2F}@#QAnhp8hf>!@AeelO%FjhBjzv?cCATDV|l6F`EQZ@L4P^Vk*M(v|Dy-kY)#*? zZs%s`>HOq7$fLI6&&$lDJhY!GFh|6*?)Fsp4@UZqrrD(<)k^3xKJ%EdE0}LZ7Ap<9D&A6S0}@| zs&7l54H|ZvLxXz1``#KSD`M#0Pbke-{w}{zxv=tglf&u#9tmy)Bx{kY_-l2H_-l29 zlEA4&Y}Q+WgOfU#k4-f#;OIwl8iMGXtG4Szs{x0wdv&4 z8rdo*Sw!X(f-)4s784JZ)glQ`m#Wvs^!k^nAKwE+cG*{($3Az-1SPwuq4eE{CtqMM zid}gbe|cyWNmTPdbXG^M4rk;i4d6j$r+l+Fqo^~=GI=nT$t75oW69!?G)PTKg*kyM zFH%6?KYy;DqQBR!S2z~JN7ChXm?ot^dMVSg=-pox?-p0 zA!JE`PV>rQ{H&2<9xjPm-}mJHRMfuquqS?3>tB&!${@1eq@+{9k<*g+cCgrN;LV-e<<^H_;j?Ug_SUZ0dFmFg)S@CpDz4?k|Tbd((ka{8Ir!#)l>9ec553wGhq$e~Gb$qTY8RvW* zcq9m$WGp}Q(!QkC=F?HZKkTb-P^Ko+@u{)Udyk$zTR_>+?()zRZ#rmqC!a6Z@;nI&8Uk&Ki6d(!9z-j))q}nktHV; zfBk7&Kr6hiqIHC^DaLmDwywG5k$trj8=9#xs?dX5ny`OaPQI6*i2;)u16{5UGwzup z{S3Ai{<%fQ=!$~?KCTM}yd-y+H!?1+yodk=9edj^J5g*t>^m$*xk5e=OX3<>_3;bo zM{#pZ2-vlJ>Xzt za;H=XO<||l@CdB_w#bSfu;Oo#4_bzhbpqa-+d@5CsBO`ydw!l$x^?7SvXTK&AY_eS z1LA(oLe`8hxmKQrts#a1HC~EN|0xdzwaG?;c7&wiUJAMzI_+tnN>LZK#|GG>l-z=( zvGD@sZ=x|?)GraOC6Q)xZWSY9NXqJDyZqd^=1jmy`ww1;NQmJqZM>N-COja5?e=P8 z2HIjUhv>ME#tAg>7P@6cG!rCN>wN>elP*Cn#^C2V3db`I$Q5!kW4#4g*w%qd8C?N; zh&0abagZ6#YgdqJB&>UR#ZDF3o{IW{@ev^<@a(2|OQ*NT&clpu+#nVAP*yM4#)Z6L zg`x)o&pi#@sicb0wez<%eifl6^c9+G9gXcuq&p`Kj@bk2%}xV@HZ<&XD&dkn>gb#f zF(X*08t-Y3X0lIkFaajnF56tUQr6|t?vCXqssI@;{jgNdocec0Aqk`ZloT?(om0)r z7OM2MQsxjy{51k&#|1f+>lFw)(I|mYnZL-yyc4fBsTLIup_<4FT++CAq|Q1vaw|Mp zIJ!qV4B?mbg+CrbEfWy}S;{P^@vySn^cxj5&lM*0yKk@9naCVA))Xdk$wtdDq63QSo0 z9fFadNE}q1_JO?N5&s|yA+5u%nKSOG`&-3Wqr_GwY!D-4Y*ZhGGkmo^pe4YpN8qK% zETC_21#)fpr|FV*l;n<@ElGHWTW!i4wtOvq|4*Y7xxP18A{X3+#y*j^l>~0!;SM|& zW^%VGeX?WOu!8JdYSX~fwuv7ur!*gDDtXwDeWJ9mQ#N>yhqzk=K5Z}*wSJtupC2~L z1i>CvP*UBecZ(}7X#)T3pew2ih99?OmkNrJ?@auX%=*x!O%6EwNX!qVZVPZFeAD4- z*Z^3G1~K8JYoQ6a5$^>XD^Is^y~Qt9w+nmd2}@)3aE&HRv)o6*237-oMFtaY%FwOo zb_1t4tzK1FA*e@zXH(uS>_fEj3Yo98YvSTc<8n#8EDv=D#t{1sJX(E5^Du>EqF6v} z6_In)GsjE~OuGs3VbZZ&n4kdy0eGgaWlzFAYO6I>EAUmjSM?*#lZl~8Pg>KqY^?SU&f#l0asCzlGM>UqfX z0V*PPyI%!Fj>@ZDRg_s(o&jq@9K3J`cT#{weascQ93(PM2vm}IcDIwCS$aqM@x7-m z>hp>c*9%aDg3z*Z*JE7@6VZq}aZIuBTV)$?0h;RLN7D_`lj@&W(lBS#d|K8MEkvN} z#$ly|VRW>e__27E4JXyZtK9U6k&qzDP4o=@kplE+IFK?;ZM>xhpC0DAF~YcHZKiO= zRFH`FhDrZR`zWrUpgWC=`Ico{*9K9N(&T|X6j#3Hun@nnorGYjlPN>FFp(~GucLg{99$;%>Uc8ES!?DaJ z8>z(cEOZP8bQzi@6UGgw%VqKQK&CUGrf#Ljac$gfVEHVG!Tc^DbYj!IzQ{_8oD!x#oYkL8hSY%&zv?`!3PN; zXe&+5*#(j@yumiTTj1fjxn(9LL==9bI?f8E8x!+%h1UO?EO!$hitqd<8C4FT@WXN! z{=TmcG?G-o(oA<9M@hqwE7WU=LTe0+)rwWX7Ilt`>(5?ks*b@EUBxa<=iaL>XukA2 ze4H8=(+pf;gPGqO0)B64;PnUE@f{>)zhlF@Q^Nj{{}>+Ao(WSMn+vw1{cyMV=@`9SeAO*F!(q{EwJ1jC#;0?&*tc>kVsF!pHGqmQ;a)xK>fY?^LrS!oXWWz= zFRGHKlZ%qGH`?c;;@#Wfkmg$1uiRip)BbZxHmMBMO@_XfGwz4G7$~BM=U-ys_aroZ?w!4aI%L z-RWQzGc;Ve|G0nrQ_k{%?}pa+^Fg3n)`88FAy%Zg-S&gs$$mFmp$+QH;H5@--Ro{B zuhq9&7e_qooOU4wOLCZU0W*3_>|gf4q6nOfsr5Mv$grK?;I_ncN6{9E)<4xHl3KyY zNaBolvoVE!kZ|+DD&*^6YjaCB8IcY(jnP4g{d`Jc_-16%s08J46TU|3a74Q5Y3d27 zFNU{Q+x16XAL*J`Sc~lm4p8BH@$`mV=scwGJM@nT12hC-r}Jz>x`9J^)c#Tl3!Knf z@8qX_bO#@v-)DmH^n6#~YyV8AQ~g<-3OZ+{Z@g`C1X`a=SG-}Rvw}1y#Vq4kMT<`mqZBH{`|_O_^7m}en1sb$C?vj5WHzepNChgoY{T!J?b)zNWX86C@e0bjA ze_j|*k7r&k`E0#@f7l?7=yQEyH;R=YHmC*h8OZ{@Yx@DKCM>^1G*#g|v{|-b={@n8 z59*}wApeyaGIJU`cHGW|q)+Hg6CRk+dXia&jISoaaCPX1PR^>tAG-Q(p78ur=p=i!p%bmnIgKWM3fV-)= z+1%PC*zU6taaeQ51qISM&chMfO=vSpL#;dsQ5P==zYO{TQyOzSn37MS$QZ6q^lp(Z zB8f*J40!i8Aft`v2Ulmuaf5!Dsl7NR-;Q+N*}y6UG(24J#eFUa%ah((^tu$dj_FJ7 z-YP{K8dPExmY2baH@Qa?gYba0b(9_i-FWy0UXDOJ>(KG2ot}oy6snRqR)#=*IQlT& z`{}tXKflo3eT8KMtn{)89)#)mV$j$rDzV&szEf?)`}B%%R4c}j950jTJwE6H5z{&jkuu<~A&5COG!tq1PV35Hd{%52AUP)UIK} zCM&eP>3&(LJ5jMPwID7eARAdt;ksaKY^i@&(CT|jS|I*`LtS`sN2m|CIF9RhY`Rwy z5AoirX=s^!ix!w()oiu7-7ym94$Jhy*!UwhY53jNq@P>kvx3ym9ws4~oHRRo)8pzJ zf}4bU@Q`C?#W+}q=VUi2nR91*z=0Phh`#Z}>cwOBQG59ms4Y`i;fzb@Y4 zMW_vD%ctQhY)9u{Pz4!;_`yb}%#_Ls6QylbSa9Z>gt#vrhu9v8(`U9jQXI5lix-BZwBT~Bzj$vV z*z`k;m!r`(^?qB9!9-^u>1;4b1HIju#*V>Adl@`PH#fHJe@U)qTZ;{J?;NU zxQ(9%%oLf4jBPEBcM58dVsyO+8S0W!P1SV|mvSSt`q0)hL^Rd0IZY7O*5XP*`LNh? zCQSG%)Hr)!zm$2m=s4Ro={z>RaE9)1Y0$9hIvcppC}^4y*+&fwttO@@oYs|*G_tb> z+TU}pGA@W3n2c0QvZh5B7f<8P(*-e&&#T#c&sz+*^avX@@qWfP6Cg4)5MFA5K8 zB3#!8DUHU$DxR_l=Hg!g4}Be4n~pJjX${u6nNbp2A=erAk_Bdmr<>l!wP}@7Zn@I+ z2Z2c!PmfLha)TI&D3sKYAyHyHE_0NGIAjo7xLXlfiT53@0>JKVc_dwlqX@`YNzIb1 zPlP3wS`H}IdLMq`?AfuF+6A2zVmK-=0W*6H(9X6RVS&z;AHdUBZ`z%Ki?engKdr<- z+I5avI>EH+>3&OsNfK}--%8CR0VM*8M+m>5P#Y_c>UlZFJrD+jt}mv>cIuBK8L--T zc3~Gm3BObHSV*O*g}L`8mMsIgari5UWYVq_e5J+WSm_ep)WkB@g`d)y$c#H`46RJ~ zm$EyK63ILROK?N{UlbD`Oxb#tQy{SxmlnYVMKR(8!~nid^cjB&Ke>``v(zhS@?EX*B z8+DCLd2NB+H;Za$1VAA(!y4h8Cu>a z@=rNL{3Kib z(u`3>9tK08EHd(Z!K1bf`AXPD3G&E5l>g`YJQ;i8GT#z@f{Bk+qqtG$`wQy9(Ot|6 zqs?}0!bn$;N7}isFQzb=3@YiX_WeZ=-a?yX5Y{J=4WnK8p>9brLy~_Y8N%*@Yb@?= zio4l&s4Mhw141534X#MzMfOXhJucq78gpCs-mVlVZVy*T4}Lv9d#(SRo!ft?0c=>e zT_r>r*W+32Q$~WnxOuVtDAp+KF4UONunj+396IEwDwgoM=|geTv1QmiKUs9pV-PA z7W2&_z~ZFKRlXrpXjTprmxtPFrdL7;L1r?0=Kpzal~p0o#zGbUFUZtE$Z6?b2%n&= zF+;{_T2yt$uzmb_=DfmeH>Jh~`?#frA|lff(uz|mBUesY4&FH{5CZ93zxKL8N=c9F z>VhoS1bIcDV5YuW&ptOSZd-}6#~cc&>jJy=V#4h-7{4N})~015rO||6@G=vaoqbSR z3v5z>dIg{?-YG^&>g`9X-i>a7$##oIw+eL~4a}?07AJ+2>}zEkg_v;guHda8@0vb! z=2a%_yIGRaFSkl{F*1_c#rdFdB?7a68H`nZS;MkYK9cr+@NT0~w~V$Dg49&Sm-0kzWMN9JruK-r=)n&o&2Izt%V zU=VSrMx_xo5QcFdhCUIO{5XI{TvZqoCP#;=4fz5zsqz|v3fyylmH34bAN7?h?pByk z{ZKnW@)s_MsgaNq)t!JO88z;g9|}slKI)bzfVZX`(ks0hw`3}U!v(qP5Pe+Xj03V6 zvoV~}z+Ln`+{czBh=q>=BQl2Zr6ndD6Im}-l(VRJL|AVaCz6slucnfh#W<2>;|8GK zBS%p&oRQ4S)wLp9B)Q{gv^_X7*wLuOViyV?RAXz-4WV#Q6#Ui7X@n<+UTCM>*63`* z_bG+N9({?bP~gzFl`$W9OT=TFM6?pOXW3-cVYnH1Fy>?hw!6uc`ys#>=gU?*p9L$` zchi}<-4+QDtXy^VgC5wj^^e`HGA^-p6((}`mXhB?-(EdM5{C=1^cU&_fW_|w6_EnW zhaP#zJ}_voI(uj)XtCJvH_VfAU=F1E40f4(tM&~^xvZ=F)kQzj9nQ`@YiM;yaIkf; zK{nDVQxeZ8P8sh600~i9fvjtdJpa*Qzn~}PV!lJj-QTo14(<1#BKQVqqX`v!x}#>p zM$*T{Y5VxF>qdBu-s8={)b@(2&q5!etR<-@{&+K6A4H?vfrG~BF;YN|y~WkWJ8(sX zUF>nO6>Aw$+1Or8Zm5`1Rd<@fXYS4Sx9eG#SwhJ+i0bBsd;g?bP-%kLk69?@EyQ^d z%ZAdj^wy8UTeh>y3I4>@{`f#E7dUe{a*!60qn#GY^01Z==DR3g+}$E$#3!gn-ZirK zMx^2?HPtuKN<>cy*twv^qw-iIU=!Ae<(Yq_OdL6F!Hez6TVKU4eiSPDoGm24f8j*QBkI42 z=V}QsPWTVuuysj*?}UyaKbL5S>7=s+*B-j?svI!_C)Enuu@F?3$#7( zWabQD9MV4f)6{ueZ!#T%>Bo?Ps3LgvS$W=c6}N$^zZ8=jY9kv(1vMYUq6&$Pn`A_D z^{%ILx-9wv(A%UFl3Imp;G)wwg`th*iul6IH5ymJgX;3)X{#2-DgwEh$~$w=tlW@>t2^^nWHz&!2r*6!a_Uduxw+C@~6qD&DN`aAfF2~>ML~@ z$Y5D6UvVbvb#VhtltkkYiJHdfZ+ z#4Rc+D%J2(O%-QC5=cgXGzleDG@%V8)wGRmT5-dP8Z~aH*+d%^CsUlLs8OTFjk!2+ z#>o^FCn_o`YV6oV4`U702-!_dWCe+XwL1VE?_>QwpU-jvBnvi+;89pv6$7m?=I}EU>227+E`7234YrD;SkH}%-1C4Enp!v(hI_5?dBL>s-{V8jd4OVxp|I-J)yzqhlazxHR0IYvM)l3 zHZOgVHZu=4S7AWpF>rCRLN3;f3cl3x)x(U(;oI5g!;G;s!k_i`;hpJ`u#(>c3u z#I7YfOSNsFuX_OYF;=TM72%pQgof|@@dC4KSem$Nnf+&Z&~AOB{=HJ(-@uJIe!$Fg zhs=o8i?xBEPcp53|JxHlTwuMN+|j#3|{6+@f4WOrS+Pb{- z>X?iz{(oupZNo1SLv2QCm>GdvySLu6T}o?`Y`)_2_-!eAE(^K&rH_|PW5S*n12Xkc zH8wFnnsI?*H_Y#n$C|3%}fvGb*6xoz8u?E~B0e#*fRf+>&7vB%Ts;nunX z>s7yDS!@jbyJb(UN$PxTVtthQxVWndxoW4cZ1D{Nni^}}xQunR02kKd_8opm7778i zMi&E94BKCjAxR#DG6pSuYNBswS-o*BBXgUkvPlCUMc>(&%rTm}A{u)e^VYUyF^NmO zTwy}?9yVj4+H2y|ko6_B6G@Z6@7K(CT4)r!E9l8MqnLKzW-FMhDpJtx;LIxLi+Qf*v+Uvy=+X3leWo*QKY2iocJa&iKe5Y*)e5pi- zCw(u{bX^l0BIAqCRP!CgilF$i@K+XL#>o1M&OU3YjUPO*7Y0k>uS*2qw;MymXB9Kl z^udzuN!Mmu*CbuER#~$;DM|R;5npWKT6cV`YP)naL9k$Ag_;>w$kg`rjjZP;5-!)= zMiCdY^yPBjewxCgW|!u3M@Mt!qn$~!HkdSPKH1pA1(nc5V6`8e`tp_7+wGFqbeK*T zpO&h$9s5Y(Y+Kt_4Bk1gjQ_(mFLw^C;GNX0NGx_6);43H>$?_{L%yct$Wg6h{xzDE zQm zx3H;))Za|vs!~W6CbG+o27L0ras5m`ni^Jn)24enb}XAl!l}pk8?4u&MEJ1-K2YN| zMeQ4rzqpbbAH&V`5p+|1iT4{3Qb=sSPXXN`w#DO|P3uqUwP^%sRcW+3>RUVN@wHm= z&Xy^*j^;++gj~^x4=z-iHFEh=Yr9b?^))-4<+aOJJ}r5Rd+j%ye8Ys2t9J?SvXw!r zXC{A;1Do5i2hP-QXZgUiLvQ5=mo9gWbiXJa`la{>GZRT?Aq~CZ&w(@ z3ORibPclVsnHD*ASoblZL;kor?GFr7eSCw!!UZ?oIN|BVxK^mUnU$`TAk(I~dj}S% z_*I$Ny%7VNQBPd{wxh!e+Mrd!x|q~X4qG%Rn}E3L)T$CHx60JFH{c7}_L=TBuEE{I z+9rnndvjC$I$zF1+h=WDttZBO)S#?7sGY$PIw9Y7S|hg}rNXvA+BUgP+SNa605Fa5 zy;W%4N>r`3jjgvVTTydEOfg1*Nz1|%ozCnj*k8F!W^e9vF~=Z;X5IIBX_<-r$;_B{ z;-hCf1Irl{SI9e>yoDHEn4W-bx3^reLp;eJUsYLzOlu&zCW5{OB`wG`aUXVG z(0NQBS|{cFGIdKM)lt6&9bw11ypp44;;n5m-IoRZ*f!jsD1q<)>(k=p>=HEaZEMY` zHmz96=D`N;gaQ8=GYj1f?X5wK3l-sj7L2yMTs=<2+1!q=qTToA@_rMpR(BtT+e7L- z*U{9)4=8WOW?)FPH8lj^nwGD|ktbe`Vd8k3b<8@aEnRVcQ+R=UHM{Fu*KQ0i@VP^K zcZ?p-r)b7W22JgqICUKrP&{f_-_eb4w8Oim%^Nqh;_Q6x0rYZBtt*;wzP_sz=92cn znV?*YjhQ;GjUvL>^AXkasf%FjX$svaEa(w5@nLs%;p!%gWZcn0o^3K`2n1n#nB9SI zjPUu;%{SMlF~NZ<@ddL48H&>|+HfF-_l(yL1kltH6RfFqLp#1)N(^mZzc7Y(o7fWT z&bE%Gj`9G#ti3ip4(cXT(xx_SY(z2TM#4?&SK~YUAP-&I+gfl|luwVQjcuqFJb5X+Vl_eTnp2hR4pAgN zj{M)oIS{io$2&Iq;2TyZ49^c?q6TkRwd&h(%t&~{y3P~pq)Y8Enw%$iN!Ug~j zJ8s#~i4&VVY+?K_-@oU*J|-oJ0FO7 zi=0KS-Q2RV)t(^bNmK7*vFXNktD+O9yO@JA*Kgg?c@K7@1g~&fhd65LZt0#;0_uBvU5W4(?~0nIEsxIF#JWYx@N&&2+_j42#jfyHDGg|E+VM@e*PrFZ zpP}U6x%$l`in?Le`qxb;2MmPq*(45ZESs$>E6uQL?ow)Qg5GGytfv25s4h_Sy{6$pLRNGA(%uyNG zZp$yjH((wJbNVRGtbksprq<=`K|EwoxcapbJN^AD*0VNDK;Vw==8Z93)*yphkpbws zf~YCa?M*kMF>3VnD6^Gfx%1J*4OZOsE?SO8VTU z4Qp{?lW8M2wnlwX{+ii8*`s2DPY7X66l%f&N}D!dkcIzRg_&1>z5N#n|T z%LeVrkgZHEbh8eQ{!45r%MUUzPnOWI*nRbJ<}X?>`xc9Gl;AZ`uH z!&cbTT1qG88Y~|-;=odlEB~-~+mMx=UFP&S)75#3PSCXk(I&KtJX{cG>~w^eESwTU z8+CM&JZoxGI}U+pZNUjRL7a(&_LzZ66DF>3_j=5F^WM}n9HPJqh@1{ynSzZRY~cY5 zUgxHn%_9=MVbcao_~W!bFGfFMO0LCOX8u)HI~yZIVKi0d%(WnGZjZ!%y0sV#@Pt^L zDTpmWo^I-5V;vxUK!UWP%ZUY*O^xf`yfYV4%LaUe;omVAV2(PoxEYgMaK~evoSo&T znx!4GpK{^!ukH>U4;XwhscEcD-rl?x^{QRWa%*KJhB2+lwc&6duTcGZSlpX7Zi+d9 zL)SK-g4&apyqHO1*u{yxY&NHNpcMN_WBW~rcjN6OiJ}l?H*9R`k}-{~4yK>iaHv5j-fnIgJ%`!2;6rG{ zk~hqnG#ha#9yAZZ*>bSSoK0$mn63@tS`YlQM!LH#ncb2pSuku`)?j-9Y+u*K$(!Xf zWLAXG%m@hsK1?imC24oS90)iz#Nn063RCo9;-aI8tq`h>srhN|gH*-Qm$*GE>9^`p z$Ya}M;tl)0o+q-RxQ4)*%%O%}JKqPk;oK=4GU#8^u5wg(=PgSv1mJvzXz`e!)HX~) zXiDsJM96lJ!CRmIEaEo91J#7WEF~5dg@@?XBXDU0VQk^fG_Sai50f;!bXAt8$9g#v zsx^FZHlxXOq$(y)TkUW?2sSm|v}t4J&eTS42?t3V5e(nhO`G8)w$GarnCH==NeIz8Q3V!o((S0uS)7M zNpYrGV)8Vh2EmKq!o&+D2p`}fhIgAL=!&Qlz~+fR>Rh|XxBjB@}9=F$66TcTN&ale%{F~n)-BL z%|o}9)@ULip+%jG+7G_M1g3ka<7k0v8ps7@X<>jMhy zA*){6$Th*Z;o*B;6s;@zumG9bre=Ab$%%@DSqs?0?pyL5Nfl}`3ni}bFb6lFPl1&T z69M+0Oh%ycA&+L4-A(CrJCm@)ijzUwFx>IqV}|Z}wEhtVP7~ z`9Tw5XSyYWWwgKRV|M{txnW)B%rP>uvQ=jm(FV0AEqk1NV2_`Tx?yp0Yk4wz(YB4> zzxvoUwdk}S+X7l%o<)3Y)8MN_!!~?}VD22;7KB0q+2M{!r@PFgrvyX2Z^~*7hJKiK z&D(}1-qniZO*V%Bn7imdu&m3i+g>-mmBW59r5M(<&8*N0U&FYmgP+0hq9uC_WAFp< zb3+jxV%dRqL}rQ8qKYZsWH?A*Ch&r1XbD?nWZQ%&cVGf06&oQ zabP)(rVq+aY*#m%B}>dJ<1S^M`|h2XqTbFs;{6+hgXHPxXlrh-mu7~Iz%3m?%EYFn z17n{0H6>HVE}EqBO*3-`4PEEPL?haAXkLO#>cc|BX3c%^?Nzl&ip#xWUeBC&!`U6H zTN*LS3tl0A%w&zs=QXw9wpbs5ffoc(Y9W&|u#%(&Ol=bsW;>U+$!;``exSC<6pNh( z(20H8MjZc|EsR*>^6~OjAniv(i%sjb9^WOa-2(;1x3ozNV#B!#3~g8<7dNhcxSa-FB5iN0fd(-zRoZ%`MyJ8k^V4L(uM$ zVXq03&YCU$jnTv2MBrc1Mlq+HM_;-c#a3Z`i7kOCV1AX_ZBODlwV&6sXZ>MCss%F+ zY73f)@5VR_x(OqP4fbSe?-)blrpCl%ca!-rL$rl7k?5LHO}q&@V=~w+oSE*A3otFK zaTY*O8QIs`f|D@37RL7uEJtLvjA#d27aQoZjg+lf3zi zm~{KGE8BDzjy=Ak-+V&tJ<|&1-)(~N#nBNB$Qe0DJWW<%@vrvdt-?Nq^1mKDO^D8LKzR94V`3iQ>n?(FnsMT>(QycBkn3Kx1CH zeFtvRC$ZVh7q?k_nIUX>b;_Ny>4_z3(#(%#LrS_qLj8u0&FHwi0oW#^cbRW=(*#&{ zuoI46yu*APE^wd1(?JZc;gWmQ7 zuMjDtfg8(Y^SkCWs32(4WK$c~%c2dGc6esbjc*r_LAt_;o90_PTbgB7+>8SuWhbG- z?vXwm0}v-SP-;;BTH+KUOF~|W**a}%ZwZ=OQz9!}@QK%B_=GpHVIi`g?4rW8H%2Rq zMH&UPWy4LI&|K*9AhQ7DW@!Q4)(UR%kwsB1U~{bOK5S2dZNd8Jnl&vA=6W2Rgrw&z zpE_`3?M(50Itkn-k?s1T@~=7ESYVK4=NQ|xe`r{_bL<#rDE z_ozA2rZlb5eJ|eP$j6^d(X4Qoy#(xfFr6F1G)2VsVEBT^d|IidXu-95Tb+(MDWVefW|LH1OgEMP8AucTI9nHKY!#hCT*Ew&Ri5MvJj;EMr zEEQqhFS=}9zJUuW;YFDxJPcj*7!%&)cLF3TWm==Nu{tO0xwa(C<&n8AugwSZY zan#)}z}*%yH{N7+7ODNjdDr5OOw8zE3<}>QBr~9hCAv7A5QXyuaPrKKQ0?vJrl2Wf zp?wuGwz}bPBcBv*W-L!t7}l~LPiKe{mTQAMrY&EGHB}QOS&#}}u_cS>G7F3gORzp` zhP_z|VbzCMzio)s#fD^dc?AucaQA_joE7!0OkmLD}uVd3d;!u(6{)+Uc&* z(#0uLcHmcHFNFGnF0MpN&c-p_`-fs=7I)31BR(_CY*3=KYlB@|6=x7_L~vFJPVxy3 zs2MgG2FzIFORa#B^|W*B%>%b3h0S!Uv>>>X5gCWmclo)gFGHm5!S6A>O_a@E4S%O# zltvThN}+4f%odT(@O__`#Mp!zglG009QB(n)@%bo`q+sw95WL4CgFay_D$Tp!9|Y} zjgOsCkT|4Pm7iD$x>%FXl^-2#`<@lcP%^cktzT~JxcLQInh~ zdad27VdoQ1PE)=gWCF^@B228b0A3$xMau)WqsUI!{Ql(mf6u%7E7 zrmp&QY1+7^n~ko_?wNw*HtPfxtqNvM8>%O3(6-k_k;g157Q%xQZ!2=VA6}a6T8*6Y zNo?$784(*fX-5Y)Hu2njiJ_(*%wqEPGQ~Nap&+*tRyA;d78Sx-sAEunA)-GkZ?!IO$Wf ztwkJQ(>w_~z*TtWCNwu`#c=$N+{8xGfhOV{b{u3o|5(UzZ=!BkXu`av43}MW*k>l2 zc#$Y=>tdO2G8@+1?h+oFp+i((Co!GP?SW2F?m!BurWMpH8%Sztin(ul+swz!>tyev z&pWr)7@VT;!6J!pqio;cU5SCQ#eSxF%do>FX^^&40!Vy}F*j~#;l;SVY?K)U&@Muq zZPmR&wqd9VPLWaR))OnRA_!Q#GQ-)PRh(k(b+O%X>EWiySZDl}nC?dYcEqVE6i#y) zrT4gTH5z=^qKD8z9wf#ZmqyJtrF0f5xN2&_Hq1DCVbn5^q3ZN{YWV%4!{%#HED{_}(WbMf zL0OxQ@5~W9g)MLKt?2=Sdw0M(SCV<$pFKC#9r43sm4jdiAim;!l|2^8NV%n}>k%}U zt&-Cz*KQkZyN5@O4!)R&5aWP%f7oI7y*OnO>bp~;xw4HQ&~$z3yC0rUvHpaQG@OYR|*Z99fIl2I@|`hMNnYf@c)6m%gv=;J71AB@KC!0Tl(mU}-zV^L%0Q0cj_IS)J zG8%ZuV|V^2)qp~#d3Jw@*{^KuD;}WPk8=Stj}y=29MoJ+6|h=r6~JLZ zJa-O9CeLU!({yruv^gH1AK9i&q&qCs_OzLzWeXL`5_ay`Yc|n=@JQ7QWSvVU`(07l z20F`c)7aUJw)Hmc)!X%C7PYY#e8k=ZTdjN@`Ms=E9G_(@-jPJ$amlxBK8C~N6%y~j;g2W&-S=`~} z<_sjB^52H>>z%nbp#U)FGPQhh#DEHW*X=3o<|v8n_o2+ymh~#>UEn7U+{8k$JyqVE zXJEJ9)UU=qYn0O5AB|a1ue4zoBc7cfKEqEWdRkBNI#sS<-iv#ldU2FY_qIFlfGGhx z2R%yeeO69Pn;6b1C}qtswXscs$IfE-hkW5_23bKVc@95@TjRR^j# zqvnLvc7|{kM9GX{Op^1a?E(HJo5U)vbzL*Q-{Dn{Oc#zX?H%VPk?zlv zs9dD2&%RAVbL7kloQ#QL088Fyq&E$4^#l*7%p%OV&C>Ui7K5=$>(Md(@3G1*&a z@P%m_h^vo&w4Je8_J@weG{^SL@r!ifP7~+d(q4g%^llBKySJhJ@b?0~bJ*s5HpjQ; zO$1^mb0@C)ay^c>DqKJqg9cmzA+s>7GkNkQdF1P@b~m{L=6K#{XOS%FC*92$Xmqb_ zf$42Wz?ml(EPAEp2{J#j+z+L;bjzMeyMhI^eJ0FE@XST7^6P%bonS?DpoSuxAxGJi!6YjfVuE0B0nZZFGd<~Rm|ze>;TWd3Vo zW-ckod`t=(%VxOwAP+n9*s0=k7b7*_Lgx;n(!T%9bjW-*`RMPUF_+iyNbVB$xaw{L znXTrPs}WJXvMq7QeAYtO-sBB~n_IwQQ)@81IL(bAf%#x7nR~$Jy>9m+rq=UzD2tXf zVC8zF%mpl7(*>itU~n*-_aS1XtK3X~P4g9t2yWTI<&pSUJsq-ldw*vQ{P#7_E@D2{ z=B3nXUvfIibUGcr&GtvDhl{f~=-~Z&v<1z_W-=x3qn8XU>?(t`qVhD)O1Npp%ne_5 zD^rWR42P|`SsS>B3KF~Ei)rqH#+;E!ilSd@!3sXVP0d0sS838Ad%DnZbEoN%GM};} zFqK^HbMYC49?ji;77_#ZFCzoo$84Fei5@u@?V?a1?R7sf{aKz-X#8AL=0ho+!yv)6 zNz@KT>Wo=&TNGy+v==BEA)_v$e=e6f!4JkxCy4W~wQ~q@O=q;qC43OKx z#g9RrB zgN}7eUd1=VeWs0;VfHu|l23yN+KvVU%R_d=HG9m%=u)}`-s2Z641Wb*=8a7b)JX_p zQX*qZ+c3$}WQP{A`cfAJwwamX%tlDMQOvku#}bZU;zOl~HfqS0%GYw9&v0D~^xk*pofq0CGIQXS7o@CpNOFHh(Mnfx+Gi{)<(>!M zXrpH7p|v>3dE;v43P`4BXjxd-($$2t^D-OIuxyKh$1PU&J6>KLWhOIcE?kt$fM{S^ z17A17xN|(Ys{(th3Txfs4)+Tcab&7(*~^+W<%WlOYH-%fOiOz^ro&eX?Pc5CO`BOx zl#6o6m2wSnIfuM2{GK~-OQVY**L<@uZFzRH%eLRkB(f+5WmwI8dgZ=qnW9|MC(Q1@ zGX}X` z807BMP6)c@BZ=Rqkee{eCKOlXd~c(Z+|3&|uIso^MM`zG*9K=vm`42qD^(^-(S_d6 z1rf93tyB+cmf>awQ(|>;ySZj^Ml4dDW?AGYm;a&AuoZOn^(<|4hqJcJv;$d5@k_zF zA4qm6Hkd7kS@c6ev13S6sod-u)(KM8>PwkrUPbIT<%*mg?D%04O%`(`edbeRoE8Y~ zGsViKpskaI+7TC+~3iH6L8(MS>C4@{VyV(E+z;Se zP3isTDYsSYKpUK^9uHw~c)=7zV{`kDLP$`#|N0{$}AO`f2dNmkkz#polkKj(ff z2>C|WRwlcv&kP;hIATVT^g-FVpe*j@lF%7WvQnyr`eIf=t>PEcuhTTch?#rvbo=Rg zd}7AWy|4`$#nSA4OXpnIo9`DJ;M~j~(_}RVl+B1q8hM=`Fl*tYxv}AX9P@>{j%VZs z`rTQ2o-CP6W#Fv6w~<`Bna#{uWn`l?Lz{bX(-n46-+k-zF+|OEDOaXKbH`ys-i~D& zCK<^sMi-NtApkafrFW`osZGRTw750=EdH5dG;r7q@(et4;L7acp=-Y#A0Km5?u&3g zb)H~}BC%D_u}X>MqD-;0{LC0j#U0a)_%0pa?)Z^~9d3)==I4Zhhjxqm+Lk!~KXdeG~ z-O)cI^-D?XL|2wlnfOHvl1%u)nYF#$0B?4`jl*Dfl{9Tax|<>|JF4wR8us&qEEKH+ z$YdCZbZlm_Gs(pFU`gacaO{*sJK?{KF?%xdO0FUQ;hVuibKqOAOrdh^4`b;phg`9c zJ68YMjH)CKe%{+BV9D)5c?(*4zBh|RNcyZonMjnJf1J&QtIH)u@Z}QA6^t_5k4$q> zdLNFrTQNUqE2y1w-Hr_mDD>Oxt}1k77s!GTn^LxMmR4Er+1?GAoi>8Lv|J$I(CnFk z?(fmmo1P)F881T7Fe#eJYB}?n5wkIx*Z1Jk^316fH|Cfd^||K8j{EAvqFW31?Jp7P%zFOo0S%$HKl$}w?WlLANJX6aR>)C2{ zqV@%*7Ba1p)$VS;Ti6V(V~1v!A!P2JgW!^L_ec*hx$@MTSu(=F_hQEfxtCdV%GID* zhJH}Fq`~g`Azm=1bFKI^7$3EcL1n2dLtgNrr2_|X_vuXtiaYU^B33)aC{xzbFK53O(- z1HUv*3sB?C85ujoUE7Jhu@|+3&2bYOQDnb4#gtzO;~)t%FtF*&p=j=X=r4Rh(XH|` zOu&mx2HO{AjxEJI%e!}ZJbMMEeEm= z;z!Y0fv_<)WI+M>M$P7ycE8Tb9aU0HlRxQjZ-W`9Lc{j6hDp?KE^zf!-_h28= zweM~i=}CBY?tlkR@QOtk7CeuNb7DmUsy#j;X!Da4d46QE)7ocxo#X?SyikR9fvIUm zW92@@36dpR5ZV#gfRDxY+k4I79lwW;&aO6FsKZ>&dR!%l_OiA0_t+N37?iT`v(n#v zakeQu%lk)EA?ziIX&w_MqBu?t#JcVFCRSiPRGQ(mP$UGv)j&HYJh>KkT!D3AmsEm@ho z?6y<#9Ru6S>_#Y`ishn;NFXD{U5U!;b{}P}MWTC&Rv;74YO^s@E@omV8%R5Acsr`} zisgS$e!5{~rk+iQKd4G+kq?O?>l0Z><#K`r`+}PnsG!ajmdGtXnI6~LJG|M!mFPB; z;WKvK*bd%r>cII(MQ9a#4d4irGx*-fxyX2hBq`Uv+L28)DXP(V5c+dUaI4@;uKh8(e zE}JTg$g)_9xjZ8di4>t|R@mq56N1LDt_|6+#uaz7Ahg}Cp}3=e+-TX=%^B80(8&#U zsT#tu>vOnPM z4je;Dq@NtN>ym0)sV-)Ag+l7MWJ!{LcBi40E@@4~n5ubIsvMN7P0@!q6wmATEN*4v zu-_rANo@};%VNkf_PV(dx;*rA3)lEoc2lBFWX4XK=IiS_*5&C?vV!>b^MZqtpiDY# zJtGOSP_!AGnSC>kh{_YRT+*b8t2QOhZo3;@?zVd_5PcILQ=e>SD+4L}$#bdgENQTQ zCs|hBvJ@}@$w=9E7_+;Ih4upJT`q<4gm)UGhhU^OQvGaur5en1F+Zxu!ZOb&D>Yt^ zTKlSYfn2Z-d?74rwVE^x>1rn+%_a_5R@}2-7XZTksN`cx-j3`mxm!(+^M>7}7D(^A zcU$*%-^pceLo0qkowY4s9n}LnI=n(0uFRV3ajT4+iD&MB;uW3^xS9$L-U}{vmz@0$ zX}0umIap?4twcW9+LIQfcI_&CJBr@*W^2^9T)1|!sc>5)5_`@ZNYI9lBrdk0&}tA2 z&@4~$oH8XlUlHtPD~xZ!@GXC$QHwv7GxIc**0AP0%Sv{+N*qdCGDV!nJqt>std?q? z?U^%^x5+TjW$o+{3o1IJ&xKCk@&K8spB)~r9LH7#q6 zu`>IT=IczRxMp+Ey5!gjS&;wmx67W;q?9T*)Y}t7ePKXn!tDRt!`!)RYU??~=|;lK6U(R^2uZqVg0? zJVA1%lRCMgm{62bQD#m#bCi`8+wO65e&n8@)cfy_gVMTjznA9fzhHQgmzw$_dK zh|zt;(9+bA=WV$%8x@{WT{Fc@xxJ``L)HU-ESotIxZK$&8drKRCz-4@MsJHhT_L6! zPOiXRZQ4z_oZ)GXGoqcBAq0{%5?LHGaZZ|--YuF5nu1J^-ELIBq+RYCOpSQV0sLkA zv`j0N4n^XEMsT?t4?egvfzpPdU3-Q$wi3Qe*D)@)s?2E4vo!IsaUDqi9XMw!FO`{- zpc)HnkBO5_`y@rf+=S5B zh}&Cc^fku(v&T*PJ?oiXCgtzAmuCd9BHz_}D^5JQyLVfWF3X6;&+>GA==`2MV_`na zD5HJ2kU0z8=8Gg@5;3!LC$8#dZfCFZ<^1$J6r5Qy<9vl!Qrw~BI6DFp6vj3Rwb^N@ zCePX?-h=7BeY00!aSJ8Qp1icpqPR>E`z|Akb(Xqre9dh;-TEB>yQ-Pibh(g2VPx9V=(yaBhaWE#xuLA*^> z;+vW@QeisF9&~iUMP{219EWE*WH}HLH|t02x-Vvcc~8cj=D^laQOqu~i}ZHZ_94?H zu6*5t3nuTz$>v!g<#I4YHe5B}yQs{AEI_SZ=HR$H?y|>pxy|YKYOiU{YAY? znY<#Zwuqus%JS-#4c#xvg~lXPJ4^LYdR#+d&wBOaZKI0K&E&$Jj_ksnu*Ar(RLvc) zo7(a`N%Fx*b4F9AX%Rb_Qg!EoDIl#xmIjF`nE_%u`g?ih$vq`MzRD6X@oTm*>jm{q z%5~D``x%Z-+|KK^;g#TVYx6o!XAv&7gp-MA!iP9z%^S;9sjSG7#8y*NpV2{-`_8gl zdZey(lXa$X$xy|_Nk&wDNR#i_eHw%@+JT6yu_C5xrP0AAmBMhek9+d^IFf_oI2WI!#^D9Rpt_tV1vDfsy(FE{yiNpYP?Utd0p;$g*8zUIyMjzPwZkPx%ueeXlm*pONs%H@?n$a+Tp* z1(RR$hLqnSnEd%YDL?YYh{;dAIbz2D#9JaJf9`D&lRx?Q5tAQ%XT;>|_D4+q_DjF|k?Mu8Kl90m$&dbP#N^v1BPL(D=)bMWxqff zvE}3R;mA||ykPoQ^O=-iDVY4oXH)*XVDdv#DL*2Z{2{>%e@Za&su+{(X!St_xI^`>W95MOD4@S)JRthG6N-+7eg2^}jcj~@bF!{ZL z>HdIV@@@Z{x*rxye)>$xFZgN15g!1F-59RxXKMtOFAO7|EKmDudp7Qg59Wnhs|C@-( zFZpf6`ZC6H2{rd!yKPs5~F~KJMhotWJ3ue(JJ`HzFFJ|?~)pLpi z4&!A}TQ#TXcbH!92VRJODgVd#_s}(SiUn}z(|hV+cxfJ+-bV#fpW+cI-zS*-k?NGM zepJNd>jX2tZGy?~6HIOJ` zDZ%8=2_|2=Jj#==6HI>mX(_*|He&LXH%3hNiv>Rs6!8Gw`}`Uap7MJHGbrLAk*|7s z>iHnnQcE}U&5&7n4r2cmb-U^CX<&OzZ|EC4t3wh#U@xNzP>i>XX zreEVTQ@&R)`Px5A`7Xiamp&`yTLqKfS)cM_g2|s^BAUn6pM{MPli#-{;#c6$)}xmf zugCjKU?0B)_=MmO0r!6?@>9U)1%C&4_m?k6O}6ften-WBhF8%HA@f*$mIx-lXI;vl z6->TvL&^^dCf|2c%I_3R{ly;@kDufB*RL<6iU+_PMsW1o=W-i5E;y`oC%_9Um_KQuL4fp07mA@DBnS1g45`IpA=HTeUdOVg`b{3qWl_;t_6i?77-_!Hq@g<$I2C-QVZEtvcn!Q={zfv&6UnQ7)k6;r&!Q^)fCcj58`7?sa z7vGHfkna*~{1?0&>APUp6{xGWf2905;VEBvBK2=vF!@7*>3)JnmU(RZI3<|;@b^-F zuVC_r1k?XX!Q@X1CVxgS`6Z`P|7!)4ulZrhZxKwsYdYn32`1nCV9NIiHvXSZ`A)&) z7yTsVmk1_5a3=E4LjF{u{O?A4>f7@{;VFOcr%~SIhhXv}KTG+2g30%vP5Hfo$?yEX z$hSeCT9g+kga59x_L=aMAN^&Nr#@4H$yfX;<>w0~U;XQpZxl>^`Zp=R=(iD*pAc-) zLooTS-$i-yTLhCoCz$+s!Q@8^-)Y$Ly;m^#lb59YIl<(6DIihF>L^{FGq2KPH&`=oP8^!-B~V z%uo4I!Q_u$mGWl=lOK9W%8v*pzn6mo^Vs|v6HLD9nv`E9nEaq%lYWB9pLY|>va`HAk-eN|7y zE<(D41DC^+@cF|eth3{KK3^Zzv_e0J^foGnEbI1MgHaVe^F(z z0Pinx>#b*m-wU32$c6s_;K7f~Eovao@IM8-A9%>&{{&t#J~yo2FubK7jp0$BM#1D4 ze=Ox!3MSwDsg$1(OuqUvkw1y>D^JAnE5lnX{7=9Wx1!7T=~wyh(Y;B3!SsL0RLb`W zCco;7DL){X{Dffoe^~Hup%3vfY&G)w^qolkTluYs>ED@?5!1h=-;J1j?e`)k-y@iO z+jPp03cea!#E6eU&g;|j<5a$1@O6+UUJ4N}zx#BQH{pqWM14jxzV2sHp8RgXbid^1 zDc>rX{D@$>-z%8>$+M~Z^Mc9m{&mW?osXFO#BU>}|HlQBKli(oUsxbh&EwjOIT6$S zNx^h~=Bkvhcu2(Ln+0Ew^d5d?I{w=dCW&l#jSr3P8D5uQ`Zso6${!OaNO8I`lm@R-BffQMTv!g}KXaMk*XVu|C2fNO!PUHkoJ;I0i7 zMI(mbzP^rfQo%enz4r=!Kjevf@h|0%;NRlriei^@|4rasT@}SKc;Wp!aBJ${X~B&D zK!3`Q3pVLF!>XInexTXh{@LpX83J_$@jc8%9GzO znEYYER3NVVCXag(+AEEJreTGf*%9iD){5i zM|=do@GY<>1vC7?C6x>gk1bzAqR$iHAMsMDpTjSY`ad1~=(5U!JE6V3HYR*0cw#OQ z`}%lZc&10yE24kY=k!p-y>NdJ{@e9htKY&`M*gqB6Q6YL3EkHTPyagwzYg-}UR6$%cwH*rDEJWM zi6=$=$m>)2ijjyv4|!sZ-_pN}JoOn7%=jG^O#YZ)@{@Z~_h$u@A9`cti5EzF#}6j_ zABk~VJR<9KN1mIf1^}wqH-vV3*jDJl2ybymh3(xRb zUVM1X_+y`7@?(O@pAby`q+s&BZ;k$upAby`tYGrz1d|`%o4P+NnEd{?rTnB|@;l$3 z@;g2lG5N}gh}YtSrt0a6VkO?QJnycAz9{dMBc^_bM4s;t{7ZCi_QZU^QAC2He@_@&~`3 z^2Y^}A3u@u^S>1_`32vOnEtm4CV%WZDbM!EJ~sZlz8f*!j|ryxQ{PK@wpZ%D2l?wU z-Lrl3nC{OBCSUv@<&XR*V)86M_Obf42`1kunEvk*Onyu-`JU6!Kk{b;KTwGm?(zLc z8|t%t?0Vza|3>%pulXkt)4w*se6na8$gixx&q{(xY*9}`Tz=HXGEe5+vc z#}=i0^&=xD-}WaF)BTWO6TV>b2L+R_est=7*JC0kfA%R6)BTp3h>iPY5tDBdOn!Jp z${!F+zHMd7_X#%P3#R`SH$-{z`)`bx{7J#&dsn6Wuwe3CoTM<%RmBoNbO!e2xfbQY zKGzleAi56N+hcU!c~7O|ZTX%O|HvP2i2hT*(}IorH7VaGnEaBpDPJR){Dffoe^@a2 z1I^KWOBTQ9W|;C_5?|t__g3Qnd~EzC#Xs`Ze;)m3_%(vbPY5PIC7Arwy43w~!Q`9U zQhri!KlCT=g&gZYupqmU<7`F+Aue!t-NK%Q9T zPYO@@Q-c2$^291%-Ja<$_zTn@Smk?!H~I_yF7?mk_X%(G7yMJ|pUIyT-smrQ9;(Kv zOuo7!(_io(L4GlCqQCG)f5D3(-rXe6c0v7YQc6pOeVu zvF*bV!Q>mCm-1bL$?xt?`2&K<@9jzXLxRayZcX_r!Q@W}X8N2KOuqNl)cpa$dr;np zCm`qBx8~bY`5wXVggmidU#0&CIN5C;n?B=$4?&)|4M!CO|L;lVD+VL}GUSN|;GdV@ zBD^Udf**wZAn>HfFTOAONBNb4sqdIz@`nYJUwVJ)zF9E&KEdY^zJJ*N)4zV<=U!=X z9o{oOR|6jeKk4uy@-K?^C;i{?vP%4)k6r&byCc=7>ctULewAQ`zeOYz_w=vk<Aqhu`Qw7gpAbyG`EOJITLqIpBAD)v3MN1Dcd7e5g30&3A?5o7lONra z@&^TzKmW#*uXt0$ z(}KxQzd3b(TCnl|Eh*nGn0)J7Q@%|w`J;kO`~{Qm+?%@Z5^UTHru*Z9$*+7{bpP5b z@xnd6y{#3V^_f`f^Pph*SBv^+AKShjl=}S^xF`1Y`vbsZaDN;Qe1A0b_fem9ct0%o zqmU==bn^cKd;)k}-0yyS>V8D<=O9mf#>syJc-Ov5&7oZk|A)Yp@3^!War`fUM}b#j zONG~O>RqY-M+DEEkNO6D)VY5s@ZNvCv{>TslYkEcGo9#PBk=rp!+*EFa4T@#0q_XI zhd0DUZ}Zsxe^~HKAW!VaTW2gnnTi2RwcRKDUv5x*bu#Cx3n zp9CHS9(MRBaP@~{`ZAuyN22?6fK`Hj2zlZz3BPANmG2XL9`eMx9=YRVsr(+n*Wklm z;&Va*+Psr+uiPk=merRXy}k;?B8Tu1+bdqn=!CsO&df;U5+c)#eg=buvfQNb^O zJn^XW|0TdRhY)`Wuk&A0_ge(-fjsfPIk7yQ`qxzcyx{jio_Me7H<`+h3jQ?p1MYY7 z-vsuLZ$BzN72UJ_AlCMyPB8PQ@^31g7qBiuON%Kd&+;*S z_R^x-r6=>h^8e6&VM><%Grx|VkM=wBYsqgfEvlS*=11-CF#dGznck-^nOB@~;W0gr zT{jtE6zFj7T~kj&MUa&%=q>I zH(nmYzaMz=%6Y{C7oXk0#~w1T;F1jG-vK=Ihx)}vH$78`D%;7v;46C*@^Pk zhV zi+t_jRDP9U_E*HdeP@4l_%msL)%ovfe>L>E)W2cD?5~Kmzv}vrw7=Q~%y=;W*k4Tn zYkzhA^N25kAWylrFQwsi3TA&r-0S>%D$;8L__QmpbieOd>VCiATOd!|>hhoRr;n%d zX9WKxI`Uie9s73beq8Y1L!Nlp>HAN>tGco-VN;Y?>oSCC)56P z?7OM|M+N@`^29qN{*9+n`DVd$tI!?*x4QCr4e*{HMEoe=lRvz)IPCPh9(ezcE-fm2 z{RKWS9dRGi--rI|1s20o|Ne8C);C{KZy3<{-ytE(SL?F zAh@SQcw;w4`4@sGUg`WB20jJ6(53$az~`~4ahHU*2<6W{R~PRAzw;UJuOjMmir73h z{m-q6nCa91tcWMzzV)M(uq14I^;zIi_`k^EY2ZoV1r9$N``C^FQv@1V_-e0ek;%1nw99dBD4ce+BTA;12;;y)(K$1>7k3N+`Np@G9U* z!M8Ia`=k3e0{001D)3&xb8#f@wBTCc#qWyldx5(IzYcgv@Hp_K;O_#T0A~4O`c>kH z-8yWN@ae(wvs3bi`86t-{9eK2TkB(fk{=RGe%0#8KOW(YVf_}x1>s}=0&p|eN|6PK~9}-OWlY+@NZ%o~{ z3MM}+nC^EACSTc>x~~#Uey?D<-zS)S+fAwaF2UrF3#R*%g30e_Pu=enOny~I$~Oxp zze_Ov9}-Odh~Qq-=K-zX;EC;XUGXaLgTm9lqvHMmc;YR1&;IR;z>BucEA~0e@D_6u zy?Jc^vQ+R-AWvKi-uI6^!ZW;H!4HR_d7()9+yC=7 zNB@2Zo_G?xPoF8_8Qu}WS1z=8iPN9q9qo$#T@QX&7T#ImSAi$4k?~yhbE12Ow^;DA zAx}Ih;T_+S%AXQE1bO1!5}*F(rt$-V_d}jo<uG>iPitp!qfjVg3m*qSpDC5YwG_l!Ao(3Ke5Wszb%!o65IiKVwK-3JoVWp zcn9Q(RemWyvN4ZsUup!u8S=y`e@J-x&-(A(v%VZ){Wp*G&$I^)lOMW0;{}uN+Meb@8r!(S-)DqLwnTr2Xe!c)FY@Y5lG6xi!)%7^eh z;3q^sx~IH-Y=1a-f5b0?{EnB#`SQO9u6hl4mwt3VEb^ZMPps=X2VWHZqx`tw??ImU zB;Nb}Y#8ZlpR0rNBo7zZ3M4$I3^f#<(Bx~IPV622)fg2^xbo2Vc8TEQm1g15r|LsFk! z4tyTGKOdFuiS1+4qvEv@lV2*B{*MbLKOvZW-Rn~Kje^M^5lr_-1(WZ3ed>OTVDhH~ zzZdb@CGk5BJS3R@PmBDekFj_Q-m|>d0x#H`+W)IY((oGv_d=dn+q)gYGrXOG-vasb zz}nuO6aI7HCo_F1Zy%c<`~EiK2O&T4*4SQGp`#gx{sS&P41ZkYSCI$qcRbT?(ceY? zC|@o3cE}T}{a^KlD8C2%F0?04hCAQhEf)Tr;E5-}`}Xpr@C@&i;A4;{R{MW?PYiDk z8rHEay!mg8{6oPLtNma5K;#+TD#6cyJn`fuQNJ^zsr-4tFN8d?+QUO{PUVLM?}I$C z%2&N5m0uwEDCCJ%ey{MT@KH(37Csup1cz1OF1Mmw!URm5I?iUJwmOQZ9lYh z&t*U#jP~TKfQO~M`Z(}z!9N0C{Jz*eJ@N_YpFfe>lRLoM$ClS!f?FX^tnKT#@YHug z@GBu-dob$T41OiB_y6PI4~u?uPkGN%{)FJ;kU#a&xc}jjCn7)pDQ#cRhSvF64<%;k|Dk&j|lI@FOT6%fS2c$@I66?GGkC6fyljA^2@@Pwefz*y&d?N;A6mT z6Oq3R72$|r=Fd5lAMgJ<{K5S2nEaq%^236k3IB%Q7wxP2fDZ|NH}E;Z-v(ZCFv>p! z1IHf0tAY0jz8CnY;P-H#iH|ZD;UDJT*MVyUUyhEmSMW1{4+tIvJ|*})z||j!{+$GF z1YY3$U-}fJFL0&9e*ruq@)N)Zfmb>HG;qZSqkoU9DT+G5w*mJHegOD@;I9Cm0j_fX zUCV+x7Q^oWt_Jq;e-m)0;Qs_35IlD|%7b9$=PAiQ=I69v@)Q3Y^Pl`t!Q_VzMSdCl zpOX6D4!j3^4MbS~=$_aHf4}@_WCMy5BFDeD$%EUm}?N zxM2D}A((vMS5x-`g2|s2O!sF6li&4UsrwO+31VDi0!$@d8+ zzfUmv{esD#5KR80VDbyUpN79kF!@fwbl)YI{D|O}z}^@U`{M(&KLpeL9+CeRd0;<3 z^{^H2|4ceRH7)XVe@5_X$P?@MY4NEvK1&4uCFBnP`|%$2-!J_8!5_)=rM!Ktz0~@H zh`#{&^FN9D{~z!<=)VgNyuH;U@{d~y`x?04@eF@Tc*gID;10+W>-^NAAI9*9z@N*; zCr5>U19;*|32*n0qI-rnBKWhAC-(aVso$RID1Q$8(us7uHZJ^SH=z6j>-^No2cvt2 zcS`U|$P-V>{M6{_RDQqUJ0VXzBJrvDpHzOO;CDctSmh^$r#^=Te*^NwD&O|MssEjV zFMlfP2k;)z=algDe_C)IO6Ym!JbHdYqmOt;F`E?5A&tvi| zj~))?%cG5AVJ7FVm0d<@ry5Z@coc zus-@z;3?pp5MY0{6cwj&e#FlMo&?_I_)*|G9AfF^j{qMM%=k~EJlV&_pZVi4`2oR9 z|6#$GJQMBthvR(E2H;E~0b75f~f|6?L=;xG8>dZY)iwzsE+r~DbgH$i^j;c0u@ z4{RTt*9rc3rZ43^Z_?wBBYr>RS3+N}|960E9(!4`KD$KZuUifK09ftc5#g!t9>MD& zPptOu&?92_uLi#p?HQWoU=NH6|0eLnli-=Y)W364bkFd*1b+qc#JWDa<&jbTss^NY z7Tzx57l9{M`}g3VM)wSFOmI8oi6<|K`t?34l^+ni2lB*;Jt#c=KQH);kSA7qcKFe$ z|GNcOHKM%)R{s|~CY4_(xCQdW>i>S>sm}qyuYx?W`d@?cV;@_;Se`s)el0?I@|gay ze0fZMw_x)71e0I&xF}D)Q!x3xk5Bn=!Q|VPqT`Gevk0mKoF}vd!{D3e=qo5;{FrB zW8r@%`bHt8kuCcOo}0`i@}zQ3aU@#WEf%1;aa66A@! zeR%C!)c2=Fdv+c00co#>fe#A)H{g9MV*6Bq01wxv_Uu{k_ObbQUhuOZPps{0&B|1M zrQjjRj{|#s8NV^%{|zrrh<=PO<$d^+@46x4@8demyn+n{>M%i{zHA}z76SVpNAG_;NIg)%q6wJ z1KKm3=YAf3GyF#Y^S;kX=l}OVg#N*u_y16QAGiX%f2dD$hHnKPnKP%D z0!RN|2+aGbz5g%I+XQK7;x^6ZrV9Ip#bP;>)i^fAFk%MYZ#919+ad%kxUeZv|e6`&oVd-wn+3 z$E)XB{>8-ZVR|&Y3;yH&7_ZM;!1wI1=WWox{h9oSGyL5Q|0=@^+KPhnVY?9){aXjj z^R|1cEWQJH{(VS1!xzQNf%i-PjR2n;no|rq_a6e@hx_!bzSkCC0ABj@ONxFMzZ1az zVf=oc;deZMm#_8VT~%Ch6Us})6{h|$KOPRu`v@1g{Com1?+2=O`02p@q5tbM{A}PI zbEAKE0`q%QRy)T3)tUS|GW@9we*>87v+Q5#{>pYcp7j2$2JRY~8_MrCVE<6R7Xeq| zG8K*|=zbKK_nY^FBmOWj@3&*kpIu81E4%=-k;e^(~|QSws$znJk4X86~@{9e|V zheex^U(dsND!#k|^St@_UVq@-H(-C7!(G7sq5i$ZIRAYm{-J(@8UGsKlP51JDqMcO zJ>#b`{7vA>S6yoQ5Bm2bU~}I9!e@E=MJ9jM&0ZUZUlfnc@G~>K1$b=U9L!N#eV=z{ z>b{@yJ1fljG7Nur=Kft7p33lp87?-b{y#LsPswl#Fu!L%3}*~~D=_yv3_1LI;9h4R zGd(^G%=^ZMDy;k$fX{VaVa}5x|07_Y?_P)cNdNvGh8pkJSm5ISf8co@^0@Q=nlAW< z@=@pTDq#OGeLH}6-ZaOo&(ObHGk!3`Zvp0hBVXP=58VICIiWs&9hmz)Cg3;yyX+R! zuk}&>(ZIYP579IE{}f>VP@g{s-q}83 zX^{VZ#{UwS=XD--?yr0{{%{Y&Uj@wj zZ(yD;;Pt;^i`Rzffw2!T_bYh)`+#}BxW~JJ4@i6u0Q-mTKMTzL8eX4Y01y22ywHC= z_PMCf3+;J%O#fzJ|Iq#2z=v?&WiS3_fATV5&d=5XQ~ppU|3%=wZ_WwvIR(t|iI-pa zJUDa@{a*&m`$(GcH~rfTJn>3D%AIB@ciEX2r$EIy3bJNe{^5fjr7F)kFW3b!2TiM z4$SZISK)8EzZ`|NaknT*e1a z>Op^m{a7O|ydGfQ2RrWCtNVccL;YUegZ}($QJ)8ZhjE@<1vs{M?*ZD%3O z_nn0xZ;Yq}QHd*wsF)B%<3e6YBuWyZW5k3giD(snl|+)bpz`#6zQ3wEr%v@U zeNB#DroX4^)T#Bi)vtaB+6L~dT#@tlQ^8-o_9(v|9-EyHmOo7x?*rh?FaMeB*yXN2 z4}$f*b&17#-|vA}A6cy5_k-^t{T44z{~0)3djH=h_&)H;gDdm#`F4{2s2RqO_{yQb zSA#F%eb-Tz-hUE!er-{o7l7q28q!|@4wv5l+sgmVLjQVqlK!U&{%f$#Gl%u@EwH|q z9_IUTjL-2~muI&vclCKDcsum?YTl*#odVv*`k3D2^x6MQvT`&ox5Tm5ketncI9>c{t=lKlS; z{=nyu|9$@d3x4EKVP83Rjs$ zul%c`|KFFCcYA_Aqvu~-$P3?2(pS#Yf3IC-@yyiz$AiPA_dNrw{lYN6FOJfmN%{fs zw#Yv5X7FvSPsJZqdG7)DFIb)(tKEVb+^yIvOzlhB&_e# zz~R#K7bm!p;9m#pd%;1zd`*&mJ$U8fitGX3|F;$@*W zZhRgOeh7ZE&6js-l74Q2XOtiN!X>`E%fKDvp`*a!XV-vlpu7`(`X7O(B6;Z$I9%$F zAA&c2e!2N4*m&-Ne&a73+CK#jm-4?9e0(H-%!5}#|5uWx_wNJW9nH7j13w(`?|Z@F zQh5)8@A_Zvdr*4*dtjZ{yOuQ7=h;os-vXBZz@3z(_k9a|50md9KORS-@7)y1tIq-J`|rDtbotK%>-!HuU%do; zJ>Yo%zB>4}*Ff)mc`fjDuP@f)?|`r7dvDuGSNq=smcPJuU!Q*lhfD4KZi0_*g;doi z%buO!vl6@wd=>JKF3Eo{O48>O{MH122z=|mV-Mqb_17V={GY>m`yu$N(Rd!!X1=_k z;HS?4%isB0|9lnv0p?%d<1SeK-eLaT3=Wst_ZMKDHwf=r^`4OG+JDIh>A!vM`{t_O ze~|yqA1u%913v-lJV$tbZKwSFwgg|A;42dR`UJlR-1$xHyMFvXlB9n+!Cy@9j}!c} zmoa~l?~bRM;{VS8Z;tGL>%odQFr{?bdjU9H>aRWE?|gHa#XDB|mEh~%vMf95RG0r+ z@W~j*Hj`KBH-P288048l;Hjst$mOfQ2Zu}L|8s(m?1og8mu3GS_}WO`-VA=Qz9P5R z?@01D5_}M>{hUiFOZB@hNxyJ6<@0@zn|*)(Zjyd0_$rLIclqbP1m6Cih5YjDJ6P{; zFZ9>tJu>?1IR5?SZ?eDm%PTV2jJ<#5dhOp8_VK4C`PYE=`|t5efBdB+{UY%3(91xt zdrA5=;5#o^k&ovsVEKm!dHb76=lkRVFJs~8`?R5bCxN&9$Fh9?c1(l zz8AIDx2Fq!2>bDr$8Q0LOYQ$ag8vS@19}zc{Wrm9BG2t6ugd>HlK;_(KKb+nuLIw} zcwFl1w3hK!|4-~U{(Zlh!ZHz>j>YkRNV% z9`gzQx0<{v|7z0bv5#Np@jJjeUw^B|f1Q-~f50mve)ib?Wb~Kbe*#$XDyR6Z^0$I- z_?=?@{VG`T1aI@{FK0aTJ(bYk`$<3H!-f6+T5z~*ekS;nVEMaEkxB1+BuRhF0q8%* zOu2X}eKlC;A4C50z&dXg-hUamc`f#MU*2`#KK9A29{(lyitCr>>+Q4PaH)P@2Y0aV zY~#1y_q`+QF|4=-C`@zT})cYt*s z_W{Zh{w(+rg2~_O@fX11()+%hU~?Ss8I*qP6(P6lzn=r^ykMBW7bWR!u)a4G#`jf8 z`dh*K(N9F+B#+&cq<<28-9?3d{Z;T4(fmH<<>x%s()sa^eez8eH(kDpAWAA%YX7rpa0`vegE`MkM9G^Km4G_M_+(|&-_D@|J&d*qy35> zflu1D-29>R{zv^UM(%jfTUaJbZ-v%y<`+kFo~`CkN9e4|68311G@_mvr58{ap9 zxBUV3F`s@D_)DKC*3TEf;ZpezfRDMdpx;Lv45@DZ9iQN5gO5i)pCX&)(>vb=ePh2< z*J|X0jR*PnRV&ONNA-IlI9z()OAo?N-pc-mAMZX`-%EUg$8QIp06&<@T>Cx=4ws() z`a#m+pJ6^7^9p!6`xjS}ruRJ&94_TQ1-#qW?{UkYlsy-$?^}oQ+nwa^gD;N8>zX9} zonXZ?I!M`i-<@EcH@?l|?}8s*hyLsFV}67AAIXcW!Bf%x@mb(-slLAqR=mM$`K|IV zOVSS}_#FxUaDwkh@ZW*AVNbiB@-=>6P11i1zOBuCWnoiO$%@cuU~%k{z2 z!KMZu#X=7s`C4v2EO}B`15jC|6c+>^o3&oaA%VLGO)h?dA)D%>%j6y4fEm4e-8b6 zEb_ZA?~|k}{`8c!7khY8-m$MP*XP*@-Yv|2&%A%Wm88EDd~>uO{|3AbeRw7LHGl2_ z-+!=Z|G$A1|6sc>?`L1b4}WPqHiGqixP$!G_?-=YaPNxTzVXXoec$gApFRz~WkXS3 z8yqf`_r?VOK3L!P3+?$RxOQM=?w|5$aObmyeE&_b;t_=P`!ooN&aVgjLU6eBzApIo ze=p>t-%Qfq0X}(I(Lc8)>31ghpA!6?1g}J2Q#^{$pQnOv`L~t1z2cX^cWqda-S6j1 z6&x)sPlIdNi>5q&0DJ=a+y^}VG5EG;Ezd4F+U0-D zHSjb1jgBH+`r^-lpMZVkMxTEp_+Ip1H-8?Roee(e$It^`|6Sm4ss4Si&U1wJzb8rm zbMOsk7yc)Iouq#mtnHeaAbKu*5ygaY} zRp4VIefMqPd+%Fm-{;fw?*bou0RJ`Lp7(&mrS^O}!S{moz2`TRrsuy4esi>*o^Wk= zTItX-GCp_;`$K;HpOd6F68x0kroEB={&Lcfe-rwrfBv=L?N32IUg6&VUhwtUC*Alx zIXnHC%m?f-t9|`Go|ONO;2n>w$iBMSJztqC*Z1F&{L5cgPJdE@pOfI{CU|FprxSb? zSl?F-^yAIo+ixrUqkcche>?czSF?ZeELZ>gz?Ve!*?&#)|2V^ky)tdLJn0*6cQ z+n(S#@V5O0eeNgeuTJpW68wh={w%l_t?z#Z>wL1Nq3VC^Z^5tE@%=l$K7Ss3E%N?} zq>CP`2Zu}beL;ff!5?@U`$>;=<-G#D691e11^2H*3q%k6u4!tY9+|C0oNJi-46 z*7qR7c&>UQ^cMe)Kp&n4egygO9{;{G!Mk5s$kQ(cKd^d5ZvQ+84%cz~xIV(G!MB3t zKQ4N#`W^z;SU)Fu`~~oJNArC!kG~#0e;oIYcoX^GS=47OSm*mf{b#_6hoWIp`Oo+S zKm4WV-%fhCgg*_|`Nl*1R{kF(>Cd7gPR@QthYf7JHi55uBmRay|ApX{=m(JHh&% zN$8&zc=Ph2Jy(Hmf`6(#;-5Ez!=?J~`OAP*UY31^^c(*J`@Eme|D5FiPJ)kpYx((K z0H5^G(b=+NU4L!^AA@{(6WLVXUrF-Mf%SdigFd|vzT}d^o_-Bj@d!iue%`+sc_@t6 zN0g4eU+G$JcYwpC_N)b;h`lA$=RWYE=ezHxsy;tR@*ju(87@74VuH_4@MYke;J0Bu zUYn%f01o=>vGo7@!1~^D;3ppiZ$E^*%DYsbJCoyd9G`TrE~ z6_NhF8yqgRXFkDKf&=~4e%Cc%#k&de&28W(oPxfz($(*8!5bHg_TCQ;m&*TEg1-;` z>Wd3{b1d@19iLs9)0=05!=>lXPw*W05b}GVAA1?ElU|H}gO@KZC%toKVeh^sdH(hU ze?GzAN$}HtC)CERhfN8-7<|;1mgn}~dGMWIS)Th(?*(t&$^MC7Kd(=oe{X_60lwoM zE3=EA==%2`lk|TBE51>fU(b0v`ab)6S5Zw{PvE;=x+>>KJ+Qt9ev?msHTdd(Tb5y> zwfxtE6|X3a@85uTW3Qj``Q=Ebcw?dbUjTkmUa`_)GAsDc+^_-It{QC-@-pC*;NG*`waU4}YnCPfhT16Z|U?UQPbl295`G`|JseT_g|LT_h41ON5J7y`p>=_9&-egk3G0an)1I894_U* z6uc5=s6HZr(ysyQds1ONy&rrKd3=hzO8*M@?#D0B(|-UCm&!ZlcLP#>)@On*1V0e* z|6Y=Q4R{Lwi~T&S_q`E(=UJ;P9+U99!HWNNrceJY_+IQSmwNm!;G6#bXp4U#`Qm$E z#p4X~`=sB4e%)2H?^nRtjz*Fo0|8>dp9|B*BKkCVrj{kU){^bP!Fu_lMpGtO@%HNvc7lAL1_CGF7(tjht zZviWwY@oM)13vS(71>osy8gHytnV96@h+{8W$$$AJ~EGVs@Pz)zrmPf!~D zyCcb81>c1}^QEI)c?Xm98^LSQN2K=YeYb(PV-G)Q3&-bfd09O3ouwD;z)xMK&VLc-c zo$T>u@b;IivUqe#zZiTn^MAWf-wzI#-uD);;^|(;Z{`1VlKzhg{#Jrl{2}xhc_57U zG2k0;me}meI{_Rny>C;3U!348z&A(q>>c2Zr!hZ&#?|i*@Ui&w9-=JO_cP$@S==>` zzXCop8t)&0Z)3f0_34lP-~8~G>az}f6!SmG^Unh-zV}t6sr(lBcJ$|CRyh1haJY`+ z$M1lz!XA1Ozm@-E;Q6O=K4g`zANW|#pQ(J&hi`yypDpC8<$uKd{7PXj{w45T_;1ep z@^*v6bsRscV8xf8;-%)@!@KckS?Suh9z3-kdCQl#Ey>?b@Na+> zuQ-hNA1j^pv6g>CzdsE=HkwZlgY~`YKrdE*q+Fjfz^jp0kN3|%AAB%6Pc{v{IHCt_ z@FTBSmHSV>8oWOm-?xCnrSaMR?tt8QeS&m-FD%sO+e!YFw^5&GV1M=f@nrB?+IJMi zsC~}@-xBR_tO18h?>j5OmnOKI;5UO$#NKf&&uYGX5PZ}=;(>U1?4wEk&n5WiKZU0bliykS*# zhfhD^qsULs#~=9!m;YGsF`s0=;24LW0oM1V!+LuzSl?SvGgN*X94@{8x54ufy*ZSm z{~h?vcXR&6kI#eP+xD-@{WZS_zBZ!QkNFt$Bl1`IMR2(EzH`A3V}DVI8`b9$<&U1< z3qJGt?1y-IeHFM9$=h#L`WK7!`n%vuqx!%6mB=e=SLOWq6QqYr@BcDb-!l#4zv}jo zs`_BhOYmvn2bh1ylcw?B4z5M?y_V#^2CR6VQ{-3s{y0hh9QXm~?Pj0;#U%Zk;FZjW z@capXMn-?Be!rODt<{j~>i>cyeHz@1{CoE&>2FH#dlP)#ufgvk|Hn@x>Gy;6eV!?* zq4pm2=Zr@*UdMv&p+>=*j^@%|+J zGhls>eY=1DA@HsE2krKF*&67 zI~EH6u9*pS~6xF4bozSl`zS^ZSY<{k7nS&MV}j4}-TIEcA!J1u-SnLjQGXKk)?We?7u}(7&EZ{@X7t^uOmO&%Y$W-?|lj z3we}(tUh~4zvr4lo_!1We)RJjeEMI2JJI;w4?cML%Iu&Y|K)$Bnz>8$Jq~>6Kym(l z6IkCnx=LxZ|5ES`-$A}#;czQ?{uK#+eS+Vc;EyEuZt(tJ#lC-nEB`Ct-LGDr`^W$4 z@4_D=`R*M0|B{Hm{*e5eqw^f=KII!{^~c{k!7oX07rdJN<88jZ*Cpw1lq{Ivuh zcW3$iKM(#6_QRWe`OizzcY>SO6F<$TPbcYDfTtq+)7z5tKL+djIbpoMmZX0leB$5X zAG_T3@2XEjk5C@M`uRCbDD{V1MAkT zTetp<)6?7Q?e^yM_6ECsMtXZAw>PX^8{M9#b@i^@UYF6zc1?}xSErlxY@uBXzv}&J zt=g|vde+M^qCMSgA-=;P6gQYgy?QoZZPl9fO0V9pbgG&J8nN(DNS%vQ!_Cqo;byhf zV$^4-VZA?3C0+5|URLceEh{tgER=d{j&z2;(rPQKCJuAAKkt4q9q8R&wclvB{Cud) zQf9VLYxS65`lm9@WU04mJF`b?KL?WrHod}6|`S-)Z9=5<+jKXuS7?lh|hxI2?ory1nlf!0jcV1dojJFJf# zJI~p@`^A-WFS_9D-RE6+L8Zbdop##B%&nDXb*56QGhXB+KNYGnFa4dK%jDk5va8o= zdaco}xI4Y9IWt%3bnAO5a?|=VvQD=>Q}6XEy{gu=uWdyGVvCf1uC^*0)@Md4d?EBs zuQ8*$-Bz#9%PDK~8Jn{zHKR%MZKm;bV~+l7R9m)+rW^fUW!)m#b+1T&4I|qE5DtoKV#d?0T+hFax)>8X=tuX^}vKtFPnljtyvZkw@K4aDp zg`iq>y*-BOH)hr^)VJAQtU>TJ_SKnwV{fB>pu(6pD3)6`9jbe)^9{gUx4qcO`ki*O zNn3jD=3eF;Q%=)m__(dzeA?Pz9qRgBwizd+UY^K?ucZv*Fv&v$J zHCoRc2+w$^zIMdjwYXAk_Nug{z1Z(8_O%wZka~@|mex)!o3A%J)>Vx@BigH078d(V zZ1=OVU(2_+VQZSzr+mGA9?kTdS-n|bs5EN%V}7L;SQ!e`i?cGL_ZCSD)%`Jo*iM%c zV_2(4S!ep)W|7mF17w7$-k$C6Z8tOi7GX-FGu`@Si}hYVW9>GqGcK=(d-RA~I&@L# zuFejPI;F%HeGb*N%sFZP>MvICqILbISab|DU>32?9 z>^GW?ext6X1FlqOjF!0h(J^+mf1z3F88=p@+Vo=Y6hD#>B-StE7ZbH55^H=4!lbR*>*Spy?-(INivH7G~sfp1;Pv``}xadkx6Qiyy z-3g?2UHQpy8;Qx$RQG(iy&=7Ad}p8xyE{;X-OWqz&9e#XUX?UBFEzQJmz&=&SE777 z&n@-EYc#%TBJkM-$Zl>)Sm){9XGrwU! zibYa-LAoU8>-!~g!bf|(>RcTW*yP@9eot+-0;fPb?DKC&WHhWc)+PT(7IKTvL*GpC zN_1#zG7p&mg%m0w4MlWFnq!Xb?g5wVD{onc62SHAkWojZY}^#)5%X8`I6#q_%51fP zASb=TgnA8D(`?ekZvPN(#ndm-EWS1PuX;ww%BFtL0V zv7B@%ZD?#Y4GHoW!nX~5WlnO#-U^agwYfF}?yYvwCm5yZ4vK-pG*U_&s?vtFn>K99 zD!CbN?Tzx1c5@YpJjEH58(8yHi+>H{T}Gu0YB*(gLpPz5k2CCwX~+r*Ef_>vKRl$yP_}+xlnhfaFki)!uA9+ot0=cWmJUom5pQO=+`=xkb)=l1z>S zPLy*v^&9zUO87=*96|F&<}Z`Gk=bHOH!_naU!y0*Vdz_L52m(0I2oP*pU&*FMo64+=4QOZ0S zk65>U!)YVCVi=pq-!Ym~rlSSETvT0;oP8<WNrS1#iepXzlXu2QZ6U5-!Jy38Gfqfe zw{hK|<`50VI4vT6r4y`2GmouBnL8#>E(VJ}EH@eb$RJx8%sUBx8UECSjAr9zj9+Ea zI#}K?jx!|JNF`-3?ch43^= z^cnILicLXv4m)bXoWkT}dc3^8X){LAF|SUG88x9!U%P%u z<-~IASDBn-|?0@ae#nc@~IY!lexDF84e4Hd1=gZ$P5qaM= znVK;41xq6Gd9}h*N24V?rp3-aZsds+wnAMkmQsb$WsTIK#tqOw z&6ZM^N#E!>zT2=je?8~9fV0zKdNFyj+n!^J;QKdR*-jc-e%QIhEbWEmL7Vtl$dmK+ zmY20uc8tbGs|F>_QB@vU#821fp}2{gVAaJ9w$U^{_N5Kes9pM`u}jhC&saae7n9P) zsq{%!CdSx(!ky*~c(&V^-7$e)KeYTHzl}5$k9}%T>KmGU5~*+GgOf>pc-#-{^9?O> znAA5s<4CD*X!cQ3-_Xn>q`vhVP9HD`4ShZq%!a7^hkYhe<<@UFV?bOU`ur%xe?(?a z*T!sg4u23@FBX*3-bCpg{ZK-7CwG0~1rVTs|FZKv;j;kPW}2 zFVm7nP14hb?Ef96sCd4{bMLev+kvBs8m`Ze$~~bmVF{1_m)~Ekh}5JoVJ(JR6_SU? z85WYP`GAkME$ob$%&y6-3!~n$R92H$FFbfyZj(RpQ!t#;vlH^1O@kYlqZ&T}+sSJ% zdV6zJ=@T-a{0*a)_NF15pXoa%F+8Ntgo!iU_An~t#Rr7T7F)Vk4_ibmIkMnQjKO*6UwD{bHh*hw2oi@*WSOx*-)}821_q=j+%K7iB zQL%_7B^{&MH(bRW)w*%I=BQ`#H;-02w+v&GyMhie-UIfqul=c+WnS@6!sPrifAqh? zGE0R_#x*yr+cYuP%!^+N<18LpGVjbET`K#`9~?LTIKLtZ9J@0v9y*Yb|rtq5fu8adz``s`3 z6zEQ+a_-J8=buwK=Yp-3N>+LP_6yJ2vb}QQx##XWXLn`ymb12>!`-bfzF^Dw=bcSv z6_kgoQs{!{21R!J#NX*}!4Ats2ow4O7D9QDRK*A8=GAYgm5UR^Lv(RusQJbFWov?GG@Uv`Iklh-C4 zqoXNi^3SGeRoo_g48$a|9D_J^ivehrJ!qhSn4(kBJ&ZgbUp2IdZHpXq=<8$Tsd+H!+Yb-j@ruQ`y(xsI_9k zgeYaWu4{ZM3b-?y`9ZsYI~R6JgQhXofoLDIg(z!2k`zZUE3-9k+&A5g!9tWhI+R4aFAUgtO!|Ev5;7yD6ND_E1R|WW^r#% zFiLKT5r4?$Kt^Dw`lgMFkw%mcVxU-57*(6j#D-!1&n_;EpX3?U^~*FF7y__$yA4Y5 zqRasoam3Ds7j2{Ld@{`Mb0Nlh7QU*tIPHSj6l1?;ZFgp4HpglH%0j16AqTBJd*_Q; zO6%5a^5K$nx?N}ZUCbsQ5TvYqCDV0aaai~}PW%KrT!olDYK9Ar<2Tv!YFoEAidq&S zi!7i`UI@-f1WzN%4Qp;jk#Q7}jph^X3eag2=w!M{%NqP3l9mhKrM@@+4GZUU)_E6d z5l>BBxZ|7)cJ10;S;zm@W@J*#E>V$q_}#2OueQs1Eih%bY~8)9-`=6vXvBuHuwu<# z#RZb;WwW;0S!)Fi(m?5m{tVH)AQ|f_v&2hS#23PsnanWN)*osi)S3>~!i;U+Fh_ON zNbSxR7ddWAbTLjV(;pmJuC*7YrY^eZysZo)tqJ36Ese%Ed08@wj_CLxL2foD4~*j8 zs&&k&i16kEKtU5-F9nkBv@IAzpE2vkJ!LCWF%;@9++ofI!XtVM6lS&o@hJJ^5WkGN zi+1=H8A*OwqmS{2ICB(f>%oHfF&gfcTO;Nzk)ISNRFhu=oI_@fxKLz7y{RvBIPO6O z*oGkHUoWorY}a-8OrtZ;4y_NJL5^Zw^{Vj9hK_Rd+KXLA(Z)^h_OYhQ3&otTFdn$P zP9kcwiM@Eh1#l_FlkgRGM36TqG8+tt7){CGC8J$~VhuOR53Xj5&gb=HmKkB?+KxG7 zc_2eu;Y2o4%pM9*RKV~6gQe(JIBZ!I5r{pffjKW+qAzLU+&$@Ei_+!$nziid;tp98qJe6|>G!wRsMOHsi5Ox=e2~ zQ)+&0--{Hbn}sZqPb3Q*WzlssR&JP$Nj90PYhg9a%&fiT+)Z_=%@K7_Bsv5*LbnjI~wcs=f ziLAn~TllnoCPLcbQ0D+8747j$frkjuR9Xq*{C-~8K_jDe3ODGPcuFQ%>OF?e9I-;+ z8e-HyKfL^-+7jE)p}7APSpTuuXy;5lzyEy$jn`L0Y!qcgNmT6N~GZSHT4>&}8spk2*Jw zfl+0)6EX~u6e)bmi!R!I-uB9An*u2abz-hrNK{AQSj1sc4@!Il5nUvb1CmhtaN30W zH(?C<95;b5X~xigmpkrI-lx)`8y0OtS^C{YL}&sOX(ldMRLE*yBrTd6{W@{`h;3Kt zuAE1~0N30U-E_pR={~&C84V zY6Oet;S5kR#U~pqUftkhM2gE6Z*ZceWA@7AWFiK(YSD*Wfo`3;VRuRhVU`f-MZ9hK z7P3pHy$_itW};;gG->*}AF z$S@>E=`40toKPMWi)Tdyv~u;u%;U4MCINaXtj?)6=h_PNMxe5BeX6V?@F7Oecvj=J z$WE2G!byEbb!Ta6OG*s%!|?>V(`)`7hX|$8s#s;3CW=9*7`2M^>SOkDN1QRCIftnd zj|3uSUg1ghczUIZ7#lN07KMKiMvDlGBjSg$$iko(0cepGkm5_+#m3i3V$e=Sw@j)s zO6u0iNjJw~UG07K63<`kH6h3dS?<-&EqKyb#lo<8aVYSLv|3t|A|3J@y5aoAe(Dpq1uVHE^OKO*op&k zw6o-37^@TOyuGNH^nsk%x6Z^JS|ONZtEVvC>1-a=eKrFbG;|Hchp*K4 z@^D_UKtsIh(B+E;{q{UWcpzuiX_ULem1*;8AhtPE12wrK;j-0ckvSt{+?hjo?>hH# z&QnG`NJwj9(GOH}g5?o&#IQgL=zA)$JGlqsZF6Qqd86%KjyW6mWm4i zQfU4Q^#x?GT&~7G*VxOGUJ(>&bpg>CrWQTUw$f5*t3*7nf9O$#EXjMMyVz>!M%52z z2zRe>^^5lG3%8zgcI7lamQj`*lLe`f5Gi9aJ9)gjX=5X(Z_tT0amWM9$@f__`}^&< zqe_%YE33o~gH2Q~Tg2+XM6ro%1_;|U?bPdg(wdIqyT;Kp@4QNC#wx__nTEwYbe&yn z12R(jv`JiB`@n&>iWs4eOp#(l&IE#ia}Q&n`Z|+REMCAbFbIcc^F&Q#7uA@FSa1RE zY5&2=Me3EzNjGtx@#CXe5Qbeg)YeR=^agJAq+N6h|nj;)DZJl7|()9z(qL@`|eR)g5Mnw$rTAe$S`Iw|E9=ZJ3jEJClXapNQ|J zJ6}wSMl7g0!4ol1#)RDT;yrzK+?zGrqbB~wl zL|}t-uT7jGJV6v9>J}zn6*Vc;$f*rR2uvr*D`QhAo$dQv0~yHBhA}4w0oHeQs2%t-}*9mNG+|MVl8(bCRHVIrY;1W-2+jEtlDDn@?C^ zhUS}Qq3e1$n8agdTkO#LX*=<)Fenv8hmqSB2%=3y8U2N-}uEUSkWl zyj^KzhHYcn*6D6|S9z)pEhhe|ilZ}rW6SLinX%Dk)&zw?#qCQlVI6yI4~l-+Z9~uc zjDP!H3mT_Up<+d68Zmi?q^d3Qc!!-AObg<}m6EY4&?rsYMVWB(IrcKnrpL1rnl%6KW2(k3JZm!-%YrLWFs*}dfE1)y#07Ull8idU&k7b zhB-Ppe>4A-c`lt*tvNnzWYBw?#naP^Kjt1KPb`yh?9p9*YR6CVMLm;jaWo>lIX&t$ z0rxGAFd}aJoI->74H=iOtF>sF9z`x%$W1n}@4)yu!P>UZRk|(QG9ele22J+T?!Ja5 zrp=;a>N`6Zz-(Zx$pl{4ww9T`jAa@H2pfqpvm6?7v`|5k7U*RIVOzzP=;dUt1h(Y` zVK{`j@Yv>KuJY!jLY4zR=JLkKoc~amBu=qSOeoBLjM3#a+hpgybVobpZkNdkC{_Fo z@;Ok@qf$LmR^n|eCWeavu)z_?Rk69JwdlmBXm8Iub$LglMLvesXZ`cVNAUy+?p{7| zbF<#RkS{oO-Q3>Bm+svMlM(7IWlT;s>s-LZDFy+r?BG1Z7WGz z$3YWK`yi3lRN`>IswAT$^OCsoK~~!XH7a%sl0Hw0(fbmw2a_s7p*a=Nkj4z0jnqI3 zFE=-={6oVPYyw_pK!Ra^6Q>w&qBA8-+cRdSvsvOk#h9z_`fjoR8fQV^gKV>uX`bQ9ba;P9D^ie}I^MlsGAx5)g6pOWM=F3_Y4uDF+cOJF5&Q6a$g2bml(l#c=4AXHJ_Sg=+hQOO) z%0g}7NymH4irkmGc+nmhxm>`!eCRVJotf0b=5}e*FZh9I%Gt+@G&;_cPy0sh{6nf_It|IqsmLWqVc z7?o%1^_n@pd3C{XQyTs$({4 z^Qnb4`{X0Oagi)PO=l=dDN@J6p4rB1X_F?1m$*7~9BGJs@SX4&4}PhYW@?yg1lM>L zkEsE6Rx)r(K5eaUcA`x{52{_QM7HUrS%ffqq&a8Au(ReqNUUiRc@-AKH=#OlCyF(o ze}u^QYMk5fhhpgK-viEhc_pM3BmYvDb4kJ9Z%w=YE~g+J1Xa*+Md=3mg;4XH$|hnIlG`wFBdXFE4vi zDXEJKEK;qB4*m~)HlRyH%&YdpyzSAy`IdRuG4mpB(KvgRW@b1_#T?IF>rB+(Q|3c< z*VLpSQ}w&-3&qotkGbn3(auxCcIT!MLE;>xk#_SfaB;WS5@${=3HcU_=*LC&mGN{A zEb4ojwpl^d>T~+QNQyT~ z3K&>ZNgse}21@>lLklyp=w z8C4smmk*}usrX#5Ai%!(&0drNpJJL6 zn;qpaf%8$kU7LR;dJY@8yk$(3s{2})c;;-gsoXNY?ZzRZ(AkbrOz@0J0;b0V5u>nR zX(GDs07sIWpBYN?T84bAy$V``Cs-TIww4Hao^j`bhE5jc9|RVw=+zTBJtt>+8_UKS zyYtglPE0rqiPEeHOhww6dfoevL{4FWa|}$mLgTV#;(*2)sCd2?wz2UjaR-%b#!%K? z+3(|7k&MKk{GMi@))5WBeyc8!c^RMNd3IvVqT=bBnrPS^u5|MUp`Y`W(~WA+nWk-^ z&GK78yOf;O(l_MgBIFF}ZfZMqP1f7ODbbOVLUu2&>QQ7)wemVnVaE&llRtF~?J(~p z3@b&RKHkMJ%>e^evfK-0D&B6h(^{hGa@d)yqJx8>b0w{|Tql}#{LkbCl>9dQh2vJ; z>OQwYKo@dqC)zH6s&sXD4y(MeY`1k#GVAyQQ0WF7M=0X?;@M2Rqg?PAW3l)SX%^_K ze0~7kY$%Rf6$@EI;r%JZM2poNJh0t`i~caK`>>QXP}8hK}VqR83$j}GCxo++kwnEo0!(|L2| z<9vjmaXNO5$CIXDG2<|hGoN$9mkaBRmo3$Nk5p8m&n#s32|YCK&IuQT6He4RXYg?= zbU>-5?4mk)sTh!{;qh@6ecv7JgahDqShA3!v^banE?Q4gGnM9PGI=8RjHsZ_%I)NH^3fo!eq7UQdZVzu`dAI7UfV{` zNb$(2bFM=$LfCyi&Y8ZuArDh3s#vKRY{z6kv)#v32A)h&if^ zUIek8k+eBkF5_gE5wU=h8EAsH&q@@4)?6<*>J)3hxlD1+s8E&|NgOpIZnuPr*@E72 zjtmRva?{w$@ZQ?3dZU$vYy+xqf$cj822rYeCw3DmIz8?PSmHY-n(XanckeLn0oMFL=2q6 zu$UyBI&@xWdq7qo?@yLKBd!pr2Mdj*SCc<&A6uMyq>D$GB86x{+3tAQij`$13MLe& zOIarCO*i_Ha{#idKgz~SJ>}9ECvFa*tlTpy>dj&6(Fr9!OpxslEfUj0q}LR1+i?mc z&54rGtzv`GgeBR?gMGkpN9|k0y2zNm9u%-Udv^K#D?9vGrXMEj*tUo!Y#?^Q9P9bx zu!hUZd`qVL&G&1I{o^KxpP&r@bB!W0`ykP)YRdInxtuOrDvmc+fL{@N17 z+{|t)H?^`%Nc5+CotB6!O!3ZFiw;lRyaxM>!dr=cJZv3nnbihOTP)A=>c-td9qko- z9o03J#CDg>PGSv_(u;bSbE(DB$%DviG+j3-nGB{z|3NUXxtahNWMH&&xu2H&R>G!K z-Na4WW@IL~Ero?yi~&Vxfl7@*l(3KFa?3WH>2Ar&#VnA&V-jpDi2j%_6m^6}9+`~1 zRmk%w-vS#~?FOdgUi41ljM>I|<{_)C$#}Xc{03WcTH+;{E|>R2d|Hl1MBHxtT#@MH zBr9SZA$4f4OvZtjn)(u;_CsQJ+1ypz&*kZhw(i)0h2BD&RRC~!Q0_e_g9NiEcn`Yy z7@xHfMMKCbc_e6zI6mr4*CBwJc0K8!UX`I!NUN|U9ACL?vDzI_=+il-g=*SE$#X8} z$7O54u!nOyxL@UwM?x{h%GNv!y^x#zHDJ#4HU4E`HAEJ>bODVLO{{^rl{2BbLmOpZ zt@9&b?BBPpWPUB$mus1NljZlOgO+!U!>gjc*BREbGwTN7-T%9i~Eix~KI+`PH7uT~= znS((%(k*{@tyE9Ez1~n-k3Ra7GipJnXXZ<-jtm4rvR$9p6+-+rV-5N8ik6mKLI}_^ z=$BibXt9W+F{0)E`{ycoNJOl~mOlX{>D`VJh|f#25kbrM^1T2z-V!*>qXkj}6Exw> zmtjoN9PN_XFl_SDUs#2lHCyUPkOqQ6QZgoc0-Z;zvVOOqPPDz?eubXnSkkakd@Al5pj zo=IFT1{l(=n`Q;sFtb@@y4`vi;bG-iZwggF1bBT4Y zgDl#6-P>!K%wF=R3Z5{cAWmj3OUffX|4c75f5~w2qC6IwEL1?$Z_Fm2L<6VLgo=3H zuAK~mgd~>Bpi2$m>N&5+5mT{n98tx_TMC3DRVEK7k&kDh$~5LI#2#H9lT7ozs7{-y zymgtxck@LYKZ9}`zFX%`O70Lq0V_LZG>M?u942m0!2QsttTLI`qI<~eC6AiJR6~-3 ziF;To#jAJE+Zr4StRHi6(~VqVL-hPzHn>W?rcbS%QNdWD z(;>)QM#yHH)wvJ`(}|~WUpopCF~aekTv%d=%_fh*;Q#J;aSN~MWsI%7 zgM<5*=6vD;Cc>b3Yjd$g7Fk1`_wILz68&~JdAaWn4DGUN?Fv%53{9v2X1PcLCaKf3*zkf zz2>nGs2WZlY1JCnwG$*dGvZEJ7o$`A)EJz_p|+2-yaRRWM0V_Mtst4Q5l*mKF}juVnYJw z6k)xBB^o^k<+HR{B(Rt1gO)jx4R#N&HWdaT)M=OthHZFz4^@vGg|)=T{uxgPO)`iS zzo4Jy=k_nJcl{U+xXG(GtS)(wQ$|4>LX548qdvjAy~bsSY28LZ4I#(|FVnFd$x(TE z8okKWS7;>E15u*binbLzgo~x#&CaUakxd+55l9zpq7%(4%lQR*BTSZyqUi>AZuWr~ zXuC8_Jb#SKuL#xJ)YE)LcoI?!W^QC7OsVV|#YU}~O+JEMB^{~}@X|> znGVbkt`09lc?-ECF6Oplb4zFTwX&r6NjxQ-tCK%Vn-!E=k~Ul^s=t%XwItGxi$uMJ zHnP9Bb-Os}euSd|$SrX4D}ZYpE=-%aSuO{f6+H|_+4ofezgWF8Kyh9{FR%+x9w z((D0nwN&DVFaY0Qy^vhA;!WNvFV2nW$b3*tq-$u=n7Ce4&aU*^r;%f+`<$HPRL zt?T6ygGJJ4bndN`nf~qtW^;HFAMgROwbvNPtJy19e2gr^mb(K6t_E@CJbwW^;3CIppbh?^)x5`Akj@|0rNfT)JPAI5Fx!L1n3TxL{KdP z3p=Vc^A}xGjJYTbYM1-kIGD7G35KBnlTqL zAv`7z?vNaXI*u7I;1uphov=88S z!%KwE6c!W*1d8O`T7HPCd?+n94k+GOME>v@_cO@6sJwvdeVSNlElyKw(Xk&KeL}=o zT&Y4bnR_EE|LQzfun5t3F(F4j8iSg*Q@m^`MvJ;PXdyk!2#1|6PXsM|v-y$fLjpkq zoXb>mK*K^=PgT2RHVW8rA&7kCOCNmmF;O- z(BgE899|MNdO5iFLU?%knyKo&zoRWrw*uqdN#^CKmm1*{{qf5p2$e&;VR(6n^(&Qh zrn#tZRh2_VB# zN(_a3BMbG*qIV#=m;BqDj^oZuny9bgGaHE8Hf0;hStIbEL0>if-wdUpPU?uQjp`ig zJdCxsD|`^dS4tQ_BB^ZB_qnA-@5<==)o20>OuDCAeLKf$zu!J5Q9i$MFRC!0bZ$NYYx-mSpG8c+0twW0LYzr#XJ5 z{p5%=vlW1pzB&6cJfp+RP>xK;Gj@W7`Rh+c&Aam}<4C4j8cmWwNiw?WKE9hHPL_( z9`iBL8qvA31>g~*@0mKoozY@vDPal+Z8*0bSbP_W@BR#l?Hm=GqW(Z!9KqYKo17_X z-&EwHmTs*l?Tst12~G7v*b1i{n!CAnos$REA=FUw-W)vU>|?Dya;rv!0Eo%KJ;l^U z|K%6~TZJxwn!L1C%}fHBKATgB*#!kty@K^OsYSwuO{~*w4a6pPwv`WXh%g=E1zM<9 z>R`{<7DwQd7^o3>i2W`jv&D(=OWA9##rm~ldr9S!YR0`BO%bTFLQcr}T$|)*e_lBY z_RF|~)i$$Tnr6IL+vJg@GaS)~a#`Em+thB*B5Axgs>m=%R# zO^v+X3ik>I%=Y|}HjJW6vK+)g^6Bpa{vO4j^6hxSGXApc{%2&_5nvv%Jje3yi}+i? zpYpBUw2Z$jJLl&C{!(6DzsBEB{CoGlbXm6drCD|+FADF~^ZMJv-;YE6s?@KVWp`7E z-ur0&R5x9h^LI3VdgiK`W&H7eeuwgu?!W;k zpM7L@_t{5Sy_Kdby!V~_)_c|7d&ze%`4;x+j=Ms>_W-%u`3?Tv_OYY*JJQNi-{}hZ z{*d3HydC7*LB5a{=ldAoG5l$acl`O${0-*QbD!exu|D5!^6egx?{nl)AFI5J$#*gN zZl)r-r%PpN+`h!0>Zg3!9gN@|M`qiV;4kIV-#_yg^6j{j`rUbCw(-e66aR!qzV3gk zysN)*bavHOj?6Cpz+k=yxuxkKFd&zfd0Q{>wx>oTc z{1*NK`COxfiZqs3Ip6<(WcI&5r53s?l(9O=clG;^$sT`x$W@fFCdv2q?;n$WT*Sj) z;n#DMe3#BYCR_cAkgJ$4&-eL4za0F=W3rEZJe1GztC+8J}$d7B$5_> F{y!l6t#beX From 82c7f4eff07148b002762a954d8ea404490e35d3 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sun, 26 Dec 2021 12:13:42 -0700 Subject: [PATCH 514/878] extract several pieces of client metadata for AP2 --- MQTT.md | 9 ++++++++- mqtt.c | 12 ++++++++++++ rtsp.c | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/MQTT.md b/MQTT.md index ec41a0b1..95779c76 100644 --- a/MQTT.md +++ b/MQTT.md @@ -107,7 +107,10 @@ Here are some of the `core` codes commonly passed from the source: Here are the 'ssnc' codes defined so far: * `PICT` -- the payload is a picture, either a JPEG or a PNG. Check the first few bytes to see which. * `acre` -- Active Remote + * `cdid` -- Client advertised Device ID * `clip` -- the payload is the IP address of the client, i.e. the sender of audio. Can be an IPv4 or an IPv6 address. + * `cmac` -- Client advertised MAC address + * `cmod` -- Client advertised model ("iPhone14,2") * `daid` -- DACP ID * `dapo` -- DACP Port * `mden` -- a sequence of metadata has ended. The RTP timestamp associated with the metadata sequence is included as data, if available. @@ -120,7 +123,7 @@ Here are the 'ssnc' codes defined so far: * `prsm` -- play stream resume. No arguments * `prgr` -- progress -- this is metadata from AirPlay consisting of RTP timestamps for the start of the current play sequence, the current play point and the end of the play sequence. * `pvol` -- play volume. The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. The "airplay_volume" is what's sent by the source (e.g. iTunes) to the player, and is from 0.00 down to -30.00, with -144.00 meaning "mute". This is linear on the volume control slider of iTunes or iOS AirPlay. If the volume setting is being ignored by Shairport Sync itself, the volume, lowest_volume and highest_volume values are zero. - * `snam` -- a device e.g. "Joe's iPhone" has started a play session. Specifically, it's the "X-Apple-Client-Name" string. + * `snam` -- a device e.g. "Joe's iPhone" has started a play session. Specifically, it's the "X-Apple-Client-Name" string for AP1, or direct from the configuration Plist for AP2. * `snua` -- a "user agent" e.g. "iTunes/12..." has started a play session. Specifically, it's the "User-Agent" string. * `stal` -- this is an error message meaning that reception of a large piece of metadata, usually a large picture, has stalled; bad things may happen. * `svip` -- the payload is the IP address of the server, i.e. shairport-sync. Can be an IPv4 or an IPv6 address. @@ -135,6 +138,10 @@ The MQTT service can parse the above raw messages into a subset of human-readabl * `artist` -- text of artist name * `album` -- text of album name * `client_ip` -- IP address of the connected client +* `client_device_id` -- Client advertised Device ID +* `client_mac_address` -- Client advertised MAC address +* `client_model` -- Client advertised model ("iPhone14,2") +* `client_name` -- Client advertised name ("Joe's iPhone") * `dacp_id` -- DACP ID * `format` -- ?? * `genre` -- text of genre diff --git a/mqtt.c b/mqtt.c index 912d618c..b898ab9e 100644 --- a/mqtt.c +++ b/mqtt.c @@ -173,6 +173,15 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'clip': mqtt_publish("client_ip", data, length); break; + case 'cdid': + mqtt_publish("client_device_id", data, length); + break; + case 'cmac': + mqtt_publish("client_mac_address", data, length); + break; + case 'cmod': + mqtt_publish("client_model", data, length); + break; case 'daid': mqtt_publish("dacp_id", data, length); break; @@ -196,6 +205,9 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'pvol': mqtt_publish("volume", data, length); break; + case 'snam': + mqtt_publish("client_name", data, length); + break; case 'svip': mqtt_publish("server_ip", data, length); break; diff --git a/rtsp.c b/rtsp.c index cd76c537..408d6132 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2613,6 +2613,19 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } #ifdef CONFIG_AIRPLAY_2 + +static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, uint32_t metadata_code) +{ + plist_t item = plist_dict_get_item(messagePlist, plist_key); + if (item) { + char *value; + plist_get_string_val(item, &value); + if (value != NULL) { + send_metadata('ssnc', metadata_code, value, strlen(value), NULL, 0); + } + } +} + void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; debug(2, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); @@ -2845,6 +2858,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", conn->connection_number); } + +#ifdef CONFIG_METADATA + check_and_send_plist_metadata(messagePlist, "name", 'snam'); + check_and_send_plist_metadata(messagePlist, "deviceID", 'cdid'); + check_and_send_plist_metadata(messagePlist, "model", 'cmod'); + check_and_send_plist_metadata(messagePlist, "macAddress", 'cmac'); +#endif } else if (conn->airplay_stream_category == ntp_stream) { debug(1, "SETUP on Connection %d: ntp stream handling is not implemented!", conn->connection_number, req); From cdbb0484f4f85ebfad76e5e46eb08dfd0c86e501 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:14:55 +0000 Subject: [PATCH 515/878] Fix for dbus restart. --- docker/Dockerfile | 2 +- docker/etc/services.d/01-dbus-daemon/finish | 2 -- docker/etc/services.d/01-dbus-daemon/run | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 docker/etc/services.d/01-dbus-daemon/finish diff --git a/docker/Dockerfile b/docker/Dockerfile index d8241654..9db54c89 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -67,7 +67,7 @@ RUN make install ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12 +FROM crazymax/alpine-s6:3.12-edge RUN apk -U add \ alsa-lib \ diff --git a/docker/etc/services.d/01-dbus-daemon/finish b/docker/etc/services.d/01-dbus-daemon/finish deleted file mode 100644 index 1809d88d..00000000 --- a/docker/etc/services.d/01-dbus-daemon/finish +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/execlineb -P -rm -f /var/run/dbus.pid \ No newline at end of file diff --git a/docker/etc/services.d/01-dbus-daemon/run b/docker/etc/services.d/01-dbus-daemon/run index 488884bd..7bb0350e 100644 --- a/docker/etc/services.d/01-dbus-daemon/run +++ b/docker/etc/services.d/01-dbus-daemon/run @@ -1,2 +1,3 @@ -#!/usr/bin/execlineb -P +#!/bin/sh +rm -rf /var/run/dbus.pid dbus-daemon --system --nofork \ No newline at end of file From 966d68bdec9655eefc5cac9ad82d38eae4cfdc42 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 27 Dec 2021 12:19:01 +0000 Subject: [PATCH 516/878] Begin to get SPS to give a unique shm interface name to nqptp. Also clang format --- common.h | 3 ++- mqtt.c | 1 - nqptp-shm-structures.h | 11 +++----- player.c | 22 ++++++++-------- ptp-utilities.c | 57 ++++++++++++++++++++++++++---------------- rtp.c | 14 +++++------ rtsp.c | 41 +++++++++++++++--------------- shairport.c | 54 ++++++++++++++++++++++++++------------- 8 files changed, 119 insertions(+), 84 deletions(-) diff --git a/common.h b/common.h index f4139e56..904b9279 100644 --- a/common.h +++ b/common.h @@ -152,7 +152,7 @@ typedef struct { int mqtt_publish_cover; int mqtt_enable_remote; #endif - uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number + uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; int udp_port_base; int udp_port_range; @@ -302,6 +302,7 @@ typedef struct { char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist + char *nqptp_shared_memory_interface_name; // client name for nqptp service #endif } shairport_cfg; diff --git a/mqtt.c b/mqtt.c index 912d618c..462ebd2a 100644 --- a/mqtt.c +++ b/mqtt.c @@ -199,7 +199,6 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'svip': mqtt_publish("server_ip", data, length); break; - } } } diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index f4db8c6e..82837778 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -20,14 +20,12 @@ #ifndef NQPTP_SHM_STRUCTURES_H #define NQPTP_SHM_STRUCTURES_H -#define STORAGE_ID "/nqptp" -#define MAX_CLOCKS 32 -#define NQPTP_SHM_STRUCTURES_VERSION 6 +#define NQPTP_SHM_STRUCTURES_VERSION 7 #define NQPTP_CONTROL_PORT 9000 -// the control port will accept a UDP packet with the first letter being: -// "T", followed by a space and then a space-delimited -// list of ip numbers, either IPv4 or IPv6 +// The control port will accept a UDP packet with the first letter being: +// "T", followed by the name of the shared memory interface, followed by +// a space and then a space-delimited list of ip numbers, either IPv4 or IPv6 // the whole not to exceed 4096 characters in total // The IPs will become the new list of timing peers, replacing any previous @@ -37,7 +35,6 @@ struct shm_structure { pthread_mutex_t shm_mutex; // for safely accessing the structure uint16_t version; // check this is equal to NQPTP_SHM_STRUCTURES_VERSION - uint32_t flags; // unused uint64_t master_clock_id; // the current master clock char master_clock_ip[64]; // where it's coming from uint64_t local_time; // the time when the offset was calculated diff --git a/player.c b/player.c index db7ec169..27b8e692 100644 --- a/player.c +++ b/player.c @@ -1064,21 +1064,23 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; if (lt < 130000000) { - debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + debug(1, + "Connection %d: Short lead time for first frame %" PRId64 + ": %f seconds. Flushing 0.5 seconds", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); } else { debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); } -/* - int64_t lateness = local_time_now - conn->first_packet_time_to_play; - if (lateness > 0) { - debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 seconds", - lateness); + /* + int64_t lateness = local_time_now - conn->first_packet_time_to_play; + if (lateness > 0) { + debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 + seconds", lateness); - } -*/ + } + */ } if (conn->first_packet_time_to_play != 0) { diff --git a/ptp-utilities.c b/ptp-utilities.c index fa1c6407..78a43ed4 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #ifdef COMPILE_FOR_FREEBSD @@ -148,27 +149,41 @@ int ptp_shm_interface_close() { } void ptp_send_control_message_string(const char *msg) { - debug(2, "Send control message to NQPTP: \"%s\"", msg); - int s; - unsigned short port = htons(NQPTP_CONTROL_PORT); - struct sockaddr_in server; + size_t full_message_size = + strlen(config.nqptp_shared_memory_interface_name) + strlen(" ") + strlen(msg) + 1; + char *full_message = malloc(full_message_size); + if (full_message != NULL) { + *full_message = '\0'; + snprintf(full_message, full_message_size, "%s %s", config.nqptp_shared_memory_interface_name, + msg); + debug(1, "Send control message to NQPTP: \"%s\"", full_message); + int s; + unsigned short port = htons(NQPTP_CONTROL_PORT); + struct sockaddr_in server; - /* Create a datagram socket in the internet domain and use the - * default protocol (UDP). - */ - if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { - die("Can't open a socket to NQPTP"); + /* Create a datagram socket in the internet domain and use the + * default protocol (UDP). + */ + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + die("Can't open a socket to NQPTP"); + } + + /* Set up the server name */ + server.sin_family = AF_INET; /* Internet Domain */ + server.sin_port = port; /* Server Port */ + server.sin_addr.s_addr = 0; /* Server's Address */ + + /* Send the message in buf to the server */ + if (sendto(s, full_message, full_message_size, 0, (struct sockaddr *)&server, sizeof(server)) < + 0) { + die("error sending timing_peer_list to NQPTP"); + } + /* Deallocate the socket */ + close(s); + + /* deallocate the message string */ + free(full_message); + } else { + debug(1, "Couldn't allocate memory to prepare a qualified ptp control message string."); } - - /* Set up the server name */ - server.sin_family = AF_INET; /* Internet Domain */ - server.sin_port = port; /* Server Port */ - server.sin_addr.s_addr = 0; /* Server's Address */ - - /* Send the message in buf to the server */ - if (sendto(s, msg, (strlen(msg) + 1), 0, (struct sockaddr *)&server, sizeof(server)) < 0) { - die("error sending timing_peer_list to NQPTP"); - } - /* Deallocate the socket */ - close(s); } diff --git a/rtp.c b/rtp.c index 348ac869..cf53c545 100644 --- a/rtp.c +++ b/rtp.c @@ -290,8 +290,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -302,19 +302,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " diff --git a/rtsp.c b/rtsp.c index cd76c537..17fcf397 100644 --- a/rtsp.c +++ b/rtsp.c @@ -536,7 +536,7 @@ void release_play_lock(rtsp_conn_info *conn) { } int get_play_lock(rtsp_conn_info *conn) { - debug(2, "Connection %d: request play lock.", conn->connection_number); + debug(2, "Connection %d: request play lock.", conn->connection_number); // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but // interrupted an existing session int response = 0; @@ -1176,9 +1176,9 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes ssize_t buflen = 4096; #ifdef CONFIG_METADATA if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) - buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded + buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded #endif - int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay + int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay char *buf = malloc(buflen + 1); // add a NUL at the end if (!buf) { warn("Connection %d: rtsp_read_request: can't get a buffer.", conn->connection_number); @@ -1348,9 +1348,12 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { char *string; }; - struct response_t responses[] = { - {200, "OK"}, {400, "Bad Request"}, {403, "Unauthorized"}, {451, "Unavailable"}, {501, "Not Implemented"}}; - // 451 is really "Unavailable For Legal Reasons"! + struct response_t responses[] = {{200, "OK"}, + {400, "Bad Request"}, + {403, "Unauthorized"}, + {451, "Unavailable"}, + {501, "Not Implemented"}}; + // 451 is really "Unavailable For Legal Reasons"! int found = 0; char *respcode_text = "Unauthorized"; for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { @@ -2302,7 +2305,8 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * uint64_t length = 0; plist_get_data_val(the_item, &buff, &length); // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); - if ((buff != NULL) && (length >= strlen("bplist00")) && (strstr(buff, "bplist00") == buff)) { + if ((buff != NULL) && (length >= strlen("bplist00")) && + (strstr(buff, "bplist00") == buff)) { // debug(1,"Contains a plist."); plist_t subsidiary_plist = NULL; plist_from_memory(buff, length, &subsidiary_plist); @@ -2756,9 +2760,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) memset(buf, 0, sizeof(buf)); inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); // don't insist there are in the same subnet... - //if (!different) { - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); + // if (!different) { + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); //} } } @@ -2773,12 +2777,12 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); // if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == // (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); - // no longer insisting they are in the same subnet - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); + char buf[32]; + memset(buf, 0, sizeof(buf)); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + // no longer insisting they are in the same subnet + // debug(1, "%s is in the same subnet as %s.", buf, ip_address); + plist_array_append_item(addresses, plist_new_string(buf)); // } } } @@ -2838,8 +2842,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); - - resp->respcode = 200; } else { debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", @@ -2875,8 +2877,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_dict_set_item(setupResponsePlist, "eventPort", plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", - plist_new_uint(0)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); cancel_all_RTSP_threads( remote_control_stream, conn->connection_number); // kill all the other remote control listeners diff --git a/shairport.c b/shairport.c index 88db7924..eaa020fa 100644 --- a/shairport.c +++ b/shairport.c @@ -289,7 +289,8 @@ void usage(char *progname) { #ifdef CONFIG_METADATA printf(" -M, --metadata-enable ask for metadata from the source and process it.\n"); printf(" --metadata-pipename=PIPE send metadata to PIPE, e.g. " - "--metadata-pipename=/tmp/%s-metadata.\n", config.appName); + "--metadata-pipename=/tmp/%s-metadata.\n", + config.appName); printf(" The default is /tmp/%s-metadata.\n", config.appName); printf(" -g, --get-coverart send cover art through the metadata pipe.\n"); #endif @@ -1262,7 +1263,7 @@ int parse_options(int argc, char **argv) { strcat(temp_metadata_pipe_name, config.appName); strcat(temp_metadata_pipe_name, "-metadata"); config.metadata_pipename = strdup(temp_metadata_pipe_name); - debug(1,"default metadata_pipename is \"%s\".", temp_metadata_pipe_name); + debug(1, "default metadata_pipename is \"%s\".", temp_metadata_pipe_name); } #endif @@ -1320,7 +1321,7 @@ int parse_options(int argc, char **argv) { char temp_pid_dir[4096]; strcpy(temp_pid_dir, "/var/run/"); strcat(temp_pid_dir, config.appName); - debug(1,"default pid filename is \"%s\".", temp_pid_dir); + debug(1, "default pid filename is \"%s\".", temp_pid_dir); char *use_this_pid_dir = temp_pid_dir; #endif // debug(1,"config.piddir \"%s\".",config.piddir); @@ -1445,6 +1446,14 @@ void exit_function() { #ifdef CONFIG_AIRPLAY_2 if (config.regtype2) free(config.regtype2); + if (config.nqptp_shared_memory_interface_name) + free(config.nqptp_shared_memory_interface_name); + if (config.airplay_device_id) + free(config.airplay_device_id); + if (config.airplay_pin) + free(config.airplay_pin); + if (config.airplay_pi) + free(config.airplay_pi); #endif #ifdef CONFIG_LIBDAEMON @@ -1462,6 +1471,7 @@ void exit_function() { config_destroy(config.cfg); if (config.appName) free(config.appName); + // probably should be freeing malloc'ed memory here, including strdup-created strings... #ifdef CONFIG_LIBDAEMON @@ -1833,32 +1843,39 @@ int main(int argc, char **argv) { // use the start of the config.hw_addr and the PID to generate the default airplay_device_id uint64_t apid = nctoh64(config.hw_addr); apid = apid >> 16; // we only use the first 6 bytes but have imported 8. - + int64_t aid; - + // add the airplay_device_id_offset if provided if (config_lookup_int64(config.cfg, "general.airplay_device_id_offset", &aid)) { apid += aid; - } - + } + // replace the airplay_device_id with this, if provided if (config_lookup_int64(config.cfg, "general.airplay_device_id", &aid)) { apid = aid; } - - char apids[6*2+5+1]; // six pairs of digits, 5 colons and a NUL - apids[6*2+5] = 0; // NUL termination + + char shared_memory_interface_name[256] = ""; + snprintf(shared_memory_interface_name, sizeof(shared_memory_interface_name), "/%s-%" PRIx64 "", + config.appName, apid); + debug(1, "smi name: \"%s\"", shared_memory_interface_name); + + config.nqptp_shared_memory_interface_name = strdup(shared_memory_interface_name); + + char apids[6 * 2 + 5 + 1]; // six pairs of digits, 5 colons and a NUL + apids[6 * 2 + 5] = 0; // NUL termination int i; char hexchar[] = "0123456789abcdef"; for (i = 5; i >= 0; i--) { - apids[i*3+1] = hexchar[apid & 0xF]; + apids[i * 3 + 1] = hexchar[apid & 0xF]; apid = apid >> 4; - apids[i*3] = hexchar[apid & 0xF]; + apids[i * 3] = hexchar[apid & 0xF]; apid = apid >> 4; if (i != 0) - apids[i*3-1] = ':'; + apids[i * 3 - 1] = ':'; } - + config.airplay_device_id = strdup(apids); // now generate a UUID @@ -2017,9 +2034,12 @@ int main(int argc, char **argv) { debug(1, "run_this_after_play_ends action is \"%s\".", strnull(config.cmd_stop)); debug(1, "wait-cmd status is %d.", config.cmd_blocking); debug(1, "run_this_before_play_begins may return output is %d.", config.cmd_start_returns_output); - debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", strnull(config.cmd_unfixable)); - debug(1, "run_this_before_entering_active_state action is \"%s\".", strnull(config.cmd_active_start)); - debug(1, "run_this_after_exiting_active_state action is \"%s\".", strnull(config.cmd_active_stop)); + debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", + strnull(config.cmd_unfixable)); + debug(1, "run_this_before_entering_active_state action is \"%s\".", + strnull(config.cmd_active_start)); + debug(1, "run_this_after_exiting_active_state action is \"%s\".", + strnull(config.cmd_active_stop)); debug(1, "active_state_timeout is %f seconds.", config.active_state_timeout); debug(1, "mdns backend \"%s\".", strnull(config.mdns_name)); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); From 42e508b449d2708a513301dc385ae16d59b9a190 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 27 Dec 2021 12:33:14 +0000 Subject: [PATCH 517/878] Add some conditional compilation statements to avoid a few warnings. --- rtsp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rtsp.c b/rtsp.c index 408d6132..f6930dd6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2614,6 +2614,8 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 + +#ifdef CONFIG_METADATA static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, uint32_t metadata_code) { plist_t item = plist_dict_get_item(messagePlist, plist_key); @@ -2625,6 +2627,7 @@ static void check_and_send_plist_metadata(plist_t messagePlist, const char *plis } } } +#endif void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { int err; From 636ccf98f08bc3634bb8f5520b58e677cedc040e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 27 Dec 2021 12:35:08 +0000 Subject: [PATCH 518/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c9ca93cf..93ed43d9 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,6 +1,12 @@ +Version-4.1-dev-146-g42e508b4 +==== +#### Enhancements +* Extract several very useful extra pieces of client metadata for AP2. Many thanks to [Cody Cutrer](https://github.com/ccutrer). +* Enable a cleaner restart of the D-Bus daemon in a Docker image. Thanks again to [Charles Omer](https://github.com/charlesomer). + Version-4.1-dev-138-g5617f40e ==== -### Minor Internal Changes +#### Minor Internal Changes * A small number of internal chages made. Version-4.1-dev-127-g4d0460b9 From 040bcd4f109bc6a620266b1af64217876311eb25 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 27 Dec 2021 12:37:49 +0000 Subject: [PATCH 519/878] Clang format --- rtsp.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rtsp.c b/rtsp.c index 83dd4913..34a7c49e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2618,10 +2618,9 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 - #ifdef CONFIG_METADATA -static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, uint32_t metadata_code) -{ +static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, + uint32_t metadata_code) { plist_t item = plist_dict_get_item(messagePlist, plist_key); if (item) { char *value; From 9e99e9accaf39ad03ea6b0ba3017558ccbe3d786 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 27 Dec 2021 23:27:36 +0000 Subject: [PATCH 520/878] Alpine 3.15 test --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9db54c89..da0fa547 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine:3.15 AS builder # Check required arguments exist. These will be provided by the Github Action # Workflow and are required to ensure the correct branches are being used. @@ -67,7 +67,7 @@ RUN make install ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-edge +FROM crazymax/alpine-s6:3.15-edge RUN apk -U add \ alsa-lib \ From f83ddf3e751e5539037ae68f66dadadbe3618642 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:18:06 +0000 Subject: [PATCH 521/878] Roll back --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index da0fa547..9db54c89 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.15 AS builder +FROM alpine:3.12 AS builder # Check required arguments exist. These will be provided by the Github Action # Workflow and are required to ensure the correct branches are being used. @@ -67,7 +67,7 @@ RUN make install ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.15-edge +FROM crazymax/alpine-s6:3.12-edge RUN apk -U add \ alsa-lib \ From 35317687f9c9d90b379d9d3d2a010c109229e32b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 29 Dec 2021 10:22:44 +0000 Subject: [PATCH 522/878] access the SMI named in the nqptp_shared_memory_interface_name --- ptp-utilities.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 78a43ed4..304eb6c2 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -117,22 +117,27 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint int ptp_shm_interface_open() { mapped_addr = NULL; - int shared_memory_file_descriptor = shm_open("/nqptp", O_RDWR, 0); - int response = 0; - if (shared_memory_file_descriptor >= 0) { - mapped_addr = - // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the - // mutex to lock and unlock - mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, - shared_memory_file_descriptor, 0); - if (mapped_addr == MAP_FAILED) { - response = -1; - } - if (close(shared_memory_file_descriptor) == -1) { + int response = -1; + if (strcmp(config.nqptp_shared_memory_interface_name,"") != 0) { + response = 0; + int shared_memory_file_descriptor = shm_open(config.nqptp_shared_memory_interface_name, O_RDWR, 0); + if (shared_memory_file_descriptor >= 0) { + mapped_addr = + // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the + // mutex to lock and unlock + mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, + shared_memory_file_descriptor, 0); + if (mapped_addr == MAP_FAILED) { + response = -1; + } + if (close(shared_memory_file_descriptor) == -1) { + response = -1; + } + } else { response = -1; } } else { - response = -1; + debug(1,"No config.nqptp_shared_memory_interface_name"); } return response; } From 04aba03b4f1f7254ee3ab3aa7f20b557f47cada0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 30 Dec 2021 11:03:34 +0000 Subject: [PATCH 523/878] Stop storing the timing peer list. Clean up generation of device's timing addresses (and fix a bug). Use unique SMI interface for access to NQPTP. --- player.h | 3 - ptp-utilities.c | 9 +-- rtsp.c | 151 +++++++++++++++++------------------------------- 3 files changed, 59 insertions(+), 104 deletions(-) diff --git a/player.h b/player.h index edf80837..526c99f2 100644 --- a/player.h +++ b/player.h @@ -350,9 +350,6 @@ typedef struct { uint64_t type; uint64_t networkTimeTimelineID; // the clock ID used by the player uint8_t groupContainsGroupLeader; // information coming from the SETUP - - char *ap2_timing_peer_list_message; - #endif // used as the initials values for calculating the rate at which the source thinks it's sending diff --git a/ptp-utilities.c b/ptp-utilities.c index 304eb6c2..e04058c6 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -118,9 +118,10 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint int ptp_shm_interface_open() { mapped_addr = NULL; int response = -1; - if (strcmp(config.nqptp_shared_memory_interface_name,"") != 0) { + if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { response = 0; - int shared_memory_file_descriptor = shm_open(config.nqptp_shared_memory_interface_name, O_RDWR, 0); + int shared_memory_file_descriptor = + shm_open(config.nqptp_shared_memory_interface_name, O_RDWR, 0); if (shared_memory_file_descriptor >= 0) { mapped_addr = // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the @@ -137,7 +138,7 @@ int ptp_shm_interface_open() { response = -1; } } else { - debug(1,"No config.nqptp_shared_memory_interface_name"); + debug(1, "No config.nqptp_shared_memory_interface_name"); } return response; } @@ -161,7 +162,7 @@ void ptp_send_control_message_string(const char *msg) { *full_message = '\0'; snprintf(full_message, full_message_size, "%s %s", config.nqptp_shared_memory_interface_name, msg); - debug(1, "Send control message to NQPTP: \"%s\"", full_message); + debug(2, "Send control message to NQPTP: \"%s\"", full_message); int s; unsigned short port = htons(NQPTP_CONTROL_PORT); struct sockaddr_in server; diff --git a/rtsp.c b/rtsp.c index 34a7c49e..0240f001 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1848,18 +1848,6 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message // reset_anchor_info(conn); // needed if the player resumes } pthread_cleanup_pop(1); // unlock the conn->flush_mutex - - if ((rate & 1) != 0) { - // keep this outside the flush_mutex lock - if (conn->ap2_timing_peer_list_message) { - // ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - } else { - debug(1, "Connection %d: No timing peer list!", conn->connection_number); - } - } else { - // player_full_flush(conn); - // ptp_send_control_message_string("T"); // ensure an obsolete clock isn't picked up later. - } } pthread_cleanup_pop(1); // plist_free the messagePlist; } else { @@ -2407,15 +2395,7 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp if (ip_address != NULL) free(ip_address); } - - if (conn->ap2_timing_peer_list_message != NULL) - free(conn->ap2_timing_peer_list_message); - conn->ap2_timing_peer_list_message = strdup(timing_list_message); - // if this rtsp thread is playing... - // if (try_to_hold_play_lock(conn) == 0) { - ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - // release_hold_on_play_lock(conn); - // } + ptp_send_control_message_string(timing_list_message); } plist_free(addresses_array); resp->respcode = 200; @@ -2498,10 +2478,6 @@ void teardown_phase_two(rtsp_conn_info *conn) { free(conn->dacp_active_remote); conn->dacp_active_remote = NULL; } - if (conn->ap2_timing_peer_list_message) { - free(conn->ap2_timing_peer_list_message); - conn->ap2_timing_peer_list_message = NULL; - } release_play_lock(conn); } } @@ -2677,7 +2653,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(2, "Connection %d: SETUP: Remote Control setup detected.", + debug(1, "Connection %d: SETUP: Remote Control setup detected.", conn->connection_number); conn->airplay_stream_category = remote_control_stream; } else { @@ -2727,87 +2703,30 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "No groupContainsGroupLeader in SETUP"); } + char timing_list_message[4096]; + timing_list_message[0] = 'T'; + timing_list_message[1] = 0; + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); if (timing_peer_info) { - // first, get and retain the incoming plist. - plist_t addresses = plist_new_array(); // to hold the device's peer interfaces + // first, get the incoming plist. plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); if (addresses_array) { // iterate through the array of items uint32_t items = plist_array_get_size(addresses_array); if (items) { - uint32_t item; - struct ifaddrs *addrs, *iap; - getifaddrs(&addrs); 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, "Timing peer: %s", ip_address); - // find its family and convert it into an IPv4/6 address - - struct sockaddr_storage peer_address; - struct sockaddr_in6 *pa6 = (struct sockaddr_in6 *)&peer_address; - struct sockaddr_in *pa4 = (struct sockaddr_in *)&peer_address; - - if (inet_pton(AF_INET6, ip_address, &pa6->sin6_addr) == - 1) { // is an IPv6 address... - peer_address.ss_family = AF_INET6; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET6)) { - struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); - struct sockaddr_in6 *mask6 = (struct sockaddr_in6 *)(iap->ifa_netmask); - unsigned int i; - int different = 0; - for (i = 0; ((i < 16) && (different == 0)); i++) { - unsigned char host_byte = - (addr6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - unsigned char peer_byte = - (pa6->sin6_addr.s6_addr[i] & mask6->sin6_addr.s6_addr[i]); - if (host_byte != peer_byte) - different = 1; - } - - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); - // don't insist there are in the same subnet... - // if (!different) { - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); - //} - } - } - } else if (inet_pton(AF_INET, ip_address, &pa4->sin_addr) == 1) { - peer_address.ss_family = AF_INET; - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0) && - (iap->ifa_addr->sa_family == AF_INET)) { - struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); - // not needed if not checking for same subnet - // struct sockaddr_in *mask = (struct sockaddr_in *)(iap->ifa_netmask); - // if ((addr->sin_addr.s_addr & mask->sin_addr.s_addr) == - // (pa4->sin_addr.s_addr & mask->sin_addr.s_addr)) { - char buf[32]; - memset(buf, 0, sizeof(buf)); - inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); - // no longer insisting they are in the same subnet - // debug(1, "%s is in the same subnet as %s.", buf, ip_address); - plist_array_append_item(addresses, plist_new_string(buf)); - // } - } - } - } else { - debug(1, "SETUP on Connection %d: Don't recognise this as an IP address.", - conn->connection_number); - } + 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)); free(ip_address); } - freeifaddrs(addrs); } else { debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", conn->connection_number); @@ -2819,6 +2738,49 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // make up the timing peer info list part of the response... // debug(1,"Create timingPeerInfoPlist"); plist_t timingPeerInfoPlist = plist_new_dict(); + plist_t addresses = plist_new_array(); // to hold the device's interfaces + plist_array_append_item(addresses, plist_new_string(conn->self_ip_string)); +// debug(1,"self ip: \"%s\"", conn->self_ip_string); + + struct ifaddrs *addrs, *iap; + getifaddrs(&addrs); + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + // debug(1, "Interface index %d, name: \"%s\"",if_nametoindex(iap->ifa_name), iap->ifa_name); + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0)) { + char buf[INET6_ADDRSTRLEN + 1]; // +1 for a NUL + memset(buf, 0, sizeof(buf)); + if (iap->ifa_addr->sa_family == AF_INET6) { + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); + inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); + plist_array_append_item(addresses, plist_new_string(buf)); + // debug(1, "Own address IPv6: %s", buf); + + // strncat(timing_list_message, " ", + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // strncat(timing_list_message, buf, + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + + + } else { + struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + plist_array_append_item(addresses, plist_new_string(buf)); + // debug(1, "Own address IPv4: %s", buf); + + // strncat(timing_list_message, " ", + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // strncat(timing_list_message, buf, + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + + } + } + } + freeifaddrs(addrs); + + // debug(1,"initial timing peer command: \"%s\".", timing_list_message); + ptp_send_control_message_string(timing_list_message); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); @@ -3048,11 +3010,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->input_bit_depth = conn->stream.fmtp[3]; conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); debug(2, "Realtime Stream Play"); - if (conn->ap2_timing_peer_list_message) { - // ptp_send_control_message_string(conn->ap2_timing_peer_list_message); - } else { - debug(1, "No timing peer list!"); - } activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); From 7d350c3d08670862daceabb9f8c99df4bd5e5ee6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 31 Dec 2021 10:07:00 +0000 Subject: [PATCH 524/878] Simplify validation of the mDNS interface setting, if any. --- shairport.c | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/shairport.c b/shairport.c index eaa020fa..0edba7e7 100644 --- a/shairport.c +++ b/shairport.c @@ -777,32 +777,15 @@ int parse_options(int argc, char **argv) { config.interface = strdup(str); if (config_lookup_string(config.cfg, "general.interface", &str)) { - int specified_interface_found = 0; - - struct if_nameindex *if_ni, *i; - - if_ni = if_nameindex(); - if (if_ni == NULL) { - debug(1, "Can't get a list of interface names."); - } else { - for (i = if_ni; !(i->if_index == 0 && i->if_name == NULL); i++) { - // printf("%u: %s\n", i->if_index, i->if_name); - if (strcmp(i->if_name, str) == 0) { - config.interface_index = i->if_index; - specified_interface_found = 1; - } - } - } - - if_freenameindex(if_ni); - - if (specified_interface_found == 0) { + + config.interface_index = if_nametoindex(config.interface); + + if (config.interface_index == 0) { inform( "The mdns service interface \"%s\" was not found, so the setting has been ignored.", config.interface); free(config.interface); config.interface = NULL; - config.interface_index = 0; } } @@ -1859,7 +1842,7 @@ int main(int argc, char **argv) { char shared_memory_interface_name[256] = ""; snprintf(shared_memory_interface_name, sizeof(shared_memory_interface_name), "/%s-%" PRIx64 "", config.appName, apid); - debug(1, "smi name: \"%s\"", shared_memory_interface_name); + // debug(1, "smi name: \"%s\"", shared_memory_interface_name); config.nqptp_shared_memory_interface_name = strdup(shared_memory_interface_name); From c849c1fcf0ef2f0d80e08063706ebc86e8c3984c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 1 Jan 2022 12:13:43 +0000 Subject: [PATCH 525/878] clang-format --- rtsp.c | 25 ++++++++++++------------- shairport.c | 4 ++-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/rtsp.c b/rtsp.c index 0240f001..68180a56 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2724,7 +2724,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) 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)); + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); free(ip_address); } } else { @@ -2740,12 +2740,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t timingPeerInfoPlist = plist_new_dict(); plist_t addresses = plist_new_array(); // to hold the device's interfaces plist_array_append_item(addresses, plist_new_string(conn->self_ip_string)); -// debug(1,"self ip: \"%s\"", conn->self_ip_string); + // debug(1,"self ip: \"%s\"", conn->self_ip_string); struct ifaddrs *addrs, *iap; getifaddrs(&addrs); for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - // debug(1, "Interface index %d, name: \"%s\"",if_nametoindex(iap->ifa_name), iap->ifa_name); + // debug(1, "Interface index %d, name: \"%s\"",if_nametoindex(iap->ifa_name), + // iap->ifa_name); if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && ((iap->ifa_flags & IFF_LOOPBACK) == 0)) { char buf[INET6_ADDRSTRLEN + 1]; // +1 for a NUL @@ -2757,30 +2758,28 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // debug(1, "Own address IPv6: %s", buf); // strncat(timing_list_message, " ", - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); // strncat(timing_list_message, buf, - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - } else { struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); plist_array_append_item(addresses, plist_new_string(buf)); // debug(1, "Own address IPv4: %s", buf); - + // strncat(timing_list_message, " ", - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); // strncat(timing_list_message, buf, - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); } } } 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); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); diff --git a/shairport.c b/shairport.c index 0edba7e7..86d073c8 100644 --- a/shairport.c +++ b/shairport.c @@ -777,9 +777,9 @@ int parse_options(int argc, char **argv) { config.interface = strdup(str); if (config_lookup_string(config.cfg, "general.interface", &str)) { - + config.interface_index = if_nametoindex(config.interface); - + if (config.interface_index == 0) { inform( "The mdns service interface \"%s\" was not found, so the setting has been ignored.", From 59d92db39f89dfedda0686c860b8b1790f10acde Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 1 Jan 2022 19:01:20 +0000 Subject: [PATCH 526/878] Improve a few debug messages. --- player.c | 2 +- rtp.c | 3 +-- rtsp.c | 13 +++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/player.c b/player.c index 27b8e692..db753ba8 100644 --- a/player.c +++ b/player.c @@ -1063,7 +1063,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - if (lt < 130000000) { + if (lt < 100000000) { debug(1, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", diff --git a/rtp.c b/rtp.c index cf53c545..bc3fe8fb 100644 --- a/rtp.c +++ b/rtp.c @@ -1452,9 +1452,8 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, *anchorRTP = conn->last_anchor_rtptime; if (anchorLocalTime != NULL) *anchorLocalTime = conn->last_anchor_local_time; - - response = clock_ok; } + return response; } diff --git a/rtsp.c b/rtsp.c index 68180a56..0a68425e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2633,7 +2633,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_get_string_val(timingProtocol, &timingProtocolString); if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { - debug(2, "Connection %d: SETUP: PTP setup detected.", conn->connection_number); + debug(1, "Connection %d: PTP 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->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; get_play_lock(conn); @@ -2653,8 +2654,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: SETUP: Remote Control setup detected.", - conn->connection_number); + debug(1, "Connection %d: Remote Control 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->airplay_stream_category = remote_control_stream; } else { debug(1, @@ -2962,7 +2963,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) switch (item_value) { case 96: { - debug(1, "Connection %d. Realtime Audio Stream Detected.", conn->connection_number); + debug(1, "Connection %d. Realtime Audio Stream.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); // get_play_lock(conn); conn->airplay_stream_type = realtime_stream; @@ -3016,7 +3017,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; case 103: { - debug(1, "SETUP on Connection %d. Buffered Audio Stream Detected.", + debug(1, "Connection %d. Buffered Audio Stream.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); // get_play_lock(conn); @@ -5170,7 +5171,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(1, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(2, "Connection %d: new 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); } else { From 4322f67fe2066cfc5c96397bbbf40cc40dbc8796 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 10:06:36 +0000 Subject: [PATCH 527/878] Exit with a message in the log if the name to be used for the service is already in use. --- mdns_avahi.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mdns_avahi.c b/mdns_avahi.c index 7103b1ee..3dc58565 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -242,14 +242,21 @@ static void register_service(AvahiClient *c) { else selected_interface = AVAHI_IF_UNSPEC; if (ap2_text_record_string_list) { - ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, ap2_service_name, config.regtype2, NULL, NULL, port, ap2_text_record_string_list); + if (ret == AVAHI_ERR_COLLISION) { + die("Error: AirPlay 2 name \"%s\" is already in use.", ap2_service_name); + } + } if ((ret == 0) && (text_record_string_list)) { ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, service_name, config.regtype, NULL, NULL, port, text_record_string_list); + if (ret == AVAHI_ERR_COLLISION) { + die("Error: AirPlay 1 name \"%s\" is already in use.", service_name); + } } if (ret == 0) { ret = avahi_entry_group_commit(group); From b5860fc4d221f7f6272b8fca3d071a45a69cc2c6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 10:42:48 +0000 Subject: [PATCH 528/878] latest version of the nqptp interface header --- nqptp-shm-structures.h | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 82837778..0791197d 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -1,6 +1,6 @@ /* * This file is part of the nqptp distribution (https://github.com/mikebrady/nqptp). - * Copyright (c) 2021 Mike Brady. + * Copyright (c) 2021--2022 Mike Brady. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,11 +23,24 @@ #define NQPTP_SHM_STRUCTURES_VERSION 7 #define NQPTP_CONTROL_PORT 9000 -// The control port will accept a UDP packet with the first letter being: -// "T", followed by the name of the shared memory interface, followed by -// a space and then a space-delimited list of ip numbers, either IPv4 or IPv6 -// the whole not to exceed 4096 characters in total -// The IPs will become the new list of timing peers, replacing any previous +// The control port expects a UDP packet with the first space-delimited string +// being the name of the shared memory interface (SMI) to be used. +// This allows client applications to have a dedicated named SMI interface with +// a timing peer list independent of other clients. +// The name given must be a valid SMI name and must contain no spaces. +// If the named SMI interface doesn't exist it will be created by NQPTP. +// The SMI name should be delimited by a space and followed by a command letter. +// At present, the only command is "T", which must followed by nothing or by +// a space and a space-delimited list of IPv4 or IPv6 numbers, +// the whole not to exceed 4096 characters in total. +// The IPs, if provided, will become the new list of timing peers, replacing any +// previous list. If the master clock of the new list is the same as that of the old list, +// the master clock is retained without resynchronisation; this means that non-master +// devices can be added and removed without disturbing the SMI's existing master clock. +// If no timing list is provided, the existing timing list is deleted. +// (In future version of NQPTP the SMI interface may also be deleted at this point.) +// SMI interfaces are not currently deleted or garbage collected. + #include #include From f6a198438df07bb70cdf0576c366a54bf71368a5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 10:59:20 +0000 Subject: [PATCH 529/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 93ed43d9..4d6d5035 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,19 @@ +Version-4.1-dev-158-gb5860fc4 +==== +#### Enhancements +The following enhancements are mainly experimental, for advanced users only, and are intended to enable multiple instances of Shairport Sync (SPS) to run in one computer and provide multiple AirPlay 2 endpoints. At this point, multiple instances of SPS can indeed run on one machine and provide multiple AirPlay 2 endpoints, but unfortunately the presence of multiple services at the same address seems to confuse AirPlay 2 clients. So, it seems that while multiple instances of SPS can now run successfully on one device, they can't be used on the same network. +* Use a uniquely-named named SMI interface to get dedicated access to NQPTP. +* Simplify validation of the mDNS interface setting, if given. +* Exit with a warning if the player is using a name that already exists. +* Stop storing the timing peer list. +* Generate an initial timing list during setup — don’t wait for the first timing peer list message. May gain around 50 ms. +* Fix a bug and clean up generation of device's timing addresses. +* Add some control to the generation of the Device ID: +1. Revise how the Device ID is generated from the device’s MAC addresses +2. Add a setting to add a fixed offset to the automatically-generated Device ID. +3. Add a setting to specify a complete Device ID, replacing the automatically-generated Device ID. +* Be more careful to create default file names based on the name of the app. + Version-4.1-dev-146-g42e508b4 ==== #### Enhancements From 9c2f9f7ada48e0d631048e589c24a1c4481dc25a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 16:58:09 +0000 Subject: [PATCH 530/878] Do a proper TEARDOWN when an AP1 RTSP thread is cancelled. In particular, kill any player thread that may be running. --- rtsp.c | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/rtsp.c b/rtsp.c index 0a68425e..72e443a4 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1473,6 +1473,9 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (conn->player_thread) warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); else { + debug(1, "Connection %d: AP1 ALAC Stream, 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); activity_monitor_signify_activity(1); player_prepare_to_play(conn); player_play(conn); // the thread better be 0 @@ -2522,20 +2525,28 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag } #endif +void teardown(rtsp_conn_info *conn) { + player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } +} + void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, rtsp_message *resp) { debug_log_rtsp_message(2, "TEARDOWN request", req); debug(2, "Connection %d: TEARDOWN", conn->connection_number); if (have_play_lock(conn)) { - resp->respcode = 200; - msg_add_header(resp, "Connection", "close"); debug( 3, "TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).", conn->connection_number); - player_stop(conn); + teardown(conn); release_play_lock(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + resp->respcode = 200; + msg_add_header(resp, "Connection", "close"); debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", conn->connection_number); } else { @@ -2543,10 +2554,6 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message conn->connection_number); resp->respcode = 451; } - if (conn->dacp_active_remote != NULL) { - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); // exit(EXIT_SUCCESS); } @@ -2633,7 +2640,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_get_string_val(timingProtocol, &timingProtocolString); if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { - debug(1, "Connection %d: PTP connection from %s:%u to self at %s:%u.", conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + debug(1, "Connection %d: AP2 PTP 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->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; @@ -2654,7 +2662,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: Remote Control connection from %s:%u to self at %s:%u.", conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + debug(1, "Connection %d: Remote Control 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->airplay_stream_category = remote_control_stream; } else { @@ -2963,7 +2972,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) switch (item_value) { case 96: { - debug(1, "Connection %d. Realtime Audio Stream.", conn->connection_number); + debug(1, "Connection %d. AP2 Realtime Audio Stream.", conn->connection_number); debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); // get_play_lock(conn); conn->airplay_stream_type = realtime_stream; @@ -3017,8 +3026,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->rtp_running = 1; // hack! } break; case 103: { - debug(1, "Connection %d. Buffered Audio Stream.", - conn->connection_number); + debug(1, "Connection %d. AP2 Buffered Audio Stream.", conn->connection_number); debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); // get_play_lock(conn); conn->airplay_stream_type = buffered_stream; @@ -4655,8 +4663,15 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(2, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", conn->connection_number); #ifdef CONFIG_AIRPLAY_2 + // AP2 teardown_phase_one(conn); teardown_phase_two(conn); +#else + // AP1 + if (have_play_lock(conn)) { + teardown(conn); + release_play_lock(conn); + } #endif debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", From c01771392b241a9fcfe4eb156415d2920deab15c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:09:57 +0000 Subject: [PATCH 531/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 4d6d5035..0a1be528 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-160-g9c2f9f7a +==== +#### Bugfix +* Fix a crashing bug in AirPlay 1 ("Classic") mode, whereby if a session was abruptly interrupted, e.g. by the sudden loss of a network, Shairport Sync would crash. The problem was that the interrupted session was not correctly terminated. Thanks to [th0u](https://github.com/th0u) for reporting issue [#1398](https://github.com/mikebrady/shairport-sync/issues/1398). + Version-4.1-dev-158-gb5860fc4 ==== #### Enhancements From 73eec0a1bea5373d87335c30b6be6c873668b272 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:37:23 +0000 Subject: [PATCH 532/878] Add some code to investigate possible unclean teardowns in AP2 modes. Also add code which might obviate the problem! --- player.c | 2 ++ rtsp.c | 1 + 2 files changed, 3 insertions(+) diff --git a/player.c b/player.c index db753ba8..914a3b43 100644 --- a/player.c +++ b/player.c @@ -1306,6 +1306,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { wait = 1; // keep waiting until the timing information becomes available } if (wait) { + if (conn->input_rate == 0) + die("input_rate is zero -- should never happen!"); uint64_t time_to_wait_for_wakeup_ns = 1000000000 / conn->input_rate; // this is time period of one frame time_to_wait_for_wakeup_ns *= 2 * 352; // two full 352-frame packets diff --git a/rtsp.c b/rtsp.c index 72e443a4..c4e8d831 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2506,6 +2506,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_free(streams); debug(2, "Connection %d: TEARDOWN phase one complete", conn->connection_number); } else { + teardown_phase_one(conn); // try to do phase one anyway teardown_phase_two(conn); debug(2, "Connection %d: TEARDOWN phase two complete", conn->connection_number); } From 5077ff8227cb2040ab3fc756839c2a6060ff1f2f Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 4 Jan 2022 14:37:29 +0000 Subject: [PATCH 533/878] Try updating images again. --- docker/Dockerfile | 4 ++-- docker/etc/services.d/{01-dbus-daemon => 01-dbus}/run | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename docker/etc/services.d/{01-dbus-daemon => 01-dbus}/run (100%) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9db54c89..132699da 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine AS builder # Check required arguments exist. These will be provided by the Github Action # Workflow and are required to ensure the correct branches are being used. @@ -67,7 +67,7 @@ RUN make install ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-edge +FROM crazymax/alpine-s6:edge RUN apk -U add \ alsa-lib \ diff --git a/docker/etc/services.d/01-dbus-daemon/run b/docker/etc/services.d/01-dbus/run similarity index 100% rename from docker/etc/services.d/01-dbus-daemon/run rename to docker/etc/services.d/01-dbus/run From f79222f71dc370489119976bd7bc5a0c38a200ca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 4 Jan 2022 18:06:16 +0000 Subject: [PATCH 534/878] BB. Fix a crash that occurred in AP2 mode if no configuration file was present, duh. --- shairport.c | 180 +++++++++++++++++++++++++++++----------------------- 1 file changed, 99 insertions(+), 81 deletions(-) diff --git a/shairport.c b/shairport.c index 86d073c8..765732cc 100644 --- a/shairport.c +++ b/shairport.c @@ -2,7 +2,7 @@ * Shairport, an Apple Airplay receiver * Copyright (c) James Laird 2013 * All rights reserved. - * Modifications and additions (c) Mike Brady 2014--2021 + * Modifications and additions (c) Mike Brady 2014--2022 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -469,6 +469,44 @@ int parse_options(int argc, char **argv) { // not made active again (not used) #endif +#ifdef CONFIG_AIRPLAY_2 + // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 + // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as + // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: + // 496155702020608 this setting here is the source of both the plist features response and the + // mDNS string. + // note: 0x300401F4A00 works but with weird delays and stuff + // config.airplay_features = 0x1C340405FCA00; + uint64_t mask = + ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); + config.airplay_features = + 0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below) + // Advertised with mDNS and returned with GET /info, see + // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN + // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio + // cable attached, OneTimePairingRequired, device was setup for Homekit access control + config.airplay_statusflags = 0x04; + // Set to NULL to work with transient pairing + config.airplay_pin = NULL; + + // use the start of the config.hw_addr and the PID to generate the default airplay_device_id + uint64_t temporary_airplay_id = nctoh64(config.hw_addr); + temporary_airplay_id = + temporary_airplay_id >> 16; // we only use the first 6 bytes but have imported 8. + + // now generate a UUID + // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c + // with thanks + uuid_t binuuid; + uuid_generate_random(binuuid); + + char *uuid = malloc(UUID_STR_LEN); + // Produces a UUID string at uuid consisting of lower-case letters + uuid_unparse_lower(binuuid, uuid); + config.airplay_pi = uuid; + +#endif + // config_setting_t *setting; const char *str = 0; int value = 0; @@ -1150,6 +1188,21 @@ int parse_options(int argc, char **argv) { "avahi enabled, or disable remote control."); } #endif +#endif + +#ifdef CONFIG_AIRPLAY_2 + int64_t aid; + + // replace the airplay_device_id with this, if provided + if (config_lookup_int64(config.cfg, "general.airplay_device_id", &aid)) { + temporary_airplay_id = aid; + } + + // add the airplay_device_id_offset if provided + if (config_lookup_int64(config.cfg, "general.airplay_device_id_offset", &aid)) { + temporary_airplay_id += aid; + } + #endif } @@ -1218,6 +1271,44 @@ int parse_options(int argc, char **argv) { // here, we are finally finished reading the options + // finish the Airplay 2 options + +#ifdef CONFIG_AIRPLAY_2 + + char shared_memory_interface_name[256] = ""; + snprintf(shared_memory_interface_name, sizeof(shared_memory_interface_name), "/%s-%" PRIx64 "", + config.appName, temporary_airplay_id); + // debug(1, "smi name: \"%s\"", shared_memory_interface_name); + + config.nqptp_shared_memory_interface_name = strdup(shared_memory_interface_name); + + char apids[6 * 2 + 5 + 1]; // six pairs of digits, 5 colons and a NUL + apids[6 * 2 + 5] = 0; // NUL termination + int i; + char hexchar[] = "0123456789abcdef"; + for (i = 5; i >= 0; i--) { + apids[i * 3 + 1] = hexchar[temporary_airplay_id & 0xF]; + temporary_airplay_id = temporary_airplay_id >> 4; + apids[i * 3] = hexchar[temporary_airplay_id & 0xF]; + temporary_airplay_id = temporary_airplay_id >> 4; + if (i != 0) + apids[i * 3 - 1] = ':'; + } + + config.airplay_device_id = strdup(apids); + +#ifdef CONFIG_METADATA + // If we are asking for metadata, turn on the relevant bits + if (config.metadata_enabled != 0) { + config.airplay_features |= (1 << 17) | (1 << 16); // 16 is progress, 17 is text + // If we are asking for artwork, turn on the relevant bit + if (config.get_coverart) + config.airplay_features |= (1 << 15); // 15 is artwork + } +#endif + +#endif + #ifdef CONFIG_LIBDAEMON if ((daemonisewith) && (daemonisewithout)) die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have selected " @@ -1792,86 +1883,13 @@ int main(int argc, char **argv) { #endif #ifdef CONFIG_AIRPLAY_2 - // the features code is a 64-bit number, but in the mDNS advertisement, the least significant 32 - // bit are given first for example, if the features number is 0x1C340405F4A00, it will be given as - // features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist: - // 496155702020608 this setting here is the source of both the plist features response and the - // mDNS string. - // note: 0x300401F4A00 works but with weird delays and stuff - // config.airplay_features = 0x1C340405FCA00; - uint64_t mask = - ((uint64_t)1 << 17) | ((uint64_t)1 << 16) | ((uint64_t)1 << 15) | ((uint64_t)1 << 50); - config.airplay_features = - 0x1C340405D4A00 & (~mask); // APX + Authentication4 (b14) with no metadata (see below) - // Advertised with mDNS and returned with GET /info, see - // https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN - // required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio - // cable attached, OneTimePairingRequired, device was setup for Homekit access control - -#ifdef CONFIG_METADATA - // If we are asking for metadata, turn on the relevant bits - if (config.metadata_enabled != 0) { - config.airplay_features |= (1 << 17) | (1 << 16); // 16 is progress, 17 is text - // If we are asking for artwork, turn on the relevant bit - if (config.get_coverart) - config.airplay_features |= (1 << 15); // 15 is artwork - } -#endif - - debug(1, "Features: 0x%" PRIx64 ".", config.airplay_features); - config.airplay_statusflags = 0x04; - // Set to NULL to work with transient pairing - config.airplay_pin = NULL; - - // use the start of the config.hw_addr and the PID to generate the default airplay_device_id - uint64_t apid = nctoh64(config.hw_addr); - apid = apid >> 16; // we only use the first 6 bytes but have imported 8. - - int64_t aid; - - // add the airplay_device_id_offset if provided - if (config_lookup_int64(config.cfg, "general.airplay_device_id_offset", &aid)) { - apid += aid; - } - - // replace the airplay_device_id with this, if provided - if (config_lookup_int64(config.cfg, "general.airplay_device_id", &aid)) { - apid = aid; - } - - char shared_memory_interface_name[256] = ""; - snprintf(shared_memory_interface_name, sizeof(shared_memory_interface_name), "/%s-%" PRIx64 "", - config.appName, apid); - // debug(1, "smi name: \"%s\"", shared_memory_interface_name); - - config.nqptp_shared_memory_interface_name = strdup(shared_memory_interface_name); - - char apids[6 * 2 + 5 + 1]; // six pairs of digits, 5 colons and a NUL - apids[6 * 2 + 5] = 0; // NUL termination - int i; - char hexchar[] = "0123456789abcdef"; - for (i = 5; i >= 0; i--) { - apids[i * 3 + 1] = hexchar[apid & 0xF]; - apid = apid >> 4; - apids[i * 3] = hexchar[apid & 0xF]; - apid = apid >> 4; - if (i != 0) - apids[i * 3 - 1] = ':'; - } - - config.airplay_device_id = strdup(apids); - - // now generate a UUID - // from https://stackoverflow.com/questions/51053568/generating-a-random-uuid-in-c - // with thanks - uuid_t binuuid; - uuid_generate_random(binuuid); - - char *uuid = malloc(UUID_STR_LEN); - // Produces a UUID string at uuid consisting of lower-case letters - uuid_unparse_lower(binuuid, uuid); - config.airplay_pi = uuid; - debug(1, "Started in Airplay 2 mode on device \"%s\"!", config.airplay_device_id); + uint64_t apf = config.airplay_features; + uint64_t apfh = config.airplay_features; + apfh = apfh >> 32; + uint32_t apf32 = apf; + uint32_t apfh32 = apfh; + debug(1, "Started in Airplay 2 mode with features 0x%" PRIx32 ",0x%" PRIx32 " on device \"%s\"!", + apf32, apfh32, config.airplay_device_id); #else debug(1, "Started in Airplay 1 mode!"); #endif From b566d641b9fa4cd7e35601ed1c77326a040b488f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 4 Jan 2022 18:11:33 +0000 Subject: [PATCH 535/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 0a1be528..77d47dd9 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-164-gf79222f7 +==== +#### Bugfix +* Fix a crashing bug in AirPlay 2 mode which occurs if no configuration file is found. This particularly affects the Docker image, as it contains no configuration file. Thanks to [Charles Omer](https://github.com/charlesomer) for reporting. + Version-4.1-dev-160-g9c2f9f7a ==== #### Bugfix From 55c111cabb0920bf5799e35ff4d03c589e9f8c4c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 11 Jan 2022 16:34:33 +0000 Subject: [PATCH 536/878] Lower the default starting volume bu 6dB. --- shairport.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shairport.c b/shairport.c index 765732cc..89a57961 100644 --- a/shairport.c +++ b/shairport.c @@ -416,7 +416,7 @@ int parse_options(int argc, char **argv) { config.tolerance = 1.0 * fTolerance / 44100; config.audio_backend_silent_lead_in_time_auto = 1; // start outputting silence as soon as packets start arriving - config.airplay_volume = -18.0; // if no volume is ever set, default to initial default value if + config.airplay_volume = -24.0; // if no volume is ever set, default to initial default value if // nothing else comes in first. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; From 582e2a1f89d3a8137016e2976d35ab6d98aabeed Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 17 Jan 2022 10:51:53 +0000 Subject: [PATCH 537/878] Update AIRPLAY2.md --- AIRPLAY2.md | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 757d017e..aeeb05db 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,17 +1,6 @@ AirPlay 2 === - -Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs. It does not work for iTunes on Windows. - -The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work. - -For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. - -AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. - -The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. - -The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. +Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs. ### What Works - AirPlay 2 for iOS, HomePod mini, AppleTV and Mac players. @@ -20,21 +9,23 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi - No Remote Control - No AirPlay 2 for Windows iTunes -### What Partly Works -- Lossless or High Definition Lossless material over AirPlay 2 is automatically transcoded to ALAC or AAC before sending it via AirPlay 2 to Shairport Sync. +### General +The focus of the development effort has been on getting good audio performance for iOS and Mac. -### Limitations -- Only a single instance of Shairport Sync can run on a device. -- The AirPlay 2 version of Shairport Sync will not work if installed on macOS, since `nqptp` can not be installed on macOS. +For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. + +The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. + +Shairport Sync does not support lossless playback of Lossless or High Definition Lossless material. Instead, this material is automatically transcoded to ALAC or AAC before sending it via AirPlay 2 to Shairport Sync. AirPlay 2 -- What You Need --- -AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. +AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. A Raspberry Pi 2 or Raspberry Pi Zero 2 W or better is required. -So, here are some guideline requirements: -* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2. +Here are some guidelines: +* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2 or a Raspberry Pi Zero 2 W. * A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. -* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. +* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). You can use an application called [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) is available to test hardware `alsa` audio devices on your device for suitability. The `stdout` and `pipe` backends continue to work as before. Guides --- From 18f452025f1430c230983fd10a51d855ce75e75b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 17 Jan 2022 11:04:50 +0000 Subject: [PATCH 538/878] Update AIRPLAY2.md --- AIRPLAY2.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index aeeb05db..e0205130 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -10,13 +10,9 @@ Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, - No AirPlay 2 for Windows iTunes ### General -The focus of the development effort has been on getting good audio performance for iOS and Mac. +Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation in AirPlay 2. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. -For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. - -The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. - -Shairport Sync does not support lossless playback of Lossless or High Definition Lossless material. Instead, this material is automatically transcoded to ALAC or AAC before sending it via AirPlay 2 to Shairport Sync. +Shairport Sync does not support lossless playback of Lossless or High Definition Lossless material. Instead, this material is automatically transcoded to AAC before sending it via AirPlay 2 to Shairport Sync. AirPlay 2 -- What You Need --- @@ -25,7 +21,7 @@ AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronis Here are some guidelines: * Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2 or a Raspberry Pi Zero 2 W. * A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. -* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). You can use an application called [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) is available to test hardware `alsa` audio devices on your device for suitability. The `stdout` and `pipe` backends continue to work as before. +* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). You can use an application called [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware `alsa` audio devices on your device. Other backends continue to work as with "classic" Shairport Sync. Guides --- @@ -38,6 +34,10 @@ AirPlay 2 -- More About What Works * Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. * Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. +Note +---- +The functionality offered by Shairport Sync is the result of lots of study and analysis of the AirPlay 1 and AirPlay 2 protocols by many people over the years. These protocols have not been officially published, and there is no assurance that Shairport Sync will continue to work with AirPlay in future. + Acknowledgements ---- Huge thanks are due to a number of individuals who made direct and valuable contributions to Shairport Sync: @@ -50,4 +50,4 @@ Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at Finally ---- -Did we mention AirPlay 2 support is experimental? +AirPlay 2 support is experimental. From 6ced6bc67dbc199cc2061c78d4470080d987839a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:18:13 +0100 Subject: [PATCH 539/878] Add a "mixer_control_index" setting to the "alsa" part of ther configuration file. A mixer is fully identified by a name and index, duh. Change some debug messages and add an entry to the sample configuration file. --- audio_alsa.c | 9 +++++++-- scripts/shairport-sync.conf | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index a007c419..88a4bb0c 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -298,10 +298,10 @@ int open_mixer() { debug(1, "Failed to load mixer element"); response = -4; } else { - debug(3, "Mixer Control name is \"%s\".", alsa_mix_ctrl); + debug(3, "Mixer control is \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index); alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid); if (!alsa_mix_elem) { - warn("failed to find mixer control \"%s\".", alsa_mix_ctrl); + warn("failed to find mixer control \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index); response = -5; } else { response = 1; // we found a hardware mixer and successfully opened it @@ -1057,6 +1057,11 @@ static int init(int argc, char **argv) { alsa_mix_ctrl = (char *)str; } + // Get the Mixer Control Index + if (config_lookup_int(config.cfg, "alsa.mixer_control_index", &value)) { + alsa_mix_index = value; + } + /* Get the disable_synchronization setting. */ if (config_lookup_string(config.cfg, "alsa.disable_synchronization", &str)) { if (strcasecmp(str, "no") == 0) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index ee96fe6f..edb204d5 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -98,7 +98,8 @@ sessioncontrol = alsa = { // output_device = "default"; // the name of the alsa output device. Use "shairport-sync -h" to discover the names of ALSA hardware devices. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. -// mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. +// mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. No default. If not specified, no mixer is used and volume in adjusted in software. +// mixer_control_index = 0; // the index of the mixer to use to adjust output volume. Default is 0. The mixer is fully identified by the combination of the mixer_control_name and the mixer_control_index, e.g. "PCM",0 would be such a specification. // mixer_device = "default"; // the mixer_device default is whatever the output_device is. Normally you wouldn't have to use this. // output_rate = "auto"; // can be "auto", 44100, 88200, 176400 or 352800, but the device must have the capability. From fba2f33b6bfc2e3046162520922dd09559c03048 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:26:25 +0100 Subject: [PATCH 540/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 77d47dd9..0df981c5 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-169-g6ced6bc6 +==== +#### Enhancement +* Add a new `mixer_control_index` setting to the `alsa` section of the configuration file. A mixer is fully identified by a name and index. The index defaults to zero, and, until now, it could only be set using the `alsa` command-line argument `-i`. Thanks to [flipoidix](https://github.com/flipoidix) for bringing this issue to notice. + Version-4.1-dev-164-gf79222f7 ==== #### Bugfix From 703717a58ad87db6573a0290f9a8a859413bbf74 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 23 Jan 2022 10:04:04 +0100 Subject: [PATCH 541/878] Reset the realtime clock information when a realtimne stream play terminates. --- player.c | 1 + rtp.c | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/player.c b/player.c index 914a3b43..59ff4ccc 100644 --- a/player.c +++ b/player.c @@ -1703,6 +1703,7 @@ void player_thread_cleanup_handler(void *arg) { #ifdef CONFIG_AIRPLAY_2 } // ptp_send_control_message_string("T"); // remove all timing peers to force the master to 0 + reset_anchor_info(conn); #endif if (conn->outbuf) { diff --git a/rtp.c b/rtp.c index bc3fe8fb..07942ae9 100644 --- a/rtp.c +++ b/rtp.c @@ -1213,7 +1213,7 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { if (conn->anchor_clock != clock_id) { - debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); + debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; } // debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id, @@ -1453,7 +1453,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (anchorLocalTime != NULL) *anchorLocalTime = conn->last_anchor_local_time; } - + return response; } @@ -1651,6 +1651,8 @@ void *rtp_ap2_control_receiver(void *arg) { uint8_t packet[4096]; ssize_t nread; int keep_going = 1; + uint64_t start_time = get_absolute_time_in_ns(); + uint64_t packet_number = 0; while (keep_going) { SOCKADDR from_sock_addr; socklen_t from_sock_addr_length = sizeof(SOCKADDR); @@ -1658,8 +1660,24 @@ void *rtp_ap2_control_receiver(void *arg) { nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); + uint64_t time_now = get_absolute_time_in_ns(); - if (nread > 0) { + int64_t time_since_start = time_now - start_time; + + if (time_since_start <= 10000000) { + debug(1, "Discarding early timing packet."); + } + + if ((nread > 0) && (time_since_start > 10000000)) { + packet_number++; + + if (packet_number == 1) { + if ((packet[0] & 0x10) != 0) { + debug(2, "First packet is a sentinel packet."); + } else { + debug(1, "First packet is a not a sentinel packet!"); + } + } // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); if ((config.diagnostic_drop_packet_fraction == 0.0) || From 16f5c7b7c34f4159f5bd9f1feabe73a08d6fda42 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 23 Jan 2022 10:09:55 +0100 Subject: [PATCH 542/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 0df981c5..d49c0c1f 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-171-g703717a5 +==== +#### Bugfix +* Clear PTP clock information when a Realtime stream stops playing. This _may_ address one of the issues reported in [#1404](https://github.com/mikebrady/shairport-sync/issues/1404). + Version-4.1-dev-169-g6ced6bc6 ==== #### Enhancement From deb11654ab4ef4144fec2fb322968792f970ff8c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 24 Jan 2022 10:17:07 +0100 Subject: [PATCH 543/878] Add some sanity checking of latency-affecting settings. --- player.c | 2 + player.h | 9 +-- rtp.c | 175 +++++++++++++++++++++++++++++++------------------------ 3 files changed, 106 insertions(+), 80 deletions(-) diff --git a/player.c b/player.c index 59ff4ccc..dadfc48b 100644 --- a/player.c +++ b/player.c @@ -1743,6 +1743,8 @@ void *player_thread_func(void *arg) { int previous_frames_played_valid = 0; // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); + conn->latency_warning_issued = + 0; // be permitted to generate a warning each time a play is attempted conn->packet_count = 0; conn->packet_count_since_flush = 0; conn->previous_random_number = 0; diff --git a/player.h b/player.h index 526c99f2..453c5f49 100644 --- a/player.h +++ b/player.h @@ -145,10 +145,11 @@ typedef struct flush_request_t { #endif typedef struct { - int connection_number; // for debug ID purposes, nothing else... - int resend_interval; // this is really just for debugging - char *UserAgent; // free this on teardown - int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency + int connection_number; // for debug ID purposes, nothing else... + int resend_interval; // this is really just for debugging + char *UserAgent; // free this on teardown + int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency + int latency_warning_issued; uint32_t latency; // the actual latency used for this play session uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero // otherwise diff --git a/rtp.c b/rtp.c index 07942ae9..c38ea966 100644 --- a/rtp.c +++ b/rtp.c @@ -1668,95 +1668,118 @@ void *rtp_ap2_control_receiver(void *arg) { debug(1, "Discarding early timing packet."); } - if ((nread > 0) && (time_since_start > 10000000)) { - packet_number++; + if (nread > 0) { + if ((time_since_start < 10000000) && ((packet[0] & 0x10) == 0)) { + debug(1, + "Dropping what looks like a (non-sentinel) packet left over from a previous session " + "at %f ms.", + 0.000001 * time_since_start); + } else { + packet_number++; - if (packet_number == 1) { - if ((packet[0] & 0x10) != 0) { - debug(2, "First packet is a sentinel packet."); - } else { - debug(1, "First packet is a not a sentinel packet!"); + if (packet_number == 1) { + if ((packet[0] & 0x10) != 0) { + debug(2, "First packet is a sentinel packet."); + } else { + debug(1, "First packet is a not a sentinel packet!"); + } } - } - // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); + // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); - if ((config.diagnostic_drop_packet_fraction == 0.0) || - (drand48() > config.diagnostic_drop_packet_fraction)) { - // store the from_sock_addr if we haven't already done so - // v remember to zero this when you're finished! - if (conn->ap2_remote_control_socket_addr_length == 0) { - memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length); - conn->ap2_remote_control_socket_addr_length = from_sock_addr_length; - } - switch (packet[1]) { - case 215: // code 215, effectively an anchoring announcement - { - // struct timespec tnr; - // clock_gettime(CLOCK_REALTIME, &tnr); - // uint64_t local_realtime_now = timespec_to_ns(&tnr); + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + // store the from_sock_addr if we haven't already done so + // v remember to zero this when you're finished! + if (conn->ap2_remote_control_socket_addr_length == 0) { + memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length); + conn->ap2_remote_control_socket_addr_length = from_sock_addr_length; + } + switch (packet[1]) { + case 215: // code 215, effectively an anchoring announcement + { + // struct timespec tnr; + // clock_gettime(CLOCK_REALTIME, &tnr); + // uint64_t local_realtime_now = timespec_to_ns(&tnr); - /* - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc=0;obfcinput_rate); - // the actual latency is the notified latency plus the fixed latency + the added latency + // debug(1, "we have clock_id: %" PRIx64 ".", clock_id); + // debug(1,"remote_packet_time_ns: %" PRIx64 ", local_realtime_now_ns: %" PRIx64 ".", + // remote_packet_time_ns, local_realtime_now); + uint32_t frame_1 = + nctohl(packet + 4); // this seems to be the frame with latency of 77165 included + uint32_t frame_2 = nctohl(packet + 16); // this seems to be the frame the time refers to + // this just updates the anchor information contained in the packet + // the frame and its remote time + // add in the audio_backend_latency_offset; + int32_t notified_latency = frame_2 - frame_1; + int32_t added_latency = + (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + // the actual latency is the notified latency plus the fixed latency + the added latency - if (added_latency < (-(notified_latency + 11035))) - debug(1, "the audio_backend_latency_offset is causing a negative latency!"); + int32_t net_latency = + notified_latency + 11035 + + added_latency; // this is the latency between incoming frames and the DAC + net_latency = net_latency - + (int32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); + // debug(1, "Net latency is %d frames.", net_latency); - /* - debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); - conn->remote_reference_timestamp_time = remote_packet_time_ns; - conn->reference_timestamp = - frame_1 - 11035 - added_latency; // add the latency in to the anchortime - debug_mutex_unlock(&conn->reference_time_mutex, 0); - */ - // this is now only used for calculating when to ask for resends - conn->latency = notified_latency + 11035 + added_latency; - // debug(1,"conn->latency is %d.", conn->latency); - set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, - remote_packet_time_ns); + if (net_latency <= 0) { + if (conn->latency_warning_issued == 0) { + warn("The stream latency (%f seconds) it too short to accommodate an offset of %f seconds and a backend buffer of %f seconds.", ((notified_latency + 11035) * 1.0)/conn->input_rate, config.audio_backend_latency_offset, config.audio_backend_buffer_desired_length); + warn("(FYI the stream latency needed would be %f seconds.)", config.audio_backend_buffer_desired_length - config.audio_backend_latency_offset); + conn->latency_warning_issued = 1; + } + conn->latency = notified_latency + 11035; + } else { + conn->latency = notified_latency + 11035 + added_latency; + } - } break; - case 0xd6: - // six bytes in is the sequence number at the start of the encrypted audio packet - // returns the sequence number but we're not really interested - decipher_player_put_packet(packet + 6, nread - 6, conn); - break; - default: { - char *packet_in_hex_cstring = - debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards - debug(1, + /* + debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); + conn->remote_reference_timestamp_time = remote_packet_time_ns; + conn->reference_timestamp = + frame_1 - 11035 - added_latency; // add the latency in to the anchortime + debug_mutex_unlock(&conn->reference_time_mutex, 0); + */ + // this is now only used for calculating when to ask for resends + // debug(1, "conn->latency is %d.", conn->latency); + set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, + remote_packet_time_ns); + + } break; + case 0xd6: + // six bytes in is the sequence number at the start of the encrypted audio packet + // returns the sequence number but we're not really interested + decipher_player_put_packet(packet + 6, nread - 6, conn); + break; + default: { + char *packet_in_hex_cstring = + debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards + debug( + 1, "AP2 Control Receiver Packet of first byte 0x%02X, type 0x%02X length %d received: " "\"%s\".", packet[0], packet[1], nread, packet_in_hex_cstring); - free(packet_in_hex_cstring); - } break; + free(packet_in_hex_cstring); + } break; + } + } else { + debug(1, "AP2 Control Receiver -- dropping a packet."); } - } else { - debug(1, "AP2 Control Receiver -- dropping a packet."); } } else if (nread == 0) { debug(1, "AP2 Control Receiver -- connection closed."); From 8bd31c140ec07ae9d2ed238ed90f515472d6e18b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 24 Jan 2022 10:20:23 +0100 Subject: [PATCH 544/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index d49c0c1f..1740e29c 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version-4.1-dev-173-gdeb11654 +==== +#### Enhancements +* Add some sanity checking of the latency offset and buffer size to check if they can be accommodated. +* Discard non-sentinel UDP timing packets that are received just when a timing thread starts -- they may relate to a prior session. + Version-4.1-dev-171-g703717a5 ==== #### Bugfix From 8d3bbe225b1db6af1704bbdbbcbdad4049afaaf6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 28 Jan 2022 11:14:52 +0000 Subject: [PATCH 545/878] Discard non-sentinel packets at the start of a Realtime play -- they might belong to the previous session. --- rtp.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rtp.c b/rtp.c index c38ea966..23af7708 100644 --- a/rtp.c +++ b/rtp.c @@ -1426,7 +1426,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, debug(2, "Connection %d: No NQPTP master clock.", conn->connection_number); break; case clock_no_anchor_info: - debug(1, "Connection %d: No Clock Anchor.", conn->connection_number); + debug(1, "Connection %d: No clock anchor information.", conn->connection_number); break; case clock_version_mismatch: debug(1, "Connection %d: NQPTP clock interface mismatch.", conn->connection_number); @@ -1664,12 +1664,8 @@ void *rtp_ap2_control_receiver(void *arg) { int64_t time_since_start = time_now - start_time; - if (time_since_start <= 10000000) { - debug(1, "Discarding early timing packet."); - } - if (nread > 0) { - if ((time_since_start < 10000000) && ((packet[0] & 0x10) == 0)) { + if ((time_since_start < 2000000) && ((packet[0] & 0x10) == 0)) { debug(1, "Dropping what looks like a (non-sentinel) packet left over from a previous session " "at %f ms.", From 5149854a2bf469a47960665a7d1862f9df6ab0a4 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sat, 29 Jan 2022 15:51:46 +0000 Subject: [PATCH 546/878] s6 overlay improvements. --- docker/Dockerfile | 8 +++++--- docker/README.md | 3 ++- docker/etc/services.d/02-avahi/run | 11 +++++++++-- docker/etc/services.d/03-nqptp/run | 2 +- docker/etc/services.d/04-shairport-sync/finish | 2 -- docker/etc/services.d/04-shairport-sync/run | 2 -- 6 files changed, 17 insertions(+), 11 deletions(-) delete mode 100644 docker/etc/services.d/04-shairport-sync/finish delete mode 100644 docker/etc/services.d/04-shairport-sync/run diff --git a/docker/Dockerfile b/docker/Dockerfile index 132699da..08e5e49b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine AS builder +FROM alpine:3.12 AS builder # Check required arguments exist. These will be provided by the Github Action # Workflow and are required to ensure the correct branches are being used. @@ -67,7 +67,7 @@ RUN make install ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:edge +FROM crazymax/alpine-s6:3.12-edge RUN apk -U add \ alsa-lib \ @@ -107,4 +107,6 @@ 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" ] \ No newline at end of file +ENTRYPOINT [ "/init" ] + +CMD /usr/local/bin/shairport-sync -vu --statistics \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 8ba67faa..408215fe 100644 --- a/docker/README.md +++ b/docker/README.md @@ -36,10 +36,11 @@ To get access to the full range of configuration options, pass the configuration ### Build Example (for arm7 devices) ``` -docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development -t shairport-sync:unstable-development . +docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg SHAIRPORT_SYNC_BRANCH=development --build-arg NQPTP_BRANCH=development --no-cache -t shairport-sync:unstable-development . ``` `SHAIRPORT_SYNC_BRANCH` and `NQPTP_BRANCH` are required to ensure the image is built using the expected branch. +`--no-cache` needs to be used to force buildx to pull the NQPTP branch for new updates. This slows down the build time though so can be removed when it is not beneficial during testing. ### AirPlay 1 Only diff --git a/docker/etc/services.d/02-avahi/run b/docker/etc/services.d/02-avahi/run index 848dd8dd..27cd49c0 100644 --- a/docker/etc/services.d/02-avahi/run +++ b/docker/etc/services.d/02-avahi/run @@ -1,2 +1,9 @@ -#!/usr/bin/execlineb -P -avahi-daemon --no-chroot \ No newline at end of file +#!/bin/sh +avahi-daemon --check +if [ $? -eq 0 ]; then + echo "Reloading avahi-daemon." + avahi-daemon --no-chroot --reload +else + echo "Starting avahi-daemon." + avahi-daemon --no-chroot +fi \ No newline at end of file diff --git a/docker/etc/services.d/03-nqptp/run b/docker/etc/services.d/03-nqptp/run index 7ddde248..0e3e3e4c 100644 --- a/docker/etc/services.d/03-nqptp/run +++ b/docker/etc/services.d/03-nqptp/run @@ -1,2 +1,2 @@ #!/usr/bin/execlineb -P -/usr/local/bin/nqptp \ No newline at end of file +/usr/local/bin/nqptp -v \ No newline at end of file diff --git a/docker/etc/services.d/04-shairport-sync/finish b/docker/etc/services.d/04-shairport-sync/finish deleted file mode 100644 index c9e22a21..00000000 --- a/docker/etc/services.d/04-shairport-sync/finish +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/execlineb -S0 -s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/docker/etc/services.d/04-shairport-sync/run b/docker/etc/services.d/04-shairport-sync/run deleted file mode 100644 index 395ff814..00000000 --- a/docker/etc/services.d/04-shairport-sync/run +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/execlineb -P -/usr/local/bin/shairport-sync -vu --statistics \ No newline at end of file From c37f511319dce415f31db15b21f95c858da75f04 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 30 Jan 2022 16:51:01 +0000 Subject: [PATCH 547/878] Check certain 64-bit and 32-bit conversions. --- rtp.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- rtsp.c | 1 + 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/rtp.c b/rtp.c index 23af7708..58e3b227 100644 --- a/rtp.c +++ b/rtp.c @@ -78,6 +78,68 @@ typedef struct { pthread_cond_t not_full_cv; } buffered_tcp_desc; +void check64conversion(const char *prompt, const uint8_t *source, uint64_t value) { + char converted_value[128]; + sprintf(converted_value,"%" PRIx64 "", value); + + char obf[32]; + char *obfp = obf; + int obfc; + int suppress_zeroes = 1; + for (obfc=0;obfc<8;obfc++) { + if ((suppress_zeroes == 0) || (source[obfc] != 0)){ + if (suppress_zeroes != 0) { + if (source[obfc] < 0x10) { + snprintf(obfp, 2, "%1x", source[obfc]); + obfp+=1; + } else { + snprintf(obfp, 3, "%02x", source[obfc]); + obfp+=2; + } + } else { + snprintf(obfp, 3, "%02x", source[obfc]); + obfp+=2; + } + suppress_zeroes = 0; + } + }; + *obfp=0; + if (strcmp(converted_value,obf) != 0) { + debug(1,"%s check64conversion error converting \"%s\" to %" PRIx64 ".", prompt, obf, value); + } +} + +void check32conversion(const char *prompt, const uint8_t *source, uint32_t value) { + char converted_value[128]; + sprintf(converted_value,"%" PRIx32 "", value); + + char obf[32]; + char *obfp = obf; + int obfc; + int suppress_zeroes = 1; + for (obfc=0;obfc<4;obfc++) { + if ((suppress_zeroes == 0) || (source[obfc] != 0)){ + if (suppress_zeroes != 0) { + if (source[obfc] < 0x10) { + snprintf(obfp, 2, "%1x", source[obfc]); + obfp+=1; + } else { + snprintf(obfp, 3, "%02x", source[obfc]); + obfp+=2; + } + } else { + snprintf(obfp, 3, "%02x", source[obfc]); + obfp+=2; + } + suppress_zeroes = 0; + } + }; + *obfp=0; + if (strcmp(converted_value,obf) != 0) { + debug(1,"%s check32conversion error converting \"%s\" to %" PRIx32 ".", prompt, obf, value); + } +} + void rtp_initialise(rtsp_conn_info *conn) { conn->rtp_time_of_last_resend_request_error_ns = 0; conn->rtp_running = 0; @@ -1212,6 +1274,7 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { + // debug(1,"clock: %" PRIx64 ", rtptime: %" PRIu32 ".", clock_id, rtptime); if (conn->anchor_clock != clock_id) { debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); conn->anchor_clock_is_new = 1; @@ -1304,7 +1367,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if (actual_clock_id == conn->anchor_clock) { // if the master clock and the anchor clock are the same // wait at least this time before using the new master clock - // note that mastershipo may be backdated + // note that mastership may be backdated if (duration_of_mastership < 1500000000) { debug(3, "master not old enough yet: %f ms", 0.000001 * duration_of_mastership); response = clock_not_ready; @@ -1314,7 +1377,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // and it at least is the minimum age. conn->last_anchor_rtptime = conn->anchor_rtptime; conn->last_anchor_local_time = conn->anchor_time - actual_offset; - conn->last_anchor_time_of_update = get_absolute_time_in_ns(); + conn->last_anchor_time_of_update = time_now; if (conn->last_anchor_info_is_valid == 0) conn->last_anchor_validity_start_time = start_of_mastership; conn->last_anchor_info_is_valid = 1; @@ -1404,7 +1467,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP new master clock %" PRIx64 ".", conn->connection_number, + debug(1, "Connection %d: NQPTP new master clock %" PRIx64 " with anchor frame %" PRIx32 ".", conn->connection_number, actual_clock_id); break; case clock_not_ready: @@ -1710,18 +1773,24 @@ void *rtp_ap2_control_receiver(void *arg) { */ uint64_t remote_packet_time_ns = nctoh64(packet + 8); + check64conversion("remote_packet_time_ns", packet + 8, remote_packet_time_ns); uint64_t clock_id = nctoh64(packet + 20); + check64conversion("clock_id", packet + 20, clock_id); // debug(1, "we have clock_id: %" PRIx64 ".", clock_id); // debug(1,"remote_packet_time_ns: %" PRIx64 ", local_realtime_now_ns: %" PRIx64 ".", // remote_packet_time_ns, local_realtime_now); uint32_t frame_1 = nctohl(packet + 4); // this seems to be the frame with latency of 77165 included + check32conversion("frame_1", packet + 4, frame_1); uint32_t frame_2 = nctohl(packet + 16); // this seems to be the frame the time refers to + check32conversion("frame_2", packet + 16, frame_2); // this just updates the anchor information contained in the packet // the frame and its remote time // add in the audio_backend_latency_offset; int32_t notified_latency = frame_2 - frame_1; + if (notified_latency != 77175) + debug(1,"Notified latency is %d frames.", notified_latency); int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); // the actual latency is the notified latency plus the fixed latency + the added latency @@ -1753,6 +1822,7 @@ void *rtp_ap2_control_receiver(void *arg) { */ // this is now only used for calculating when to ask for resends // debug(1, "conn->latency is %d.", conn->latency); + // debug(1,"frame_1: %" PRIu32 ", added latency: %" PRId32 ".", frame_1, added_latency); set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, remote_packet_time_ns); diff --git a/rtsp.c b/rtsp.c index c4e8d831..b8d5ad18 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1827,6 +1827,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message uint32_t anchorRTPTime = rtpTime; int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, added_latency); set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, anchorTimeNanoseconds); } From 8127b1c4702cd09116081ca261581a968bc860b3 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Mon, 31 Jan 2022 17:57:54 +0000 Subject: [PATCH 548/878] Fix for v3 of s6 overlay and other improvements. --- .dockerignore | 2 ++ docker/Dockerfile | 7 ++++--- docker/etc/{services.d => s6-overlay/s6-rc.d}/01-dbus/run | 2 +- docker/etc/s6-overlay/s6-rc.d/01-dbus/type | 1 + docker/etc/{services.d => s6-overlay/s6-rc.d}/02-avahi/run | 4 ++-- docker/etc/s6-overlay/s6-rc.d/02-avahi/type | 1 + docker/etc/s6-overlay/s6-rc.d/03-nqptp/run | 2 ++ docker/etc/s6-overlay/s6-rc.d/03-nqptp/type | 1 + docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus | 0 docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi | 0 docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp | 0 docker/etc/services.d/03-nqptp/run | 2 -- 12 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .dockerignore rename docker/etc/{services.d => s6-overlay/s6-rc.d}/01-dbus/run (50%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/01-dbus/type rename docker/etc/{services.d => s6-overlay/s6-rc.d}/02-avahi/run (63%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/02-avahi/type create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-nqptp/run create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-nqptp/type create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp delete mode 100644 docker/etc/services.d/03-nqptp/run diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..98c4e3ee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.github +documents \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 08e5e49b..2a465a38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -64,10 +64,11 @@ RUN ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ss --with-apple-alac --with-convolution RUN make -j RUN make install +WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-edge +FROM crazymax/alpine-s6:3.12-3.0.0.2 RUN apk -U add \ alsa-lib \ @@ -93,12 +94,12 @@ COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ COPY --from=builder /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ -COPY ./docker/etc /etc +COPY ./docker/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d # Create non-root user for running the container -- running as the user 'shairport-sync' also allows # Shairport Sync to provide the D-Bus and MPRIS interfaces within the container -RUN addgroup shairport-sync +RUN addgroup shairport-sync RUN adduser -D shairport-sync -G shairport-sync # Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff diff --git a/docker/etc/services.d/01-dbus/run b/docker/etc/s6-overlay/s6-rc.d/01-dbus/run similarity index 50% rename from docker/etc/services.d/01-dbus/run rename to docker/etc/s6-overlay/s6-rc.d/01-dbus/run index 7bb0350e..096e093a 100644 --- a/docker/etc/services.d/01-dbus/run +++ b/docker/etc/s6-overlay/s6-rc.d/01-dbus/run @@ -1,3 +1,3 @@ #!/bin/sh rm -rf /var/run/dbus.pid -dbus-daemon --system --nofork \ No newline at end of file +exec dbus-daemon --system --nofork \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/type b/docker/etc/s6-overlay/s6-rc.d/01-dbus/type new file mode 100644 index 00000000..1780f9f4 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-dbus/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/docker/etc/services.d/02-avahi/run b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run similarity index 63% rename from docker/etc/services.d/02-avahi/run rename to docker/etc/s6-overlay/s6-rc.d/02-avahi/run index 27cd49c0..e1cdd1c8 100644 --- a/docker/etc/services.d/02-avahi/run +++ b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run @@ -2,8 +2,8 @@ avahi-daemon --check if [ $? -eq 0 ]; then echo "Reloading avahi-daemon." - avahi-daemon --no-chroot --reload + exec avahi-daemon --no-chroot --reload else echo "Starting avahi-daemon." - avahi-daemon --no-chroot + exec avahi-daemon --no-chroot fi \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/type b/docker/etc/s6-overlay/s6-rc.d/02-avahi/type new file mode 100644 index 00000000..1780f9f4 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-avahi/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run new file mode 100644 index 00000000..8b07c68f --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/local/bin/nqptp -v \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/type b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/type new file mode 100644 index 00000000..1780f9f4 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus new file mode 100644 index 00000000..e69de29b diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi new file mode 100644 index 00000000..e69de29b diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp new file mode 100644 index 00000000..e69de29b diff --git a/docker/etc/services.d/03-nqptp/run b/docker/etc/services.d/03-nqptp/run deleted file mode 100644 index 0e3e3e4c..00000000 --- a/docker/etc/services.d/03-nqptp/run +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/execlineb -P -/usr/local/bin/nqptp -v \ No newline at end of file From f135d857c06df35ee80b2be1f0f955d04a6e956f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:44:43 +0000 Subject: [PATCH 549/878] Ensure that when ignore_volume_control is true, the volume metadata has the current, max and min values of the [now ignored] attenuator set to 0.0. --- player.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/player.c b/player.c index dadfc48b..1dd5a0e7 100644 --- a/player.c +++ b/player.c @@ -3162,14 +3162,18 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c // is being used by shairport sync to control the output volume char dv[128]; memset(dv, 0, 128); - if (volume_mode == vol_both) { - // normalise the maximum output to the hardware device's max output - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, - (scaled_attenuation - max_db + hw_max_db) / 100.0, - (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); + if (config.ignore_volume_control == 0) { + if (volume_mode == vol_both) { + // normalise the maximum output to the hardware device's max output + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, + (scaled_attenuation - max_db + hw_max_db) / 100.0, + (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); + } else { + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, + min_db / 100.0, max_db / 100.0); + } } else { - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, - min_db / 100.0, max_db / 100.0); + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); } send_ssnc_metadata('pvol', dv, strlen(dv), 1); #endif From 8b483c09a1faebafea5436cc3a4ecb2d44cef72c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:57:36 +0000 Subject: [PATCH 550/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 1740e29c..4d2e79b7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version-4.1-dev-188-gf135d857 +==== +#### Bugfixes +* Ensure that when `ignore_volume_control` is true, the volume metadata has the current, max and min values of the [now ignored] attenuator set to 0.0. Thanks to [Michael Daley](https://github.com/greend139) for reporting in Issue [#1417](https://github.com/mikebrady/shairport-sync/issues/1417). +* Add a fix for v3 of s6 overlay and other improvements to the Docker image. Thanks to [Charles Omer](https://github.com/charlesomer) for the fix and the improvements and thanks to [rdrdog](https://github.com/rdrdog) for reporting the issue. + +#### Other Changes +* Add code to check certain 32-bit and 64-bit conversions. + Version-4.1-dev-173-gdeb11654 ==== #### Enhancements From 3d720d65bf506d246a5d7210e391e0090de4b7bc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:36:08 +0000 Subject: [PATCH 551/878] MQTT -- add code to deal with empty payloads by adding a dummy payload to empty messages by default. --- common.h | 1 + mqtt.c | 12 ++++++++++-- scripts/shairport-sync.conf | 10 +++++++++- shairport.c | 15 ++++++++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/common.h b/common.h index 904b9279..f81540b7 100644 --- a/common.h +++ b/common.h @@ -151,6 +151,7 @@ typedef struct { int mqtt_publish_parsed; int mqtt_publish_cover; int mqtt_enable_remote; + char *mqtt_empty_payload_substitute; #endif uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; diff --git a/mqtt.c b/mqtt.c index c4982d99..c6808515 100644 --- a/mqtt.c +++ b/mqtt.c @@ -93,12 +93,20 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, } // helper function to publish under a topic and automatically append the main topic -void mqtt_publish(char *topic, char *data, uint32_t length) { +void mqtt_publish(char *topic, char *data_in, uint32_t length_in) { + char *data = data_in; + uint32_t length = length_in; + + if ((length == 0) && (config.mqtt_empty_payload_substitute != NULL)) { + length = strlen(config.mqtt_empty_payload_substitute); + data = config.mqtt_empty_payload_substitute; + } + char fulltopic[strlen(config.mqtt_topic) + strlen(topic) + 3]; snprintf(fulltopic, strlen(config.mqtt_topic) + strlen(topic) + 2, "%s/%s", config.mqtt_topic, topic); debug(2, "[MQTT]: publishing under %s", fulltopic); - + int rc; if ((rc = mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0)) != MOSQ_ERR_SUCCESS) { diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index edb204d5..730b4c1c 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -224,6 +224,12 @@ metadata = // How to enable the MQTT-metadata/remote-service // For this section to be operative, Shairport Sync must be built with the following configuration flag: // --with-mqtt-client + +// Note that, for compatability with many MQTT brokers and applications, +// every message that has no extra data is given a +// payload consisting of the string "--". +// You can change this or you can enable empty payloads -- see below. + mqtt = { // enabled = "no"; // set this to yes to enable the mqtt-metadata-service @@ -238,8 +244,10 @@ mqtt = // topic = NULL; //MQTT topic where this instance of shairport-sync should publish. If not set, the general.name value is used. // publish_raw = "no"; //whether to publish all available metadata under the codes given in the 'metadata' docs. // publish_parsed = "no"; //whether to publish a small (but useful) subset of metadata under human-understandable topics +// empty_payload_substitute = "--"; // MQTT messages with empty payloads often are invisible or have special significance to MQTT brokers and readers. +// To avoid empty payload problems, the string here is used instead of any empty payload. Set it to the empty string -- "" -- to leave the payload empty. // Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip, -// Additionally, empty messages at the topics play_start,play_end,play_flush,play_resume are published +// Additionally, messages at the topics play_start,play_end,play_flush,play_resume are published // publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker // enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote. // Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup" diff --git a/shairport.c b/shairport.c index 89a57961..e9fdd8d2 100644 --- a/shairport.c +++ b/shairport.c @@ -1181,6 +1181,14 @@ int parse_options(int argc, char **argv) { die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover"); } config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote); + if (config_lookup_string(config.cfg, "mqtt.empty_payload_substitute", &str)) { + if (strlen(str) == 0) + config.mqtt_empty_payload_substitute = NULL; + else + config.mqtt_empty_payload_substitute = strdup(str); + } else { + config.mqtt_empty_payload_substitute = strdup("--"); + } #ifndef CONFIG_AVAHI if (config.mqtt_enable_remote) { die("You have enabled MQTT remote control which requires shairport-sync to be built with " @@ -1191,7 +1199,7 @@ int parse_options(int argc, char **argv) { #endif #ifdef CONFIG_AIRPLAY_2 - int64_t aid; + long long aid; // replace the airplay_device_id with this, if provided if (config_lookup_int64(config.cfg, "general.airplay_device_id", &aid)) { @@ -1510,6 +1518,11 @@ void exit_function() { if (config.service_name) free(config.service_name); +#ifdef CONFIG_MQTT + if (config.mqtt_empty_payload_substitute) + free(config.mqtt_empty_payload_substitute); +#endif + #ifdef CONFIG_CONVOLUTION if (config.convolution_ir_file) free(config.convolution_ir_file); From e07c202b7619fa250adacc7d0ee2c2b94d60140f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:37:49 +0000 Subject: [PATCH 552/878] Quieten a compiler warning -- might not work. --- rtp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index 58e3b227..ad27b29c 100644 --- a/rtp.c +++ b/rtp.c @@ -90,7 +90,7 @@ void check64conversion(const char *prompt, const uint8_t *source, uint64_t value if ((suppress_zeroes == 0) || (source[obfc] != 0)){ if (suppress_zeroes != 0) { if (source[obfc] < 0x10) { - snprintf(obfp, 2, "%1x", source[obfc]); + snprintf(obfp, 3, "%1x", source[obfc]); obfp+=1; } else { snprintf(obfp, 3, "%02x", source[obfc]); @@ -121,7 +121,7 @@ void check32conversion(const char *prompt, const uint8_t *source, uint32_t value if ((suppress_zeroes == 0) || (source[obfc] != 0)){ if (suppress_zeroes != 0) { if (source[obfc] < 0x10) { - snprintf(obfp, 2, "%1x", source[obfc]); + snprintf(obfp, 3, "%1x", source[obfc]); obfp+=1; } else { snprintf(obfp, 3, "%02x", source[obfc]); From bd55f1e42683556426f51e7a5ad2d98ec6b9d0e5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:57:22 +0000 Subject: [PATCH 553/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 4d2e79b7..90cbefbd 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version-4.1-dev-191-ge07c202b +==== +#### Enhancement +* This enhancement relates to MQTT with empty payloads. Many MQTT brokers and clients treat messages with empty payloads in a special way. For instance, [MQTT Explorer](http://mqtt-explorer.com) hides messages with empty payloads and [Node-RED](https://nodered.org) seems to use them to release and garbage-collect data received previously. +Many Shairport Sync messages contain no extra data and so would naturally have empty payloads. This is causing problems -- see [#1425](https://github.com/mikebrady/shairport-sync/issues/1425) and [#1375](https://github.com/mikebrady/shairport-sync/issues/1375). To avoid these problems, a short "dummy" payload consisting of the character string "--" is now added to MQTT messages that contain no extra data. This can be changed with a new MQTT option in the configuration file: `empty_payload_substitute`. Thanks to [DOCaCola](https://github.com/DOCaCola) and [UMB8998](https://github.com/UMB8998) for reporting on this. + Version-4.1-dev-188-gf135d857 ==== #### Bugfixes From 4848608dd554abb8da439ce1daca8d3a492606d8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Feb 2022 13:39:19 +0000 Subject: [PATCH 554/878] Don't try to decode commands at debug level 2 -- leave it to level 3. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index b8d5ad18..444a6594 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2271,7 +2271,7 @@ void handle_feedback(__attribute__((unused)) rtsp_conn_info *conn, void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { - debug_log_rtsp_message(2, "POST /command", req); + debug_log_rtsp_message(3, "POST /command", req); if (rtsp_message_contains_plist(req)) { plist_t command_dict = NULL; plist_from_memory(req->content, req->contentlength, &command_dict); @@ -2305,7 +2305,7 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * if (subsidiary_plist) { char *printable_plist = plist_content(subsidiary_plist); if (printable_plist) { - debug(2, "\n%s", printable_plist); + debug(3, "\n%s", printable_plist); free(printable_plist); } else { debug(1, "Can't print the plist!"); From b622c179bffab7ba09819be534c8e2108a2300f2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Feb 2022 13:41:35 +0000 Subject: [PATCH 555/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 90cbefbd..5fe0ecba 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version-4.1-dev-193-g4848608d +==== +* Don't try to decode commands at debug level 2 -- leave it to level 3. + Version-4.1-dev-191-ge07c202b ==== #### Enhancement From c6fd0fbfdb93b776dd41f1519951c8f8d94bf4f5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:27:45 +0000 Subject: [PATCH 556/878] Open the SHM instrface atstartup and close it on shutdown. --- shairport.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shairport.c b/shairport.c index e9fdd8d2..ea7b0b95 100644 --- a/shairport.c +++ b/shairport.c @@ -47,6 +47,7 @@ #include #include #include +#include "ptp-utilities.h" #endif #ifdef CONFIG_MBEDTLS @@ -1541,6 +1542,7 @@ void exit_function() { free(config.airplay_pin); if (config.airplay_pi) free(config.airplay_pi); + ptp_shm_interface_close(); // close it if it's open #endif #ifdef CONFIG_LIBDAEMON @@ -2208,6 +2210,10 @@ int main(int argc, char **argv) { } #endif +#ifdef CONFIG_AIRPLAY_2 + ptp_shm_interface_open(); +#endif + activity_monitor_start(); // not yet for AP2 pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL); atexit(exit_rtsp_listener); From c0feef51f82b0e6069662baab68752b135a0f414 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:28:49 +0000 Subject: [PATCH 557/878] Always try to open the shm interface when needed -- it should be already open, but ya never know. --- rtsp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/rtsp.c b/rtsp.c index 444a6594..d277b7c0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2686,6 +2686,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and // timingPeerList if (conn->airplay_stream_category == ptp_stream) { + ptp_shm_interface_open(); // it should be open already, but just in case it isn't... ptp_send_control_message_string("T"); // remove all previous history debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); From 341f67acd2f1e1bc1d1c3ac52e9a40eb0d2cd321 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:32:15 +0000 Subject: [PATCH 558/878] Redesign the ptp shm utilities to cater for opening once and closing once but potentially called for opening many times. --- ptp-utilities.c | 102 ++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index e04058c6..73fe70c1 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -62,6 +62,13 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { memcpy(nqptp_data, (char *)mapped_addr, sizeof(struct shm_structure)); pthread_cleanup_pop(1); // release the mutex response = 0; + } else { + if (mapped_addr == NULL) + debug(1, "get_nqptp_data because the mapped_addr is NULL"); + else if (mapped_addr == MAP_FAILED) + debug(1, "get_nqptp_data because the mapped_addr is MAP_FAILED"); + else + debug(1, "get_nqptp_data failed"); } return response; } @@ -69,7 +76,6 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; - pthread_cleanup_debug_mutex_lock(&ptp_access_mutex, 50000, 1); if (actual_clock_id != NULL) *actual_clock_id = 0; if (raw_offset != NULL) @@ -78,67 +84,68 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint *time_of_sample = 0; if (mastership_start_time != NULL) *mastership_start_time = 0; - if (ptp_shm_interface_open() == 0) { - struct shm_structure nqptp_data; - if (get_nqptp_data(&nqptp_data) == 0) { - if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { - // assuming a clock id can not be zero - if (nqptp_data.master_clock_id != 0) { - if (actual_clock_id != NULL) - *actual_clock_id = nqptp_data.master_clock_id; - if (time_of_sample != NULL) - *time_of_sample = nqptp_data.local_time; - if (raw_offset != NULL) - *raw_offset = nqptp_data.local_to_master_time_offset; - if (mastership_start_time != NULL) - *mastership_start_time = nqptp_data.master_clock_start_time; - } else { - response = clock_no_master; - } + // if (ptp_shm_interface_open() == 0) { + struct shm_structure nqptp_data; + if (get_nqptp_data(&nqptp_data) == 0) { + if (nqptp_data.version == NQPTP_SHM_STRUCTURES_VERSION) { + // assuming a clock id can not be zero + if (nqptp_data.master_clock_id != 0) { + if (actual_clock_id != NULL) + *actual_clock_id = nqptp_data.master_clock_id; + if (time_of_sample != NULL) + *time_of_sample = nqptp_data.local_time; + if (raw_offset != NULL) + *raw_offset = nqptp_data.local_to_master_time_offset; + if (mastership_start_time != NULL) + *mastership_start_time = nqptp_data.master_clock_start_time; } else { - // the version can not be zero. If zero is returned here, it means that the shared memory is - // not yet initialised, so not availalbe - if (nqptp_data.version == 0) - response = clock_service_unavailable; - else - response = clock_version_mismatch; + response = clock_no_master; } } else { - response = clock_data_unavailable; + // the version can not be zero. If zero is returned here, it means that the shared memory is + // not yet initialised, so not availalbe + if (nqptp_data.version == 0) + response = clock_service_unavailable; + else + response = clock_version_mismatch; } - if (ptp_shm_interface_close() != 0) - response = clock_access_error; } else { - response = clock_service_unavailable; + response = clock_data_unavailable; } - pthread_cleanup_pop(1); // release the mutex return response; } int ptp_shm_interface_open() { - mapped_addr = NULL; - int response = -1; - if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { - response = 0; - int shared_memory_file_descriptor = - shm_open(config.nqptp_shared_memory_interface_name, O_RDWR, 0); - if (shared_memory_file_descriptor >= 0) { - mapped_addr = - // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the - // mutex to lock and unlock - mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, - shared_memory_file_descriptor, 0); - if (mapped_addr == MAP_FAILED) { - response = -1; - } - if (close(shared_memory_file_descriptor) == -1) { + int response = 0; + if (mapped_addr == NULL) { + response = -1; + debug(1, "ptp_shm_interface_open"); + if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { + response = 0; + int shared_memory_file_descriptor = + shm_open(config.nqptp_shared_memory_interface_name, O_RDWR, 0); + if (shared_memory_file_descriptor >= 0) { + mapped_addr = + // needs to be PROT_READ | PROT_WRITE to allow the mapped memory to be writable for the + // mutex to lock and unlock + mmap(NULL, sizeof(struct shm_structure), PROT_READ | PROT_WRITE, MAP_SHARED, + shared_memory_file_descriptor, 0); + if (mapped_addr == MAP_FAILED) { + response = -1; + } + if (close(shared_memory_file_descriptor) == -1) { + response = -1; + } + } else { response = -1; } } else { - response = -1; + debug(1, "No config.nqptp_shared_memory_interface_name"); } + if (response != 0) + mapped_addr = NULL; // in case we try again later... } else { - debug(1, "No config.nqptp_shared_memory_interface_name"); + debug(1, "ptp_shm_interface_open -- already open!"); } return response; } @@ -146,6 +153,7 @@ int ptp_shm_interface_open() { int ptp_shm_interface_close() { int response = -1; if ((mapped_addr != MAP_FAILED) && (mapped_addr != NULL)) { + debug(1, "ptp_shm_interface_close"); response = munmap(mapped_addr, sizeof(struct shm_structure)); if (response != 0) debug(1, "error unmapping shared memory."); From 70ef99e924c6e51c5b468b46eb93d52ffded980f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:33:19 +0000 Subject: [PATCH 559/878] Add a special-purpose flush routine called sub_flush. It doesn't close the device ever. --- audio_alsa.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 88a4bb0c..d971ca2e 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1834,6 +1834,24 @@ int do_close() { return derr; } +int sub_flush() { + if (alsa_backend_state == abm_disconnected) + debug(1, "alsa: do_flush() -- closing the output device when it is already " + "disconnected"); + int derr = 0; + if (alsa_handle) { + debug(2,"alsa: do_flush() -- flushing the output device"); + frames_sent_break_occurred = 1; + if ((derr = snd_pcm_drop(alsa_handle))) + debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); + if ((derr = snd_pcm_prepare(alsa_handle))) + debug(1, "Error %d (\"%s\") preparing output device after flush.", derr, snd_strerror(derr)); + } else { + debug(1, "alsa: do_flush() -- output device already closed."); + } + return derr; +} + int play(void *buf, int samples) { // play() will change the state of the alsa_backend_mode to abm_playing @@ -1896,18 +1914,9 @@ int prepare(void) { static void flush(void) { // debug(2,"audio_alsa flush called."); pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); - // mute_requested_internally = 1; // request a mute for backend's reasons - // debug(2, "flush() set_mute_state"); - // set_mute_state(); - // do_mute(1); // mute for backend's own reasons if (alsa_backend_state != abm_disconnected) { // must be playing or connected... - if (config.keep_dac_busy != 0) { - debug(2, "alsa: flush() -- alsa_backend_state => abm_connected."); - alsa_backend_state = abm_connected; - } else { - debug(2, "alsa: flush() -- closing the output device"); - do_close(); // will change the state to disconnected - debug(2, "alsa: flush() -- alsa_backend_state => abm_disconnected."); + if (config.keep_dac_busy == 0) { + sub_flush(); } } else debug(3, "alsa: flush() -- called on a disconnected alsa backend"); From 322f07720afe0fa104561bc16629fa9a3e38d689 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 27 Feb 2022 13:00:03 +1100 Subject: [PATCH 560/878] Require NQPTP to be running and create the SHM interface at startup. --- ptp-utilities.c | 15 +++++++++++---- ptp-utilities.h | 1 + rtsp.c | 3 +-- shairport.c | 7 ++++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 73fe70c1..8b05101e 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -46,7 +46,7 @@ #include "ptp-utilities.h" int shm_fd; -void *mapped_addr = NULL; +void *mapped_addr; static pthread_mutex_t ptp_access_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -115,9 +115,13 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint return response; } +void ptp_shm_interface_init() { + mapped_addr = NULL; +} + int ptp_shm_interface_open() { int response = 0; - if (mapped_addr == NULL) { + if ((mapped_addr == NULL) || (mapped_addr == MAP_FAILED)) { response = -1; debug(1, "ptp_shm_interface_open"); if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { @@ -138,12 +142,15 @@ int ptp_shm_interface_open() { } } else { response = -1; + } + if (response == 0) + debug(1, "ptp_shm_interface_open successful"); + else + debug(1, "ptp_shm_interface_open failed"); } else { debug(1, "No config.nqptp_shared_memory_interface_name"); } - if (response != 0) - mapped_addr = NULL; // in case we try again later... } else { debug(1, "ptp_shm_interface_open -- already open!"); } diff --git a/ptp-utilities.h b/ptp-utilities.h index f18169d9..86d2b499 100644 --- a/ptp-utilities.h +++ b/ptp-utilities.h @@ -34,6 +34,7 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint void ptp_send_control_message_string(const char *msg); +void ptp_shm_interface_init(); int ptp_shm_interface_open(); int ptp_shm_interface_close(); diff --git a/rtsp.c b/rtsp.c index d277b7c0..29d1e4e0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2686,8 +2686,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and // timingPeerList if (conn->airplay_stream_category == ptp_stream) { - ptp_shm_interface_open(); // it should be open already, but just in case it isn't... - ptp_send_control_message_string("T"); // remove all previous history + ptp_send_control_message_string("T"); // incidentally create the named SHM if necessary and remove all previous history debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { diff --git a/shairport.c b/shairport.c index ea7b0b95..c5dc81fd 100644 --- a/shairport.c +++ b/shairport.c @@ -2211,7 +2211,12 @@ int main(int argc, char **argv) { #endif #ifdef CONFIG_AIRPLAY_2 - ptp_shm_interface_open(); + ptp_shm_interface_init(); + ptp_send_control_message_string("T"); // incidentally create the named SHM and remove all previous history + usleep(100000); // wait for it to get done (?) + if (ptp_shm_interface_open() != 0) { + debug(1,"unable to open the shm interface at startup!"); + } #endif activity_monitor_start(); // not yet for AP2 From 18ed1bda876e973993fe2b56c5b14cd93deef28a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:23:14 +1100 Subject: [PATCH 561/878] Add the opening of the shm interface to the startup process and die if it can't be done. Quieten some debug messages. --- ptp-utilities.c | 8 ++++---- shairport.c | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 8b05101e..23ce53ea 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -123,7 +123,7 @@ int ptp_shm_interface_open() { int response = 0; if ((mapped_addr == NULL) || (mapped_addr == MAP_FAILED)) { response = -1; - debug(1, "ptp_shm_interface_open"); + debug(2, "ptp_shm_interface_open"); if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { response = 0; int shared_memory_file_descriptor = @@ -145,14 +145,14 @@ int ptp_shm_interface_open() { } if (response == 0) - debug(1, "ptp_shm_interface_open successful"); + debug(2, "ptp_shm_interface_open successful"); else - debug(1, "ptp_shm_interface_open failed"); + debug(2, "ptp_shm_interface_open failed"); } else { debug(1, "No config.nqptp_shared_memory_interface_name"); } } else { - debug(1, "ptp_shm_interface_open -- already open!"); + debug(2, "ptp_shm_interface_open -- already open!"); } return response; } diff --git a/shairport.c b/shairport.c index c5dc81fd..b0229e40 100644 --- a/shairport.c +++ b/shairport.c @@ -2214,8 +2214,10 @@ int main(int argc, char **argv) { ptp_shm_interface_init(); ptp_send_control_message_string("T"); // incidentally create the named SHM and remove all previous history usleep(100000); // wait for it to get done (?) - if (ptp_shm_interface_open() != 0) { - debug(1,"unable to open the shm interface at startup!"); + if (ptp_shm_interface_open() == 0) { + debug(1,"shm interface opened successfully!"); + } else { + die("Unable to open the shm interface at startup!"); } #endif From 0aea8107355e756294054fdbc59a9bab4c57f32d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:59:41 +1100 Subject: [PATCH 562/878] experiment with different startup regimes --- rtsp.c | 9 +++++++++ scripts/shairport-sync.service.in | 3 +++ shairport.c | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index 29d1e4e0..d43ea8e0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2687,6 +2687,15 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // timingPeerList if (conn->airplay_stream_category == ptp_stream) { ptp_send_control_message_string("T"); // incidentally create the named SHM if necessary and remove all previous history + + if (ptp_shm_interface_open() == 0) { + debug(1,"shm interface opened successfully!"); + } else { + warn("Unable to open the shm interface at SETUP!"); + } + + + debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { diff --git a/scripts/shairport-sync.service.in b/scripts/shairport-sync.service.in index d1dca5af..efd99a43 100644 --- a/scripts/shairport-sync.service.in +++ b/scripts/shairport-sync.service.in @@ -1,6 +1,9 @@ [Unit] Description=Shairport Sync - AirPlay Audio Receiver +Provides=shairport-sync.service After=sound.target +Requires=nqptp.service +Wants=nqptp.service Requires=avahi-daemon.service After=avahi-daemon.service Wants=network-online.target diff --git a/shairport.c b/shairport.c index b0229e40..3dcdcff5 100644 --- a/shairport.c +++ b/shairport.c @@ -2213,11 +2213,11 @@ int main(int argc, char **argv) { #ifdef CONFIG_AIRPLAY_2 ptp_shm_interface_init(); ptp_send_control_message_string("T"); // incidentally create the named SHM and remove all previous history - usleep(100000); // wait for it to get done (?) + usleep(1000000); // wait for it to get done (?) if (ptp_shm_interface_open() == 0) { debug(1,"shm interface opened successfully!"); } else { - die("Unable to open the shm interface at startup!"); + warn("Unable to open the shm interface at startup!"); } #endif From e8fec7fa704950c10ddf5178a7941953ec508d7f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:58:15 +1100 Subject: [PATCH 563/878] Wait for two seconds for the NQPTP service to appear. --- shairport.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/shairport.c b/shairport.c index 3dcdcff5..dc8b5408 100644 --- a/shairport.c +++ b/shairport.c @@ -2211,13 +2211,23 @@ int main(int argc, char **argv) { #endif #ifdef CONFIG_AIRPLAY_2 - ptp_shm_interface_init(); - ptp_send_control_message_string("T"); // incidentally create the named SHM and remove all previous history - usleep(1000000); // wait for it to get done (?) - if (ptp_shm_interface_open() == 0) { - debug(1,"shm interface opened successfully!"); + ptp_send_control_message_string("T"); // get nqptp to create the named shm interface + int ptp_check_times = 0; + const int ptp_wait_interval_us = 5000; + // wait for up to two seconds for NQPTP to come online + do { + ptp_send_control_message_string("T"); // get nqptp to create the named shm interface + usleep(ptp_wait_interval_us); + ptp_check_times++; + } while ((ptp_shm_interface_open() != 0) && (ptp_check_times < (2000000 / ptp_wait_interval_us))); + + if (ptp_shm_interface_open() != 0) { + die("Can't access NQPTP! Is it installed and running?"); } else { - warn("Unable to open the shm interface at startup!"); + if (ptp_check_times == 1) + debug(1,"NQPTP is online."); + else + debug(1,"NQPTP is online after %u microseconds.",ptp_check_times * ptp_wait_interval_us); } #endif From 73766b070569b83cc2bbba21c87f2fe274c6f7e8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:59:43 +1100 Subject: [PATCH 564/878] Clean up ptp_shm_interface_open and add code to warn if the timing sample shared in the shm is too old -- may indicate NQPTP is gone. --- ptp-utilities.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 23ce53ea..993711d7 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -46,7 +46,7 @@ #include "ptp-utilities.h" int shm_fd; -void *mapped_addr; +void *mapped_addr = NULL; static pthread_mutex_t ptp_access_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -73,6 +73,7 @@ int get_nqptp_data(struct shm_structure *nqptp_data) { return response; } +int age_signalled = 0; int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, uint64_t *mastership_start_time) { int response = clock_ok; @@ -94,6 +95,15 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint *actual_clock_id = nqptp_data.master_clock_id; if (time_of_sample != NULL) *time_of_sample = nqptp_data.local_time; + int64_t age_of_sample = get_absolute_time_in_ns() - nqptp_data.local_time; + int64_t too_old_time = 1000000000; + too_old_time = too_old_time * 10; // seconds + if ((age_of_sample >= too_old_time) && (age_signalled == 0)) { + warn("PTP sample time is old -- is NQPTP running?"); + age_signalled = 1; + } + if (age_of_sample < too_old_time) + age_signalled = 0; if (raw_offset != NULL) *raw_offset = nqptp_data.local_to_master_time_offset; if (mastership_start_time != NULL) @@ -115,15 +125,16 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint return response; } -void ptp_shm_interface_init() { - mapped_addr = NULL; -} - int ptp_shm_interface_open() { int response = 0; + debug(2, "ptp_shm_interface_open with mapped_addr = %" PRIuPTR "", mapped_addr); if ((mapped_addr == NULL) || (mapped_addr == MAP_FAILED)) { response = -1; - debug(2, "ptp_shm_interface_open"); + if (mapped_addr == NULL) + debug(3, "ptp_shm_interface_open is NULL"); + if (mapped_addr == MAP_FAILED) + debug(3, "ptp_shm_interface_open is MAP_FAILED"); + if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { response = 0; int shared_memory_file_descriptor = @@ -142,15 +153,14 @@ int ptp_shm_interface_open() { } } else { response = -1; - } - if (response == 0) - debug(2, "ptp_shm_interface_open successful"); - else - debug(2, "ptp_shm_interface_open failed"); } else { debug(1, "No config.nqptp_shared_memory_interface_name"); } + if (response == 0) + debug(2, "ptp_shm_interface_open -- success!"); + else + debug(2, "ptp_shm_interface_open -- fail!"); } else { debug(2, "ptp_shm_interface_open -- already open!"); } From 2706a6230471c117c368ed36c19cff5432f70484 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:00:25 +1100 Subject: [PATCH 565/878] Fix a diagnostic message. --- rtp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index ad27b29c..7e38bbec 100644 --- a/rtp.c +++ b/rtp.c @@ -1467,7 +1467,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, if ((clock_status_t)response != conn->clock_status) { switch (response) { case clock_ok: - debug(1, "Connection %d: NQPTP new master clock %" PRIx64 " with anchor frame %" PRIx32 ".", conn->connection_number, + debug(1, "Connection %d: NQPTP new master clock %" PRIx64 ".", conn->connection_number, actual_clock_id); break; case clock_not_ready: From afc82b180016b8c19778068089ba1dda96493275 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:04:55 +1100 Subject: [PATCH 566/878] Declare the service provided. --- scripts/shairport-sync.service.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/shairport-sync.service.in b/scripts/shairport-sync.service.in index efd99a43..a1ee6197 100644 --- a/scripts/shairport-sync.service.in +++ b/scripts/shairport-sync.service.in @@ -2,8 +2,6 @@ Description=Shairport Sync - AirPlay Audio Receiver Provides=shairport-sync.service After=sound.target -Requires=nqptp.service -Wants=nqptp.service Requires=avahi-daemon.service After=avahi-daemon.service Wants=network-online.target From e635d7b3fbfe6070e2d312c1667695f422e33ab7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:06:49 +1100 Subject: [PATCH 567/878] Die if the NQPTP service is absent when trying to start a play session. --- rtsp.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/rtsp.c b/rtsp.c index d43ea8e0..9eb88e4c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2686,16 +2686,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and // timingPeerList if (conn->airplay_stream_category == ptp_stream) { - ptp_send_control_message_string("T"); // incidentally create the named SHM if necessary and remove all previous history - - if (ptp_shm_interface_open() == 0) { - debug(1,"shm interface opened successfully!"); - } else { - warn("Unable to open the shm interface at SETUP!"); - } - - - + if (ptp_shm_interface_open() != 0) // it should be open already, but just in case it isn't... + die("Can not access the NQPTP service. Has it stopped running?"); + ptp_send_control_message_string("T"); // remove all previous history debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { @@ -2800,7 +2793,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // debug(1,"initial timing peer command: \"%s\".", timing_list_message); ptp_send_control_message_string(timing_list_message); - plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); From 0f5d1ec929ef64e137262e6d1518a16126fb3421 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:09:19 +1100 Subject: [PATCH 568/878] Remove redundant ptp_access_mutex mutex -- don't need it anymore. --- ptp-utilities.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index 993711d7..ce04f2cd 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -48,8 +48,6 @@ int shm_fd; void *mapped_addr = NULL; -static pthread_mutex_t ptp_access_mutex = PTHREAD_MUTEX_INITIALIZER; - // returns a copy of the shared memory data from the nqptp // shared memory interface, so long as it's open. int get_nqptp_data(struct shm_structure *nqptp_data) { From d8bad42b648d0439263df414fad1d57e8f6299f2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 2 Mar 2022 09:29:09 +1100 Subject: [PATCH 569/878] FIx a misspelled comment --- activity_monitor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activity_monitor.c b/activity_monitor.c index deeab2b6..652d3720 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -110,7 +110,7 @@ void going_inactive(int block) { } void activity_monitor_signify_activity(int active) { - // this could be pthread_cancelled and they is likely to be cancellation points in the + // this could be pthread_cancelled and there is likely to be cancellation points in the // hooked-on procedures pthread_cleanup_debug_mutex_lock(&activity_monitor_mutex, 10000, 1); player_state = active == 0 ? ps_inactive : ps_active; From b4cd4fbcc4015bee8c7d483b177f571811da58ae Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 2 Mar 2022 09:30:06 +1100 Subject: [PATCH 570/878] Fix a bug that caused a crash if an incorrect wait_for_completion option was chosen. Fixed a few similar bugs too. --- shairport.c | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/shairport.c b/shairport.c index dc8b5408..f1f12beb 100644 --- a/shairport.c +++ b/shairport.c @@ -605,7 +605,7 @@ int parse_options(int argc, char **argv) { "support. Change the \"general/interpolation\" setting in the configuration file."); #endif else - die("Invalid interpolation option choice. It should be \"auto\", \"basic\" or \"soxr\""); + die("Invalid interpolation option choice \"%s\". It should be \"auto\", \"basic\" or \"soxr\"", str); } #ifdef CONFIG_SOXR @@ -681,7 +681,7 @@ int parse_options(int argc, char **argv) { config.debugger_show_file_and_line = 1; else die("Invalid diagnostics log_show_file_and_line option choice \"%s\". It should be " - "\"yes\" or \"no\""); + "\"yes\" or \"no\"", str); } /* Get the show elapsed time in debug messages setting. */ @@ -692,7 +692,7 @@ int parse_options(int argc, char **argv) { config.debugger_show_elapsed_time = 1; else die("Invalid diagnostics log_show_time_since_startup option choice \"%s\". It should be " - "\"yes\" or \"no\""); + "\"yes\" or \"no\"", str); } /* Get the show relative time in debug messages setting. */ @@ -703,7 +703,7 @@ int parse_options(int argc, char **argv) { config.debugger_show_relative_time = 1; else die("Invalid diagnostics log_show_time_since_last_message option choice \"%s\". It " - "should be \"yes\" or \"no\""); + "should be \"yes\" or \"no\"", str); } /* Get the statistics setting. */ @@ -714,7 +714,7 @@ int parse_options(int argc, char **argv) { config.statistics_requested = 1; else die("Invalid diagnostics statistics option choice \"%s\". It should be \"yes\" or " - "\"no\""); + "\"no\"", str); } /* Get the disable_resend_requests setting. */ @@ -727,7 +727,7 @@ int parse_options(int argc, char **argv) { else die("Invalid diagnostic disable_resend_requests option choice \"%s\". It should be " "\"yes\" " - "or \"no\""); + "or \"no\"", str); } /* Get the drop packets setting. */ @@ -764,7 +764,7 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.ignore_volume_control = 1; else - die("Invalid ignore_volume_control option choice \"%s\". It should be \"yes\" or \"no\""); + die("Invalid ignore_volume_control option choice \"%s\". It should be \"yes\" or \"no\"", str); } /* Get the optional volume_max_db setting. */ @@ -792,7 +792,7 @@ int parse_options(int argc, char **argv) { config.playback_mode = ST_right_only; else die("Invalid playback_mode choice \"%s\". It should be \"stereo\" (default), \"mono\", " - "\"reverse stereo\", \"both left\", \"both right\""); + "\"reverse stereo\", \"both left\", \"both right\"", str); } /* Get the volume control profile setting -- "standard" or "flat" */ @@ -803,7 +803,7 @@ int parse_options(int argc, char **argv) { config.volume_control_profile = VCP_flat; else die("Invalid volume_control_profile choice \"%s\". It should be \"standard\" (default) " - "or \"flat\""); + "or \"flat\"", str); } config_set_lookup_bool(config.cfg, "general.volume_control_combined_hardware_priority", @@ -855,7 +855,7 @@ int parse_options(int argc, char **argv) { inform("Support for the Apple ALAC decoder has not been compiled into this version of " "Shairport Sync. The default decoder will be used."); } else - die("Invalid alac_decoder option choice \"%s\". It should be \"hammerton\" or \"apple\""); + die("Invalid alac_decoder option choice \"%s\". It should be \"hammerton\" or \"apple\"", str); } /* Get the resend control settings. */ @@ -916,7 +916,7 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.metadata_enabled = 1; else - die("Invalid metadata enabled option choice \"%s\". It should be \"yes\" or \"no\""); + die("Invalid metadata enabled option choice \"%s\". It should be \"yes\" or \"no\"", str); } if (config_lookup_string(config.cfg, "metadata.include_cover_art", &str)) { @@ -926,7 +926,7 @@ int parse_options(int argc, char **argv) { config.get_coverart = 1; else die("Invalid metadata include_cover_art option choice \"%s\". It should be \"yes\" or " - "\"no\""); + "\"no\"", str); } if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) { @@ -957,8 +957,8 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.retain_coverart = 1; else - die("Invalid metadata retain_cover_art option choice \"%s\". It should be \"yes\" or " - "\"no\""); + die("Invalid metadata \"retain_cover_art\" option choice \"%s\". It should be \"yes\" or " + "\"no\"", str); } #endif @@ -982,7 +982,7 @@ int parse_options(int argc, char **argv) { if (config_lookup_float(config.cfg, "sessioncontrol.active_state_timeout", &dvalue)) { if (dvalue < 0.0) - warn("Invalid value \"%f\" for sessioncontrol.active_state_timeout. It must be positive. " + warn("Invalid value \"%f\" for \"active_state_timeout\". It must be positive. " "The default of %f will be used instead.", dvalue, config.active_state_timeout); else @@ -1000,8 +1000,8 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.cmd_blocking = 1; else - die("Invalid session control wait_for_completion option choice \"%s\". It should be " - "\"yes\" or \"no\""); + warn("Invalid \"wait_for_completion\" option choice \"%s\". It should be " + "\"yes\" or \"no\". It is set to \"no\".", str); } if (config_lookup_string(config.cfg, "sessioncontrol.before_play_begins_returns_output", @@ -1011,9 +1011,9 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.cmd_start_returns_output = 1; else - die("Invalid session control before_play_begins_returns_output option choice \"%s\". It " + die("Invalid \"before_play_begins_returns_output\" option choice \"%s\". It " "should be " - "\"yes\" or \"no\""); + "\"yes\" or \"no\"", str); } if (config_lookup_string(config.cfg, "sessioncontrol.allow_session_interruption", &str)) { @@ -1023,9 +1023,9 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.allow_session_interruption = 1; else - die("Invalid session control allow_interruption option choice \"%s\". It should be " + die("Invalid \"allow_interruption\" option choice \"%s\". It should be " "\"yes\" " - "or \"no\""); + "or \"no\"", str); } if (config_lookup_int(config.cfg, "sessioncontrol.session_timeout", &value)) { @@ -1040,7 +1040,7 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.convolution = 1; else - die("Invalid dsp.convolution. It should be \"yes\" or \"no\""); + die("Invalid dsp.convolution setting \"%s\". It should be \"yes\" or \"no\"", str); } if (config_lookup_float(config.cfg, "dsp.convolution_gain", &dvalue)) { @@ -1073,7 +1073,7 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.loudness = 1; else - die("Invalid dsp.convolution. It should be \"yes\" or \"no\""); + die("Invalid dsp.loudness \"%s\". It should be \"yes\" or \"no\"", str); } if (config_lookup_float(config.cfg, "dsp.loudness_reference_volume_db", &dvalue)) { @@ -1106,7 +1106,7 @@ int parse_options(int argc, char **argv) { config.dbus_service_bus_type = DBT_session; else die("Invalid dbus_service_bus option choice \"%s\". It should be \"system\" (default) or " - "\"session\""); + "\"session\"", str); } #endif @@ -1119,7 +1119,7 @@ int parse_options(int argc, char **argv) { config.mpris_service_bus_type = DBT_session; else die("Invalid mpris_service_bus option choice \"%s\". It should be \"system\" (default) or " - "\"session\""); + "\"session\"", str); } #endif From 0e7312de736aa4c5ded9725573e6a49f79bd35d7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:24:59 +1100 Subject: [PATCH 571/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 5fe0ecba..167f5ee9 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,19 @@ +Version-4.1-dev-209-gb4cd4fbc +==== +***Pesky Change You Can't Ignore*** + +A change has been made the `shairport-sync` `systemd` service file, so before updating, please remove the existing service file with the following command: +``` +# rm /lib/systemd/system/shairport-sync.service +``` +**Enhancements** +* Improve the timing service interface: Instead of opening and closing a Shared Memory Interface (SMI) whenever timing information was needed from NQPTP, the SMI is now opened only once, at startup. Overall, this reduces overhead at critical times and seems to improve the initial accuracy of synchronisation. It also requires the NQPTP service to be available and accessible at startup. +* Improve `alsa` flush performance: Add new code to better handle a flush request in the `alsa` backend. This new code results in smoother operation and timing accuracy is improved when continuing after a flush. +* Define the Shairport Sync service in the `systemd` service file. + +**Bug Fix** +* Fix a bug that caused a crash if an incorrect `wait_for_completion` option was chosen. Fixed a few similar bugs too. Thanks to [HiFiBerry](https://github.com/hifiberry) for reporting the issue at [#1431](https://github.com/mikebrady/shairport-sync/issues/1431). + Version-4.1-dev-193-g4848608d ==== * Don't try to decode commands at debug level 2 -- leave it to level 3. From 9bac04e2d7207995576798f27d5a40ededcb0d12 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 4 Mar 2022 10:02:40 +1100 Subject: [PATCH 572/878] Check for NULL pointers before deferencing delay and state variable pointers in audio_alsa.c's delay and delay_and_status functions. --- audio_alsa.c | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index d971ca2e..9f845551 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1430,27 +1430,38 @@ static void start(__attribute__((unused)) int i_sample_rate, int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps) { int ret = 0; + snd_pcm_state_t state_temp; + snd_pcm_sframes_t delay_temp; + if (using_update_timestamps) *using_update_timestamps = YNDK_NO; - *state = snd_pcm_state(alsa_handle); - if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) { - ret = snd_pcm_delay(alsa_handle, delay); + state_temp = snd_pcm_state(alsa_handle); + if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { + ret = snd_pcm_delay(alsa_handle, &delay_temp); } else { // not running, thus no delay information, thus can't check for frame // rates // frame_index = 0; // we'll be starting over... // measurement_data_is_valid = 0; - *delay = 0; + delay_temp = 0; } stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes + if (delay != NULL) + *delay = delay_temp; + if (state != NULL) + *state = state_temp; + return ret; } int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps) { + snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED; // just to stop a compiler warning + snd_pcm_sframes_t delay_temp; + snd_pcm_status_t *alsa_snd_pcm_status; snd_pcm_status_alloca(&alsa_snd_pcm_status); @@ -1477,9 +1488,9 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, #endif */ - *state = snd_pcm_status_get_state(alsa_snd_pcm_status); + state_temp = snd_pcm_status_get_state(alsa_snd_pcm_status); - if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) { + if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { // uint64_t update_timestamp_ns = // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; @@ -1514,9 +1525,9 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } if (update_timestamp_ns == 0) { - ret = snd_pcm_delay(alsa_handle, delay); + ret = snd_pcm_delay(alsa_handle, &delay_temp); } else { - snd_pcm_sframes_t delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status); + delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status); /* // It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though @@ -1585,11 +1596,10 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); delay_temp = delay_temp - frames_played_since_last_interrupt_sized; } - *delay = delay_temp; } } else { // not running, thus no delay information, thus can't check for // stall - *delay = 0; + delay_temp = 0; stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes @@ -1601,6 +1611,12 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } else { debug(1, "alsa: can't get device's status."); } + + if (delay != NULL) + *delay = delay_temp; + if (state != NULL) + *state = state_temp; + return ret; } @@ -1615,12 +1631,11 @@ int delay(long *the_delay) { // the sps_extra_code_output_stalled or the // sps_extra_code_output_state_cannot_make_ready codes int ret = 0; - *the_delay = 0; + snd_pcm_sframes_t my_delay = 0; int oldState; snd_pcm_state_t state; - snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); @@ -1634,7 +1649,8 @@ int delay(long *the_delay) { pthread_cleanup_pop(0); pthread_setcancelstate(oldState, NULL); - *the_delay = my_delay; // note: snd_pcm_sframes_t is a long + if (the_delay != NULL) // can't imagine why this might happen + *the_delay = my_delay; // note: snd_pcm_sframes_t is a long return ret; } @@ -1840,7 +1856,7 @@ int sub_flush() { "disconnected"); int derr = 0; if (alsa_handle) { - debug(2,"alsa: do_flush() -- flushing the output device"); + debug(2, "alsa: do_flush() -- flushing the output device"); frames_sent_break_occurred = 1; if ((derr = snd_pcm_drop(alsa_handle))) debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); From fbcba0deb1242205aaa76fbaea557c4dd3b4efd7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 4 Mar 2022 10:08:18 +1100 Subject: [PATCH 573/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 167f5ee9..14d29302 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-211-g9bac04e2 +==== +**Bug Fix** +* Check for `NULL` pointers before dereferencing delay and state variable pointers in `audio_alsa.c`'s `delay` and `*-delay_and_status` functions. Thanks to [leirace](https://github.com/leirace) for the report, Issue [#1441](https://github.com/mikebrady/shairport-sync/issues/1441). + Version-4.1-dev-209-gb4cd4fbc ==== ***Pesky Change You Can't Ignore*** From 8476e5efacf19f9429a1b9fcf95a6d7eea6b4b90 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:32:16 +1100 Subject: [PATCH 574/878] Check to see if the alsa_handle is NULL in precision_delay_and_status. Don't see how it could happen though. --- audio_alsa.c | 280 ++++++++++++++++++++++++--------------------------- 1 file changed, 133 insertions(+), 147 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 9f845551..9d21d8ec 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -332,22 +332,6 @@ void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double v } } -void actual_close_alsa_device() { - debug(1, "actual close"); - if (alsa_handle) { - int derr; - if ((derr = snd_pcm_hw_free(alsa_handle))) - debug(1, - "Error %d (\"%s\") freeing the output device hardware while " - "closing it.", - derr, snd_strerror(derr)); - - if ((derr = snd_pcm_close(alsa_handle))) - debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr)); - alsa_handle = NULL; - } -} - // This array is a sequence of the output rates to be tried if automatic speed selection is // requested. // There is no benefit to upconverting the frame rate, other than for compatibility. @@ -1453,170 +1437,172 @@ int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, *delay = delay_temp; if (state != NULL) *state = state_temp; - return ret; } int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps) { - snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED; // just to stop a compiler warning - snd_pcm_sframes_t delay_temp; + snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED; + snd_pcm_sframes_t delay_temp = 0; + if (using_update_timestamps) + *using_update_timestamps = YNDK_DONT_KNOW; + int ret = ENODEV; snd_pcm_status_t *alsa_snd_pcm_status; snd_pcm_status_alloca(&alsa_snd_pcm_status); - if (using_update_timestamps) - *using_update_timestamps = YNDK_DONT_KNOW; - struct timespec tn; // time now snd_htimestamp_t update_timestamp; // actually a struct timespec + if (alsa_handle != NULL) { + ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status); + if (ret == 0) { + snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); - int ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status); - if (ret == 0) { + /* + // must be 1.1 or later to use snd_pcm_status_get_driver_htstamp + #if SND_LIB_MINOR != 0 + snd_htimestamp_t driver_htstamp; + snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp); + uint64_t driver_htstamp_ns = driver_htstamp.tv_sec; + driver_htstamp_ns = driver_htstamp_ns * 1000000000; + driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec; + debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001); + #endif + */ - snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); + state_temp = snd_pcm_status_get_state(alsa_snd_pcm_status); - /* - // must be 1.1 or later to use snd_pcm_status_get_driver_htstamp - #if SND_LIB_MINOR != 0 - snd_htimestamp_t driver_htstamp; - snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp); - uint64_t driver_htstamp_ns = driver_htstamp.tv_sec; - driver_htstamp_ns = driver_htstamp_ns * 1000000000; - driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec; - debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001); - #endif - */ + if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { - state_temp = snd_pcm_status_get_state(alsa_snd_pcm_status); + // uint64_t update_timestamp_ns = + // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; - if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { + uint64_t update_timestamp_ns = update_timestamp.tv_sec; + update_timestamp_ns = update_timestamp_ns * 1000000000; + update_timestamp_ns = update_timestamp_ns + update_timestamp.tv_nsec; - // uint64_t update_timestamp_ns = - // update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; - - uint64_t update_timestamp_ns = update_timestamp.tv_sec; - update_timestamp_ns = update_timestamp_ns * 1000000000; - update_timestamp_ns = update_timestamp_ns + update_timestamp.tv_nsec; - - // if the update_timestamp is zero, we take this to mean that the device doesn't report - // interrupt timings. (It could be that it's not a real hardware device.) - // so we switch to getting the delay the regular way - // i.e. using snd_pcm_delay () - if (using_update_timestamps) { - if (update_timestamp_ns == 0) - *using_update_timestamps = YNDK_NO; - else - *using_update_timestamps = YNDK_YES; - } - - // user information - if (update_timestamp_ns == 0) { - if (delay_type_notified != 1) { - debug(2, "alsa: update timestamps unavailable"); - delay_type_notified = 1; + // if the update_timestamp is zero, we take this to mean that the device doesn't report + // interrupt timings. (It could be that it's not a real hardware device.) + // so we switch to getting the delay the regular way + // i.e. using snd_pcm_delay () + if (using_update_timestamps) { + if (update_timestamp_ns == 0) + *using_update_timestamps = YNDK_NO; + else + *using_update_timestamps = YNDK_YES; } - } else { - // diagnostic - if (delay_type_notified != 0) { - debug(2, "alsa: update timestamps available"); - delay_type_notified = 0; - } - } - if (update_timestamp_ns == 0) { - ret = snd_pcm_delay(alsa_handle, &delay_temp); - } else { - delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status); - - /* - // It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though - // the check for monotonic returns true. Might have to watch out for this. - #if SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR < 28 - clock_gettime(CLOCK_REALTIME, &tn); - #else - clock_gettime(CLOCK_MONOTONIC, &tn); - #endif - */ - - if (use_monotonic_clock) - clock_gettime(CLOCK_MONOTONIC, &tn); - else - clock_gettime(CLOCK_REALTIME, &tn); - - // uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; - uint64_t time_now_ns = tn.tv_sec; - time_now_ns = time_now_ns * 1000000000; - time_now_ns = time_now_ns + tn.tv_nsec; - - // see if it's stalled - - if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == delay_temp)) { - // hasn't outputted anything since the last call to delay() - - if (((update_timestamp_ns - stall_monitor_start_time) > stall_monitor_error_threshold) || - ((time_now_ns - stall_monitor_start_time) > stall_monitor_error_threshold)) { - debug(2, - "DAC seems to have stalled with time_now_ns: %" PRIX64 - ", update_timestamp_ns: %" PRIX64 ", stall_monitor_start_time %" PRIX64 - ", stall_monitor_error_threshold %" PRIX64 ".", - time_now_ns, update_timestamp_ns, stall_monitor_start_time, - stall_monitor_error_threshold); - debug(2, - "DAC seems to have stalled with time_now: %lx,%lx" - ", update_timestamp: %lx,%lx, stall_monitor_start_time %" PRIX64 - ", stall_monitor_error_threshold %" PRIX64 ".", - tn.tv_sec, tn.tv_nsec, update_timestamp.tv_sec, update_timestamp.tv_nsec, - stall_monitor_start_time, stall_monitor_error_threshold); - ret = sps_extra_code_output_stalled; + // user information + if (update_timestamp_ns == 0) { + if (delay_type_notified != 1) { + debug(2, "alsa: update timestamps unavailable"); + delay_type_notified = 1; } } else { - stall_monitor_start_time = update_timestamp_ns; - stall_monitor_frame_count = delay_temp; + // diagnostic + if (delay_type_notified != 0) { + debug(2, "alsa: update timestamps available"); + delay_type_notified = 0; + } } - if (ret == 0) { - uint64_t delta = time_now_ns - update_timestamp_ns; + if (update_timestamp_ns == 0) { + ret = snd_pcm_delay(alsa_handle, &delay_temp); + } else { + delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status); - // uint64_t frames_played_since_last_interrupt = - // ((uint64_t)config.output_rate * delta) / 1000000000; + /* + // It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though + // the check for monotonic returns true. Might have to watch out for this. + #if SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR < 28 + clock_gettime(CLOCK_REALTIME, &tn); + #else + clock_gettime(CLOCK_MONOTONIC, &tn); + #endif + */ - uint64_t frames_played_since_last_interrupt = config.output_rate; - frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta; - frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000; + if (use_monotonic_clock) + clock_gettime(CLOCK_MONOTONIC, &tn); + else + clock_gettime(CLOCK_REALTIME, &tn); - snd_pcm_sframes_t frames_played_since_last_interrupt_sized = - frames_played_since_last_interrupt; - if ((frames_played_since_last_interrupt_sized < 0) || - ((uint64_t)frames_played_since_last_interrupt_sized != - frames_played_since_last_interrupt)) - debug(1, - "overflow resizing frames_played_since_last_interrupt %" PRIx64 - " to frames_played_since_last_interrupt %lx.", - frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); - delay_temp = delay_temp - frames_played_since_last_interrupt_sized; + // uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; + uint64_t time_now_ns = tn.tv_sec; + time_now_ns = time_now_ns * 1000000000; + time_now_ns = time_now_ns + tn.tv_nsec; + + // see if it's stalled + + if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == delay_temp)) { + // hasn't outputted anything since the last call to delay() + + if (((update_timestamp_ns - stall_monitor_start_time) > + stall_monitor_error_threshold) || + ((time_now_ns - stall_monitor_start_time) > stall_monitor_error_threshold)) { + debug(2, + "DAC seems to have stalled with time_now_ns: %" PRIX64 + ", update_timestamp_ns: %" PRIX64 ", stall_monitor_start_time %" PRIX64 + ", stall_monitor_error_threshold %" PRIX64 ".", + time_now_ns, update_timestamp_ns, stall_monitor_start_time, + stall_monitor_error_threshold); + debug(2, + "DAC seems to have stalled with time_now: %lx,%lx" + ", update_timestamp: %lx,%lx, stall_monitor_start_time %" PRIX64 + ", stall_monitor_error_threshold %" PRIX64 ".", + tn.tv_sec, tn.tv_nsec, update_timestamp.tv_sec, update_timestamp.tv_nsec, + stall_monitor_start_time, stall_monitor_error_threshold); + ret = sps_extra_code_output_stalled; + } + } else { + stall_monitor_start_time = update_timestamp_ns; + stall_monitor_frame_count = delay_temp; + } + + if (ret == 0) { + uint64_t delta = time_now_ns - update_timestamp_ns; + + // uint64_t frames_played_since_last_interrupt = + // ((uint64_t)config.output_rate * delta) / 1000000000; + + uint64_t frames_played_since_last_interrupt = config.output_rate; + frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta; + frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000; + + snd_pcm_sframes_t frames_played_since_last_interrupt_sized = + frames_played_since_last_interrupt; + if ((frames_played_since_last_interrupt_sized < 0) || + ((uint64_t)frames_played_since_last_interrupt_sized != + frames_played_since_last_interrupt)) + debug(1, + "overflow resizing frames_played_since_last_interrupt %" PRIx64 + " to frames_played_since_last_interrupt %lx.", + frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized); + delay_temp = delay_temp - frames_played_since_last_interrupt_sized; + } } + } else { // not running, thus no delay information, thus can't check for + // stall + delay_temp = 0; + stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush + stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes + + // not running, thus no delay information, thus can't check for frame + // rates + // frame_index = 0; // we'll be starting over... + // measurement_data_is_valid = 0; } - } else { // not running, thus no delay information, thus can't check for - // stall - delay_temp = 0; - stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush - stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes - - // not running, thus no delay information, thus can't check for frame - // rates - // frame_index = 0; // we'll be starting over... - // measurement_data_is_valid = 0; + } else { + debug(1, "alsa: can't get device's status."); } - } else { - debug(1, "alsa: can't get device's status."); - } + } else { + debug(1, "alsa_handle is NULL!"); + // already set to ret = ENODEV; + } if (delay != NULL) *delay = delay_temp; if (state != NULL) *state = state_temp; - return ret; } @@ -1649,7 +1635,7 @@ int delay(long *the_delay) { pthread_cleanup_pop(0); pthread_setcancelstate(oldState, NULL); - if (the_delay != NULL) // can't imagine why this might happen + if (the_delay != NULL) // can't imagine why this might happen *the_delay = my_delay; // note: snd_pcm_sframes_t is a long return ret; From 71d902b0e9faa4261064582c52fffd97c4696c07 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:35:02 +1100 Subject: [PATCH 575/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 14d29302..3858812c 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-214-gadf0845e +==== +**Enhancement** +* Check to see if the alsa_handle is NULL in `precision_delay_and_status`. + Version-4.1-dev-211-g9bac04e2 ==== **Bug Fix** From 713492a709a3536b2fee84c7625ffe70901d69c6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Mar 2022 16:23:59 +1100 Subject: [PATCH 576/878] Check to see if the alsa_handle is NULL in standard_delay_and_status. Don't see how it could happen though. --- audio_alsa.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 9d21d8ec..47d43f9e 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1413,21 +1413,26 @@ static void start(__attribute__((unused)) int i_sample_rate, int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps) { - int ret = 0; - snd_pcm_state_t state_temp; - snd_pcm_sframes_t delay_temp; - + int ret = ENODEV; if (using_update_timestamps) *using_update_timestamps = YNDK_NO; - state_temp = snd_pcm_state(alsa_handle); - if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { - ret = snd_pcm_delay(alsa_handle, &delay_temp); + + snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED; + snd_pcm_sframes_t delay_temp = 0; + if (alsa_handle != NULL) { + state_temp = snd_pcm_state(alsa_handle); + if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) { + ret = snd_pcm_delay(alsa_handle, &delay_temp); + } else { + // not running, thus no delay information, thus can't check for frame + // rates + // frame_index = 0; // we'll be starting over... + // measurement_data_is_valid = 0; + // delay_temp = 0; + ret = 0; + } } else { - // not running, thus no delay information, thus can't check for frame - // rates - // frame_index = 0; // we'll be starting over... - // measurement_data_is_valid = 0; - delay_temp = 0; + debug(1, "alsa_handle is NULL in standard_delay_and_status."); } stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush @@ -1596,7 +1601,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } } else { - debug(1, "alsa_handle is NULL!"); + debug(1, "alsa_handle is NULL in precision_delay_and_status!"); // already set to ret = ENODEV; } if (delay != NULL) From 0b4b4a73fc4bb4cf7184c7e7972d369bb4fd9adc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 17:41:53 +1100 Subject: [PATCH 577/878] make close() a separate function and make flush do a close if we don't have to keep the dac active. --- audio_alsa.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 47d43f9e..4630d3c5 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1817,6 +1817,7 @@ int do_open(int do_auto_setup) { } int do_close() { + debug(2,"alsa: do_close()"); if (alsa_backend_state == abm_disconnected) debug(1, "alsa: do_close() -- closing the output device when it is already " "disconnected"); @@ -1828,8 +1829,6 @@ int do_close() { usleep(5000); // this make the function pthread cancellable if ((derr = snd_pcm_hw_free(alsa_handle))) debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr)); - - // flush also closes the device debug(2, "alsa: do_close() -- closing alsa handle"); if ((derr = snd_pcm_close(alsa_handle))) debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr)); @@ -1853,6 +1852,7 @@ int sub_flush() { debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); if ((derr = snd_pcm_prepare(alsa_handle))) debug(1, "Error %d (\"%s\") preparing output device after flush.", derr, snd_strerror(derr)); + alsa_backend_state = abm_connected; } else { debug(1, "alsa: do_flush() -- output device already closed."); } @@ -1923,6 +1923,10 @@ static void flush(void) { pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); if (alsa_backend_state != abm_disconnected) { // must be playing or connected... if (config.keep_dac_busy == 0) { + // debug(1,"flush is actually a close"); + do_close(); + } else { + // debug(1,"flush is a real flush"); sub_flush(); } } else @@ -1932,8 +1936,16 @@ static void flush(void) { } static void stop(void) { - // debug(2,"audio_alsa stop called."); - flush(); // flush will also close the device if appropriate + debug(1,"audio_alsa stop called."); + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); + if (alsa_backend_state != abm_disconnected) { // must be playing or connected... + if (config.keep_dac_busy == 0) { + do_close(); + } + } else + debug(3, "alsa: stop() -- called on a disconnected alsa backend"); + debug_mutex_unlock(&alsa_mutex, 3); + pthread_cleanup_pop(0); // release the mutex } static void parameters(audio_parameters *info) { From 6b6d5986d70255512db93ab1a3f008a268de39a2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 17:43:12 +1100 Subject: [PATCH 578/878] Handle keep_dac_busy correctly if disable_standby_mode is disable_standby_auto. --- activity_monitor.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 652d3720..c98bf91a 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -74,11 +74,8 @@ void going_active(int block) { #ifdef CONFIG_DBUS_INTERFACE if (dbus_service_is_running()) shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE); - else - config.keep_dac_busy = 1; -#else - config.keep_dac_busy = 1; #endif + config.keep_dac_busy = 1; } } @@ -101,11 +98,8 @@ void going_inactive(int block) { #ifdef CONFIG_DBUS_INTERFACE if (dbus_service_is_running()) shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE); - else - config.keep_dac_busy = 0; -#else - config.keep_dac_busy = 0; #endif + config.keep_dac_busy = 0; } } From dac2ad7616d2b0968c8c6d31085430914150ee2e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 17:44:16 +1100 Subject: [PATCH 579/878] Change debugging of RTSP messages to quieten POST as well as OPTION logging. --- rtsp.c | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/rtsp.c b/rtsp.c index 9eb88e4c..fc4bc51c 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1836,7 +1836,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message if (item != NULL) { uint64_t rate; plist_get_uint_val(item, &rate); - debug(2, "anchor rate 0x%016" PRIx64 ".", rate); + debug(3, "anchor rate 0x%016" PRIx64 ".", rate); debug_mutex_lock(&conn->flush_mutex, 1000, 1); pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); conn->ap2_rate = rate; @@ -1848,8 +1848,6 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(2, "Connection %d: Stop playing.", conn->connection_number); activity_monitor_signify_activity(0); conn->ap2_play_enabled = 0; - // not sure this is needed yet - // reset_anchor_info(conn); // needed if the player resumes } pthread_cleanup_pop(1); // unlock the conn->flush_mutex } @@ -1880,8 +1878,8 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) resp->respcode = 500; } -void handle_post(__attribute((unused)) rtsp_conn_info *conn, - __attribute((unused)) rtsp_message *req, +void handle_post(rtsp_conn_info *conn, + rtsp_message *req, __attribute((unused)) rtsp_message *resp) { debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); @@ -2367,7 +2365,7 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } else if (strcmp(req->path, "/command") == 0) { handle_command(conn, req, resp); } else { - debug(2, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, + debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "POST request", req); } @@ -4808,16 +4806,15 @@ static void *rtsp_conversation_thread_func(void *pconn) { resp = msg_init(); pthread_cleanup_push(msg_cleanup_function, (void *)&resp); resp->respcode = 501; // Not Implemented - /* - if (strcmp(req->method, "OPTIONS") != - 0) // the options message is very common, so don't log it until level 3 - debug_level = 2; - debug(debug_level, - "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, - req->method), - debug_print_msg_headers(debug_level, req); - */ - + int dl = debug_level; + if ((strcmp(req->method, "OPTIONS") == + 0) || (strcmp(req->method, "POST") == + 0)) // the options message is very common, so don't log it until level 3 + dl = 3; + debug(dl, + "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, + req->method), + debug_print_msg_headers(dl, req); apple_challenge(conn->fd, req, resp); hdr = msg_get_header(req, "CSeq"); if (hdr) @@ -4871,8 +4868,8 @@ static void *rtsp_conversation_thread_func(void *pconn) { } } } - debug(debug_level, "Connection %d: RTSP Response:", conn->connection_number); - debug_print_msg_headers(debug_level, resp); + debug(dl, "Connection %d: RTSP Response:", conn->connection_number); + debug_print_msg_headers(dl, resp); if (conn->stop == 0) { int err = msg_write_response(conn, resp); From dbf622cb171809180a6081c12a78fb91da35d22c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 19:25:04 +1100 Subject: [PATCH 580/878] Create ADDINGTOHOME.md --- ADDINGTOHOME.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ADDINGTOHOME.md diff --git a/ADDINGTOHOME.md b/ADDINGTOHOME.md new file mode 100644 index 00000000..eae5d8ec --- /dev/null +++ b/ADDINGTOHOME.md @@ -0,0 +1,29 @@ +Here is a brief guide to adding a Shairport Sync device to the Apple Home application on iOS 15.3.1. Note that Shairport Sync has to be built with AirPlay 2 support. + +*** + +Step 1: Open the "Home" app +--- + +Tap the `Add Accessory` panel or choose "Add Accessory" from the `+` symbol in the top right: + +![IMG_3678](https://user-images.githubusercontent.com/4265913/157615721-e42a07e3-f93e-49d0-9233-e7b92a577459.jpg) + +*** + +Step 2: "More options..." +--- + +Step 2: Tap `More options...` + +![IMG_3679](https://user-images.githubusercontent.com/4265913/157616118-79c6d494-ce6e-4666-a6de-53cb40d0e751.jpg) + + +*** + +Step 3: Select your Shairport Sync device: +--- + +Your Shairport Sync device should appear here, though you may have to scroll. Tap the appropriate icon and follow the remaining instructions. + +![IMG_3680](https://user-images.githubusercontent.com/4265913/157616475-22ee2eb6-56f4-4368-a1ed-d925f9984f1e.jpg) From b941dfd3ed27d817012d94200c3426a7e5f1d550 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 19:27:13 +1100 Subject: [PATCH 581/878] Update TROUBLESHOOTING.md --- TROUBLESHOOTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 23d1ebea..74734fb0 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -61,6 +61,9 @@ To see the AirPlay service Shairport Sync provides, your devices must be on the ### Faulty WiFi For an example of what it can take to track down a bad WiFi situation – in this case, a faulty WiFi adapter – please look at [this report](https://github.com/mikebrady/shairport-sync/issues/689). +### Using your iOS device as a hotspot. +An iOS device that is being used as a WiFi hotspot can not play audio to another device. + ### Can't play from iTunes on Windows **Problem** From cc74705a1477036b4a25c68d03dcaa5f2f41e43e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 10 Mar 2022 19:30:46 +1100 Subject: [PATCH 582/878] Update BUILDFORAP2.md --- BUILDFORAP2.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 77ddba95..c98cb5ad 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -148,6 +148,10 @@ The Shairport Sync AirPlay service should appear on the network with a service n Connect and enjoy... +### Adding to Home + +Follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your Shairport Sync device to the Apple Home system. + ### Restart Your Mac! At the time of writing, there seems to be a bug in AirPlay 2 on Apple Silicon Macs and on the Apple TV. It appears that, if the Mac/ATV has been sleeping, the AirPlay 2 infrastructure is not fully reenabled when it wakens up. The `nqptp` program incorporates a workaround which fixes this issue for ATVs and mostly fixes it for macOS. However, it does not completely fix macOS Music on Apple Silicon Macs. The only known solution is to reboot the Mac. From fbe0565a68e257f1ce4b0ef69d4a87d7ef07683c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 11 Mar 2022 09:38:59 +1100 Subject: [PATCH 583/878] Remove the illegal "Provides" entry. --- scripts/shairport-sync.service.in | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/shairport-sync.service.in b/scripts/shairport-sync.service.in index a1ee6197..d1dca5af 100644 --- a/scripts/shairport-sync.service.in +++ b/scripts/shairport-sync.service.in @@ -1,6 +1,5 @@ [Unit] Description=Shairport Sync - AirPlay Audio Receiver -Provides=shairport-sync.service After=sound.target Requires=avahi-daemon.service After=avahi-daemon.service From 9d14becc5c68f77c0214b3f2e87f95d068556e55 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 11 Mar 2022 16:14:49 +1100 Subject: [PATCH 584/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 3858812c..d0bde323 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,19 @@ +Version-4.1-dev-224-g29d2155e +==== +***Pesky Change You Can't Ignore*** + +A change has been made the `shairport-sync` `systemd` service file, so before updating, please remove the existing service file with the following command: +``` +# rm /lib/systemd/system/shairport-sync.service +``` +**Bug Fixes** +* Interpret `disable_standby_mode` to determine if a flush should also close an `alsa` device as follows: + 1. If `disable_standby_mode` is set to `"no"` (default) or `"never"`, then a flush will immediately close the `alsa` device. + 2. If `disable_standby_mode` is `"auto"`, a flush will not close the `alsa` device. The device will be closed at the end of the active timeout. + 3. If `disable_standby_mode` is `"yes"` or `"always"`, a flush will not close the `alsa` device. The device will never be closed. + +* Remove the invalid `Provides` entry from the `systemd` service file. Thanks to [David Crook](https://github.com/idcrook) for reporting Issue [#1448](https://github.com/mikebrady/shairport-sync/issues/1448). + Version-4.1-dev-214-gadf0845e ==== **Enhancement** From 3d96f79ddc845f0f11cac8cf75820da8699a3f18 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 11 Mar 2022 17:03:49 +1100 Subject: [PATCH 585/878] Quieten a few debug messages. --- audio_alsa.c | 1 - rtsp.c | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 4630d3c5..3217eafd 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1936,7 +1936,6 @@ static void flush(void) { } static void stop(void) { - debug(1,"audio_alsa stop called."); pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); if (alsa_backend_state != abm_disconnected) { // must be playing or connected... if (config.keep_dac_busy == 0) { diff --git a/rtsp.c b/rtsp.c index fc4bc51c..6a9d5637 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2344,6 +2344,15 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * } } +void handle_audio_mode(rtsp_conn_info *conn, + rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); +} + + void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { @@ -2364,6 +2373,8 @@ void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { handle_feedback(conn, req, resp); } else if (strcmp(req->path, "/command") == 0) { handle_command(conn, req, resp); + } else if (strcmp(req->path, "/audioMode") == 0) { + handle_audio_mode(conn, req, resp); } else { debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); From 08b874960329f5eacc2818cdbdd97cbeb315bd4c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 13 Mar 2022 12:33:01 +1100 Subject: [PATCH 586/878] Update MOREINFO.md --- MOREINFO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MOREINFO.md b/MOREINFO.md index 68736eda..3ae6af7e 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -244,6 +244,8 @@ The is the smallest number of frames of audio in the DAC's hardware queue. If it The is the smallest number of packets of audio in the queue to be processed in the last interval. It is related to the overall latency in AirPlay 1 and AirPlay 2 Realtime Streams. If it comes close to zero it's often a sign that the network is poor. ##### max buffers The is the largest number of packets of audio in the queue to be processed in the last interval. +##### min buffer size +The is smallest remaining number of bytes in the Buffered Audio buffer in the last interval. It can legitimately be zero when a track ends or begins. If it reaches zero while a track is playing, it means that audio data is not arriving at Shairport Sync quickly enough and may indicate network problems. AirPlay 2 Buffered Audio streams only. ##### nominal fps This is the rate specified in the AirPlay stream itself. AirPlay 1 only. ##### received fps From 08afa822f536800276183532b7bde48613855e60 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 13 Mar 2022 12:33:53 +1100 Subject: [PATCH 587/878] Add buffer size to statistcs information for a Buffered Audio stream. --- player.c | 25 +++++++++++++++++++------ player.h | 1 + rtp.c | 30 ++++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/player.c b/player.c index 1dd5a0e7..a87198cd 100644 --- a/player.c +++ b/player.c @@ -1574,13 +1574,13 @@ int ap1_synced_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 0, 1, 2, 2, 0, 0}; -int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 0, 1, 0, 0, 0, 0}; -int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 0, 1, 0, 0, 0, 0}; +int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 2, 2, 0, 0}; +int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0}; +int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0}; -int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 2, 2, 0, 0}; -int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}; -int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 2, 2, 0, 0}; +int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}; // clang-format on void statistics_item(const char *heading, const char *format, ...) { @@ -1900,6 +1900,10 @@ void *player_thread_func(void *arg) { int32_t minimum_buffer_occupancy = INT32_MAX; int32_t maximum_buffer_occupancy = INT32_MIN; +#ifdef CONFIG_AIRPLAY_2 + conn->ap2_audio_buffer_minimum_size = -1; +#endif + conn->playstart = time(NULL); conn->raw_frame_rate = 0.0; @@ -2432,6 +2436,12 @@ void *player_thread_func(void *arg) { statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); +#ifdef CONFIG_AIRPLAY_2 + if ( conn->ap2_audio_buffer_minimum_size > 10 * 1024) + statistics_item("min buffer size", "%*" PRIu32 "k", 14, conn->ap2_audio_buffer_minimum_size/1024); + else + statistics_item("min buffer size", "%*" PRIu32 "", 15, conn->ap2_audio_buffer_minimum_size); +#endif statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); statistics_item("received fps", "%*.2f", 12, conn->input_frame_rate); if (conn->frame_rate_valid) { @@ -2466,6 +2476,9 @@ void *player_thread_func(void *arg) { minimum_dac_queue_size = UINT64_MAX; // hack reset maximum_buffer_occupancy = INT32_MIN; // can't be less than this minimum_buffer_occupancy = INT32_MAX; // can't be more than this +#ifdef CONFIG_AIRPLAY_2 + conn->ap2_audio_buffer_minimum_size = -1; +#endif at_least_one_frame_seen = 0; } diff --git a/player.h b/player.h index 453c5f49..7bc19631 100644 --- a/player.h +++ b/player.h @@ -320,6 +320,7 @@ typedef struct { uint64_t last_anchor_validity_start_time; ssize_t ap2_audio_buffer_size; + ssize_t ap2_audio_buffer_minimum_size; flush_request_t *flush_requests; // if non-null, there are flush requests, mutex protected int ap2_flush_requested; int ap2_flush_from_valid; diff --git a/rtp.c b/rtp.c index 7e38bbec..beefeaea 100644 --- a/rtp.c +++ b/rtp.c @@ -1926,12 +1926,18 @@ void *rtp_realtime_audio_receiver(void *arg) { pthread_exit(NULL); } -ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { +ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count, size_t *bytes_remaining) { ssize_t response = -1; if (pthread_mutex_lock(&descriptor->mutex) != 0) debug(1, "problem with mutex"); pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); if (descriptor->closed == 0) { + if ((descriptor->buffer_occupancy == 0) && (descriptor->error_code == 0)) { + if (count == 2) + debug(2, "buffered_read: waiting for %u bytes (okay at start of a track).", count); + else + debug(1, "buffered_read: waiting for %u bytes.", count); + } while ((descriptor->buffer_occupancy == 0) && (descriptor->error_code == 0)) { if (pthread_cond_wait(&descriptor->not_empty_cv, &descriptor->mutex)) debug(1, "Error waiting for buffered read"); @@ -1940,8 +1946,9 @@ ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { if (descriptor->buffer_occupancy != 0) { ssize_t bytes_to_move = count; - if (descriptor->buffer_occupancy < count) + if (descriptor->buffer_occupancy < count) { bytes_to_move = descriptor->buffer_occupancy; + } ssize_t top_gap = descriptor->buffer + descriptor->buffer_max_size - descriptor->toq; if (top_gap < bytes_to_move) @@ -1952,6 +1959,8 @@ ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count) { if (descriptor->toq == descriptor->buffer + descriptor->buffer_max_size) descriptor->toq = descriptor->buffer; descriptor->buffer_occupancy -= bytes_to_move; + if (bytes_remaining != NULL) + *bytes_remaining = descriptor->buffer_occupancy; response = bytes_to_move; if (pthread_cond_signal(&descriptor->not_full_cv)) debug(1, "Error signalling"); @@ -2026,7 +2035,7 @@ void *buffered_tcp_reader(void *arg) { if (nread < 0) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "error in buffered_read %d: \"%s\". Could not recv a packet.", errno, errorstring); + debug(1, "error in buffered_tcp_reader %d: \"%s\". Could not recv a packet.", errno, errorstring); descriptor->error_code = errno; } else if (nread == 0) { descriptor->closed = 1; @@ -2081,13 +2090,13 @@ void av_packet_alloc_cleanup_handler(void *arg) { // this will read a block of the size specified to the buffer // and will return either with the block or on error -ssize_t lread_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count) { +ssize_t lread_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count, size_t *bytes_remaining) { ssize_t response, nread; size_t inbuf = 0; // bytes already in the buffer int keep_trying = 1; do { - nread = buffered_read(descriptor, buf + inbuf, count - inbuf); + nread = buffered_read(descriptor, buf + inbuf, count - inbuf, bytes_remaining); if (nread == 0) { // a blocking read that returns zero means eof -- implies connection closed debug(3, "read_sized_block connection closed."); @@ -2375,7 +2384,7 @@ void *rtp_buffered_audio_processor(void *arg) { int flush_newly_complete = 0; int play_newly_stopped = 0; // are we in in flush mode, or just about to leave it? - debug_mutex_lock(&conn->flush_mutex, 10000, 1); // 10ms is a long time to wait! + debug_mutex_lock(&conn->flush_mutex, 25000, 1); // 25 ms is a long time to wait! uint32_t flushUntilSeq = conn->ap2_flush_until_sequence_number; uint32_t flushUntilTS = conn->ap2_flush_until_rtp_timestamp; @@ -2586,7 +2595,10 @@ void *rtp_buffered_audio_processor(void *arg) { // do we will get in a packet of audio uint16_t data_len; // here we read from the buffer that our thread has been reading - nread = lread_sized_block(buffered_audio, &data_len, sizeof(data_len)); + size_t bytes_remaining_in_buffer; + nread = lread_sized_block(buffered_audio, &data_len, sizeof(data_len), &bytes_remaining_in_buffer); + if ((conn->ap2_audio_buffer_minimum_size < 0) || (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) + conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer; if (nread < 0) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); @@ -2597,7 +2609,9 @@ void *rtp_buffered_audio_processor(void *arg) { } data_len = ntohs(data_len); // debug(1,"buffered audio packet of size %u detected.", data_len - 2); - nread = lread_sized_block(buffered_audio, packet, data_len - 2); + nread = lread_sized_block(buffered_audio, packet, data_len - 2, &bytes_remaining_in_buffer); + if ((conn->ap2_audio_buffer_minimum_size < 0) || (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) + conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer; // debug(1, "buffered audio packet of size %u received.", nread); if (nread < 0) { char errorstring[1024]; From 12be2c6a6b9b66cda795003c0b4607aef8263190 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 13 Mar 2022 12:42:18 +1100 Subject: [PATCH 588/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index d0bde323..41ac6543 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Versiob-4.1-dev-229-g08afa822 +==== +**Enhancement** +* Add _Minimum Buffer Size_ to statistcs information for a Buffered Audio stream (and remove the minimum and maximum number of decoded buffers -- they are not too interesting). The Minimum Buffer Size gives the smallest size, in bytes of kilibytes, of the buffer of audio that has yet to be decoded during the last statistics interval. The larger it is, the longer Shairport Sync can play while a network outage is occuring. It can legitimately be zero at the start of a track, and obviously it can reach zero at the end of a track, but if it is zero while a track is playing, it may indicate problems with the network. + Version-4.1-dev-224-g29d2155e ==== ***Pesky Change You Can't Ignore*** From 3c27849328adf4ab158f41c91dd4e7b26984e8d7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 13 Mar 2022 13:29:55 +1100 Subject: [PATCH 589/878] Quieten two debug messages. --- rtp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index beefeaea..08d04849 100644 --- a/rtp.c +++ b/rtp.c @@ -2837,13 +2837,13 @@ void *rtp_buffered_audio_processor(void *arg) { } } else { // nread is 0 -- the port has been closed - debug(1, "buffered audio port closed!"); + debug(2, "buffered audio port closed!"); finished = 1; } } } while (finished == 0); - debug(1, "Buffered Audio Receiver RTP thread \"normal\" exit."); + debug(2, "Buffered Audio Receiver RTP thread \"normal\" exit."); pthread_cleanup_pop(1); // deallocate the swr pthread_cleanup_pop(1); // deallocate the av_packet pthread_cleanup_pop(1); // av_parser_init_cleanup_handler From 424c376b8651c4a48e48029ec8611da6a3736089 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 14 Mar 2022 11:53:48 +1100 Subject: [PATCH 590/878] Remove a diagnostic setting. --- rtp.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rtp.c b/rtp.c index 08d04849..84deb83a 100644 --- a/rtp.c +++ b/rtp.c @@ -2646,10 +2646,12 @@ void *rtp_buffered_audio_processor(void *arg) { int have_time_information = frame_to_local_time(timestamp, &local_should_be_time, conn); int64_t local_lead_time = 0; int64_t requested_lead_time_ns = (int64_t)(requested_lead_time * 1000000000); - requested_lead_time_ns = (int64_t)(-300000000); + // requested_lead_time_ns = (int64_t)(-300000000); + // debug(1,"requested_lead_time_ns is actually %f milliseconds.", requested_lead_time_ns * 1E-6); int outdated = 0; if (have_time_information == 0) { local_lead_time = local_should_be_time - get_absolute_time_in_ns(); + // debug(1,"local_lead_time is actually %f milliseconds.", local_lead_time * 1E-6); outdated = (local_lead_time < requested_lead_time_ns); // if (outdated != 0) // debug(1,"Frame is outdated %d if lead_time %" PRId64 " is less than requested lead time From a382ac667217f458840ffdb40258884eec7edecc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 14 Mar 2022 11:56:43 +1100 Subject: [PATCH 591/878] Clang Format --- activity_monitor.c | 2 +- audio_alsa.c | 2 +- mdns_avahi.c | 15 ++++---- mqtt.c | 6 ++-- nqptp-shm-structures.h | 1 - player.c | 14 ++++---- ptp-utilities.c | 2 +- rtp.c | 81 ++++++++++++++++++++++++------------------ rtsp.c | 28 +++++++-------- shairport.c | 62 ++++++++++++++++++++------------ 10 files changed, 121 insertions(+), 92 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index c98bf91a..eb8c5057 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -99,7 +99,7 @@ void going_inactive(int block) { if (dbus_service_is_running()) shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE); #endif - config.keep_dac_busy = 0; + config.keep_dac_busy = 0; } } diff --git a/audio_alsa.c b/audio_alsa.c index 3217eafd..d02c8a78 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1817,7 +1817,7 @@ int do_open(int do_auto_setup) { } int do_close() { - debug(2,"alsa: do_close()"); + debug(2, "alsa: do_close()"); if (alsa_backend_state == abm_disconnected) debug(1, "alsa: do_close() -- closing the output device when it is already " "disconnected"); diff --git a/mdns_avahi.c b/mdns_avahi.c index 3dc58565..2c4ec771 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -242,21 +242,20 @@ static void register_service(AvahiClient *c) { else selected_interface = AVAHI_IF_UNSPEC; if (ap2_text_record_string_list) { - ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, + ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, ap2_service_name, config.regtype2, NULL, NULL, port, ap2_text_record_string_list); - if (ret == AVAHI_ERR_COLLISION) { - die("Error: AirPlay 2 name \"%s\" is already in use.", ap2_service_name); - } - + if (ret == AVAHI_ERR_COLLISION) { + die("Error: AirPlay 2 name \"%s\" is already in use.", ap2_service_name); + } } if ((ret == 0) && (text_record_string_list)) { ret = avahi_entry_group_add_service_strlst(group, selected_interface, AVAHI_PROTO_UNSPEC, 0, service_name, config.regtype, NULL, NULL, port, text_record_string_list); - if (ret == AVAHI_ERR_COLLISION) { - die("Error: AirPlay 1 name \"%s\" is already in use.", service_name); - } + if (ret == AVAHI_ERR_COLLISION) { + die("Error: AirPlay 1 name \"%s\" is already in use.", service_name); + } } if (ret == 0) { ret = avahi_entry_group_commit(group); diff --git a/mqtt.c b/mqtt.c index c6808515..a007cbc6 100644 --- a/mqtt.c +++ b/mqtt.c @@ -96,17 +96,17 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, void mqtt_publish(char *topic, char *data_in, uint32_t length_in) { char *data = data_in; uint32_t length = length_in; - + if ((length == 0) && (config.mqtt_empty_payload_substitute != NULL)) { length = strlen(config.mqtt_empty_payload_substitute); data = config.mqtt_empty_payload_substitute; } - + char fulltopic[strlen(config.mqtt_topic) + strlen(topic) + 3]; snprintf(fulltopic, strlen(config.mqtt_topic) + strlen(topic) + 2, "%s/%s", config.mqtt_topic, topic); debug(2, "[MQTT]: publishing under %s", fulltopic); - + int rc; if ((rc = mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0)) != MOSQ_ERR_SUCCESS) { diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 0791197d..f3861b44 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -41,7 +41,6 @@ // (In future version of NQPTP the SMI interface may also be deleted at this point.) // SMI interfaces are not currently deleted or garbage collected. - #include #include diff --git a/player.c b/player.c index a87198cd..2d91dea0 100644 --- a/player.c +++ b/player.c @@ -1900,7 +1900,7 @@ void *player_thread_func(void *arg) { int32_t minimum_buffer_occupancy = INT32_MAX; int32_t maximum_buffer_occupancy = INT32_MIN; -#ifdef CONFIG_AIRPLAY_2 +#ifdef CONFIG_AIRPLAY_2 conn->ap2_audio_buffer_minimum_size = -1; #endif @@ -2436,11 +2436,13 @@ void *player_thread_func(void *arg) { statistics_item("min DAC queue", "%*" PRIu64 "", 13, minimum_dac_queue_size); statistics_item("min buffers", "%*" PRIu32 "", 11, minimum_buffer_occupancy); statistics_item("max buffers", "%*" PRIu32 "", 11, maximum_buffer_occupancy); -#ifdef CONFIG_AIRPLAY_2 - if ( conn->ap2_audio_buffer_minimum_size > 10 * 1024) - statistics_item("min buffer size", "%*" PRIu32 "k", 14, conn->ap2_audio_buffer_minimum_size/1024); +#ifdef CONFIG_AIRPLAY_2 + if (conn->ap2_audio_buffer_minimum_size > 10 * 1024) + statistics_item("min buffer size", "%*" PRIu32 "k", 14, + conn->ap2_audio_buffer_minimum_size / 1024); else - statistics_item("min buffer size", "%*" PRIu32 "", 15, conn->ap2_audio_buffer_minimum_size); + statistics_item("min buffer size", "%*" PRIu32 "", 15, + conn->ap2_audio_buffer_minimum_size); #endif statistics_item("nominal fps", "%*.2f", 11, conn->remote_frame_rate); statistics_item("received fps", "%*.2f", 12, conn->input_frame_rate); @@ -2476,7 +2478,7 @@ void *player_thread_func(void *arg) { minimum_dac_queue_size = UINT64_MAX; // hack reset maximum_buffer_occupancy = INT32_MIN; // can't be less than this minimum_buffer_occupancy = INT32_MAX; // can't be more than this -#ifdef CONFIG_AIRPLAY_2 +#ifdef CONFIG_AIRPLAY_2 conn->ap2_audio_buffer_minimum_size = -1; #endif at_least_one_frame_seen = 0; diff --git a/ptp-utilities.c b/ptp-utilities.c index ce04f2cd..aa14a503 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -132,7 +132,7 @@ int ptp_shm_interface_open() { debug(3, "ptp_shm_interface_open is NULL"); if (mapped_addr == MAP_FAILED) debug(3, "ptp_shm_interface_open is MAP_FAILED"); - + if (strcmp(config.nqptp_shared_memory_interface_name, "") != 0) { response = 0; int shared_memory_file_descriptor = diff --git a/rtp.c b/rtp.c index 84deb83a..a4fad346 100644 --- a/rtp.c +++ b/rtp.c @@ -80,63 +80,63 @@ typedef struct { void check64conversion(const char *prompt, const uint8_t *source, uint64_t value) { char converted_value[128]; - sprintf(converted_value,"%" PRIx64 "", value); - + sprintf(converted_value, "%" PRIx64 "", value); + char obf[32]; char *obfp = obf; int obfc; int suppress_zeroes = 1; - for (obfc=0;obfc<8;obfc++) { - if ((suppress_zeroes == 0) || (source[obfc] != 0)){ + for (obfc = 0; obfc < 8; obfc++) { + if ((suppress_zeroes == 0) || (source[obfc] != 0)) { if (suppress_zeroes != 0) { if (source[obfc] < 0x10) { snprintf(obfp, 3, "%1x", source[obfc]); - obfp+=1; + obfp += 1; } else { - snprintf(obfp, 3, "%02x", source[obfc]); - obfp+=2; + snprintf(obfp, 3, "%02x", source[obfc]); + obfp += 2; } } else { snprintf(obfp, 3, "%02x", source[obfc]); - obfp+=2; + obfp += 2; } suppress_zeroes = 0; } }; - *obfp=0; - if (strcmp(converted_value,obf) != 0) { - debug(1,"%s check64conversion error converting \"%s\" to %" PRIx64 ".", prompt, obf, value); + *obfp = 0; + if (strcmp(converted_value, obf) != 0) { + debug(1, "%s check64conversion error converting \"%s\" to %" PRIx64 ".", prompt, obf, value); } } void check32conversion(const char *prompt, const uint8_t *source, uint32_t value) { char converted_value[128]; - sprintf(converted_value,"%" PRIx32 "", value); - + sprintf(converted_value, "%" PRIx32 "", value); + char obf[32]; char *obfp = obf; int obfc; int suppress_zeroes = 1; - for (obfc=0;obfc<4;obfc++) { - if ((suppress_zeroes == 0) || (source[obfc] != 0)){ + for (obfc = 0; obfc < 4; obfc++) { + if ((suppress_zeroes == 0) || (source[obfc] != 0)) { if (suppress_zeroes != 0) { if (source[obfc] < 0x10) { snprintf(obfp, 3, "%1x", source[obfc]); - obfp+=1; + obfp += 1; } else { - snprintf(obfp, 3, "%02x", source[obfc]); - obfp+=2; + snprintf(obfp, 3, "%02x", source[obfc]); + obfp += 2; } } else { snprintf(obfp, 3, "%02x", source[obfc]); - obfp+=2; + obfp += 2; } suppress_zeroes = 0; } }; - *obfp=0; - if (strcmp(converted_value,obf) != 0) { - debug(1,"%s check32conversion error converting \"%s\" to %" PRIx32 ".", prompt, obf, value); + *obfp = 0; + if (strcmp(converted_value, obf) != 0) { + debug(1, "%s check32conversion error converting \"%s\" to %" PRIx32 ".", prompt, obf, value); } } @@ -1790,7 +1790,7 @@ void *rtp_ap2_control_receiver(void *arg) { // add in the audio_backend_latency_offset; int32_t notified_latency = frame_2 - frame_1; if (notified_latency != 77175) - debug(1,"Notified latency is %d frames.", notified_latency); + debug(1, "Notified latency is %d frames.", notified_latency); int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); // the actual latency is the notified latency plus the fixed latency + the added latency @@ -1804,8 +1804,14 @@ void *rtp_ap2_control_receiver(void *arg) { if (net_latency <= 0) { if (conn->latency_warning_issued == 0) { - warn("The stream latency (%f seconds) it too short to accommodate an offset of %f seconds and a backend buffer of %f seconds.", ((notified_latency + 11035) * 1.0)/conn->input_rate, config.audio_backend_latency_offset, config.audio_backend_buffer_desired_length); - warn("(FYI the stream latency needed would be %f seconds.)", config.audio_backend_buffer_desired_length - config.audio_backend_latency_offset); + warn("The stream latency (%f seconds) it too short to accommodate an offset of %f " + "seconds and a backend buffer of %f seconds.", + ((notified_latency + 11035) * 1.0) / conn->input_rate, + config.audio_backend_latency_offset, + config.audio_backend_buffer_desired_length); + warn("(FYI the stream latency needed would be %f seconds.)", + config.audio_backend_buffer_desired_length - + config.audio_backend_latency_offset); conn->latency_warning_issued = 1; } conn->latency = notified_latency + 11035; @@ -1926,7 +1932,8 @@ void *rtp_realtime_audio_receiver(void *arg) { pthread_exit(NULL); } -ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count, size_t *bytes_remaining) { +ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count, + size_t *bytes_remaining) { ssize_t response = -1; if (pthread_mutex_lock(&descriptor->mutex) != 0) debug(1, "problem with mutex"); @@ -1937,7 +1944,7 @@ ssize_t buffered_read(buffered_tcp_desc *descriptor, void *buf, size_t count, si debug(2, "buffered_read: waiting for %u bytes (okay at start of a track).", count); else debug(1, "buffered_read: waiting for %u bytes.", count); - } + } while ((descriptor->buffer_occupancy == 0) && (descriptor->error_code == 0)) { if (pthread_cond_wait(&descriptor->not_empty_cv, &descriptor->mutex)) debug(1, "Error waiting for buffered read"); @@ -2035,7 +2042,8 @@ void *buffered_tcp_reader(void *arg) { if (nread < 0) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "error in buffered_tcp_reader %d: \"%s\". Could not recv a packet.", errno, errorstring); + debug(1, "error in buffered_tcp_reader %d: \"%s\". Could not recv a packet.", errno, + errorstring); descriptor->error_code = errno; } else if (nread == 0) { descriptor->closed = 1; @@ -2090,7 +2098,8 @@ void av_packet_alloc_cleanup_handler(void *arg) { // this will read a block of the size specified to the buffer // and will return either with the block or on error -ssize_t lread_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count, size_t *bytes_remaining) { +ssize_t lread_sized_block(buffered_tcp_desc *descriptor, void *buf, size_t count, + size_t *bytes_remaining) { ssize_t response, nread; size_t inbuf = 0; // bytes already in the buffer int keep_trying = 1; @@ -2596,8 +2605,10 @@ void *rtp_buffered_audio_processor(void *arg) { uint16_t data_len; // here we read from the buffer that our thread has been reading size_t bytes_remaining_in_buffer; - nread = lread_sized_block(buffered_audio, &data_len, sizeof(data_len), &bytes_remaining_in_buffer); - if ((conn->ap2_audio_buffer_minimum_size < 0) || (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) + nread = lread_sized_block(buffered_audio, &data_len, sizeof(data_len), + &bytes_remaining_in_buffer); + if ((conn->ap2_audio_buffer_minimum_size < 0) || + (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer; if (nread < 0) { char errorstring[1024]; @@ -2610,7 +2621,8 @@ void *rtp_buffered_audio_processor(void *arg) { data_len = ntohs(data_len); // debug(1,"buffered audio packet of size %u detected.", data_len - 2); nread = lread_sized_block(buffered_audio, packet, data_len - 2, &bytes_remaining_in_buffer); - if ((conn->ap2_audio_buffer_minimum_size < 0) || (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) + if ((conn->ap2_audio_buffer_minimum_size < 0) || + (bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size)) conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer; // debug(1, "buffered audio packet of size %u received.", nread); if (nread < 0) { @@ -2647,11 +2659,12 @@ void *rtp_buffered_audio_processor(void *arg) { int64_t local_lead_time = 0; int64_t requested_lead_time_ns = (int64_t)(requested_lead_time * 1000000000); // requested_lead_time_ns = (int64_t)(-300000000); - // debug(1,"requested_lead_time_ns is actually %f milliseconds.", requested_lead_time_ns * 1E-6); + // debug(1,"requested_lead_time_ns is actually %f milliseconds.", requested_lead_time_ns * + // 1E-6); int outdated = 0; if (have_time_information == 0) { local_lead_time = local_should_be_time - get_absolute_time_in_ns(); - // debug(1,"local_lead_time is actually %f milliseconds.", local_lead_time * 1E-6); + // debug(1,"local_lead_time is actually %f milliseconds.", local_lead_time * 1E-6); outdated = (local_lead_time < requested_lead_time_ns); // if (outdated != 0) // debug(1,"Frame is outdated %d if lead_time %" PRId64 " is less than requested lead time diff --git a/rtsp.c b/rtsp.c index 6a9d5637..4d3d4459 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1827,7 +1827,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message uint32_t anchorRTPTime = rtpTime; int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); - // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, added_latency); + // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, + // added_latency); set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, anchorTimeNanoseconds); } @@ -1878,8 +1879,7 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused) resp->respcode = 500; } -void handle_post(rtsp_conn_info *conn, - rtsp_message *req, +void handle_post(rtsp_conn_info *conn, rtsp_message *req, __attribute((unused)) rtsp_message *resp) { debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); @@ -2344,15 +2344,13 @@ void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message * } } -void handle_audio_mode(rtsp_conn_info *conn, - rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { +void handle_audio_mode(rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, NULL, req); } - void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { resp->respcode = 200; if (strcmp(req->path, "/pair-setup") == 0) { @@ -2695,9 +2693,10 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and // timingPeerList if (conn->airplay_stream_category == ptp_stream) { - if (ptp_shm_interface_open() != 0) // it should be open already, but just in case it isn't... + if (ptp_shm_interface_open() != + 0) // it should be open already, but just in case it isn't... die("Can not access the NQPTP service. Has it stopped running?"); - ptp_send_control_message_string("T"); // remove all previous history + ptp_send_control_message_string("T"); // remove all previous history debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { @@ -4818,14 +4817,13 @@ static void *rtsp_conversation_thread_func(void *pconn) { pthread_cleanup_push(msg_cleanup_function, (void *)&resp); resp->respcode = 501; // Not Implemented int dl = debug_level; - if ((strcmp(req->method, "OPTIONS") == - 0) || (strcmp(req->method, "POST") == - 0)) // the options message is very common, so don't log it until level 3 + if ((strcmp(req->method, "OPTIONS") == 0) || + (strcmp(req->method, "POST") == + 0)) // the options message is very common, so don't log it until level 3 dl = 3; - debug(dl, - "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, + debug(dl, "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, req->method), - debug_print_msg_headers(dl, req); + debug_print_msg_headers(dl, req); apple_challenge(conn->fd, req, resp); hdr = msg_get_header(req, "CSeq"); if (hdr) diff --git a/shairport.c b/shairport.c index f1f12beb..0d0dc7dd 100644 --- a/shairport.c +++ b/shairport.c @@ -44,10 +44,10 @@ #include "config.h" #ifdef CONFIG_AIRPLAY_2 +#include "ptp-utilities.h" #include #include #include -#include "ptp-utilities.h" #endif #ifdef CONFIG_MBEDTLS @@ -605,7 +605,9 @@ int parse_options(int argc, char **argv) { "support. Change the \"general/interpolation\" setting in the configuration file."); #endif else - die("Invalid interpolation option choice \"%s\". It should be \"auto\", \"basic\" or \"soxr\"", str); + die("Invalid interpolation option choice \"%s\". It should be \"auto\", \"basic\" or " + "\"soxr\"", + str); } #ifdef CONFIG_SOXR @@ -681,7 +683,8 @@ int parse_options(int argc, char **argv) { config.debugger_show_file_and_line = 1; else die("Invalid diagnostics log_show_file_and_line option choice \"%s\". It should be " - "\"yes\" or \"no\"", str); + "\"yes\" or \"no\"", + str); } /* Get the show elapsed time in debug messages setting. */ @@ -692,7 +695,8 @@ int parse_options(int argc, char **argv) { config.debugger_show_elapsed_time = 1; else die("Invalid diagnostics log_show_time_since_startup option choice \"%s\". It should be " - "\"yes\" or \"no\"", str); + "\"yes\" or \"no\"", + str); } /* Get the show relative time in debug messages setting. */ @@ -703,7 +707,8 @@ int parse_options(int argc, char **argv) { config.debugger_show_relative_time = 1; else die("Invalid diagnostics log_show_time_since_last_message option choice \"%s\". It " - "should be \"yes\" or \"no\"", str); + "should be \"yes\" or \"no\"", + str); } /* Get the statistics setting. */ @@ -714,7 +719,8 @@ int parse_options(int argc, char **argv) { config.statistics_requested = 1; else die("Invalid diagnostics statistics option choice \"%s\". It should be \"yes\" or " - "\"no\"", str); + "\"no\"", + str); } /* Get the disable_resend_requests setting. */ @@ -727,7 +733,8 @@ int parse_options(int argc, char **argv) { else die("Invalid diagnostic disable_resend_requests option choice \"%s\". It should be " "\"yes\" " - "or \"no\"", str); + "or \"no\"", + str); } /* Get the drop packets setting. */ @@ -764,7 +771,8 @@ int parse_options(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.ignore_volume_control = 1; else - die("Invalid ignore_volume_control option choice \"%s\". It should be \"yes\" or \"no\"", str); + die("Invalid ignore_volume_control option choice \"%s\". It should be \"yes\" or \"no\"", + str); } /* Get the optional volume_max_db setting. */ @@ -792,7 +800,8 @@ int parse_options(int argc, char **argv) { config.playback_mode = ST_right_only; else die("Invalid playback_mode choice \"%s\". It should be \"stereo\" (default), \"mono\", " - "\"reverse stereo\", \"both left\", \"both right\"", str); + "\"reverse stereo\", \"both left\", \"both right\"", + str); } /* Get the volume control profile setting -- "standard" or "flat" */ @@ -803,7 +812,8 @@ int parse_options(int argc, char **argv) { config.volume_control_profile = VCP_flat; else die("Invalid volume_control_profile choice \"%s\". It should be \"standard\" (default) " - "or \"flat\"", str); + "or \"flat\"", + str); } config_set_lookup_bool(config.cfg, "general.volume_control_combined_hardware_priority", @@ -855,7 +865,8 @@ int parse_options(int argc, char **argv) { inform("Support for the Apple ALAC decoder has not been compiled into this version of " "Shairport Sync. The default decoder will be used."); } else - die("Invalid alac_decoder option choice \"%s\". It should be \"hammerton\" or \"apple\"", str); + die("Invalid alac_decoder option choice \"%s\". It should be \"hammerton\" or \"apple\"", + str); } /* Get the resend control settings. */ @@ -926,7 +937,8 @@ int parse_options(int argc, char **argv) { config.get_coverart = 1; else die("Invalid metadata include_cover_art option choice \"%s\". It should be \"yes\" or " - "\"no\"", str); + "\"no\"", + str); } if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) { @@ -958,7 +970,8 @@ int parse_options(int argc, char **argv) { config.retain_coverart = 1; else die("Invalid metadata \"retain_cover_art\" option choice \"%s\". It should be \"yes\" or " - "\"no\"", str); + "\"no\"", + str); } #endif @@ -1001,7 +1014,8 @@ int parse_options(int argc, char **argv) { config.cmd_blocking = 1; else warn("Invalid \"wait_for_completion\" option choice \"%s\". It should be " - "\"yes\" or \"no\". It is set to \"no\".", str); + "\"yes\" or \"no\". It is set to \"no\".", + str); } if (config_lookup_string(config.cfg, "sessioncontrol.before_play_begins_returns_output", @@ -1013,7 +1027,8 @@ int parse_options(int argc, char **argv) { else die("Invalid \"before_play_begins_returns_output\" option choice \"%s\". It " "should be " - "\"yes\" or \"no\"", str); + "\"yes\" or \"no\"", + str); } if (config_lookup_string(config.cfg, "sessioncontrol.allow_session_interruption", &str)) { @@ -1025,7 +1040,8 @@ int parse_options(int argc, char **argv) { else die("Invalid \"allow_interruption\" option choice \"%s\". It should be " "\"yes\" " - "or \"no\"", str); + "or \"no\"", + str); } if (config_lookup_int(config.cfg, "sessioncontrol.session_timeout", &value)) { @@ -1106,7 +1122,8 @@ int parse_options(int argc, char **argv) { config.dbus_service_bus_type = DBT_session; else die("Invalid dbus_service_bus option choice \"%s\". It should be \"system\" (default) or " - "\"session\"", str); + "\"session\"", + str); } #endif @@ -1119,7 +1136,8 @@ int parse_options(int argc, char **argv) { config.mpris_service_bus_type = DBT_session; else die("Invalid mpris_service_bus option choice \"%s\". It should be \"system\" (default) or " - "\"session\"", str); + "\"session\"", + str); } #endif @@ -2218,16 +2236,16 @@ int main(int argc, char **argv) { do { ptp_send_control_message_string("T"); // get nqptp to create the named shm interface usleep(ptp_wait_interval_us); - ptp_check_times++; + ptp_check_times++; } while ((ptp_shm_interface_open() != 0) && (ptp_check_times < (2000000 / ptp_wait_interval_us))); - + if (ptp_shm_interface_open() != 0) { die("Can't access NQPTP! Is it installed and running?"); } else { if (ptp_check_times == 1) - debug(1,"NQPTP is online."); + debug(1, "NQPTP is online."); else - debug(1,"NQPTP is online after %u microseconds.",ptp_check_times * ptp_wait_interval_us); + debug(1, "NQPTP is online after %u microseconds.", ptp_check_times * ptp_wait_interval_us); } #endif From e87218893b7c43fda7aa29f018574135fecc6a87 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 15 Mar 2022 12:31:37 +1100 Subject: [PATCH 592/878] Allow for a longer group ID string. Fixes an issue with playing from a HomePod mini on homeOS 15.4. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index 4d3d4459..fe8c9096 100644 --- a/rtsp.c +++ b/rtsp.c @@ -110,8 +110,8 @@ char pkString[128]; char deviceIdString[64]; char featuresString[64]; char statusflagsString[32]; -char piString[64]; -char gidString[64]; +char piString[128]; +char gidString[128]; #endif #define METADATA_SNDBUF (4 * 1024 * 1024) From 1df7939d6a8bdd9966e13482a24bcbaa38da2674 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 15 Mar 2022 12:40:21 +1100 Subject: [PATCH 593/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 41ac6543..964f895e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,9 @@ -Versiob-4.1-dev-229-g08afa822 +Version-4.1-dev-234-ge8721889 +==== +**Bug Fix** +* Fixed a bug using HomePod Software 15.4. The bug was that when a Shairport Sync device was added as a HomePod output device, visually it didn't appear to join the group of output devices and additionally its volume control could not be accessed. The bug was fixed by increasing the maximum permissible length of ID strings in Shairport Sync. + +Version-4.1-dev-229-g08afa822 ==== **Enhancement** * Add _Minimum Buffer Size_ to statistcs information for a Buffered Audio stream (and remove the minimum and maximum number of decoded buffers -- they are not too interesting). The Minimum Buffer Size gives the smallest size, in bytes of kilibytes, of the buffer of audio that has yet to be decoded during the last statistics interval. The larger it is, the longer Shairport Sync can play while a network outage is occuring. It can legitimately be zero at the start of a track, and obviously it can reach zero at the end of a track, but if it is zero while a track is playing, it may indicate problems with the network. From 8490c4c1c10a16e54a00f8bc8fa737047f35cdb2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 10:19:15 +1100 Subject: [PATCH 594/878] Always record frames successfully sent to DAC even if they are less than requested. --- audio_alsa.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index d02c8a78..373b1db9 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1727,9 +1727,10 @@ int do_play(void *buf, int samples) { snd_pcm_state_t prior_state = state; // keep this for afterwards.... // debug(3, "alsa: write %d frames.", samples); ret = alsa_pcm_write(alsa_handle, buf, samples); + if (ret > 0) + frames_sent_for_playing += ret; // this is the number of frames accepted if (ret == samples) { stall_monitor_frame_count += samples; - frames_sent_for_playing += samples; } else { frames_sent_break_occurred = 1; // note than an output error has occurred if (ret == -EPIPE) { /* underrun */ From e7098c452929ee0428afa757e6b2e74bbd7054aa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 10:21:54 +1100 Subject: [PATCH 595/878] List the total number of frames sent to DAC and current DAC delay to diagnostics when a large sync error occurs. Stop listing the total number of packets in statistics. --- player.c | 64 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/player.c b/player.c index 2d91dea0..538f289b 100644 --- a/player.c +++ b/player.c @@ -1570,17 +1570,17 @@ int *statistics_print_profile; // be printed -- 2 means print, 1 means print only in a debug mode, 0 means skip // clang-format off -int ap1_synced_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1}; -int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; +int ap1_synced_statistics_print_profile[] = {2, 2, 2, 0, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1}; +int ap1_nosync_statistics_print_profile[] = {2, 0, 0, 0, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 1, 0}; +int ap1_nodelay_statistics_print_profile[] = {0, 0, 0, 0, 2, 1, 1, 2, 0, 1, 1, 1, 1, 0, 0, 1, 0}; -int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 2, 2, 0, 0}; -int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0}; -int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0}; +int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 0, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 2, 2, 0, 0}; +int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 0, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0}; +int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 0, 2, 1, 1, 2, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0}; -int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 2, 2, 0, 0}; -int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}; -int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 2, 2, 0, 0}; +int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}; +int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}; // clang-format on void statistics_item(const char *heading, const char *format, ...) { @@ -2629,35 +2629,57 @@ void *player_thread_func(void *arg) { } else { sync_error_out_of_bounds = 0; } - + if (sync_error_out_of_bounds > 3) { - // debug(1, "New lost sync with source for %d consecutive packets -- flushing and " + // debug(1, "lost sync with source for %d consecutive packets -- flushing and " // "resyncing. Error: %lld.", // sync_error_out_of_bounds, sync_error); sync_error_out_of_bounds = 0; + uint64_t frames_sent_for_play = 0; + uint64_t actual_delay = 0; + + if ((config.output->delay) && (config.no_sync == 0) && (config.output->stats)) { + uint64_t raw_measurement_time; + uint64_t corrected_measurement_time; + config.output->stats(&raw_measurement_time, &corrected_measurement_time, + &actual_delay, &frames_sent_for_play); + } + + int64_t filler_length = (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { debug(1, - "Large positive sync error of: %" PRId64 - " frames (%f seconds), with frame: %u.", + "Large positive sync error of %" PRId64 + " frames (%f seconds), at frame: %" PRIu32 ".", sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); - int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio; + debug(1,"%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", + frames_sent_for_play, actual_delay); + // the sync error is output frames, but we have to work out how many source frames to drop + // there may be a multiple (the conn->output_sample_ratio) of output frames per input frame... + int64_t source_frames_to_drop = sync_error; + source_frames_to_drop = source_frames_to_drop / conn->output_sample_ratio; + + // add some time to give the pipeline a chance to recover -- a bit hacky + double extra_time_to_drop = 0.1; // seconds + int64_t extra_frames_to_drop = (int64_t)(conn->input_rate * extra_time_to_drop); + source_frames_to_drop += extra_frames_to_drop; - // add 100 ms to give the pipeline a chance to recover -- a bit hacky - local_frames_to_drop += (conn->input_rate) * 100 / 1000; - - uint32_t frames_to_drop_sized = local_frames_to_drop; - do_flush(inframe->given_timestamp + frames_to_drop_sized, conn); + uint32_t frames_to_drop = source_frames_to_drop; + uint32_t flush_to_frame = inframe->given_timestamp + frames_to_drop; + + do_flush(flush_to_frame, conn); } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, - "Large negative sync error of: %" PRId64 - " frames (%f seconds), with frame: %" PRIu32 ".", + "Large negative sync error of %" PRId64 + " frames (%f seconds), at frame: %" PRIu32 ".", sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); + debug(1,"%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", + frames_sent_for_play, actual_delay); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) silence_length = filler_length * 5; From 1dc481c3d39ed9532b558c485457015ee33ebedb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:38:05 +1100 Subject: [PATCH 596/878] Clean up some of the code for accepting or diacarding buffered audio depending on its timeliness. --- rtp.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rtp.c b/rtp.c index a4fad346..ec575608 100644 --- a/rtp.c +++ b/rtp.c @@ -2515,15 +2515,13 @@ void *rtp_buffered_audio_processor(void *arg) { if (frame_to_local_time(pcm_buffer_read_point_rtptime, &buffer_should_be_time, conn) == 0) { int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns(); - double lead_time_ms = lead_time * 0.000001; - // debug(1,"lead time in buffered_audio is %f milliseconds.", lead_time_ms); // it seems that some garbage blocks can be left after the flush, so // only accept them if they have sensible lead times - if ((lead_time_ms < 5000.0) && (lead_time > -1000.0)) { + if ((lead_time < (int64_t)5000000000L) && (lead_time >= 0)) { // if it's the very first block (thus no priming needed) if ((blocks_read == 1) || (blocks_read_since_flush > 3)) { - if ((lead_time >= (int64_t)(requested_lead_time * 1000000000)) || + if ((lead_time >= (int64_t)(requested_lead_time * 1000000000L)) || (streaming_has_started != 0)) { if (streaming_has_started == 0) debug(2, @@ -2563,7 +2561,7 @@ void *rtp_buffered_audio_processor(void *arg) { } else { debug(2, "Dropping packet %u from block %u with out-of-range lead_time: %.3f seconds.", - pcm_buffer_read_point_rtptime, seq_no, 0.001 * lead_time_ms); + pcm_buffer_read_point_rtptime, seq_no, 0.000000001 * lead_time); } pcm_buffer_read_point_rtptime += 352; From 7cfc28d4f4b2dc32094ec274512c9c0b8bd3994d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:42:48 +1100 Subject: [PATCH 597/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 964f895e..c574d529 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-238-g1dc481c3 +==== +**Enhancement** +* Add some extra diagnostic information whe large sync errors occur. If available, add the total number of frames sent to the output device and also, if available, add the current delay, in frames, in the output device. + Version-4.1-dev-234-ge8721889 ==== **Bug Fix** From 7af70d8688fa2136e14d2b6539c6790674e86b1f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:43:17 +1100 Subject: [PATCH 598/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c574d529..b54dc3e1 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version-4.1-dev-238-g1dc481c3 ==== **Enhancement** -* Add some extra diagnostic information whe large sync errors occur. If available, add the total number of frames sent to the output device and also, if available, add the current delay, in frames, in the output device. +* Add some extra diagnostic information when large sync errors occur: if available, add the total number of frames sent to the output device and also, if available, add the current delay, in frames, in the output device. Version-4.1-dev-234-ge8721889 ==== From ace5537ad3fc16a4f0975dadfca25e309d4af031 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:51:00 +1100 Subject: [PATCH 599/878] Quieten a few debug messages. --- ptp-utilities.c | 2 +- shairport.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ptp-utilities.c b/ptp-utilities.c index aa14a503..e30824f8 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -168,7 +168,7 @@ int ptp_shm_interface_open() { int ptp_shm_interface_close() { int response = -1; if ((mapped_addr != MAP_FAILED) && (mapped_addr != NULL)) { - debug(1, "ptp_shm_interface_close"); + debug(2, "ptp_shm_interface_close"); response = munmap(mapped_addr, sizeof(struct shm_structure)); if (response != 0) debug(1, "error unmapping shared memory."); diff --git a/shairport.c b/shairport.c index 0d0dc7dd..fcc3ed68 100644 --- a/shairport.c +++ b/shairport.c @@ -1591,7 +1591,7 @@ void exit_function() { debug(1, "exit_function libdaemon exit"); } #else - debug(1, "exit_function exit"); + debug(1, "normal exit"); #endif } else { debug(1, "emergency exit"); From 3877acc79aee4b49845fc410e53408b1a9976009 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Mar 2022 09:12:35 +1100 Subject: [PATCH 600/878] Remember to close() the coverart directory file when finished iterating through it to delete prior coverart files. --- metadata_hub.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metadata_hub.c b/metadata_hub.c index a899574a..4d21de52 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -298,8 +298,11 @@ char *metadata_write_image_file(const char *buf, int len) { } } } + if (close(dir_fd) < 0) + debug(1, "Error %d closing directory \"%s\"", errno, config.cover_art_cache_dir); } else { - debug(1, "Can't open the directory for deletion."); + debug(1, "Can't open the directory \"%s\" for deletion -- error %d.", + config.cover_art_cache_dir, errno); } free(full_filename); closedir(d); From e2c987596f109fff9507e08c41298c1a107184c5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Mar 2022 09:18:28 +1100 Subject: [PATCH 601/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b54dc3e1..18123d60 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version-4.1-dev-242-g3877acc7 +==== +**Bug Fix** +* Fix a crash that would occur after days of continuous play. The issue was that each time a cover art file was created (and previous cover art deleted) the coverart directory was opened with `open()`, but was not closed with `close()`. + Version-4.1-dev-238-g1dc481c3 ==== **Enhancement** From 9ae4dee5ae8f34e08bd8e63188ac8f231bb0bd6a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Mar 2022 18:49:58 +1100 Subject: [PATCH 602/878] Simplify a comment. --- player.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.h b/player.h index 7bc19631..be76e2f7 100644 --- a/player.h +++ b/player.h @@ -257,7 +257,7 @@ typedef struct { int rtp_running; uint64_t rtp_time_of_last_resend_request_error_ns; - char client_ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client + char client_ip_string[INET6_ADDRSTRLEN]; // the ip string of the client uint16_t client_rtsp_port; char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it uint16_t self_rtsp_port; // could be one of many, so we need to know it From f34a69fa32b9e59bfac909ff764f13f001794df5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Mar 2022 18:53:01 +1100 Subject: [PATCH 603/878] Add two new metadata 'ssnc' codes: 'conn' for AirPlay 2 connect, sent before killing any current play session, and 'disc' for AirPlay 2 disconnect. Might be useful for amplifier switching, etc. --- rtsp.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index fe8c9096..cf313b28 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2478,6 +2478,12 @@ void teardown_phase_two(rtsp_conn_info *conn) { if (conn->airplay_gid != NULL) { free(conn->airplay_gid); conn->airplay_gid = NULL; + +#ifdef CONFIG_METADATA + // this is here to ensure it's only performed once during a teardown of a ptp stream + send_ssnc_metadata('disc', conn->client_ip_string, strlen(conn->client_ip_string), 1); +#endif + } conn->groupContainsGroupLeader = 0; config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay @@ -2654,6 +2660,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; +#ifdef CONFIG_METADATA + send_ssnc_metadata('conn', conn->client_ip_string, strlen(conn->client_ip_string), 1); // before disconnecting an existing play +#endif get_play_lock(conn); #ifdef CONFIG_METADATA send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); @@ -3383,11 +3392,22 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // 'acre' -- this is the source's Active-Remote token, necessary if you want // to send commands to the source's remote control (if it has one). // `clip` -- the payload is the IP number of the client, i.e. the sender of audio. -// Can be an IPv4 or an IPv6 number. +// Can be an IPv4 or an IPv6 number. In AirPlay 2 operation, it is sent as soon +// as the client has exclusive access to the player and after any existing +// play session has been interrupted and terminated. +// `conn` -- 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 as soon as the client requests access to the player. +// If Shairport Sync is already playing, this message is sent before the current +// 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. +// `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. // `dapo` -- the payload is the port number (as text) on the server to which remote // control commands should be sent. It is 3689 for iTunes but varies for iOS devices. +// `` // A special sub-protocol is used for sending large data items over UDP // If the payload exceeded 4 MB, it is chunked using the following format: From 0dfce2873540f9006bc7d4ad47f585056cb4a9d1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 22 Mar 2022 19:02:36 +1100 Subject: [PATCH 604/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 18123d60..934558f5 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,15 @@ +Version-4.1-dev-245-gf34a69fa +==== +**Enhancement** + +* AirPlay 2 only. Two new metadata messages have been added. They might be useful for amplifier switching, etc. +1. The `conn` message signifies "AirPlay 2 connect" and is sent when an AirPlay 2 device selects this player. It is sent _before_ killing any current play session. + + (Note: in AirPlay 2 operation, the existing `clip` message is identical, but it is sent _after_ any existing play session has been terminated.) + +2. The `disc` message signifies "AirPlay 2 disconnect". + + Version-4.1-dev-242-g3877acc7 ==== **Bug Fix** From a295c2a2910cd2105bd9265617eebf0cdefe6542 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 3 Apr 2022 10:35:48 +0100 Subject: [PATCH 605/878] Improve the logic for supressing clicks when the alsa disable_standby_mode is active. --- audio_alsa.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 373b1db9..e019ea56 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1923,15 +1923,13 @@ static void flush(void) { // debug(2,"audio_alsa flush called."); pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); if (alsa_backend_state != abm_disconnected) { // must be playing or connected... + // do nothing for a flush if config.keep_dac_busy is true if (config.keep_dac_busy == 0) { - // debug(1,"flush is actually a close"); - do_close(); - } else { - // debug(1,"flush is a real flush"); sub_flush(); } - } else + } else { debug(3, "alsa: flush() -- called on a disconnected alsa backend"); + } debug_mutex_unlock(&alsa_mutex, 3); pthread_cleanup_pop(0); // release the mutex } From bc97f76a160db062f76dcfa2e670157cbc344a00 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 3 Apr 2022 10:43:49 +0100 Subject: [PATCH 606/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 84 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 934558f5..c80e7f0e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version-4.1-dev-247-ga295c2a2 +==== +**Bug Fix** + +* Fix a bug in the logic for supressing clicks when the `alsa` `disable_standby_mode` is active. + Version-4.1-dev-245-gf34a69fa ==== **Enhancement** @@ -8,7 +14,7 @@ Version-4.1-dev-245-gf34a69fa (Note: in AirPlay 2 operation, the existing `clip` message is identical, but it is sent _after_ any existing play session has been terminated.) 2. The `disc` message signifies "AirPlay 2 disconnect". - + Version-4.1-dev-242-g3877acc7 ==== @@ -192,7 +198,7 @@ Version 4.1-dev-81-g24a5bbbe ==== This is a pretty big under-the-hood update. -#### Significant Changes +#### Significant Changes * Change clock base to improve stability. You need to use the updated `nqptp` that has also been changed. If you forget, Shairport Sync will not work properly and will leave a message in the log. * Avoid skipping the first 100 milliseconds or so of a newly-selected track. * Clean up the output of statistics — shorter when log verbosity is zero. @@ -316,7 +322,7 @@ Version 4.0-dev-153-g9ccde50 Big Update ---- -Version 4 brings [limited AirPlay 2 functionality](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md) to Shairport Sync. +Version 4 brings [limited AirPlay 2 functionality](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md) to Shairport Sync. For information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). @@ -366,7 +372,7 @@ There are absent from the repository -- see `3.3.8rc0` and `rc1` in the `master` Version 3.3.7d19 ==== **Bug Fix** -* Ensure the metadata pipe is created, if necessary, when the `--with-metadata` configuration option is chosen without the `--with-dbus-interface`, `--with-mpris-interface` or the `--with-mqtt-client` options. Super thanks to [Philip Howard](https://github.com/Gadgetoid) for finding this and for developing a possible solution. +* Ensure the metadata pipe is created, if necessary, when the `--with-metadata` configuration option is chosen without the `--with-dbus-interface`, `--with-mpris-interface` or the `--with-mqtt-client` options. Super thanks to [Philip Howard](https://github.com/Gadgetoid) for finding this and for developing a possible solution. **Enhancements** * Add a default name for the `pipe` backend. If you don't specify a name for the `pipe` backend's named pipe, it will be `/tmp/shairport-sync-audio`. Note that Shairport Sync will need to have write permission to `/tmp` to create the pipe. @@ -431,7 +437,7 @@ Version 3.3.7d9 Timing drift estimates are now saved when a sessions ends, keyed to the client IP number. When a new session starts from that IP number, the stored estimate is used until a new estimate can be generated from the new session. - In practice, the timing techniques in use up to now have been very accurate, but this should result in slightly smoother rates of correction. + In practice, the timing techniques in use up to now have been very accurate, but this should result in slightly smoother rates of correction. Version 3.3.7d8 ==== @@ -508,7 +514,7 @@ Version 3.3.6d9 Version 3.3.6d8 ==== **Bug Fixes** -* Tidy up compilation of the `audio-dummy` and `audio-soundio` back ends when the `-fno-common` flag is used. +* Tidy up compilation of the `audio-dummy` and `audio-soundio` back ends when the `-fno-common` flag is used. * Remove a superflous extra definition of a variable which would cause it to be used uninitialised. Version 3.3.6d7 @@ -627,7 +633,7 @@ Version 3.3.3d8 **Enhancement** * Expose the settings controlling the resend request logic. The new settings are in the `general` section: * `resend_control_first_check_time` is the time allowed to elapse before a packet is considered missing, defaulting to 0.1 sec. UDP packets don't always arrive in order and they don't need to be re-requested just because they arrive out of sequence. Essentially, therefore, this parameter is to prevent needless resend requests for packets that are already in transit. - * `resend_control_check_interval_time` is the interval between repeated requests for a missing packet, defaulting to 0.25 seconds. + * `resend_control_check_interval_time` is the interval between repeated requests for a missing packet, defaulting to 0.25 seconds. * `resend_control_last_check_time` is the time by which the last check should be done before the estimated time of a missing packet's transfer to the output buffer, defaulting to 0.1 seconds. In other words, if a packet is still missing 0.1 seconds before it is due to be transferred to the DAC's output buffer, don't bother asking for a resend. @@ -735,7 +741,7 @@ Version 3.3rc2 **Bug Fix** * Compatibility has been restored with virtual ALSA devices. Sometimes, an ALSA output device isn't actually a real hardware device -- for example, if PulseAudio is installed in your system, the "default" ALSA output device may in fact a virtual device that provides a route into the PulseAudio sound server for audio from ALSA-compatible applications. Such virtual devices don't always provide the precise delay timing that Shairport Sync uses. The bug fix is to fall back to the standard calls when precise delay timing is not available. -* If precise delay timing data is not available, the `disable_standby_mode` is turned off, as it relies on high precision timing. +* If precise delay timing data is not available, the `disable_standby_mode` is turned off, as it relies on high precision timing. Version 3.3rc1 ==== @@ -787,9 +793,9 @@ This is an ALSA-specific attempt to remove the annoying low-level clicking sound It is extremely difficult to remove these clicks completely from the hardware, so this new feature ensures that the output device avoids situations where these clicks might be generated by *always* playing audio. To accomplish this, if Shairport Sync isn't actually playing anything, audio frames consisting of silence are sent to the output device, keeping it playing. Apart from the initial startup transition, the output device never stops playing and thus never transitions to and from standby mode, avoiding the possibility of generating associated audio disturbances. -To enable this feature, a new `alsa` group setting with the name `disable_standby_mode` is available. (If you do a full `$ sudo make install`, a new sample configuration file with this setting in it is installed at `/etc/shairport-sync.conf.sample` or `/usr/local/etc/shairport-sync.conf.sample`.) +To enable this feature, a new `alsa` group setting with the name `disable_standby_mode` is available. (If you do a full `$ sudo make install`, a new sample configuration file with this setting in it is installed at `/etc/shairport-sync.conf.sample` or `/usr/local/etc/shairport-sync.conf.sample`.) -A downside to this feature is that the output device totally dedicated to Shairport Sync. For this reason, this new feature is disabled by default. +A downside to this feature is that the output device totally dedicated to Shairport Sync. For this reason, this new feature is disabled by default. Note -- this setting is likely to change. It will probably be necessary to modify it somehow to enable it to be used in integrated systems. Your feedback would be welcome. @@ -817,7 +823,7 @@ Here is a flavour of some of the issues addressed: * If a play session is interrupted, wait for it to terminate for up to three seconds. * If a play session is interrupted, ensure the new session uses a different set of UDP ports. This is to ensure that data from the interrupted session – which might be still in transit – doesn't enter the new session. * Make all ALSA command sequences un-cancellable. This is to ensure that ALSA subsystem will not be left in a partially-initialised state if Shairport Sync terminates abruptly. -* When a connection is terminated abruptly, ensure that all the UDP ports is use are closed properly. +* When a connection is terminated abruptly, ensure that all the UDP ports is use are closed properly. * Impose timeouts on both reading and writing to the supervisory RTSP connection governing a session. * When closing the RTSP connection due to an error, close it immediately, without waiting for a full TCP handshake, because, if the other end has erred, the handshake may never come. * Fix a parameter initialisation error in a situation where there is no hardware mixer. @@ -908,7 +914,7 @@ Some information, e.g. the true output rate, not implemented yet. Version 3.3d5 ==== -* Introducing a very rough implementation of a Jack Audio backend. The JACK Audio Connection Kit "provides a basic infrastructure for audio applications to communicate with each other and with audio hardware. Through JACK, users are enabled to build powerful systems for signal processing and music production." +* Introducing a very rough implementation of a Jack Audio backend. The JACK Audio Connection Kit "provides a basic infrastructure for audio applications to communicate with each other and with audio hardware. Through JACK, users are enabled to build powerful systems for signal processing and music production." Version 3.3d4 ==== @@ -934,7 +940,7 @@ Version 3.3d1 Internal changes are being made for version 3.3 to avoid using `SIGUSR1` and `pthread_kill` to stop threads; the standard `pthread_cancel` and friends are being used instead. This should lead to more reliable and orderly cancellation of sessions and threads. However, it is quite a complex change, so bugs may have been introduced or reactivated. Lots of testing needed. **Bug Fix** -* Fix an arithmetic overflow in frame rate calculations that could occur after 2^32 frames – approximately 27 hours at 44,100 frames per second. +* Fix an arithmetic overflow in frame rate calculations that could occur after 2^32 frames – approximately 27 hours at 44,100 frames per second. Version 3.3d0 ==== @@ -991,7 +997,7 @@ Version 3.2d61 **Bug Fix** * Add a pthread_cancel cleanup routine to put_packet to unlock the player rw mutex when cancelled. Sincere thanks to [David Krmpotić](https://github.com/davidhq) for help investigating this. -Version 3.2d57, 3.2d58, 3.2d59, 3.2d60 +Version 3.2d57, 3.2d58, 3.2d59, 3.2d60 ==== **Bug Chasing** * Debug messaging has been enhanced when a `TEARDOWN` message is received. @@ -1063,7 +1069,7 @@ Version 3.2d44 Version 3.2d42 ==== -This version just brings some small changes made to the Release Candidates back into the `development` branch. +This version just brings some small changes made to the Release Candidates back into the `development` branch. Version 3.2d41 ==== @@ -1197,7 +1203,7 @@ Version 3.2d18 Version 3.2d16 ==== **Bug Fix** -* In recent versions of iOS (11.2) and mac OS (10.13.2), when play is resumed after a pause, the volume level is not always restored, and, if software volume control is being used, Shairport Sync plays at full volume. This issue has been addressed by storing the last software volume setting when a play session ends and using it as a default when a new play session begins. +* In recent versions of iOS (11.2) and mac OS (10.13.2), when play is resumed after a pause, the volume level is not always restored, and, if software volume control is being used, Shairport Sync plays at full volume. This issue has been addressed by storing the last software volume setting when a play session ends and using it as a default when a new play session begins. Version 3.2d15 ==== @@ -1246,7 +1252,7 @@ Two extra configuration options are provided: `--with-dbus` and `--with-dbus-tes Version 3.1.2 ==== -Shairport Sync is more stable playing audio from YouTube and SoundCloud on the Mac. +Shairport Sync is more stable playing audio from YouTube and SoundCloud on the Mac. **Pesky Changes You Should Not Ignore** * When you update from a previous version of Shairport Sync, your output device may have been left in a muted state. You should use a command line tool like `alsamixer` or `amixer` to unmute the output device before further use. @@ -1258,7 +1264,7 @@ Shairport Sync is more stable playing audio from YouTube and SoundCloud on the M * Fixed bugs that made Shairport Sync drop out or become unavailable when playing YouTube videos, SoundCloud streams etc. from the Mac. Background: there has been a persistent problem with Shairport Sync becoming unavailable after playing, say, a YouTube clip in a browser on the Mac. Shairport Sync 3.1.2 incorporates a change to how certain AirPlay messages are handled. Introduced in nascent form in 3.1.1, further follow-on changes have improved the handling of player lock and have simplified and improved the handling of unexpected loss of connection. Shairport Sync also now works properly with SoundCloud clips played in a browser on the Mac. * Using [infer](https://github.com/facebook/infer/blob/master/README.md), a number of silent issues have been detected, such as not checking some calls to `malloc` to ensure the response is not NULL. Most of these have been addressed by additional checks. -Version 3.1.1 +Version 3.1.1 ==== **Bug Fixes** @@ -1269,7 +1275,7 @@ Version 3.1.1 Version 3.1 ==== -Version 3.1 brings two new backends, optional loudness and convolution filters, improvements in non-synchronised backends, enhancements, stability improvements and bug fixes. +Version 3.1 brings two new backends, optional loudness and convolution filters, improvements in non-synchronised backends, enhancements, stability improvements and bug fixes. **New Features** * A `sndio` backend gives Shairport Sync native fully synchronised output on OpenBSD and FreeBSD, thanks to the work of [Tobias Kortkamp (t6)](https://github.com/t6). @@ -1282,7 +1288,7 @@ Version 3.1 brings two new backends, optional loudness and convolution filters, * If you are using a System V (aka `systemv`) installation, please note that the default location for PID file has moved -- it is now stored at `/var/run/shairport-sync/shairport-sync.pid`. This change is needed to improve security a little and to improve compatibility across platforms. If you're not doing anything strange, this should make no difference. **Enhancements** -* Resynchronisation, which happens when the synchronisation is incorrect by more than 50 ms by default, should be a lot less intrusive when it occurs – it should now either insert silence or skip frames, as appropriate. +* Resynchronisation, which happens when the synchronisation is incorrect by more than 50 ms by default, should be a lot less intrusive when it occurs – it should now either insert silence or skip frames, as appropriate. * The Linux installer has been improved and simplified and a FreeBSD installer introduced. * A new setting, `audio_backend_silent_lead_in_time`, allows you to set the duration of the period of silence played (the "silent lead-in") before a play session starts. * A new command-line option, `--logOutputLevel`, allows you to output the volume levels to the log whenever they are changed. This may be useful during setup. @@ -1322,7 +1328,7 @@ The most obvious audible change is if you are using software volume control and Here is the list of new features: **New Features** -* 8-bit, 16-bit, 24-bit, 24-bit three-byte (S24_3LE and S24_3BE) and 32-bit output to ALSA devices. +* 8-bit, 16-bit, 24-bit, 24-bit three-byte (S24_3LE and S24_3BE) and 32-bit output to ALSA devices. * 44,100, 88,200, 176,400 and 352,800 sample per second output. This is done using simple upsampling. It's only worth doing if 44,100 samples per second output is not available. * Internal processing including software volume control and interpolation is done after sample size and rate conversion. * Apple ALAC decoder support. This needs the `libalac` library, available at [ALAC](https://github.com/mikebrady/alac), to be installed. Add the flag `--with-apple-alac` to the `./configure` arguments. Then you can choose the Apple ALAC decoder in the configuration file. @@ -1415,13 +1421,13 @@ Version 2.8.6 – Stable Candidate ---- **Enhancements** -* This release contains a small change – it identifies itself as a ShairportSync device rather than an AirPort device. This should make it possible for Tuneblade, and possibly other players, to recognise it correctly. +* This release contains a small change – it identifies itself as a ShairportSync device rather than an AirPort device. This should make it possible for Tuneblade, and possibly other players, to recognise it correctly. Version 2.8.5 – Stable Version ---- This release includes bug fixes and minor enhancements and is recommended for all users. -Note: if you're upgrading, there is a new `./configure` option: +Note: if you're upgrading, there is a new `./configure` option: ==== The build process now uses the directory path `sysconfdir` to determine where to place the configuration file `shairport-sync.conf`. The default value for `sysconfdir` is `/usr/local/etc` which is used in the BSD family, whereas `/etc` is normally used in Linux. @@ -1516,9 +1522,9 @@ The following is a summary of the bug fixes and enhancements since version 2.8.3 * Substitutions can now be made in the service name. The following substitutions can be used in the service name: `%h` for the `hostname`, `%H` for the `hostname` with the first letter capitalised, `%v` for the version number and `%V` for the full version string. Maximum length is 50 characters. -* An existing `shairport-sync.service` file will not be overwritten by `sudo make install`. +* An existing `shairport-sync.service` file will not be overwritten by `sudo make install`. -* The text strings advertising the capabilities of Shairport Sync over Bonjour/Zeroconf/Avahi have been changed and now more closely match those of an AirPort Express Base Station (First Generation). +* The text strings advertising the capabilities of Shairport Sync over Bonjour/Zeroconf/Avahi have been changed and now more closely match those of an AirPort Express Base Station (First Generation). * It is now possible to set the amount of time to wait for the metadata pipe to become ready for writing. The setting is called `pipe_timeout` in the `metadata` section. Default is 5,000 milliseconds. @@ -1551,7 +1557,7 @@ Version 2.8.3.9 – Development Version * The new default service name is simply the hostname, with its first character capitalised. -* An existing `shairport-sync.service` file will not be overwritten by `sudo make install`. +* An existing `shairport-sync.service` file will not be overwritten by `sudo make install`. Version 2.8.3.7 – Development Version ---- @@ -1559,7 +1565,7 @@ Version 2.8.3.7 – Development Version * Shairport Sync now works with AllConnect/Streambels on Android with password protection. (As with all Android clients, you should set the `drift` to something large, like 500 or 1,000, as the timekeeping of these clients isn't as accurate as that of iTunes, etc.) -* The text strings advertising the capabilities of Shairport Sync over Bonjour/Zeroconf/Avahi have been changed and now more closely match those of an AirPort Express Base Station (First Generation). +* The text strings advertising the capabilities of Shairport Sync over Bonjour/Zeroconf/Avahi have been changed and now more closely match those of an AirPort Express Base Station (First Generation). Version 2.8.3.6 – Development Version ---- @@ -1613,7 +1619,7 @@ Version 2.9.5.7 contains general bug fixes and enhancements for some special sit **Bug Fixes** -* Getting delay and latency information from the `alsa` subsystem has been improved -- bugs fixed, error codes handled better, arithmetic handling (hopefully) better. +* Getting delay and latency information from the `alsa` subsystem has been improved -- bugs fixed, error codes handled better, arithmetic handling (hopefully) better. * If latency information is temporarily unavailable from the `alsa` subsystem, skip trying to synchronise until the next time. * Some condition variables and a mutex were uninitialised, yikes! Fixed. * A bug that set the output volume to maximum at the same time as muting the output has been fixed. AFAIK, this was inaudible, but it was scary looking. @@ -1666,7 +1672,7 @@ Version 2.8 is derived from version 2.7.10 with slight documentation updates. He **Enhancements** * The `man` manual and the html version of it are automagically rebuilt if `xml2man` and friends are available. -* Volume-setting metadata is now sent even when the volume level is to be ignored by Shairport Sync itself. +* Volume-setting metadata is now sent even when the volume level is to be ignored by Shairport Sync itself. * Shairport Sync waits a little longer before asking for missing packets to be resent. Sometimes packets are just arriving slightly out of order and don't need to be asked for again. * The build scripts have been modified to be a little more compatible with standard practice. * A Continuous Integration (CI) system – Travis CI – is now used to do some limited build checking (thanks guys!). @@ -1753,7 +1759,7 @@ Version 2.7.2 -- Development Version * Fix a bug that suppressed output of the `rtptime` associated with metadata and with picture information coming from the audio source and passed on via the metadata pipe. **Other Changes** -* Added some more information to the log whenever problems are detected with the proposed alsa device. +* Added some more information to the log whenever problems are detected with the proposed alsa device. Version 2.7.1 -- Development Version ---- @@ -1803,7 +1809,7 @@ Using source-specified latencies is now automatic unless non-standard static lat * Initial timing accuracy improved. The estimate of when to play the starting frame of the audio sequence has improved significantly. This leads to fewer corrections being needed at the start. * Volume ratios expressed in decibels are now consistently denominated in voltage decibels rather than power decibels. The rationale is that the levels refer to voltage levels, and power is proportional to the square of voltage. Thus a ratio of levels of 65535 to 1 is 96.3 dB rather than the 48.15 dB used before. -* The latency figure returned to the source as part of the response to an rtsp request packet is 11,025, which may (?) be meant to indicate the minimum latency the device is capable of. +* The latency figure returned to the source as part of the response to an rtsp request packet is 11,025, which may (?) be meant to indicate the minimum latency the device is capable of. * An experimental handler for a GET_PARAMETER rtsp request has been added. It does nothing except log the occurrence. * The RTSP request dispatcher now logs an event whenever an unrecognised rtsp has been made. @@ -1819,7 +1825,7 @@ Changes from the previous stable version -- 2.4 -- are summarised here: * A compiler error has been fixed that occurred if metadata was enabled and tinysvcmdns was included. * A crash has been fixed that occurred if metadata was enabled and a metadata pipe name was not specified. (Thanks to the contributors who reported bugs.) - + **Small Changes** * If a mixer being used to control volume does not have a control denominated in dB, a warning is logged and the mixer is not used. * Slight revisions have been made to the configuration file `configure.ac` to make compilation on FreeBSD a little easier. @@ -1843,7 +1849,7 @@ Changes from the previous stable version -- 2.2.5 -- are summarised here: * Bug fixes. Please note that building instructions have changed slightly from the previous version. -Also, the `-t hardware/software` option has been deprecated in the alsa back end. +Also, the `-t hardware/software` option has been deprecated in the alsa back end. Version 2.3.13 ---- @@ -1909,7 +1915,7 @@ Version 2.3.8 * Allow more stack space to prevent a segfault in certain configurations (thanks to https://github.com/joerg-krause). * Add missing header files(thanks to https://github.com/joerg-krause). * Removed some (hopefully) mostly silent bugs from the configure.ac file. - + Version 2.3.7 ---- * Changes @@ -1941,7 +1947,7 @@ Version 2.3.4 * Fix an out-of-stack-space error that can occur in certain cases (thanks to https://github.com/joerg-krause). * Fix a couple of compiler warnings (thanks to https://github.com/joerg-krause). * Tidy up a couple of debug messages that were emitting misleading information. - + Version 2.3.3.2 ---- * Bugfix -- fixed an error in the sample configuration file. @@ -1950,7 +1956,7 @@ Version 2.3.3.1 ---- * Enhancement * Metadata format has changed slightly -- the format of each item is now `........`, where the `..` part is present if the length is non-zero. The change is that everything is now enclosed in an `..` pair. - + Version 2.3.2 and 2.3.3 ---- These releases were faulty and have been deleted. @@ -2090,7 +2096,7 @@ Version 2.1.8: * (This feature is intended to be useful to integrators.) Shairport Sync now the ability to immediately disconnect and reconnect to the sound output device while continuing to stream audio data from its client. Send a `SIGUSR2` to the shairport-sync process to disconnect or send it a `SIGHUP` to reconnect. If shairport-sync has been started as a daemon using `shairport-sync -d`, then executing `shairport-sync -D` or `--disconnectFromOutput` will request the daemon to disconnect, and executing `shairport-sync -R` or `--reconnectToOutput` will request it to reconnect. With this feature, you can allow Shairport Sync always to advertise and provide the streaming service, but still be able to disconnect it locally to enable other audio services to access the output device. - + * Annoying things you should know about if you're updating from a previous version: * Options `--with-openssl`, `--with-polarssl` have been replaced with a new option `--with-ssl=

` | +| `--with-systemv` | +| `--with-freebsd-service` | +| `--with-sygwin-service` | + +Daemon programs such as Shairport Sync need to be started automatically, so that the service they provide becomes available without further intervention. Typically this is done using startup scripts. Four options are provided – two for Linux, one for FreeBSD and one for CYGWIN. In Linux, the choice depends on whether [systemd](https://en.wikipedia.org/wiki/Systemd) is used or not. If `systemd` is installed, then the `--with-systemd` option is suggested. If not, the `--with-systemv` option is suggested. + +- `--with-systemd` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. Note: an associated special-purpose option allows you to specify where the `systemd` service file will be placed: + - `--with-systemdsystemunitdir=` Specifies the directory for `systemd` service files. +- `--with-systemv` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. +- `--with-freebsd-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on FreeBSD. Default is not to to install. +- `--with-cygwin-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on CYGWIN. Default is not to to install. + +### Cryptography +| Flags | +| ----- | +| `--with-ssl=openssl` | +| `--with-ssl=mbedtls` | +| `--with-ssl=polarssl` | + +AirPlay 2 operation requires the OpenSSL libraries, so the option `--with-ssl=openssl` is mandatory. + +For classic Shairport Sync, the options are as follows: + +- `--with-ssl=openssl` Uses the [OpenSSL](https://www.openssl.org) cryptography toolkit. This is mandatory for AirPlay 2 operation. +- `--with-ssl=mbedtls` Uses the [Mbed TLS](https://github.com/Mbed-TLS/mbedtls) cryptography library. Only suitable for classic AirPlay operation – do not include it in an AirPlay 2 configuration. +- `--with-ssl=polarssl` Uses the PolarSSL cryptography library. This is deprecated – PolarSSL has been replaced by Mbed TLS. Only suitable for classic AirPlay operation – do not include it in an AirPlay 2 configuration. + +### Zeroconf/Bonjour +| Flags | +| ----- | +| `--with-avahi` | +| `--with-tinysvcmdns` | +| `--with-external-mdns` | +| `--with-dns_sd` | + +AirPlay devices advertise their existence and status using [Zeroconf](http://www.zeroconf.org) (aka [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software))). + +AirPlay 2 operation requires the [Avahi](https://www.avahi.org) libraries, so the option `--with-avahi` is mandatory. + +For classic Shairport Sync, the options are as follows: + +The Zeroconf-related options are as follows: +- `--with-avahi` Chooses [Avahi](https://www.avahi.org)-based Zeroconf support. This is mandatory for AirPlay 2 operation. +- `--with-tinysvcmdns` Chooses [tinysvcmdns](https://github.com/philippe44/TinySVCmDNS)-based Zeroconf support (deprecated). +- `--with-external-mdns` Supports the use of ['avahi-publish-service'](https://linux.die.net/man/1/avahi-publish-service) or 'mDNSPublish' to advertise the service on Bonjour/ZeroConf. +- `--with-dns_sd` Chooses `dns-sd` Zeroconf support. + +### Miscellaneous +| Flag | Significance | +| -------------------- | ---- | +| `--with-os=` | Specifies the Operating System to target: One of `linux` (default), `freebsd`, `openbsd` or `darwin`. | +| `--with-configfiles` | Installs configuration files (including a sample configuration file) during `make install`. | +| `--with-pkg-config` | Specifies the use of `pkg-config` to find libraries. (Obselete for AirPlay 2. Special purpose use only.) | From 5fdfcac6f3353420e1994e5da0ad4fb9a370f64f Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:11:36 -0700 Subject: [PATCH 762/878] Makefile.am: allow building from separate build directory --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index e5674d2b..6054e284 100644 --- a/Makefile.am +++ b/Makefile.am @@ -161,7 +161,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) dbus-interface.c: org.gnome.ShairportSync.xml - gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface org.gnome.ShairportSync.xml + gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $< dbus-interface.h: dbus-interface.c touch dbus-interface.h endif @@ -178,7 +178,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) mpris-interface.c: org.mpris.MediaPlayer2.xml - gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface org.mpris.MediaPlayer2.xml + gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $< mpris-interface.h: mpris-interface.c touch mpris-interface.h endif From 17e26a03e6de1f467a965d90ab47bd7028555fc5 Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:12:14 -0700 Subject: [PATCH 763/878] Makefile.am: allow installing from separate build directory --- Makefile.am | 121 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/Makefile.am b/Makefile.am index 6054e284..a03a8689 100644 --- a/Makefile.am +++ b/Makefile.am @@ -199,59 +199,98 @@ shairport_sync_mpris_test_client_SOURCES = shairport-sync-mpris-test-client.c shairport_sync_mpris_test_client_LDADD = lib_mpris_interface.a endif -install-exec-hook: -if BUILD_FOR_LINUX -DBUS_POLICY_DIR=$(DESTDIR)/etc/dbus-1/system.d -else -DBUS_POLICY_DIR=$(DESTDIR)$(sysconfdir)/dbus-1/system.d -endif if INSTALL_CONFIG_FILES - [ -e $(DESTDIR)$(sysconfdir) ] || mkdir $(DESTDIR)$(sysconfdir) - cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample - [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf + +configdir = $(sysconfdir) +config_DATA = scripts/shairport-sync.conf + +if BUILD_FOR_LINUX +dbusdir = /etc/dbus-1/system.d +else +dbusdir = $(sysconfdir)/dbus-1/system.d +endif + if USE_DBUS - [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) + if INSTALL_CYGWIN_SERVICE - cp scripts/shairport-sync-dbus-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf +dbus_DATA = scripts/shairport-sync-dbus-policy-cygwin.conf else - cp scripts/shairport-sync-dbus-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf -endif -endif +dbus_DATA = scripts/shairport-sync-dbus-policy.conf +endif # INSTALL_CYGWIN_SERVIC + +endif # USE_DBUS + if USE_MPRIS - [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) + +mprisdir = $(dbusdir) + if INSTALL_CYGWIN_SERVICE - cp scripts/shairport-sync-mpris-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf +mpris_DATA = scripts/shairport-sync-mpris-policy-cygwin.conf else - cp scripts/shairport-sync-mpris-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf -endif -endif -endif +mpris_DATA = scripts/shairport-sync-mpris-policy.conf +endif # INSTALL_CYGWIN_SERVICE + +endif # USE_MPRIS + +endif # INSTALL_CONFIG_FILES + +INSTALL_GROUP_TARGET = install-group-local + +$(INSTALL_GROUP_TARGET): + getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null + +INSTALL_USER_TARGET = install-user-local + +$(INSTALL_USER_TARGET): $(INSTALL_GROUP_TARGET) + getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null + if INSTALL_SYSTEMV - getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null - getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null - [ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d - [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ -endif + +INSTALL_SYSTEMV_TARGET = install-systemv-local + +$(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET) + install -d $(DESTDIR)$(sysconfdir)/init.d + install -m 0644 $< $(DESTDIR)$(sysconfdir)/init.d + +endif # INSTALL_SYSTEMV + if INSTALL_SYSTEMD - getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null - getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null - [ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir) + if USE_AVAHI - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service-avahi $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service +SYSTEMD_SERVICE = shairport-sync.service-avahi else - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) -endif -endif +SYSTEMD_SERVICE = shairport-sync.service +endif # USE_AVAHI + +INSTALL_SYSTEMD_TARGET = install-systemd-local + +$(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET) + install -d $(DESTDIR)$(systemdsystemunitdir) + install -m 0644 $< $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service + +endif # INSTALL_SYSTEMD + if INSTALL_FREEBSD_SERVICE - # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." + +# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." +install-freebsd-user-local: pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup -n shairport-sync -g 801 > /dev/null 2>&1 pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1 - [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync - chown shairport-sync:shairport-sync /var/run/shairport-sync - # Always update the startup script -- this is different to the Linux systemd situation, where the startup script is untouched if it exists. - cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync - chmod 555 /usr/local/etc/rc.d/shairport_sync -endif + +INSTALL_FREEBSD_TARGET = install-freebsd-local + +$(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-local + install -d -o shairport-sync -g shairport-sync $(DESTDIR)/var/run/shairport-sync + install -d $(DESTDIR)/usr/local/etc/rc.d/ + install -m 0555 $< $(DESTDIR)/usr/local/etc/rc.d/shairport_sync + +endif # INSTALL_FREEBSD_SERVICE + if INSTALL_CYGWIN_SERVICE - [ -e /usr/local/bin/shairport-sync-config ] || cp scripts/shairport-sync-config /usr/local/bin -endif +cygwindir = /usr/local/bin +cygwin_DATA = scripts/shairport-sync-config +endif # INSTALL_CYGWIN_SERVICE + +install-exec-hook: $(INSTALL_SYSTEMV_TARGET) \ + $(INSTALL_SYSTEMD_TARGET) \ + $(INSTALL_FREEBSD_TARGET) From ce23941f6a01df3147ef5b28bfad46cf076d60f1 Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:14:24 -0700 Subject: [PATCH 764/878] gitignore: add build directory --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a094f6e7..1c6f3d96 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ shairport-sync.xcodeproj # version string generation gitversion.h +# separate build directory +build From 02da7be3a7654bf31783e8480c9a9157f47b8575 Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:14:45 -0700 Subject: [PATCH 765/878] gitignore: add vscode directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1c6f3d96..52c49f30 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ gitversion.h # separate build directory build + +# vscode +.vscode From 6bfeb49fbaffe7da5eb1d4b04c9a2c5f554cfe07 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 8 Jul 2022 15:18:25 +0100 Subject: [PATCH 766/878] Reduce the settling time given for a change in master clock from 5 sec to 0.75 sec. --- rtp.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rtp.c b/rtp.c index 7d50aa5c..2a099f5d 100644 --- a/rtp.c +++ b/rtp.c @@ -1363,21 +1363,16 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, debug(3, "Current master clock %" PRIx64 " and anchor_clock %" PRIx64 " are different", actual_clock_id, conn->anchor_clock); // the anchor clock and the actual clock are different - // this could happen because the master clock has changed or - // because the anchor clock has changed (this can happen with a Realtime Stream) if (conn->last_anchor_info_is_valid != 0) { int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; - if (time_since_last_update > 5000000000) { + if (time_since_last_update > 750000000) { int64_t duration_of_mastership = time_now - start_of_mastership; debug(2, "Connection %d: Master clock has changed to %" PRIx64 ". History: %.3f milliseconds.", conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); - // Here we adjust the time of the anchor rtptime. - // We know its local time, so we use the new clocks's offset to - // calculate what time that must be on the new clock. // Now, the thing is that while the anchor clock and master clock for a // buffered session start off the same, From 5ee5c1ed0396304db878a2aa867f775da5f1899b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 8 Jul 2022 15:30:22 +0100 Subject: [PATCH 767/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index de29fe1a..73a9d149 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-409-g6bfeb49f +==== +**Bug Fix** +* Shorten the settling time (from 5.0 seconds to 0.75 seconds) allowed when a new AirPlay 2 player, with its own clock, is added or removed from the devices currently playing. The problem was that if something else happens during the settling time, the changeover to the new clock may fail. This doesn't eliminate the issue, but hopefully it stops it being a problem for the user. + Version 4.1-dev-376-gca6e768f ==== **Bug Fix** From 623b760ba5d60bd320d454a4a9a7a6d14e4c1ac5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 9 Jul 2022 09:55:28 +0100 Subject: [PATCH 768/878] Reinstate the 5.0 second settling time for a clock change. --- rtp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index 2a099f5d..61535053 100644 --- a/rtp.c +++ b/rtp.c @@ -1365,15 +1365,16 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, // the anchor clock and the actual clock are different if (conn->last_anchor_info_is_valid != 0) { + int64_t time_since_last_update = get_absolute_time_in_ns() - conn->last_anchor_time_of_update; - if (time_since_last_update > 750000000) { + if (time_since_last_update > 5000000000) { int64_t duration_of_mastership = time_now - start_of_mastership; debug(2, "Connection %d: Master clock has changed to %" PRIx64 ". History: %.3f milliseconds.", conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); - + // Now, the thing is that while the anchor clock and master clock for a // buffered session start off the same, // the master clock can change without the anchor clock changing. @@ -1384,7 +1385,9 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, conn->anchor_time = conn->last_anchor_local_time + actual_offset; conn->anchor_clock = actual_clock_id; + } + } else { response = clock_not_valid; // no current clock information and no previous clock info } From f218a4d45e13171cd6ff037b0fa37ec8a5d9b4c1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 9 Jul 2022 09:55:54 +0100 Subject: [PATCH 769/878] Improve a debug message. --- rtsp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index 757f72e3..076a20d7 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1880,7 +1880,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); conn->ap2_rate = rate; if ((rate & 1) != 0) { - debug(2, "Connection %d: Start playing.", conn->connection_number); + debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); conn->ap2_play_enabled = 1; } else { From 8018ceadafecc360d951e914b6a51009645cbf0f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 9 Jul 2022 10:18:53 +0100 Subject: [PATCH 770/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 73a9d149..37e58bc2 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-412-gf218a4d4 +==== + +**Note** -- A bug has been fixed in NQPTP -- please update it. + +**Reversion** +* The change to the allowed settling time mentioned below has been reverted back to 5.0 seconds – the issue this was meant to address turned out to be a bug in NQPTP. + Version 4.1-dev-409-g6bfeb49f ==== **Bug Fix** From 7f8f62662c99373f53ec2e611e04da90c1a1dbb5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 10 Jul 2022 19:55:31 +0100 Subject: [PATCH 771/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 37e58bc2..5118daad 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-418-g5011dc09 +==== +**Enhancement** +* The `Makefile.am` file has been updated to allow allow building and installing from a separate build directory. Thanks to [Lukas Rusak](https://github.com/lrusak) for the [contribution](https://github.com/mikebrady/shairport-sync/pull/1493). +**Note** After pulling this update, you should rerun the `$ autotools -fi` and `$ ./configure...` steps. + Version 4.1-dev-412-gf218a4d4 ==== From 23d0502ffae54ae5f41720dbff3304967bd31689 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 11 Jul 2022 11:12:39 +0100 Subject: [PATCH 772/878] Revert changes at 5011dc09d5e19d191616c11c2083b87d24272090 -- not quite ready yet. --- Makefile.am | 121 ++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 80 deletions(-) diff --git a/Makefile.am b/Makefile.am index a03a8689..e5674d2b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -161,7 +161,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) dbus-interface.c: org.gnome.ShairportSync.xml - gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $< + gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface org.gnome.ShairportSync.xml dbus-interface.h: dbus-interface.c touch dbus-interface.h endif @@ -178,7 +178,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) mpris-interface.c: org.mpris.MediaPlayer2.xml - gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $< + gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface org.mpris.MediaPlayer2.xml mpris-interface.h: mpris-interface.c touch mpris-interface.h endif @@ -199,98 +199,59 @@ shairport_sync_mpris_test_client_SOURCES = shairport-sync-mpris-test-client.c shairport_sync_mpris_test_client_LDADD = lib_mpris_interface.a endif -if INSTALL_CONFIG_FILES - -configdir = $(sysconfdir) -config_DATA = scripts/shairport-sync.conf - +install-exec-hook: if BUILD_FOR_LINUX -dbusdir = /etc/dbus-1/system.d +DBUS_POLICY_DIR=$(DESTDIR)/etc/dbus-1/system.d else -dbusdir = $(sysconfdir)/dbus-1/system.d +DBUS_POLICY_DIR=$(DESTDIR)$(sysconfdir)/dbus-1/system.d endif - +if INSTALL_CONFIG_FILES + [ -e $(DESTDIR)$(sysconfdir) ] || mkdir $(DESTDIR)$(sysconfdir) + cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample + [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf if USE_DBUS - + [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) if INSTALL_CYGWIN_SERVICE -dbus_DATA = scripts/shairport-sync-dbus-policy-cygwin.conf + cp scripts/shairport-sync-dbus-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf else -dbus_DATA = scripts/shairport-sync-dbus-policy.conf -endif # INSTALL_CYGWIN_SERVIC - -endif # USE_DBUS - + cp scripts/shairport-sync-dbus-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf +endif +endif if USE_MPRIS - -mprisdir = $(dbusdir) - + [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) if INSTALL_CYGWIN_SERVICE -mpris_DATA = scripts/shairport-sync-mpris-policy-cygwin.conf + cp scripts/shairport-sync-mpris-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf else -mpris_DATA = scripts/shairport-sync-mpris-policy.conf -endif # INSTALL_CYGWIN_SERVICE - -endif # USE_MPRIS - -endif # INSTALL_CONFIG_FILES - -INSTALL_GROUP_TARGET = install-group-local - -$(INSTALL_GROUP_TARGET): - getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null - -INSTALL_USER_TARGET = install-user-local - -$(INSTALL_USER_TARGET): $(INSTALL_GROUP_TARGET) - getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null - + cp scripts/shairport-sync-mpris-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf +endif +endif +endif if INSTALL_SYSTEMV - -INSTALL_SYSTEMV_TARGET = install-systemv-local - -$(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET) - install -d $(DESTDIR)$(sysconfdir)/init.d - install -m 0644 $< $(DESTDIR)$(sysconfdir)/init.d - -endif # INSTALL_SYSTEMV - + getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null + getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null + [ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d + [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ +endif if INSTALL_SYSTEMD - + getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null + getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null + [ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir) if USE_AVAHI -SYSTEMD_SERVICE = shairport-sync.service-avahi + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service-avahi $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service else -SYSTEMD_SERVICE = shairport-sync.service -endif # USE_AVAHI - -INSTALL_SYSTEMD_TARGET = install-systemd-local - -$(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET) - install -d $(DESTDIR)$(systemdsystemunitdir) - install -m 0644 $< $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service - -endif # INSTALL_SYSTEMD - + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) +endif +endif if INSTALL_FREEBSD_SERVICE - -# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." -install-freebsd-user-local: + # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup -n shairport-sync -g 801 > /dev/null 2>&1 pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1 - -INSTALL_FREEBSD_TARGET = install-freebsd-local - -$(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-local - install -d -o shairport-sync -g shairport-sync $(DESTDIR)/var/run/shairport-sync - install -d $(DESTDIR)/usr/local/etc/rc.d/ - install -m 0555 $< $(DESTDIR)/usr/local/etc/rc.d/shairport_sync - -endif # INSTALL_FREEBSD_SERVICE - + [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync + chown shairport-sync:shairport-sync /var/run/shairport-sync + # Always update the startup script -- this is different to the Linux systemd situation, where the startup script is untouched if it exists. + cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync + chmod 555 /usr/local/etc/rc.d/shairport_sync +endif if INSTALL_CYGWIN_SERVICE -cygwindir = /usr/local/bin -cygwin_DATA = scripts/shairport-sync-config -endif # INSTALL_CYGWIN_SERVICE - -install-exec-hook: $(INSTALL_SYSTEMV_TARGET) \ - $(INSTALL_SYSTEMD_TARGET) \ - $(INSTALL_FREEBSD_TARGET) + [ -e /usr/local/bin/shairport-sync-config ] || cp scripts/shairport-sync-config /usr/local/bin +endif From 7b51c9d462ce72d287b94052a2425a243bd7f363 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 11 Jul 2022 11:17:27 +0100 Subject: [PATCH 773/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 5118daad..58b16732 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-420-g23d0502f +==== +**Bug Fix** +* This commit reverses the enhancement in Version 4.1-dev-418-g5011dc09 because it doesn't work properly. Specifically, some service files are not being renamed and installed correctly. Apologies for the inconvenience. +**Note** After pulling this update, you should rerun the `$ autotools -fi` and `$ ./configure...` steps. + Version 4.1-dev-418-g5011dc09 ==== **Enhancement** From 524a8a12031efef655034def75674dcb0813f7c6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:04:02 +0100 Subject: [PATCH 774/878] Quieten a debug message. --- rtp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index 61535053..c43ef2b8 100644 --- a/rtp.c +++ b/rtp.c @@ -1566,9 +1566,9 @@ void rtp_event_receiver_cleanup_handler(void *arg) { void *rtp_event_receiver(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (conn->airplay_stream_category == remote_control_stream) - debug(1, "Connection %d (RC): AP2 Event Receiver started", conn->connection_number); + debug(2, "Connection %d (RC): AP2 Event Receiver started", conn->connection_number); else - debug(1, "Connection %d: AP2 Event Receiver started", conn->connection_number); + debug(2, "Connection %d: AP2 Event Receiver started", conn->connection_number); pthread_cleanup_push(rtp_event_receiver_cleanup_handler, arg); listen(conn->event_socket, 5); From 129343c165bd56830988f5c6254ec1259fdda432 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:04:42 +0100 Subject: [PATCH 775/878] Don't quit if the ALSA output device is or becomes busy and resume when it becomes available again. --- audio_alsa.c | 33 +++++++++++++++++++++------------ player.c | 6 ++++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index effdc436..9673da5f 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1,7 +1,7 @@ /* * libalsa output driver. This file is part of Shairport. * Copyright (c) Muffinman, Skaman 2013 - * Copyright (c) Mike Brady 2014 -- 2021 + * Copyright (c) Mike Brady 2014 -- 2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -131,6 +131,7 @@ yndk_type precision_delay_available_status = YNDK_DONT_KNOW; // initially, we don't know if the device can do precision delay snd_pcm_t *alsa_handle = NULL; +int alsa_handle_status = ENODEV; // if alsa_handle is NULL, this should say why with a unix error code static snd_pcm_hw_params_t *alsa_params = NULL; static snd_pcm_sw_params_t *alsa_swparams = NULL; static snd_ctl_t *ctl = NULL; @@ -416,8 +417,17 @@ int actual_open_alsa_device(int do_auto_setup) { audio_alsa.delay = NULL; ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0); - if (ret < 0) { - if (ret == -ENOENT) { + if (ret == 0) { + if (alsa_handle_status == -EBUSY) + warn("The output device \"%s\" is no longer busy and will be used by Shairport Sync.", alsa_out_dev); + alsa_handle_status = ret; // all cool + } else { + alsa_handle = NULL; // to be sure to be sure + if (ret == -EBUSY) { + if (alsa_handle_status != -EBUSY) + warn("The output device \"%s\" is busy and can't be used by Shairport Sync at present.", alsa_out_dev); + debug(2, "the alsa output_device \"%s\" is busy.", alsa_out_dev); + } else if (ret == -ENOENT) { die("the alsa output_device \"%s\" can not be found.", alsa_out_dev); } else { char errorstring[1024]; @@ -425,6 +435,8 @@ int actual_open_alsa_device(int do_auto_setup) { die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, alsa_out_dev); } + alsa_handle_status = ret; + frames_sent_break_occurred = 1; return ret; } @@ -1413,7 +1425,7 @@ static void start(__attribute__((unused)) int i_sample_rate, int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps) { - int ret = ENODEV; + int ret = alsa_handle_status; if (using_update_timestamps) *using_update_timestamps = YNDK_NO; @@ -1451,7 +1463,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, snd_pcm_sframes_t delay_temp = 0; if (using_update_timestamps) *using_update_timestamps = YNDK_DONT_KNOW; - int ret = ENODEV; + int ret = alsa_handle_status; snd_pcm_status_t *alsa_snd_pcm_status; snd_pcm_status_alloca(&alsa_snd_pcm_status); @@ -1601,8 +1613,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, } } else { - debug(1, "alsa_handle is NULL in precision_delay_and_status!"); - // already set to ret = ENODEV; + debug(2, "alsa_handle is NULL in precision_delay_and_status!"); } if (delay != NULL) *delay = delay_temp; @@ -1631,10 +1642,7 @@ int delay(long *the_delay) { pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); - if (alsa_handle == NULL) - ret = ENODEV; - else - ret = delay_and_status(&state, &my_delay, NULL); + ret = delay_and_status(&state, &my_delay, NULL); debug_mutex_unlock(&alsa_mutex, 0); pthread_cleanup_pop(0); @@ -1666,7 +1674,7 @@ int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); if (alsa_handle == NULL) { - ret = ENODEV; + ret = alsa_handle_status; } else { *raw_measurement_time = get_absolute_time_in_ns(); // this is not conditioned ("disciplined") by NTP @@ -1834,6 +1842,7 @@ int do_close() { if ((derr = snd_pcm_close(alsa_handle))) debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr)); alsa_handle = NULL; + alsa_handle_status = ENODEV; // no device open } else { debug(1, "alsa: do_close() -- output device already closed."); } diff --git a/player.c b/player.c index b24395bb..1b24e77f 100644 --- a/player.c +++ b/player.c @@ -2544,8 +2544,10 @@ void *player_thread_func(void *arg) { "done.", conn->connection_number); } - } else - debug(1, "Delay error %d when checking running latency.", resp); + } else { + if (resp != -EBUSY) // delay errors can be reported if the device is (hopefully temporarily) busy + debug(1, "Delay error %d when checking running latency.", resp); + } } } From 4729e325e044eb83eba3974d7aaf79a4e204cd3c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:05:07 +0100 Subject: [PATCH 776/878] Wait up to 10 seconds for NQPTP to come online at startup. --- shairport.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shairport.c b/shairport.c index db89a496..bbecc558 100644 --- a/shairport.c +++ b/shairport.c @@ -2323,12 +2323,12 @@ int main(int argc, char **argv) { ptp_send_control_message_string("T"); // get nqptp to create the named shm interface int ptp_check_times = 0; const int ptp_wait_interval_us = 5000; - // wait for up to two seconds for NQPTP to come online + // wait for up to ten seconds for NQPTP to come online do { ptp_send_control_message_string("T"); // get nqptp to create the named shm interface usleep(ptp_wait_interval_us); ptp_check_times++; - } while ((ptp_shm_interface_open() != 0) && (ptp_check_times < (2000000 / ptp_wait_interval_us))); + } while ((ptp_shm_interface_open() != 0) && (ptp_check_times < (10000000 / ptp_wait_interval_us))); if (ptp_shm_interface_open() != 0) { die("Can't access NQPTP! Is it installed and running?"); From a90e35197c884f2c5b9b41290dd82fb8e57d0bae Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:12:01 +0100 Subject: [PATCH 777/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 58b16732..5128537b 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-424-g4729e325 +==== +**Enhancements** +* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Sometimes the main ALSA device will go busy when the sound server uses it. Shairport Sync should no longer terminate if it encounters a busy output device. Instead it will simply wait until it becomes available and use it then. +* Wait up to ten seconds (was two seconds) for NQPTP to come online when starting up -- AirPlay 2 only. + Version 4.1-dev-420-g23d0502f ==== **Bug Fix** From d37393252b392a8d04e09222e26780b6e2221157 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:15:22 +0100 Subject: [PATCH 778/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 5128537b..bd553839 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,8 @@ Version 4.1-dev-424-g4729e325 ==== **Enhancements** -* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Sometimes the main ALSA device will go busy when the sound server uses it. Shairport Sync should no longer terminate if it encounters a busy output device. Instead it will simply wait until it becomes available and use it then. +* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Sometimes the main ALSA device will go busy when the sound server uses it. Shairport Sync should no longer terminate if it encounters a busy ALSA output device. Instead it will simply wait until the device becomes available. Shairport Sync retains exclusive access to it and the sound server can not use it until Shairport Sync is finished playing. + * Wait up to ten seconds (was two seconds) for NQPTP to come online when starting up -- AirPlay 2 only. Version 4.1-dev-420-g23d0502f From f3399203b49948d447d8833ba6667aa004e361f5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:19:20 +0100 Subject: [PATCH 779/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index bd553839..f66059c8 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-424-g4729e325 ==== **Enhancements** -* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Sometimes the main ALSA device will go busy when the sound server uses it. Shairport Sync should no longer terminate if it encounters a busy ALSA output device. Instead it will simply wait until the device becomes available. Shairport Sync retains exclusive access to it and the sound server can not use it until Shairport Sync is finished playing. +* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Shairport Sync now simply waits until the device becomes available. Shairport Sync retains exclusive access to the ALSA output device (and so the sound server can not use it) until it is finished playing. * Wait up to ten seconds (was two seconds) for NQPTP to come online when starting up -- AirPlay 2 only. From 9c8a3328de0c7bc2b325a5c25711fce961af2d85 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:20:03 +0100 Subject: [PATCH 780/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index f66059c8..9d5f14f7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-424-g4729e325 ==== **Enhancements** -* Shairport Sync should no longer terminate if the ALSA output device is or becomes busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Shairport Sync now simply waits until the device becomes available. Shairport Sync retains exclusive access to the ALSA output device (and so the sound server can not use it) until it is finished playing. +* Shairport Sync should no longer terminate if its ALSA output device is busy. This is useful when using ALSA devices in systems with sound servers such as PulseAudio or PipeWire. Shairport Sync now simply waits until the device becomes available. Shairport Sync retains exclusive access to the device (and so the sound server can not use it) until it is finished playing. * Wait up to ten seconds (was two seconds) for NQPTP to come online when starting up -- AirPlay 2 only. From 449e024c951e691ea5ed420d070de3288c8b837a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 22 Jul 2022 15:52:16 +0100 Subject: [PATCH 781/878] If the default device is inaccessible with EHOSTDOWN, try hw:0 instead. Works for Fedora 36 Workstation. Might need to make it a bit smarter. --- audio_alsa.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 9673da5f..4ee19bcc 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -131,7 +131,8 @@ yndk_type precision_delay_available_status = YNDK_DONT_KNOW; // initially, we don't know if the device can do precision delay snd_pcm_t *alsa_handle = NULL; -int alsa_handle_status = ENODEV; // if alsa_handle is NULL, this should say why with a unix error code +int alsa_handle_status = + ENODEV; // if alsa_handle is NULL, this should say why with a unix error code static snd_pcm_hw_params_t *alsa_params = NULL; static snd_pcm_sw_params_t *alsa_swparams = NULL; static snd_ctl_t *ctl = NULL; @@ -417,15 +418,27 @@ int actual_open_alsa_device(int do_auto_setup) { audio_alsa.delay = NULL; ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0); + // EHOSTDOWN seems to signify that it's a PipeWire pseudo device that can't be accessed by this + // user. So, try the first device ALSA device and log it. + if ((ret == -EHOSTDOWN) && (strcmp(alsa_out_dev, "default") == 0)) { + ret = snd_pcm_open(&alsa_handle, "hw:0", SND_PCM_STREAM_PLAYBACK, 0); + if ((ret == 0) || (ret == -EBUSY)) { + // being busy should be okay + inform("the default ALSA device is inaccessible -- \"hw:0\" used instead.", alsa_out_dev); + set_alsa_out_dev("hw:0"); + } + } if (ret == 0) { if (alsa_handle_status == -EBUSY) - warn("The output device \"%s\" is no longer busy and will be used by Shairport Sync.", alsa_out_dev); + warn("The output device \"%s\" is no longer busy and will be used by Shairport Sync.", + alsa_out_dev); alsa_handle_status = ret; // all cool } else { alsa_handle = NULL; // to be sure to be sure if (ret == -EBUSY) { if (alsa_handle_status != -EBUSY) - warn("The output device \"%s\" is busy and can't be used by Shairport Sync at present.", alsa_out_dev); + warn("The output device \"%s\" is busy and can't be used by Shairport Sync at present.", + alsa_out_dev); debug(2, "the alsa output_device \"%s\" is busy.", alsa_out_dev); } else if (ret == -ENOENT) { die("the alsa output_device \"%s\" can not be found.", alsa_out_dev); From f3ef4ff78c372fdd1bb575d8e972d41ed6c8b7d3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:03:39 +0100 Subject: [PATCH 782/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 9d5f14f7..4ede8dcd 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-429-g449e024c +==== +**Enhancement** +* With the ALSA backend, if the `default` device isn't available, Shairport Sync will now try to use `"hw:0"` instead. Background: the default ALSA device is sometimes a pseudo device provided by a sound server such as PipeWire or PulseAudio to play audio from ALSA-compatible programs. But it's not always available to all users, especially daemons like Shairport Sync. This simple change will try to use the first ALSA hardware device -- card 0 -- if the default output device is inaccessible with error code `EHOSTDOWN`. It allows Shairport Sync to work on default settings with Fedora 36. + Version 4.1-dev-424-g4729e325 ==== **Enhancements** From 2c0498c8689e34c490606f244c09fad5c1a74e66 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:47:04 +0100 Subject: [PATCH 783/878] Create README.md --- documents/ADVANCED TOPICS/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 documents/ADVANCED TOPICS/README.md diff --git a/documents/ADVANCED TOPICS/README.md b/documents/ADVANCED TOPICS/README.md new file mode 100644 index 00000000..837957e5 --- /dev/null +++ b/documents/ADVANCED TOPICS/README.md @@ -0,0 +1,14 @@ +Here you will find links to some of the more advanced features and things you can do with Shairport Sync. +* Finish setting up an ALSA-based system. +* Getting the best from your system +* [Update Shairport Sync](https://github.com/mikebrady/shairport-sync/blob/development/UPDATING.md) in place. +* The configuration file -- which contains lots of documentation -- on your system. By default, the sample configuration file is +placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD). +You can also view it online [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf). +* Synchronisation performance. +* Setting up an [MQTT](https://github.com/mikebrady/shairport-sync/blob/development/MQTT.md) system. +* Jack Audio. +* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). +* [Build Configuration Flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). +* [Car Installation](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) +– build an isolated WiFi network with a Shairport Sync player on a Raspberry Pi. From 7f13c79ba6b495498e75145b23db5227b40a0896 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:50:01 +0100 Subject: [PATCH 784/878] Not yet... --- documents/ADVANCED TOPICS/README.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 documents/ADVANCED TOPICS/README.md diff --git a/documents/ADVANCED TOPICS/README.md b/documents/ADVANCED TOPICS/README.md deleted file mode 100644 index 837957e5..00000000 --- a/documents/ADVANCED TOPICS/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Here you will find links to some of the more advanced features and things you can do with Shairport Sync. -* Finish setting up an ALSA-based system. -* Getting the best from your system -* [Update Shairport Sync](https://github.com/mikebrady/shairport-sync/blob/development/UPDATING.md) in place. -* The configuration file -- which contains lots of documentation -- on your system. By default, the sample configuration file is -placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD). -You can also view it online [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf). -* Synchronisation performance. -* Setting up an [MQTT](https://github.com/mikebrady/shairport-sync/blob/development/MQTT.md) system. -* Jack Audio. -* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). -* [Build Configuration Flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). -* [Car Installation](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) -– build an isolated WiFi network with a Shairport Sync player on a Raspberry Pi. From 82ce9d766f5463f45ee8bf1bb6cae0611e5182e8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 24 Jul 2022 09:17:43 +0100 Subject: [PATCH 785/878] Update CONFIGURATION FLAGS.md --- CONFIGURATION FLAGS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONFIGURATION FLAGS.md b/CONFIGURATION FLAGS.md index 8daf3956..fea2b2a6 100644 --- a/CONFIGURATION FLAGS.md +++ b/CONFIGURATION FLAGS.md @@ -17,7 +17,7 @@ AirPlay (aka AirPlay 1) is an older version of the AirPlay protocol. If offers m To build Shairport Sync for AirPlay 2, include the `--with-airplay-2` option in the `./configure ...` options. You will also have to include extra libraries. Omitting this option will cause Shairport Sync to be built for the older AirPlay protocol. -## Audio Output +## Audio Output Backends | Flags | | ----- | | `--with-alsa` | From a94de485ac0377bcd44e6d500b1dd2b79274cefd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 13:43:30 +0100 Subject: [PATCH 786/878] Update FREEBSD.md --- FREEBSD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FREEBSD.md b/FREEBSD.md index 081126ea..c4447d84 100644 --- a/FREEBSD.md +++ b/FREEBSD.md @@ -96,7 +96,7 @@ Finally, edit `/usr/local/etc/shairport-sync.conf` to customise your installatio ``` shairport_sync_enable="YES" ``` -You can launch the service as superuser, or simply reboot the machine. +You can launch the service as superuser, or simply reboot the machine. Using the `sndio` backend ---- From e78a88b64adfe7b5f88fd6faedf55c57445bb240 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:44:57 +0100 Subject: [PATCH 787/878] Big documentation reorganisation. Comments wlecome! --- ADVANCED TOPICS/Events.md | 12 + ADVANCED TOPICS/GetTheBest.md | 54 ++++ ADVANCED TOPICS/InitialConfiguration.md | 205 +++++++++++++++ ADVANCED TOPICS/Metadata.md | 19 ++ ADVANCED TOPICS/README.md | 17 ++ ADVANCED TOPICS/Statistics.md | 118 +++++++++ AIRPLAY2.md | 29 +-- BUILD.md | 195 ++++++++++++++ BUILDFORAP1.md | 106 +------- BUILDFORAP2.md | 156 +---------- CONFIGURATION FLAGS.md | 45 +--- FEDORA.md | 136 +--------- FREEBSD.md | 114 +------- MOREINFO.md | 333 +----------------------- README.md | 79 ++++-- TROUBLESHOOTING.md | 19 +- UPDATING.md | 89 +------ 17 files changed, 724 insertions(+), 1002 deletions(-) create mode 100644 ADVANCED TOPICS/Events.md create mode 100644 ADVANCED TOPICS/GetTheBest.md create mode 100644 ADVANCED TOPICS/InitialConfiguration.md create mode 100644 ADVANCED TOPICS/Metadata.md create mode 100644 ADVANCED TOPICS/README.md create mode 100644 ADVANCED TOPICS/Statistics.md create mode 100644 BUILD.md diff --git a/ADVANCED TOPICS/Events.md b/ADVANCED TOPICS/Events.md new file mode 100644 index 00000000..0fd15ead --- /dev/null +++ b/ADVANCED TOPICS/Events.md @@ -0,0 +1,12 @@ +## Events +Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. +You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. +This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand +and switching it off afterwards. +Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. + +Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable +and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) + +Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`. +This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. diff --git a/ADVANCED TOPICS/GetTheBest.md b/ADVANCED TOPICS/GetTheBest.md new file mode 100644 index 00000000..cf07e19d --- /dev/null +++ b/ADVANCED TOPICS/GetTheBest.md @@ -0,0 +1,54 @@ +# Get The Best from Shairport Sync +Shairport Sync was designed to run best on dedicated stand-alone low-power "headless" Linux/FreeBSD systems with ALSA as the audio system +and with a decent CD-quality Digital to Analog Converter (DAC). + +## CPU Power and Memory +Computer power and memory requirements are modest -- a Raspberry Pi 2 or better, including the Pi Zero 2 W, is fine. +Unfortunately, while the original Raspberry Pi and the Pi Zero are powerful enough for AirPlay operation, +they are not really suitable for AirPlay 2 operation. + +## CPU Clock +For best performance, Shairport Sync requires a stable and accurate system clock. +This is because the output DAC's output rate is normally determined by the system clock (some high-end USB streamers use their own built-in clocks). +If the clock drifts, or if its actual frequency is far from its nominal frequency, Shairport Sync will have to do more interpolation, +which inevitably must degrade the audio fidelity, even if it is very hard to hear. +Some very old laptops are known to have inaccurate clocks and and some embedded systems can suffer from temperature-related clock drift. + +Recent Raspberry Pis seem to have very accurate clocks with low drift. + +## Linux +The best kind of Linux for Shairport Sync is a "bare" Linux. +Raspberry Pi OS Lite, Debian Minimal Server, Ubuntu Server, Fedora Server and Arch Linux (Minimal Configuration) are good examples. + +Shairport Sync also runs on "desktop" Linuxes such as Raspberry Pi OS with desktop, Debian with a desktop environment, +Ubuntu Desktop and Fedora Workstation. +Desktop Linuxes are less suitable because they almost always use a sound server like PulseAudio or PipeWire. +These can interfere with Shairport Sync, which needs direct and exclusive access to the audio hardware. + +## DAC +A good Digital to Analog Converter (DAC) will have a huge influence on the quality of the audio. + +Shairport Sync runs at 44,100 frames per second (FPS), each frame consisting of a pair of signed 16 bit linear samples, one for left and one for right. +Actually, Shairport Sync will take advantage to 24- or 32-bit DACs if available, and will run at 44,100 or 88,200, 176,400 or 352,800 FPS if necessary, +though there is no advantage to the higher rates. The 44,100 FPS rate was chosen to match the rate of AirPlay. + +Good DACs are available at a very wide range of prices, from low-cost USB "Sound Cards" to very high-end HiFi streaming DACs. + +In the Raspberry Pi world, many very good low-cost I2S DACs, some with integrated amplifiers, are available. Staying with the Raspberry Pi, the DAC powering the built-in audio jack is not great. It may be good enough for trying out Shairport Sync, but it has a very limited frequency response and can generate very large transients when it starts up. A separate DAC will transform the output quality. + +**Note** +Make sure that the DAC is capable of 44,100 FPS operation -- this is really mandatory. +Most recent DACs are okay, but some older DACs will only run at 48,000 FPS or multiples of it, and Shairport Sync can not use them. +## Maximum Output Level +The `volume_max_db` setting allows you to reduce the maximum level of DAC output to prevent possible overloading of the amplifier or premplifier it feeds. +## Volume Range +The volume range is the difference (technically the ratio, expressed in dB) between the highest and lowest level of the volume control. Ideally, this should give the highest volume at the high end and a barely audible sound at the lowest level. Typical volume ranges are 60 dB to 75dB. If the range is much less than this, the difference between high and low volume won't seem large enough to the listener. If the range is much more, much of the low end of the volume control range will be inaudible. (The built-in DAC of the Raspberry Pi has this problem.) Use the `volume_range_db` setting to set the volume range. The built-in attenuator will be used to augment the volume range if necessary. +## Volume Control +Audio is sent at full volume in AirPlay and AirPlay 2 with separate information being sent to set the actual volume. This volume information can be used in three ways: +* It can be used to control a built-in attenuator. This is the default. +* It can be used to control a mixer built in to the DAC. To use a mixer, set the `mixer_control_name` in the configuration file to the name of the mixer you wish to use. +* It can be ignored by Shairport Sync. This may be appropriate when you want the volume to be controlled by just one control, typically an audio system's volume control knob or the volume control of a car radio. To make Shairport Sync ignore the volume information, set `ignore_volume_control` to `"yes"`. +* AirPlay volume changes cause events when an executable -- a program or executable script -- can be called. So, you could get Shairport Sync to ignore volume control information but call an executable to control an external volume control. +## Other Settings +* The `disable_standby_mode` setting can be used to prevent the DAC from transitioning between active and standby states, which can sometimes cause a faint popping noise. If activated, Shairport Sync sends frames of silence to keep the DAC busy, preventing it from entering standby mode. +* The `disable_synchronisation` setting can be used to prevent Shairport Sync from doing any interpolation whatever. Normally, this would lead to a problem when the DAC's buffer either overflowed or underflowed, but some very high-end streamers adjust the rate at which their DACs run to match the exact rate at which audio arrives, thus preventing underflow or overflow. diff --git a/ADVANCED TOPICS/InitialConfiguration.md b/ADVANCED TOPICS/InitialConfiguration.md new file mode 100644 index 00000000..1e99e5ca --- /dev/null +++ b/ADVANCED TOPICS/InitialConfiguration.md @@ -0,0 +1,205 @@ +# Initial Configuration +Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card. + +Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` and give the service the password `secret`, then you should do the following: + +``` +general = +{ + name = "Front Room"; + password = "secret"; + // ... other general settings +}; + +``` +The password setting is only valid for classic Shairport Sync. + +(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) + +**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable. + +No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: +``` + output_backend = "pa"; +``` +to the `general` group. + +The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. + +The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. +* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. +* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: + +``` +alsa = +{ + output_device = "hw:0,5"; + mixer_device = "hw:0"; + mixer_control_name = "PCM"; + // ... other alsa settings +}; +``` + +The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. + +Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. + +**Raspberry Pi** + +The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. + +To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. + +Do the usual update and upgrade: +``` +# apt update +# apt upgrade +``` +To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: +``` +alsa = +{ + output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. + mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. + // ... other alsa settings +``` +(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. + +A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur. + +From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. + +*Command Line Arguments* + +As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html. + +Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. + +* The `-c` option allows you to specify the location of the configuration file. +* The `-V` option gives you version information about Shairport Sync and then quits. +* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. +* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. + +The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : +`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. + +Examples +-------- + +Here are some examples of complete configuration files. + +``` +general = { + name = "Joe's Stereo"; +}; + +alsa = { + output_device = "hw:0"; +}; +``` + +This gives the service a particular name — "Joe's Stereo" and specifies that audio device `hw:0` be used. + +For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. + +Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: +``` +general = { + name = "Mike's Boombox"; +}; + +alsa = { + output_device = "hw:0"; + mixer_control_name = "PCM"; +}; +``` + +Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: +``` +general = { + name = "Kitchen"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "PCM"; +}; +``` + +For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: +``` +general = { + name = "Front Room"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "Speaker"; +}; +``` + +For a first generation Griffin iMic on a Raspberry Pi: +``` +general = { + name = "Attic"; +}; + +alsa = { + output_device = "hw:1"; + mixer_control_name = "PCM"; +}; +``` + +For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: + +On an NSLU2, to drive a first generation Griffin iMic: +``` +general = { + name = "Den"; +}; + +alsa = { + mixer_control_name = "PCM"; +}; +``` + +On an NSLU2, to drive the "3D Sound" USB card: +``` +general = { + name = "TV Room"; +}; + +alsa = { + mixer_control_name = "Speaker"; +}; +``` + +Finally, here is an example of using the PulseAudio backend: +``` +general = { + name = "Zoe's Computer"; + output_backend = "pa"; +}; + +``` + + +Latency +------- +Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. + +Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. + +Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. + +Resynchronisation +------------- +Shairport Sync actively maintains synchronisation with the source. +If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. +* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. + +Tolerance +--------- +Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. +* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. + diff --git a/ADVANCED TOPICS/Metadata.md b/ADVANCED TOPICS/Metadata.md new file mode 100644 index 00000000..1620d1bc --- /dev/null +++ b/ADVANCED TOPICS/Metadata.md @@ -0,0 +1,19 @@ +# Metadata + +Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. +through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. +Sources that supply metadata include iTunes and the Music app in macOS and iOS. + + +## Metadata over UDP** + +As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync +to broadcast UDP packets containing the track metadata. + +The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. +It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. + +The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* +(both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). +The remaining bytes of the packet, if any, make up the raw value of the metadata. + diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md new file mode 100644 index 00000000..31f6995c --- /dev/null +++ b/ADVANCED TOPICS/README.md @@ -0,0 +1,17 @@ +# Advanced Topics +Here you will find links to some of the more advanced features and things you can do with Shairport Sync. +* [Finish setting up](InitialConfiguration.md). +* [Get the best](GetTheBest.md) from Shairport Sync. +* [Metadata](Metadata.md). +* [Events](Events.md). +* [Update Shairport Sync](https://github.com/mikebrady/shairport-sync/blob/development/UPDATING.md) in place. +* The configuration file -- which contains lots of documentation -- on your system. By default, the sample configuration file is +placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD). +You can also view it online [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf). +* [Statistics](Statistics.md). +* Setting up an [MQTT](https://github.com/mikebrady/shairport-sync/blob/development/MQTT.md) system. +* Jack Audio. +* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). +* [Build Configuration Flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). +* [Car Installation](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) +– build an isolated WiFi network containing a Shairport Sync player on a Raspberry Pi. Suitable for a car radio with an `aux` input or for the stereo in that broadband-free holiday cottage. diff --git a/ADVANCED TOPICS/Statistics.md b/ADVANCED TOPICS/Statistics.md new file mode 100644 index 00000000..581c9559 --- /dev/null +++ b/ADVANCED TOPICS/Statistics.md @@ -0,0 +1,118 @@ +# Statistics + +If you set the `statistics` setting in the `diagnostics` section of the configuration file to `"YES"`, some statistics will be logged at regular intervals. The items logged will depend on the type of stream being processed: AirPlay 1, AirPlay 2 Buffered Audio or AirPlay 2 Realtime Audio. If the `log_verbosity` is set to 1, 2 or 3, additional items will be logged. + +From an audio enthusiast's point of view, the most important figure is possibly the `All Sync PPM` figure. This is the total amount of interpolation needed by Shairport Sync to keep the audio stream in sync. The number represents is the ratio of frames added and removed from the audio stream relative to all the frames output in the last interval, expressed in parts per million (PPM). For reference, adding or removing one frame per second into a 44,100 frames per second stream is ± 22.68 PPM. The lower this number number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever unless the output device is adapting to the true data rate (some very high-end streamers seem to do so). You may also find that the number might be higher at the start while the system settles down. + +The second most important figure is possibly the `Sync Error ms`. This is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync has a tolerance of a sync error of ± 2.0 milliseconds without triggering interpolation. + +Two other interesting measurements of the output rate may be available -- `Output FPS (r)` and `Output FPS (c)`, where `(r)` means "raw" and the `(c)` means "corrected". + +The "raw" figure is the rate at which the output device (typically a DAC) accepts data measured relative to the computer's own system clock (specifically the `CLOCK_MONOTONIC_RAW` clock). The accuracy of the number depends on the accuracy of the clock, which will typically be accurate to within anything from 20 to 100 ppm. + +The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a _long_ period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate indeed. + +Here is a brief description of the figures that might be provided. +##### Sync Error ms +Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. Positive means late, negative means early. +##### Net Sync PPM +This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added and removed from the audio stream, expressed in parts per million (PPM) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. +##### All Sync PPM +This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added plus the number removed from the audio stream, expressed in parts per million (PPM) in the last interval. The magnitude of this should be the same as the `net sync ppm'. If it is much larger it means that Shairport Sync is overcorrecting for sync errors -- try increasing the drift tolerance to reduce it. +##### Packets +This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames. +##### Missing +This is the number of packets of audio frames that were expected but not received in the last interval. It should be zero, and if not it usually indicates a significant problem with the network. AirPlay 1 and AirPlay 2 Realtime Streams only. +##### Late +This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. AirPlay 1 and AirPlay 2 Realtime Streams only. +##### Too Late +This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets were already received, so those frames might not actually be missing when the time comes to play them. AirPlay 1 and AirPlay 2 Realtime Streams only. +##### Resend Reqs +This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. AirPlay 1 and AirPlay 2 Realtime Streams only. +##### Min DAC Queue +The is the smallest number of frames of audio in the DAC's hardware queue. If it goes too low, the DAC may begin to underrun. +##### Min Buffers +The is the smallest number of packets of audio in the queue to be processed in the last interval. It is related to the overall latency in AirPlay 1 and AirPlay 2 Realtime Streams. If it comes close to zero it's often a sign that the network is poor. +##### Max Buffers +The is the largest number of packets of audio in the queue to be processed in the last interval. +##### Min Buffer Size +The is smallest remaining number of bytes in the Buffered Audio buffer in the last interval. It can legitimately be zero when a track ends or begins. If it reaches zero while a track is playing, it means that audio data is not arriving at Shairport Sync quickly enough and may indicate network problems. AirPlay 2 Buffered Audio streams only. +##### Nominal FPS +This is the rate specified in the AirPlay stream itself. AirPlay 1 only. +##### Received FPS +This is the rate at which frames are received from the network averaged since the start of the play session. AirPlay 1 only. +##### Output FPS (r) +Output rate measured relative to the computer system's clock since the start of the play session. See above for a discussion. +##### Output FPS (c) +Output rate measured relative to the network-clock-disciplined computer system's clock since the start of the play session. See above for a discussion. +##### Source Drift PPM +This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. AirPlay 1 only. +##### Drift Samples +This is the number drift samples have been accepted for calculating the source drift. AirPlay 1 only. +#### Example +The following example is of an AirPlay 2 Buffered Audio Stream from a HomePod mini to a WiFi-connected Raspberry Pi 3 equipped with a Pimoroni "Audio DAC SHIM (Line-Out)" with `log_verbosity` set to `1` after 3 hours and 37 minutes of operation. The audio is from a radio station accessed through Apple Music: + +``` + 5.292055362 "rtsp.c:3112" Connection 1. AP2 Buffered Audio Stream. + 1.382815677 "player.c:2650" Connection 1: Playback Started -- AirPlay 2 Buffered. + 7.899808955 "player.c:2491" Sync Error ms | Net Sync PPM | All Sync PPM | Min DAC Queue | Min Buffer Size | Output FPS (r) | Output FPS (c) +... + 8.014025361 "player.c:2491" -1.69 2.8 2.8 7341 198k 44100.04 44100.35 + 7.992082237 "player.c:2491" -1.70 2.8 2.8 7332 188k 44100.04 44100.34 + 8.015987549 "player.c:2491" -1.68 0.0 0.0 7334 186k 44100.04 44100.35 + 8.010537393 "player.c:2491" -1.68 2.8 2.8 7335 187k 44100.04 44100.36 + 7.993940934 "player.c:2491" -1.85 0.0 0.0 7329 198k 44100.04 44100.37 + 8.015796195 "player.c:2491" -1.87 5.7 5.7 7325 194k 44100.04 44100.37 + 8.021665934 "player.c:2491" -1.92 5.7 5.7 7329 190k 44100.04 44100.37 + 7.990409477 "player.c:2491" -1.85 2.8 2.8 7347 201k 44100.04 44100.37 + 8.001858278 "player.c:2491" -1.96 17.0 17.0 7332 204k 44100.04 44100.37 + 8.007150986 "player.c:2491" -1.89 2.8 2.8 7327 195k 44100.04 44100.36 + 7.998612862 "player.c:2491" -1.87 2.8 2.8 7317 199k 44100.04 44100.36 + 8.020592653 "player.c:2491" -1.89 11.3 11.3 7323 203k 44100.04 44100.35 + 8.003245674 "player.c:2491" -1.89 8.5 8.5 7325 198k 44100.04 44100.35 + 7.990874789 "player.c:2491" -1.98 19.8 19.8 7103 197k 44100.04 44100.34 + 8.010600257 "player.c:2491" -1.85 2.8 2.8 7327 187k 44100.04 44100.33 + 8.004573122 "player.c:2491" -1.89 5.7 5.7 7327 187k 44100.04 44100.33 + 8.012853278 "player.c:2491" -1.89 5.7 5.7 7326 190k 44100.04 44100.32 + 8.005596664 "player.c:2491" -1.95 22.7 22.7 7322 165k 44100.04 44100.31 + 8.019198695 "player.c:2491" -1.85 5.7 5.7 7333 166k 44100.04 44100.31 + 7.999363382 "player.c:2491" -1.79 2.8 2.8 7349 153k 44100.04 44100.30 + 7.990332549 "player.c:2491" -1.87 5.7 5.7 7328 155k 44100.04 44100.29 + 8.012287445 "player.c:2491" -1.84 2.8 2.8 7352 185k 44100.04 44100.28 + 7.998929528 "player.c:2491" -1.94 14.2 14.2 7331 171k 44100.04 44100.28 + 8.008170622 "player.c:2491" -1.88 5.7 5.7 7327 188k 44100.04 44100.27 + 8.012935257 "player.c:2491" -1.87 19.8 19.8 7270 205k 44100.04 44100.26 + 8.006337966 "player.c:2491" -1.86 0.0 0.0 7334 199k 44100.04 44100.25 + 8.004206612 "player.c:2491" -1.76 2.8 2.8 7331 204k 44100.04 44100.25 + 7.998495257 "player.c:2491" -1.82 2.8 2.8 7329 204k 44100.04 44100.24 + 8.016859059 "player.c:2491" -1.88 2.8 2.8 7335 201k 44100.04 44100.23 + 7.993529320 "player.c:2491" -1.92 14.2 14.2 7329 205k 44100.04 44100.22 + 8.021956820 "player.c:2491" -1.96 11.3 11.3 7322 206k 44100.04 44100.21 + 8.006401820 "player.c:2491" -1.93 11.3 11.3 7322 199k 44100.04 44100.21 + 8.001469111 "player.c:2491" -1.89 5.7 5.7 7336 204k 44100.04 44100.20 + 7.995194320 "player.c:2491" -1.86 5.7 5.7 7330 202k 44100.04 44100.19 + 8.017805206 "player.c:2491" -1.99 22.7 22.7 7322 201k 44100.04 44100.18 + 7.995541038 "player.c:2491" -1.95 22.7 22.7 7331 203k 44100.04 44100.18 + 8.011463643 "player.c:2491" -1.87 0.0 0.0 7331 207k 44100.04 44100.17 + 7.995941403 "player.c:2491" -1.85 2.8 2.8 7331 206k 44100.04 44100.16 + 8.013330570 "player.c:2491" -1.71 2.8 2.8 7344 212k 44100.04 44100.15 + 8.012214580 "player.c:2491" -1.82 0.0 0.0 7333 203k 44100.04 44100.15 + 7.998585049 "player.c:2491" -1.85 8.5 8.5 7332 207k 44100.04 44100.14 + 8.023448799 "player.c:2491" -1.86 5.7 5.7 7328 206k 44100.04 44100.13 + 7.984586612 "player.c:2491" -1.87 8.5 8.5 7277 205k 44100.04 44100.12 + 8.017339372 "player.c:2491" -1.91 17.0 17.0 7327 207k 44100.04 44100.12 + 8.007090309 "player.c:2491" -1.94 17.0 17.0 7286 199k 44100.04 44100.11 + 8.007894164 "player.c:2491" -1.87 2.8 2.8 7334 201k 44100.04 44100.10 + 7.991787497 "player.c:2491" -1.89 2.8 2.8 7327 194k 44100.04 44100.09 + 8.019011611 "player.c:2491" -1.94 5.7 5.7 7323 185k 44100.04 44100.09 + 7.994695362 "player.c:2491" -1.99 14.2 14.2 7321 199k 44100.04 44100.08 + 8.010284632 "player.c:2491" -1.93 11.3 11.3 7325 191k 44100.04 44100.07 + 8.011451612 "player.c:2491" -1.87 8.5 8.5 7329 156k 44100.04 44100.09 + 7.993112549 "player.c:2491" -1.84 2.8 2.8 7330 150k 44100.04 44100.13 + 8.004971455 "player.c:2491" -1.77 11.3 11.3 7325 157k 44100.04 44100.16 + 8.010517133 "player.c:2491" -1.85 0.0 0.0 7322 182k 44100.04 44100.19 + 8.005598851 "player.c:2491" -2.00 39.7 39.7 7207 180k 44100.04 44100.22 + 8.006777965 "player.c:2491" -1.99 39.7 39.7 7320 183k 44100.04 44100.24 + 7.115508696 "player.c:1643" Connection 1: Playback Stopped. Total playing time 03:37:20. Output: 44100.04 (raw), 44100.24 (corrected) frames per second. +``` +For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 5c79b020..d741f6ad 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -3,9 +3,11 @@ Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, ## What Works - AirPlay 2 audio for iOS, HomePod mini, AppleTV and Mac players. - * Two types of audio are received by Shairport Sync – "Realtime" streams of CD quality ALAC (like "classic" AirPlay) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. Realtime streams generally have a latency of about two seconds. Buffered Audio streams typically have a latency of half a second or less. * Audio is synchronised with other AirPlay 2 devices. - * Shairport Sync offers "classic" AirPlay compatibility for situations where iTunes on macOS or macOS Music plays to multiple speakers and where one of more of them is compatible with AirPlay only. + * Two types of audio are received by Shairport Sync – "Realtime" streams of CD quality ALAC (like "classic" AirPlay) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. + * The selection of stream type is made by the player. + * Realtime streams generally have a latency of about two seconds. Buffered Audio streams typically have a latency of half a second or less. + * In AirPlay 2 mode, Shairport Sync reverts to "classic" AirPlay when iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay only. - Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to the Home app. @@ -14,7 +16,8 @@ Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, - Remote control facilities are not implemented. ## General -Shairport Sync uses a companion application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation in AirPlay 2. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. +Shairport Sync uses a companion application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP") +for timing and synchronisation in AirPlay 2. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. Lossless and High Definition Lossless material is transcoded to AAC before it reaches Shairport Sync. @@ -26,21 +29,9 @@ Here are some guidelines: * Ports 319 and 320 must be free to use (i.e. they must not be in use by another service such as a PTP service) and must not be blocked by a firewall. * An up-to-date Linux or FreeBSD. This is important, as some of the libraries must be the latest available. * Shairport Sync will not run on a Mac because NQPTP, on which it relies, needs ports 319 and 320, which are already used by macOS. -* A version of the [ffmpeg](https://www.ffmpeg.org) library with an AAC decoder capable of decoding Floating Planar -- `fltp` -- material. There is a guide [here](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) to help you find out if your system has it. -* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). You can use an application called [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware `alsa` audio devices on your device. Other backends continue to work as with "classic" Shairport Sync. +* A version of the [FFmpeg](https://www.ffmpeg.org) library with an AAC decoder capable of decoding Floating Planar -- `fltp` -- material. There is a guide [here](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) to help you find out if your system has it. +* An audio output, for example an ALSA device (or `sndio` in FreeBSD). The device must be capable of running at 44,100 frames per second. You can use an `sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware ALSA audio devices on your device. +Other backends continue to work as with "classic" Shairport Sync. ## Guides -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide for building on FreeBSD is available [here](https://github.com/mikebrady/shairport-sync/blob/development/FREEBSD.md). - -## Note -The functionality offered by Shairport Sync is the result of study and analysis of the AirPlay and AirPlay 2 protocols by many people over the years. These protocols have not been officially published, and there is no assurance that Shairport Sync will continue to work with AirPlay in future. - -## Acknowledgements -Huge thanks are due to a number of individuals who made direct and valuable contributions to Shairport Sync: -1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. -2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes, documentation and Docker automation (forthcoming). -4. [ckdo](https://github.com/ckdo) for their pathfinding work on AirPlay 2, especially the HomeKit pairing. - -Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. It is a pleasure to acknowledge the work of the contributors there. +* A building guide is available at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..662a4c50 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,195 @@ +# Build and Install Shairport Sync +This guide is for a basic installation of Shairport Sync in a recent (2018 onwards) Linux or FreeBSD. + +Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. Check ["What You Need"](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#what-you-need) for some basic system requirements. + +Overall, you'll be building and installing two programs – Shairport Sync itself and [NQPTP](https://github.com/mikebrady/nqptp), a companion app that Shairport Sync uses for AirPlay 2 timing. If you are building classic Shairport Sync, NQPTP is unnecessary and can be omitted. + +In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. + +## 1. Prepare +#### Remove Old Copies of Shairport Sync +Before you begin building Shairport Sync, it's best to remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: +``` +$ which shairport-sync +/usr/local/bin/shairport-sync +``` +Remove it as follows: +``` +# rm /usr/local/bin/shairport-sync +``` +Do this until no more copies of `shairport-sync` are found. +#### Remove Old Service Files +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/user/shairport-sync.service` +* `/etc/init.d/shairport-sync` + +New service files will be installed if necessary at the `# make install` stage. +#### Reboot after Cleaning Up +If you removed any installations of Shairport Sync or any of its service files in the last two steps, you should reboot. + +## 2. Get Tools and Libraries +Okay, now let's get the tools and libraries for building and installing Shairport Sync (and NQPTP). + +### Debian / Raspberry Pi OS / Ubuntu +``` +# apt update +# apt upgrade # this is optional but recommended +# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd +``` +If you are building classic Shairport Sync, the list of packages is shorter: +``` +# apt update +# apt upgrade # this is optional but recommended +# apt-get install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev +``` +### Fedora +For AirPlay 2 operation, _before you install the libraries_, please ensure the you have [enabled](https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion) RPM Fusion software repositories at least to the "Free" level. If this is not done, FFmpeg libraries will be installed that lack a suitable AAC decoder, preventing Shairport Sync from working in AirPlay 2 mode. +``` +# yum update +# yum install make automake gcc gcc-c++ \ + git xmltoman autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \ + ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-devel libuuid-devel vim-common \ + alsa-lib-devel +``` +If you are building classic Shairport Sync, the list of packages is shorter: +``` +# yum update +# yum install make automake gcc gcc-c++ \ + git xmltoman autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \ + alsa-lib-devel +``` +### Arch Linux +After you have installed the libraries, note that you should enable and start the `avahi-daemon` service. +``` +# pacman -Syu +# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig \ + libsndfile libsodium ffmpeg vim libplist +``` +If you are building classic Shairport Sync, the list of packages is shorter: +``` +# pacman -Syu +# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig +``` +Enable and start the `avahi-daemon` service. +``` +# systemctl enable avahi-daemon +# systemctl start avahi-daemon +``` +### FreeBSD +First, update everything: +``` +# freebsd-update fetch +# freebsd-update install +# pkg +# pkg update +``` +Next, install the Avahi subsystem. FYI, `avahi-app` is chosen because it doesn’t require X11. `nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this. +``` +# pkg install avahi-app nss_mdns +``` +Add these lines to `/etc/rc.conf`: +``` +dbus_enable="YES" +avahi_daemon_enable="YES" +``` +Next, change the `hosts:` line in `/etc/nsswitch.conf` to +``` +hosts: files dns mdns +``` +Reboot for these changes to take effect. + +Next, install the packages that are needed for Shairport Sync and NQPTP: +``` +# pkg install git autotools pkgconf popt libconfig openssl alsa-utils \ + libplist libsodium ffmpeg e2fsprogs-libuuid vim +``` +If you are building classic Shairport Sync, the list of packages is shorter: +``` +# pkg install git autotools pkgconf popt libconfig openssl alsa-utils +``` +## 3. Build +### NQPTP +Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync. + +Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp). + +### Shairport Sync +#### Build and Install +Download Shairport Sync, check out the `development` branch and configure, compile and install it. Before executing the commands, please note the following: + +* If building for FreeBSD, replace `--with-systemd` with `--with-os=freebsd --with-freebsd-service`. +* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync. +* If you wish to add extra features, for example an extra audio backend, take a look at the [configuration flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). For this walkthrough, though, please do not remove the `--with-alsa` flag. + +``` +$ git clone https://github.com/mikebrady/shairport-sync.git +$ cd shairport-sync +$ git checkout development +$ autoreconf -fi +$ ./configure --sysconfdir=/etc --with-alsa \ + --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 +$ make +# make install +``` +By the way, the `autoreconf` step may take quite a while – please be patient! + +## 4. Test +At this point, Shairport Sync should be built and installed but not running. If the user you are logged in as is a member of the unix `audio` group, Shairport Sync should run from the command line: +``` +$ shairport-sync +``` +* Add the `-v` command line option to get some diagnostics. +* Add the `--statistics` option to get some infomation about the audio received. + +The AirPlay service should appear on the network and the audio you play should come through to the default ALSA device. (Use `alsamixer` or similar to adjust levels.) + +If you have problems, please check the items in [Final Notes](final_notes) below, or in the [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) guide. + +Note: Shairport Sync will run indefinitely -- use Control-C it to stop it. + +## 5. Enable and Start Service +Once you are happy that Shairport Sync runs from the command line, you should Control-C out of it and enable and start the `shairport-sync` service. This will launch Shairport Sync automatically as a background "daemon" service when the system powers up: + +### Linux +``` +# systemctl enable shairport-sync +``` +### FreeBSD +To make the `shairport-sync` daemon load at startup, add the following line to `/etc/rc.conf`: +``` +shairport_sync_enable="YES" +``` +## 6. Check +Reboot the machine. The AirPlay service should once again be visible on the network and audio will be sent to the default ALSA device. + +## 7. Final Notes +A number of system settings can affect Shairport Sync. Please review them as follows: + +### Power Saving +If your computer has an `Automatic Suspend` Power Saving Option, you should experiment with disabling it, because your computer has to be available for AirPlay service at all times. +### WiFi Power Management – Linux +If you are using WiFi, you should turn off WiFi Power Management: +``` +# iwconfig wlan0 power off +``` +or +``` +# iw dev wlan0 set power_save off +``` +The motivation for this is that WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details. + +(You can find WiFi device names (e.g. `wlan0`) with `$ ifconfig`.) +### Firewall +If a firewall is running (some systems, e.g. Fedora, run a firewall by default), ensure it is not blocking ports needed by Shairport Sync and [NQPTP](https://github.com/mikebrady/nqptp/blob/main/README.md#firewall). + +## 8. Connect and enjoy... +### Add to Home +With AirPlay 2, you can follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your device to the Apple Home system. +### Wait, there's more... +Shairport Sync has many extra [features](https://github.com/mikebrady/spsdoc/blob/main/README.md#features) and [settings](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). For instance, you can [add an MQTT service](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md), change the AirPlay service name, adjust for delays in other equipment (many TVs and AVRs introduce slight delays in their audio processing chains) and more. Explore runtime settings in `/etc/shairport-sync.conf` or [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). Build configuration flags are [here](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index e21ab996..e173e594 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -1,105 +1,3 @@ -Build Instructions for AirPlay 1 -== -Here are simple instructions for building and installing Shairport Sync to provide an AirPlay 1 service on a recent Linux based on Debian, including Ubuntu and Raspberry Pi OS. +# Build Instructions for AirPlay 1 -In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. - -### Configure and Update -Do the usual update and upgrade: -``` -# apt-get update -# apt-get upgrade -``` -(Separately, on a Raspberry Pi only, if you haven't done so already, consider using the `raspi-config` tool to expand the file system to use the entire card.) - -### Turn Off WiFi Power Management -If you are using WiFi, you should turn off WiFi Power Management: -``` -# iwconfig wlan0 power off -``` -WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) - -Reboot the computer. - -### Remove Old Copies -Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: -``` -$ which shairport-sync -/usr/local/bin/shairport-sync -``` -Remove it as follows: -``` -# rm /usr/local/bin/shairport-sync -``` -Do this until no more copies of `shairport-sync` are found. - -### Remove Old Startup and Service Scripts -You should also remove the startup script and service definition files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/init.d/shairport-sync`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf` and `/etc/dbus-1/system.d/shairport-sync-mpris.conf` if they exist – new ones will be installed if necessary. - -### Reboot after Cleaning Up -If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. - -### Build and Install -Okay, now let's get the tools and sources for building and installing Shairport Sync. - -First, install the packages needed by Shairport Sync: -``` -# apt-get install build-essential git xmltoman autoconf automake libtool \ - libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev -``` -Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -$ cd shairport-sync -$ git checkout development -$ autoreconf -fi -$ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd -$ make -$ sudo make install -``` -By the way, the `autoreconf` step may take quite a while -- be patient! - -### Configure - -Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: - -``` -... -Settings and options for the audio backend "alsa": - -d output-device set the output device, default is "default". - -c mixer-control set the mixer control name, default is to use no mixer. - -m mixer-device set the mixer device, default is the output device. - -i mixer-index set the mixer index, default is 0. - hardware output devices: - "hw:Headphones" -``` -Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. - -Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: -``` -// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC -general = -{ - volume_range_db = 60; -}; - -alsa = -{ - output_device = "hw:Headphones"; - mixer_control_name = "Headphone"; -}; - -``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. - -The next step is to enable Shairport Sync to start automatically on boot up: -``` -# systemctl enable shairport-sync -``` -Finally, either reboot the Pi or start the `shairport-sync` service: -``` -# systemctl start shairport-sync -``` -The Shairport Sync AirPlay service should now appear on the network with a service name made from the computer's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ubuntu`. You can change the service name and set a password in the configuration file. - -Connect and enjoy... +This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index 5ce622fb..cf856c48 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,155 +1,3 @@ -Building Shairport Sync for AirPlay 2 -== -* Before building Shairport Sync, you might wish to [check the features and limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations) of the AirPlay 2 service it provides. -* Recent versions of Linux / FreeBSD are required for AirPlay 2 operation. At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11.3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), FreeBSD 12.1, Ubuntu 18.04 -- fully updated. A further requirement is an AAC decoder capable of decoding Planar Floating Point `"fltp"` material. Unfortuately, some prominent Linux distributions -- notably Fedora 36 -- do not include this decoder. See [here](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) for more on this. -* Be very careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! -* Shairport Sync relies on a companion program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor timing signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. - FYI, most computers do not have a PTP service running. They often use a (totally different) [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with civil time. The POSIX Shared Memory Interface (SMI) Version numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. +# Building Shairport Sync for AirPlay 2 -Instructions -== -Build instructions are different from previous versions of Shairport Sync and may change further. For now, leave the settings in the configuration file at default except as noted below. - -Overall, you'll be building and installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build and install `nqptp` first. - -In the commands below, for a Debian/Ubuntu-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. - -### Turn Off WiFi Power Management -If you are using WiFi, you should turn off WiFi Power Management: -``` -# iwconfig wlan0 power off -``` -WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) - -### Update Your System -Do the usual update and upgrade: -``` -# apt-get update -# apt-get upgrade -``` -Reboot. - -### Remove Old Copies -Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: -``` -$ which shairport-sync -/usr/local/bin/shairport-sync -``` -Remove it as follows: -``` -# rm /usr/local/bin/shairport-sync -``` -Do this until no more copies of `shairport-sync` are found. - -### Remove Old Service Files -You should also remove any of the following service files that may be present: `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf`, `/etc/dbus-1/system.d/shairport-sync-mpris.conf`, and `/etc/init.d/shairport-sync`. New ones will be installed if necessary at the `# make install` stage. - -### Reboot after Cleaning Up -If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. - -### Tools and Libraries -Okay, now let's get the tools and libraries for building and installing Shairport Sync. - -First, install the packages needed by Shairport Sync: -``` -# apt install --no-install-recommends build-essential git xxd xmltoman autoconf automake libtool \ - libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ - libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev -``` - -### nqptp ### -Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). - -### Shairport Sync - -#### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways: (1) using `git` to clone it -- recommended -- or (2) downloading the repository as a ZIP archive. Please use the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: -``` -Version with git information: -4.0-dev-138-g2789572-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc - -Version without git information: -4.0-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc -``` - -#### Build and Install -Download Shairport Sync, check out the `development` branch and configure, compile and install it: -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -$ cd shairport-sync -$ git checkout development -$ autoreconf -fi -$ ./configure --sysconfdir=/etc --with-alsa \ - --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 -$ make -j -# make install -``` -By the way, the `autoreconf` step may take quite a while -- be patient! - -#### Configure -Now to configure Shairport Sync. In this walkthrough, it is configured for an `alsa` output device. - -A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: - -``` -... -Settings and options for the audio backend "alsa": - -d output-device set the output device, default is "default". - -c mixer-control set the mixer control name, default is to use no mixer. - -m mixer-device set the mixer device, default is the output device. - -i mixer-index set the mixer index, default is 0. - hardware output devices: - "hw:Headphones" -``` -Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. - -Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: -``` -// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC -general = -{ - volume_range_db = 60; -}; - -alsa = -{ - output_device = "hw:Headphones"; - mixer_control_name = "Headphone"; -}; - -``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. - -#### Automatic Start - -To enable Shairport Sync to start automatically on boot up: -``` -# systemctl enable shairport-sync -``` -Now, either reboot or start the `shairport-sync` service: -``` -# systemctl start shairport-sync -``` - -#### Running From the Command Line - -You may wish to run Shairport Sync from the command line (but remember to ensure it is not already running as a daemon). To enable debug messages and statistics, use the following: - -``` -$ shairport-sync -vu --statistics -``` -The user doesn't need to be privileged, but must be a member of the `audio` group for access to the `alsa` subsystem. - -### Using Shairport Sync - -The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `ubuntu` gives a service name `Ubuntu`. You can change the service name in the configuration file. - -Connect and enjoy... - -### Adding to Home - -Follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your Shairport Sync device to the Apple Home system. - -### Update Your Mac! - -Many AirPlay 2 bugs have been fixed in recent versions of macOS, so it is strongly recommended that you update your Mac. +This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). diff --git a/CONFIGURATION FLAGS.md b/CONFIGURATION FLAGS.md index fea2b2a6..85e654e9 100644 --- a/CONFIGURATION FLAGS.md +++ b/CONFIGURATION FLAGS.md @@ -1,5 +1,5 @@ # Configuration Flags -When the application is being built, the *compilation configuration options* determine what capabilities are included in Shairport Sync. +When the application is being built, these flags determine what capabilities are included in Shairport Sync. For example, to include DSP capabilities – needed for a loudness filter – you would include the `--with-convolution` flag in the list of options at the `./configure ...` stage when building the application. @@ -17,7 +17,7 @@ AirPlay (aka AirPlay 1) is an older version of the AirPlay protocol. If offers m To build Shairport Sync for AirPlay 2, include the `--with-airplay-2` option in the `./configure ...` options. You will also have to include extra libraries. Omitting this option will cause Shairport Sync to be built for the older AirPlay protocol. -## Audio Output Backends +## Audio Output | Flags | | ----- | | `--with-alsa` | @@ -36,7 +36,7 @@ Here are the audio backend configuration options: - `--with-alsa` Output to the Advanced Linux Sound Architecture ([ALSA](https://www.alsa-project.org/wiki/Main_Page)) system. This is recommended for highest quality. - `--with-sndio` Output to the FreeBSD-native [sndio](https://sndio.org) system. -- `--with-pa` Include the PulseAudio audio back end. This is recommended only if your Linux installation already has [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. +- `--with-pa` Include the PulseAudio audio back end. - `--with-pw` Output to the [PipeWire](https://pipewire.org) system. - `--with-ao` Output to the [libao](https://xiph.org/ao/) system. No synchronisation. - `--with-jack` Output to the [Jack Audio](https://jackaudio.org) system. @@ -44,40 +44,12 @@ Here are the audio backend configuration options: - `--with-stdout` Include an optional backend module to enable raw audio to be output through standard output (`STDOUT`). - `--with-pipe` Include an optional backend module to enable raw audio to be output through a unix pipe. -### PulseAudio -Many recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Therefore, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. However, there are two issues with the use of PulseAudio. -* First, the PulseAudio sound system may not always be available. In its normal configuration, PulseAudio only becomes available when a user logs in via the GUI or a terminal window and disappears when the user logs out; it is not available in time when the system starts up. This means that Shairport Sync can not be installed as a daemon (see "Daemonisation" below) in a PulseAudio-based system unless PulseAudio is [configured](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/SystemWide) as a system-wide daemon. -* Second, the fidelity of the audio is unknown: once audio is delivered to PulseAudio, it is unknown what happens to it as it is processed through PulseAudio to arrives evenutally at the loudspeakers. +### PulseAudio and PipeWire +Many recent Linux distributions with a GUI -- "desktop" Linuxes -- use PulseAudio or PipeWire to handle sound. There are two things to consider with these sound servers: +1. They may not always be available: a sound server generally becomes available when a user logs in via the GUI and disappears when the user logs out; it is not available when the system starts up and it is not available to non-GUI users. This means that Shairport Sync can not run as a daemon (see "Daemonisation" below) using a sound server unless the sound server is configured as a system-wide service. +2. The fidelity of the audio is unknown: once audio is delivered to the sound server, it is unknown what happens to it as it is processed through PulseAudio to arrives eventually at the loudspeakers. -In summary, Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is [not recommended](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/WhatIsWrongWithSystemWide). - -#### Checking for PulseAudio -You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: /run/user/1000/pulse/native -Library Protocol Version: 35 -Server Protocol Version: 35 -Is Local: yes -Client Index: 8 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu2204 -Server Name: pulseaudio -Server Version: 15.99.1 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 5a9f:56ee -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend because PulseAudio will require exclusive access to the ALSA device being used as the output device. +It should be noted that both PulseAudio and PipeWire provide a default ALSA pseudo device that enables ALSA-compatible programs to send audio. Shairport Sync can therefore use the ALSA backend with PulseAudio- or PipeWire-based systems. ## Audio Options | Flags | @@ -203,3 +175,4 @@ The Zeroconf-related options are as follows: | `--with-os=` | Specifies the Operating System to target: One of `linux` (default), `freebsd`, `openbsd` or `darwin`. | | `--with-configfiles` | Installs configuration files (including a sample configuration file) during `make install`. | | `--with-pkg-config` | Specifies the use of `pkg-config` to find libraries. (Obselete for AirPlay 2. Special purpose use only.) | + diff --git a/FEDORA.md b/FEDORA.md index 5db62e23..7a8fd57a 100644 --- a/FEDORA.md +++ b/FEDORA.md @@ -1,137 +1,3 @@ # Fedora Installation Guide -This guide is for a recent version of Fedora Linux (Fedora 36) outputting to the PipeWire sound system through a built-in ALSA adapter. -Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. - -Fedora uses PipeWire for audio management [since Fedora 34](https://fedoramagazine.org/pipewire-the-new-audio-and-video-daemon-in-fedora-linux-34/). An [adapter](https://wiki.archlinux.org/title/PipeWire#ALSA_clients) is included to route audio from ALSA-compatible sources into the PipeWire infrastructure. - -It is recommended that Shairport Sync is built with the standard ALSA backend (`--with-alsa`). - -Shairport Sync also offers a PipeWire (`--with-pw`) backend – still new and still under development. The PulseAudio (`--with-pa`) backend may also be used thanks to a PulseAudio-to-PipeWire adapter. - -**Note:** The use of PipeWire, even indirectly, means that Shairport Sync can not be installed as a system service (aka a system daemon). The user must log in through the Fedora GUI for the audio received by Shairport Sync to be routed to the system audio output, e.g. the onboard speakers. - -Overall, you'll be building and installing two programs. The first one is NQPTP, a companion app that Shairport Sync uses for AirPlay 2 timing, and the second one is Shairport Sync itself. Build and install NQPTP first. If you are building classic Shairport Sync, NQPTP is unnecessary and can be omitted. - -In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `yum update` in superuser mode and you are in user mode, enter `sudo yum update`. - -## Turn Off WiFi Power Management -If you are using WiFi, you should turn off WiFi Power Management, e.g. if the WiFi device name is `wlp5s0b1`: - -``` -# iw dev wlp5s0b1 set power_save off -``` -The reason for this is that WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) - -## Enable RPM Fusion Software Repositories (AirPlay 2 Only) -For AirPlay 2, it is important to [enable](https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion) the RPM Fusion software repositories, at least to the "Free" level. This is so that [ffmpeg](https://ffmpeg.org) libraries will be installed that include a suitable AAC decoder. - -## Update Everything -``` -# yum update -``` -## Remove Old Stuff -### Remove Old Copies of Shairport Sync -Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: -``` -$ which shairport-sync -/usr/local/bin/shairport-sync -``` -Remove it as follows: -``` -# rm /usr/local/bin/shairport-sync -``` -Do this until no more copies of `shairport-sync` are found. - -### Remove Old Service Files -You should also remove any of the following service files that may be present: `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf`, `/etc/dbus-1/system.d/shairport-sync-mpris.conf`, and `/etc/init.d/shairport-sync`. New ones will be installed if necessary. - -### Reboot after Cleaning Up -If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. - - -## Install Toolchain and Libraries -Okay, now let's get the tools and libraries for building and installing Shairport Sync (and NQPTP). - -``` -# yum install make automake gcc gcc-c++ -# yum install autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel -# yum install alsa-lib-devel # for the ALSA back end -- used in this build -``` -(To build the PipeWire backend, install the `pipewire-devel` library; for the PulseAudio backend, you'll need the `pulseaudio-libs-devel` library.) - -For AirPlay 2 operation, extra libraries must be installed. Before taking this step, once again please ensure the you have [enabled](https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion) RPM Fusion software repositories at least to the "Free" level. If this is not done, ffmpeg libraries will be installed that lack a suitable AAC decoder, preventing Shairport Sync from working. - -Install the extra libraries with the following command: -``` -# yum install ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-devel libuuid-devel vim-common -``` - -## Build -### NQPTP -Skip this section if you are building Classic Shairport Sync – NQPTP is not used by Classic Shairport Sync. - -Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp). By the way, Fedora has a firewall running by default, so make sure you enable UDP traffic to and from ports 319 and 320, as noted in the NQPTP guide. - -### Shairport Sync - -#### Please use `git` to clone the repository! -As you probably know, you can download the repository in two ways: (1) using `git` to clone it – recommended – or (2) downloading the repository as a ZIP archive. Please use the `git` method. The reason it that when you use `git`, the build process can incorporate the `git` build information in the version string you get when you execute the command `$ shairport-sync -V`. This will be very useful for identifying the exact build if you are making comments or bug reports. Here is an example: -``` -Version with git information: -4.1-dev-389-gf317161a-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc - -Version without git information: -4.1-dev-AirPlay2-OpenSSL-Avahi-ALSA-soxr-sysconfdir:/etc -``` - -#### Build and Install -Download Shairport Sync, check out the `development` branch and configure, compile and install it. - -* Omit `--with-airplay-2` from the `./configure` options to build Classic Shairport Sync. - -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -$ cd shairport-sync -$ git checkout development -$ autoreconf -fi -$ ./configure --sysconfdir=/etc --with-alsa \ - --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 -``` -If you get errors during the `./configure` step, check again that you have installed the toolchain and libraries correctly. -``` -$ make -j -# make install -``` -By the way, the `autoreconf` step may take quite a while – please be patient! - -## Configuration -By default when you start Shairport Sync, it will play to the default output device, which is just what is needed here – the default ALSA device routes audio into Fedora's PipeWire infrastructure. You can configure many Shairport Sync settings in the configuration file, installed during the `# make install` step at `/etc/shairport-sync.conf` along with a sample at `/etc/shairport-sync.conf.sample`. - -### Automatic Start - -To run automatically after user login, Shairport Sync can be set up as a user service. - -Placeholder: please refer to the discussion in [Issue #1260](https://github.com/mikebrady/shairport-sync/issues/1260). - -### Running From the Command Line - -You may wish to run Shairport Sync from the command line. To enable debug messages and statistics, use the following: - -``` -$ shairport-sync -v --statistics -``` - -### Adding to Home - -If you have built it for AirPlay 2, you can follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your Shairport Sync device to the Apple Home system. - -## Update Your Mac! - -Many AirPlay 2 bugs have been fixed in recent versions of macOS, so it is strongly recommended that you update your Mac. - -## Using Shairport Sync - -The Shairport Sync AirPlay service should appear on the network with a service name made from the machine's hostname with the first letter capitalised, e.g. hostname `fedora` gives a service name `Fedora`. You can change the service name in the configuration file. - -Connect and enjoy... +For the present, this guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). diff --git a/FREEBSD.md b/FREEBSD.md index c4447d84..8a95a4ab 100644 --- a/FREEBSD.md +++ b/FREEBSD.md @@ -1,113 +1,3 @@ -Shairport Sync on FreeBSD ----- -This guide, for a recent FreeBSD system, is to build Shairport Sync to output to the [`sndio`](https://sndio.org) and/or the [ALSA](https://www.alsa-project.org/wiki/Main_Page) sound systems. +# Shairport Sync on FreeBSD -The `sndio` back end is based on the work of [Tobias Kortkamp](https://github.com/t6). - -ALSA is not "native" to FreeBSD, but it has been ported to some architectures. - -Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "Classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. - -Overall, you'll be building and installing two programs. The first one is NQPTP, a companion app that Shairport Sync uses for AirPlay 2 timing, and the second one is Shairport Sync itself. Build and install NQPTP first. If you are building Classic Shairport Sync, NQPTP is unnecessary and can be omitted. - -In the commands below, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `freebsd-update fetch` in superuser mode and you are in user mode, enter `sudo freebsd-update fetch`. - -General ----- -This build was done on a default build of `freebsd 12.3-RELEASE-p5`. - -First, update everything: -``` -# freebsd-update fetch -# freebsd-update install -``` -Next, install the `pkg` package manager and update its lists: - -``` -# pkg -# pkg update -``` - -Subsystems ----- -Install the Avahi subsystem. FYI, `avahi-app` is chosen because it doesn’t require X11. `nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this. - -``` -# pkg install avahi-app nss_mdns -``` -Add these lines to `/etc/rc.conf`: -``` -dbus_enable="YES" -avahi_daemon_enable="YES" -``` -Next, change the `hosts:` line in `/etc/nsswitch.conf` to -``` -hosts: files dns mdns -``` -Reboot for these changes to take effect. - -Building ----- - -Install the packages that are needed for Shairport Sync to be downloaded and built: -``` -# pkg install git autotools pkgconf popt libconfig openssl sndio -``` -* Add `alsa-utils` if you wish to use ALSA. -* Omit `sndio` if you don't intend to use the `sndio` subsystem. -* Add `libplist libsodium ffmpeg e2fsprogs-libuuid vim` if you are building for AirPlay 2. - -At this point, if you intend to build Shairport Sync for AirPlay 2, you should build and install `NQPTP`, a companion application that provides AirPlay 2 timing support for Shairport Sync. You'll find the software itself and an installation guide [here](https://github.com/mikebrady/nqptp/blob/main/README.md). Once you have built, installed and enabled NQPTP, you can proceed with building Shairport Sync. - -Now, download Shairport Sync from GitHub: -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -$ cd shairport-sync -``` -If you are building the AirPlay 2 version of Shairport Sync, for the present you should checkout the `development` version: -``` -$ git checkout development -``` -Next, configure the build and compile the `shairport-sync` application: -``` -$ autoreconf -i -f -$ ./configure --with-avahi --with-ssl=openssl --with-sndio --with-os=freebsd --with-freebsd-service -$ make -``` -* Add `--with-airplay-2` if you wish to build the AirPlay 2 version of Shairport Sync. -* Add `--with-alsa` if you wish to include the ALSA back end. -* Omit the `--with-sndio` if you don't want the `sndio` back end. -* Omit the `--with-freebsd-service` if you don't want to install a FreeBSD startup script, runtime folder and user and group -- see below for more details. - -Installation ----- - -Enter the superuser mode and do a `make install`: - -``` -$ su -# make install -``` - -With the `./configure` options shown above, this will install the `shairport-sync` program along with a sample configuration file at `/usr/local/etc/shairport-sync.conf`. A service startup script will also be installed to launch Shairport Sync as a daemon. In addition, a `shairport-sync` user and group will be added to enable `shairport-sync` to run with a low level of privilege (a primitive form of extra security). - -Finally, edit `/usr/local/etc/shairport-sync.conf` to customise your installation, e.g. service name, etc. To make the `shairport-sync` daemon load at startup, add the following line to `/etc/rc.conf`: - -``` -shairport_sync_enable="YES" -``` -You can launch the service as superuser, or simply reboot the machine. - -Using the `sndio` backend ----- - -The `sndio` back end does not yet have a hardware volume control facility. You should set the volume to maximum before use, using, for example, the `mixer` command described below. - -Setting Overall Volume ----- -The `mixer` command can be used for setting the output device's volume settings. You may have to experiment to figure out which settings are appropriate. - -``` -$ mixer vol 100 # sets overall volume -``` -If you've installed `alsa-utils`, then `alsamixer` and friends will also be available. +This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). diff --git a/MOREINFO.md b/MOREINFO.md index 3ae6af7e..147d195d 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -1,332 +1,3 @@ -More Information ----------- -Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). - -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. - -For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. - -For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. - -Synchronisation, Latency, "Stuffing" ---------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. - -As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. - -Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. - -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using either the PTP or NTP synchronisation protocols. - -What else? --------------- -* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. -* Hardware Mute — Shairport Sync can mute properly if the hardware supports it. -* Support for the Apple ALAC decoder. -* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. -* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. -* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`. -* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. -* Compiles on Linux and FreeBSD. -* Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io). -* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. -* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. -* A native D-Bus interface, including access to metadata and artwork, some limited remote control and some system settings. - -Heritage -------- -Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. - - -Configuring Shairport Sync --------- -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end, etc. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. - -For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. - -Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card. - -Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` asd give the service the password `secret`, then you should do the following: - -``` -general = -{ - name = "Front Room"; - password = "secret"; - // ... other general settings -}; -``` -(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) - -**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player -- the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable. - -No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: - -``` - output_backend = "pa"; -``` -to the `general` group. - -The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. - -The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. -* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. -* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: - -``` -alsa = -{ - output_device = "hw:0,5"; - mixer_device = "hw:0"; - mixer_control_name = "PCM"; - // ... other alsa settings -}; -``` - -The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. - -Shairport Sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the `sessioncontrol` group settings `run_this_before_play_begins` and `run_this_after_play_ends`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. - -Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) - -Shairport Sync can run a program whenever the volume is set or changed. You specify it using the `general` group setting `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. - -Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. - -**Raspberry Pi** - -Many Raspberry Pi Models have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. - -To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: -``` -alsa = -{ - output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. - mixer_control_name = "Headphone"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. - // ... other alsa settings -``` -(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. - -A problem with the built-in DAC that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 60 dB of it is in any way usable. To help get the most from it, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 60, the top 60 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -56dB, below which muting will occur. - -From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. - -Examples --------- - -Here are some examples of complete configuration files. - -``` -general = { - name = "Joe's Stereo"; -}; - -alsa = { - output_device = "hw:0"; -}; -``` - -This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used. - -For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. - -Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: -``` -general = { - name = "Mike's Boombox"; -}; - -alsa = { - output_device = "hw:0"; - mixer_control_name = "Headphone"; -}; -``` - -Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`: -``` -general = { - name = "Kitchen"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: -``` -general = { - name = "Front Room"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "Speaker"; -}; -``` - - -Finally, here is an example of using the PulseAudio backend: -``` -general = { - name = "Zoe's Computer"; - output_backend = "pa"; -}; - -``` - - -Metadata --------- - -Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. - -**Metadata broadcasting over UDP** - -As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. - -The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. - -The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. - -Latency -------- -Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. - -Resynchronisation -------------- -Shairport Sync actively maintains synchronisation with the source. -If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. -* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. - -Tolerance ---------- -Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. -* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. - -Statistics ---------------- -If you set the `statistics` setting in the `diagnostics` section of the configuration file to `"YES"`, some statistics will be logged at regular intervals. The items logged will depend on the type of stream being processed: AirPlay 1, AirPlay 2 Buffered Audio or AirPlay 2 Realtime Audio. If the `log_verbosity` is set to 1, 2 or 3, additional items will be logged. - -From an audio enthusiast's point of view, the most important figure is possibly the `all sync ppm` figure. This is the total amount of interpolation needed by Shairport Sync to keep the audio stream in sync. The number represents is the ratio of frames added and removed from the audio stream relative to all the frames output in the last interval, expressed in parts per million (ppm). For reference, adding or removing one frame per second into a 44,100 frames per second stream is ± 22.68 ppm. The lower this number number is, the higher the fidelity of the audio signal passed to the output device. On a well sorted system, this figure can be 0.0 for considerable periods, but it can't really be zero forever. You may also find that the number might be higher at the start while the system settles down. - -The second most important figure is possibly the `sync error ms`. This is the average synchronisation error in milliseconds in the last interval. Ideally it should be 0.0. By default, Shairport Sync has a tolerance of a sync error of ± 2.0 milliseconds without triggering interpolation. - -Two other interesting measurements of the output rate may be available -- `output fps (r)` and `output fps (c)`, where `(r)` means "raw" and the `(c)` means "corrected". - -The "raw" figure is the rate at which the output device (typically a DAC) accepts data measured relative to the computer's own system clock (specifically the `CLOCK_MONOTONIC_RAW` clock). The accuracy of the number depends on the accuracy of the clock, which will typically be accurate to within anything from 20 to 100 ppm. - -The "corrected" figure is the rate at which the output device accepts data relative to the computer's network-time-disciplined clock (specifically the `CLOCK_MONOTONIC` clock). This clock is normally adjusted ("disciplined") to keep time with network time and should be accurate to with a few tens of milliseconds over a long period. So (1) if you could run a play session for a long period -- say a day -- and (2) if network time synchronisation is enabled and (3) if the network connection to the network time service is fast and stable, then you should get an accurate absolute measure of exact frame rate of the output device. If your internet connection is not good, the corrected figure will be very inaccurate indeed. - -Here is a brief description of the figures that might be provided. -##### sync error ms -Average playback synchronisation error in milliseconds in the last interval. By default, Shairport Sync will allow a sync error of ± 2.0 milliseconds without any interpolation. Positive means late, negative means early. -##### net sync ppm -This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the total number of frames added and removed from the audio stream, expressed in parts per million (ppm) in the last interval. For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm. -##### all sync ppm -This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync. The number represents is the number of frames added plus the number removed from the audio stream, expressed in parts per million (ppm) in the last interval. The magnitude of this should be the same as the `net sync ppm'. If it is much larger it means that Shairport Sync is overcorrecting for sync errors -- try increasing the drift tolerance to reduce it. -##### packets -This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames. -##### missing -This is the number of packets of audio frames that were not received in the last interval. It should be zero, and if not it can indicate a problem with the network. AirPlay 1 and AirPlay 2 Realtime Streams only. -##### late -This is the number of packets of audio frames that were received late -- but still in time to be used -- in the last interval. AirPlay 1 and AirPlay 2 Realtime Streams only. -##### too late -This is the number of packets of audio frames that were received too late to be used in the last interval. It is possible that these packets may already have been received, so those frames might not actually be missing when the time comes to play them. AirPlay 1 and AirPlay 2 Realtime Streams only. -##### resend reqs -This is the number of times Shairport Sync requests the resending of missing frames. Requests can be for one or more frames. AirPlay 1 and AirPlay 2 Realtime Streams only. -##### min DAC queue -The is the smallest number of frames of audio in the DAC's hardware queue. If it goes too low, the DAC may begin to underrun. -##### min buffers -The is the smallest number of packets of audio in the queue to be processed in the last interval. It is related to the overall latency in AirPlay 1 and AirPlay 2 Realtime Streams. If it comes close to zero it's often a sign that the network is poor. -##### max buffers -The is the largest number of packets of audio in the queue to be processed in the last interval. -##### min buffer size -The is smallest remaining number of bytes in the Buffered Audio buffer in the last interval. It can legitimately be zero when a track ends or begins. If it reaches zero while a track is playing, it means that audio data is not arriving at Shairport Sync quickly enough and may indicate network problems. AirPlay 2 Buffered Audio streams only. -##### nominal fps -This is the rate specified in the AirPlay stream itself. AirPlay 1 only. -##### received fps -This is the rate at which frames are received from the network averaged since the start of the play session. AirPlay 1 only. -##### output fps (r) -Output rate measured relative to the computer system's clock since the start of the play session. See above for a discussion. -##### output fps (c) -Output rate measured relative to the network-clock-disciplined computer system's clock since the start of the play session. See above for a discussion. -##### source drift ppm -This is a measure of the difference between the source clock and Shairport Sync's clock expressed in parts per million. Only valid when 10 or more drift samples have been received. AirPlay 1 only. -##### drift samples -This is the number drift samples have been accepted for calculating the source drift. AirPlay 1 only. -#### Example -The following example is of an AirPlay 2 Buffered Audio Stream from a HomePod mini to a WiFi-connected Raspberry Pi 3 equipped with a Pimoroni "Audio DAC SHIM (Line-Out)" with `log_verbosity` set to `1`. - -``` - sync error ms net sync ppm all sync ppm packets min DAC queue min buffers max buffers output fps (r) output fps (c) - -1.88 8.5 8.5 526575 8455 62 63 44099.98 44099.92 - -1.92 31.2 31.2 527578 8460 62 63 44099.98 44099.92 - -1.87 2.8 2.8 528581 8467 62 63 44099.98 44099.93 - -1.98 22.7 22.7 529584 8466 62 63 44099.98 44099.93 - -2.01 73.6 73.6 530587 8468 62 63 44099.98 44099.93 - -1.99 93.5 93.5 531590 8463 62 62 44099.98 44099.94 - -1.77 5.7 5.7 532593 8466 62 63 44099.98 44099.94 - -1.84 2.8 2.8 533596 8456 62 62 44099.98 44099.95 - -1.54 0.0 0.0 534599 8477 62 63 44099.98 44099.95 - -1.19 0.0 0.0 535602 8501 62 62 44099.98 44099.95 - -1.03 0.0 0.0 536605 8505 62 62 44099.98 44099.96 - -1.10 0.0 0.0 537608 8497 62 63 44099.98 44099.96 - -1.39 0.0 0.0 538611 8485 62 63 44099.98 44099.97 - -1.43 0.0 0.0 539614 8485 62 63 44099.98 44099.97 - -1.75 0.0 0.0 540617 8481 62 62 44099.98 44099.97 - -1.52 0.0 0.0 541620 8474 62 63 44099.98 44099.98 - -1.37 0.0 0.0 542623 8488 62 62 44099.98 44099.98 - -1.63 0.0 0.0 543626 8481 62 62 44099.98 44099.98 - -2.01 102.0 102.0 544629 8465 62 62 44099.98 44099.99 - -1.88 14.2 14.2 545632 8474 62 62 44099.98 44099.99 - -1.42 0.0 0.0 546635 8486 62 63 44099.98 44099.99 - -1.92 17.0 17.0 547638 8435 62 63 44099.98 44100.00 - -1.27 0.0 0.0 548641 8479 62 62 44099.98 44100.00 - -0.64 0.0 0.0 549644 8528 62 62 44099.98 44100.00 - -1.12 0.0 0.0 550647 8476 62 62 44099.98 44100.01 - -1.88 25.5 25.5 551650 8471 62 62 44099.98 44100.01 - -1.79 0.0 0.0 552653 8477 62 62 44099.98 44100.01 - -1.79 0.0 0.0 553656 8477 62 63 44099.98 44100.02 - -1.81 19.8 19.8 554659 8467 62 62 44099.98 44100.02 - -2.04 110.5 110.5 555662 8468 62 63 44099.98 44100.02 - -1.64 2.8 2.8 556665 8476 62 62 44099.98 44100.02 - -1.29 0.0 0.0 557668 8496 62 63 44099.98 44100.03 - -1.20 0.0 0.0 558671 8500 62 63 44099.98 44100.03 - -1.33 0.0 0.0 559674 8483 62 63 44099.98 44100.03 - -1.43 0.0 0.0 560677 8468 62 63 44099.98 44100.03 - -1.47 0.0 0.0 561680 8484 62 62 44099.98 44100.04 - -0.88 0.0 0.0 562683 8512 62 62 44099.98 44100.04 - -1.16 0.0 0.0 563686 8500 62 63 44099.98 44100.04 - -1.42 0.0 0.0 564689 8488 62 62 44099.98 44100.05 - -1.58 0.0 0.0 565692 8476 62 63 44099.98 44100.05 - -1.50 0.0 0.0 566695 8488 62 63 44099.98 44100.05 - -1.79 0.0 0.0 567698 8471 62 63 44099.98 44100.05 - -1.82 8.5 8.5 568701 8473 62 62 44099.98 44100.06 - -1.87 19.8 19.8 569704 8468 62 63 44099.98 44100.06 - -1.96 85.0 85.0 570707 8456 62 63 44099.98 44100.06 - -1.80 2.8 2.8 571710 8452 62 63 44099.98 44100.06 - -1.66 0.0 0.0 572713 8477 62 62 44099.98 44100.07 -``` -For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. - -WiFi Issues ---------- -If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for more details. - -Troubleshooting ---------------- -Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for a few hints, contributed by users. - -MQTT ---------------- -Please refer to the [MQTT INFO](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md) page for additional info on building, configuring and using MQTT to interface shairport-sync with common home automation systems (contributed by users). - -Packaging status ----------------- - -[![Packaging status](https://repology.org/badge/vertical-allrepos/shairport-sync.svg)](https://repology.org/project/shairport-sync/versions) +# More Information +Information in this document has been updated and moved to [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/README.md). diff --git a/README.md b/README.md index a6962795..698bec98 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,45 @@ +# Shairport Sync +Shairport Sync is an [AirPlay](https://www.pocket-lint.com/speakers/news/apple/144646-apple-airplay-2-vs-airplay-what-s-the-difference) audio player for Linux and FreeBSD. It plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). -Shairport Sync -============= -* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). -* Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much more demanding. It requires a faster system, more storage and more RAM both at build time and run time. -* Shairport Sync does not support AirPlay video or photo streaming. +Shairport Sync can be built as an AirPlay 2 player (with [some limitations](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. -AirPlay 2 ---- -For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md). +Metadata such as artist information and cover art can be requested and provided to other applications. Shairport Sync can interface with other applications through MQTT, an MPRIS-like interface and D-Bus. -Guides ---- -* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP1.md). -* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide to building for AirPlay 1 or AirPlay 2 on FreeBSD is available at [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/development/FREEBSD.md). -* Please note, a Docker image including Airplay 2 support is available under the docker folder of this repo. +Shairport Sync does not support AirPlay video or photo streaming. -More Information ---- -For more information, please visit [MOREINFO.md](https://github.com/mikebrady/shairport-sync/blob/development/MOREINFO.md). +# Quick Start +* A building guide is available [here](https://github.com/mikebrady/spsdoc/blob/main/BUILD.md). +* A Docker image is available on the Docker Hub in [this repository](https://hub.docker.com/repository/docker/mikebrady/shairport-sync). +* Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md). +* Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). +* Runtime settings are documented [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). + +# Features +* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has experimental support for [PipeWire](https://pipewire.org) and limited support for [libao](https://xiph.org/ao/) and for [libsoundio](http://libsound.io). +* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. +* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), a popular protocol for Inter Process Communication, Machine-to-Machine, Internet of Things and Home Automation projects. The interface including access to metadata and artwork, and limited remote control. +* Digital Signal Processing facilities – please see the [DSP Wiki Page Guide](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). (Thanks to [Yann Pomarède](https://github.com/yannpom) for the code and to [Paul Wieland](https://github.com/PaulWieland) for the guide.) +* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/)-like interface, partially complete and very functional, including access to metadata and artwork, and limited remote control. +* A native D-Bus interface, including access to metadata and artwork, limited remote control and system settings. +* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. +* Support for the [Apple ALAC decoder](https://macosforge.github.io/alac/) (library available [here](https://github.com/mikebrady/alac)). +* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. +* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. + +Some features require configuration at build time – see [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). + +# Status +Shairport Sync was designed to [run best](ADVANCED%20TOPICS/GetTheBest.md) on stable, dedicated, stand-alone low-power "headless" systems with ALSA as the audio system and with a decent CD-quality Digital to Analog Converter (DAC). + +Shairport Sync runs on recent (2018 onwards) Linux systems and FreeBSD from 12.1 onwards. It requires a system with the power of a Raspberry Pi 2 or a Pi Zero 2 or better. + +Classic Shairport Sync runs on a wider variety of Linux sytems, including OpenWrt and Cygwin and it also runs on OpenBSD. Many embedded devices are powerful enough to power classic Shairport Sync. + +# Heritage and Acknowledgements +The functionality offered by Shairport Sync is the result of study and analysis of the AirPlay and AirPlay 2 protocols by many people over the years. These protocols have not been officially published, and there is no assurance that Shairport Sync will continue to work in future. + +Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Wah (aka [abrasive](https://github.com/abrasive)), James Laird and others — please see [this list](https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x) of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. -Acknowledgements ---- For the development of AirPlay 2 support, special thanks are due to: * [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement. * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). @@ -29,4 +47,25 @@ For the development of AirPlay 2 support, special thanks are due to: * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. * [Charles Omer](https://github.com/charlesomer) for Docker automation, repository management automation, testing, encouragement, enthusiasm. -And of course, thanks to everyone who has supported and improved Shairport Sync over the years. +Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. It is a pleasure to acknowledge the work of the contributors there. + +Thanks to everyone who has supported and improved Shairport Sync over the years. + +# More about Shairport Sync +The audio that Shairport Sync receives is sent to the computer's sound system, to a named unix pipe or to `STDOUT`. By far the best sound system to use is ALSA. This is because ALSA can give direct access to the Digital to Analog Converter (DAC) hardware of the machine. Audio samples can be sent through ALSA directly to the DAC, maximising fidelity, and accurate timing information can be obtained from the DAC, maximising synchronisation. Direct access to hardware is given through ALSA devices with names beginning with `hw:`. + +## Synchronised Audio +Shairport Sync offers *full audio synchronisation*. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as ALSA on Linux and `sndio` on FreeBSD – that provide very accurate timing information about audio being streamed to output devices. Ideally, Shairport Sync should have direct access to the output device used, which should be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. Using the ALSA sound system, Shairport Sync will choose the greatest bit depth available at 44,100 samples per second, resorting to multiples of 44,100 if it is not available. You'll get a message in the log if there's a problem. With all other sound systems, a sample rate of 44,100 is chosen with a bit depth of 16 bit. + +Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of ALSA or `sndio`, it is often impractical to remove or disable PulseAudio. + +For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to `STDOUT`, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. + +## Latency, "Stuffing", Timing +AirPlay protocols use an agreed *latency* – a time lag or delay – between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). Latency gives players time to compensate for network delays, processing time variations and so on. The latency is specified by the audio source when it negotiates with Shairport Sync. AirPlay sources set a latency of around 2.0 to 2.25 seconds. AirPlay 2 can use shorter latencies, around half a second. + +As mentioned previously, Shairport Sync implements full audio synchronisation when used with ALSA, `sndio` or PulseAudio audio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information coming back from the audio system itself. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert ("stuff") extra frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. If your computer is fast enough, Shairport Sync will, by default, automatically choose this method. + +Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. + +Timestamps are referenced relative to the source computer's clock – the "source clock", but timing must be done relative to the clock of the computer running Shairport Sync – the "local clock". So, Shairport Sync synchronises the source clock and the local clock, usually to within a fraction of a millisecond. In AirPlay 2, this is done with the assistance of a companion application called [NQPTP](https://github.com/mikebrady/nqptp) using a [PTP](https://en.wikipedia.org/wiki/Precision_Time_Protocol)-based timing protocol. In classic AirPlay, a variant of [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) synchronisation protocols is used. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index c85d893c..791629da 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -6,7 +6,14 @@ In this brief document will be listed some problems and some solutions, some pro 1. Before starting, ensure that your software is up-to-date. This document always refers to the most recent version of Shairport Sync -- see [here](https://github.com/mikebrady/shairport-sync/releases) for information about the most recent release. 2. If you have set `interpolation` in the `general` section of the configuration file to to `soxr`, comment it out or set it to `auto` or `basic` as the `soxr` setting can cause lower-powered devices to bog down at critical times, e.g. see [this report](https://github.com/mikebrady/shairport-sync/issues/631#issuecomment-366305203). -3. Ensure the volume setting of your output device is set to some reasonable value and ensure it is not muted. For ALSA devices, the `alsamixer` command-line tool is very good for this. For other sound systems, please consult the relevant documentation. + +### No/Low Sound +Let's say you've just installed or updated Shairport Sync and you are testing it for the first time after installing or updating. +If you are using the default ALSA backend, don't forget to check two simple things: +1. Check that the volume on the output device is turned up. If the output device has a "mixer" i.e. a volume control, Shairport Sync does not, by default, try to control it. Therefore, if it happens to be very low or even at zero, you might not hear audio that is actually coming through to the device. (You can get Shairport Sync to control a mixer -- see [here](https://github.com/mikebrady/spsdoc/blob/main/ADVANCED%20TOPICS/InitialConfiguration.md) for some hints.) +2. Check that the output device is not muted. Some audio applications (including very old versions of Shairport Sync) leave the output device mixer in a muted state after use to minimise the possibility of noise. However, this is not generally compatible with other audio players using the same device, as they generally expect the device to be unmuted. + +You can use `alsamixer` for both of theses checks. A muted output has the letter(s) `M` as its value. Select it and type `M` again to unmute. ### WiFi adapter running in power-saving / low-power mode @@ -70,9 +77,13 @@ An iOS device that is being used as a WiFi hotspot can not play audio to another You can play from other devices but not from your Windows PC. -**Possible Solution** +**Possible Issue -- AirPlay 2** +iTunes on Windows is not compatible with Shairport Sync when it is built for AirPlay 2. This is unlikely to change, unfortunately. However, iTunes on Windows remains compatible with "classical" Shairport Sync. -Allow network discovery. This setting creates a private type network and enables Windows to access the ports and protocols necessary to use Shairport Sync. +**Possible Issue -- AirPlay** +The Windows Firewall is preventing access to Shairport Sync. + +Solution: Allow network discovery. This setting creates a private type network and enables Windows to access the ports and protocols necessary to use Shairport Sync. ### UFW firewall blocking ports (commonly includes Raspberry Pi) @@ -86,7 +97,7 @@ You have installed Shairport Sync successfully, the daemon is running, you can s `sudo ufw disable` -- Try to launch a song from your remote device on the Shairport-sync one, if this works, proceed to the next step and follow the ones described below, in the solution section. +- Try to launch a song from your remote device on the Shairport Sync one. If this works, proceed to the next step and follow the ones described below, in the solution section. - Enable UFW through the following command: diff --git a/UPDATING.md b/UPDATING.md index cbfdc635..d0ebae46 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,88 +1,3 @@ +### Updating Shairport Sync -### Updating Shairport Sync [Needs updating for Airplay 2] -This guide is for building and updating an installation of Shairport Sync. - -If you updated Shairport Sync from a package, most of these instructions don't apply – please go to the section at the end called "Post Update Tasks". If you wish to build an installation of Shairport Sync to replace an installation that came from a package, instructions don't apply either – instead, please follow the guide at [INSTALL.md](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md). - -To do an update, you basically have to go through the whole process of building Shairport Sync again, -but a few steps are shorter because you've done them before; you won't have to reinstall the build tools or libraries needed, and you won't have to define the user and group or reconfigure the settings in the configuration file. - -But before you begin, you should update and upgrade any packages. - -Note that in this guide, a `$` prefix means you should be in the normal user mode; a `#` prefix means you should be in the superuser or root mode. - -Here is the sequence for Raspberry Pi OS (Buster), which is based on Debian Buster. The same commands work for Ubuntu, and maybe more. Here, a non-`root` user with `sudo` privileges is assumed. - -``` -$ sudo apt-get update -$ sudo apt-get upgrade -``` -Next, stop playing music to your device. - -### Remove Old Copies -It's a good idea to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen: -``` -$ which shairport-sync -/usr/local/bin/shairport-sync -``` -Remove it as follows: -``` -# rm /usr/local/bin/shairport-sync -``` -Do this until no more copies of `shairport-sync` are found. This is especially important if you are building Shairport Sync over an old version that was installed from packages, as the built application will be installed in a different directory to the packaged installation. - -### Remove Old Startup and Service Scripts -You should also remove the startup script and service definition files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service`, `/etc/init.d/shairport-sync`, `/etc/dbus-1/system.d/shairport-sync-dbus.conf` and `/etc/dbus-1/system.d/shairport-sync-mpris.conf` if they exist – new ones will be installed if necessary. As with the previous section, this is especially important if you are building Shairport Sync over an old version that was installed from packages, as the scripts for the built application may be different to those of the packaged version. If they are left in place, bad things can happen. - -### Reboot after Cleaning Up -If you removed any installations of Shairport Sync or any of its startup script files in the last two steps, you should reboot. - -### Building the Updated Shairport Sync -Now, to build, update and install Shairport Sync, if you still have the directory in which you previously built Shairport Sync, it will contain the repository you originally downloaded. Navigate your way to it and 'pull' the changes from GitHub: - -``` -$ git pull -``` -Otherwise – say if you deleted the repository – just pull Shairport Sync from GitHub again and move into the new directory: -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -$ cd shairport-sync -``` -(If you wish to use the `development` branch, you should enter the command `$ git checkout development` at this point. Everything else is the same.) - -Now, while in the `shairport-sync` directory, perform the following commands (note that there is a choice you must make in there): -``` -$ autoreconf -fi -``` -Please review the release notes to see if any configuration settings have been changed. For instance, in the transitions from version 2 to version 3, the `--with-ssl=polarssl` has been deprecated in favour of `--with-ssl=mbedtls`. -``` -#The following is the standard configuration for a Linux that uses the alsa backend and systemd initialisation system: -$ ./configure --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd --sysconfdir=/etc -#OR -#The following is the standard configuration for a Linux that uses the alsa backend and the older System V initialisation system: -$ ./configure --with-libdaemon --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemv --sysconfdir=/etc - -$ make -$ sudo make install -``` -At this point you have downloaded, compiled and installed the updated Shairport Sync. However, the older version is still running. So, you need to do a little more: - -If you are on a `systemd`-based system such as Raspbian Jessie or recent versions of Ubuntu and Debian, execute the following commands: -``` -$ sudo systemctl daemon-reload -$ sudo systemctl restart shairport-sync -``` -Otherwise execute the following command: -``` -$ sudo service shairport-sync restart -``` - -Your Shairport Sync should be upgraded now. - -### Post Update Tasks -1 **Unmute the Output Device Mixer** (This applies only if you are using a hardware mixer for volume control.) Certain older versions of Shairport Sync could leave the output device mixer in a muted state after use to minimise the possibility of noise. However, this is not generally compatible with other audio players using the same device, as they would generally expect the device to be unmuted. Recent versions of Shairport Sync therefore do not use the mute facility by default. When you update Shairport Sync, the output device mixer might have been muted by the previous version, so you should unmute it, using, for instance, the following command: -``` -$ sudo amixer sset unmute -``` -Alternatively you can use `alsamixer`. A muted output has the letter(s) `M` as its value. Select it and type `M` again to unmute. - +This guide has been superseded by the general building guide at [BUILD.md](https://github.com/mikebrady/shairport-sync/blob/development/BUILD.md). From acfe8134f521032b2588d5d19aa9a00d5fe27b94 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:50:55 +0100 Subject: [PATCH 788/878] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 698bec98..24f0eb49 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Metadata such as artist information and cover art can be requested and provided Shairport Sync does not support AirPlay video or photo streaming. # Quick Start -* A building guide is available [here](https://github.com/mikebrady/spsdoc/blob/main/BUILD.md). +* A building guide is available [here](BUILD.md). * A Docker image is available on the Docker Hub in [this repository](https://hub.docker.com/repository/docker/mikebrady/shairport-sync). * Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md). * Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). From ae2d22af1b1c6a21b7de20d376e47d102e5f440e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:52:39 +0100 Subject: [PATCH 789/878] Update BUILD.md --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 662a4c50..9525ee27 100644 --- a/BUILD.md +++ b/BUILD.md @@ -192,4 +192,4 @@ If a firewall is running (some systems, e.g. Fedora, run a firewall by default), ### Add to Home With AirPlay 2, you can follow the steps in [ADDINGTOHOME.md](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to add your device to the Apple Home system. ### Wait, there's more... -Shairport Sync has many extra [features](https://github.com/mikebrady/spsdoc/blob/main/README.md#features) and [settings](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). For instance, you can [add an MQTT service](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md), change the AirPlay service name, adjust for delays in other equipment (many TVs and AVRs introduce slight delays in their audio processing chains) and more. Explore runtime settings in `/etc/shairport-sync.conf` or [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). Build configuration flags are [here](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). +Shairport Sync has many extra [features](README.md#features) and [settings](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). For instance, you can [add an MQTT service](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md), change the AirPlay service name, adjust for delays in other equipment (many TVs and AVRs introduce slight delays in their audio processing chains) and more. Explore runtime settings in `/etc/shairport-sync.conf` or [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). Build configuration flags are [here](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). From e4e9497e2f9d4014773c79e98a6b6b63800752e2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:57:15 +0100 Subject: [PATCH 790/878] Update README.md --- ADVANCED TOPICS/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index 31f6995c..c681e1ff 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -1,17 +1,17 @@ # Advanced Topics -Here you will find links to some of the more advanced features and things you can do with Shairport Sync. +Here you will find links to some advanced features and things you can do with Shairport Sync. * [Finish setting up](InitialConfiguration.md). -* [Get the best](GetTheBest.md) from Shairport Sync. +* [Get the best](GetTheBest.md) from your system. * [Metadata](Metadata.md). * [Events](Events.md). -* [Update Shairport Sync](https://github.com/mikebrady/shairport-sync/blob/development/UPDATING.md) in place. -* The configuration file -- which contains lots of documentation -- on your system. By default, the sample configuration file is -placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD). -You can also view it online [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf). * [Statistics](Statistics.md). -* Setting up an [MQTT](https://github.com/mikebrady/shairport-sync/blob/development/MQTT.md) system. -* Jack Audio. +* Setting up an [MQTT](MQTT.md) system. * [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). -* [Build Configuration Flags](https://github.com/mikebrady/shairport-sync/blob/development/CONFIGURATION%20FLAGS.md). -* [Car Installation](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) +* [Car Installation](CAR%20INSTALL.md) – build an isolated WiFi network containing a Shairport Sync player on a Raspberry Pi. Suitable for a car radio with an `aux` input or for the stereo in that broadband-free holiday cottage. + +The configuration file -- which contains lots of documentation -- is on your system. By default, the sample configuration file is +placed at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` on FreeBSD). +You can also view an online version [here](https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf). + +Build configuration flags are discussed [here](CONFIGURATION%20FLAGS.md). From b7a11cae21a5b25a9df49141489fa3e03c7d53a4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:59:55 +0100 Subject: [PATCH 791/878] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24f0eb49..01e05666 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Shairport Sync does not support AirPlay video or photo streaming. # Quick Start * A building guide is available [here](BUILD.md). -* A Docker image is available on the Docker Hub in [this repository](https://hub.docker.com/repository/docker/mikebrady/shairport-sync). +* A Docker image is available on the [Docker Hub](https://hub.docker.com/repository/docker/mikebrady/shairport-sync). * Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md). +* Runtime settings are documented [here](scripts/shairport-sync.conf). * Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). -* Runtime settings are documented [here](https://github.com/mikebrady/shairport-sync/blob/development/scripts/shairport-sync.conf). # Features * Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has experimental support for [PipeWire](https://pipewire.org) and limited support for [libao](https://xiph.org/ao/) and for [libsoundio](http://libsound.io). From e390f96c584dce0a4285dda5b4f4a96d1c1b5978 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:17:01 +0100 Subject: [PATCH 792/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 4ede8dcd..e0a21b67 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version 4.1-dev-439-gb7a11cae +==== +Documentation update. Comments, corrections and suggestions welcome. + Version 4.1-dev-429-g449e024c ==== **Enhancement** From 40e28ba2c77d2369b87e5cdf7e1575a1ae71324c Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:11:36 -0700 Subject: [PATCH 793/878] Makefile.am: allow building from separate build directory --- Makefile.am | 7 ++++--- configure.ac | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile.am b/Makefile.am index e5674d2b..ae39af39 100644 --- a/Makefile.am +++ b/Makefile.am @@ -138,8 +138,9 @@ plist_xml_strings.h: plists/get_info_response.xml echo "// Do not edit!" > plist_xml_strings.h echo "// This file is automatically generated from files in the plists folder." >> plist_xml_strings.h echo "" >> plist_xml_strings.h - xxd -i plists/get_info_response.xml >> plist_xml_strings.h + xxd -i $< >> plist_xml_strings.h echo "" >> plist_xml_strings.h + sed -e 's/[^ ]*plists_get_info_response_xml/plists_get_info_response_xml/g' -i plist_xml_strings.h plist_xml_strings.c: plist_xml_strings.h touch plist_xml_strings.c @@ -161,7 +162,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) dbus-interface.c: org.gnome.ShairportSync.xml - gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface org.gnome.ShairportSync.xml + gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $< dbus-interface.h: dbus-interface.c touch dbus-interface.h endif @@ -178,7 +179,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) mpris-interface.c: org.mpris.MediaPlayer2.xml - gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface org.mpris.MediaPlayer2.xml + gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $< mpris-interface.h: mpris-interface.c touch mpris-interface.h endif diff --git a/configure.ac b/configure.ac index 356d3d4c..3b55a109 100644 --- a/configure.ac +++ b/configure.ac @@ -9,10 +9,10 @@ AC_CONFIG_HEADERS([config.h]) AC_PROG_RANLIB AC_CHECK_PROGS([GIT], [git]) -if test -n "$GIT" && test -e ".git/index" ; then +if test -n "$GIT" && test -e "$srcdir/.git/index" ; then AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.]) fi -AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e ".git/index" ]) +AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e "$srcdir/.git/index" ]) # Derived from the Avahi configure.ac file # Specifying the OS type, defaulting to linux. From 931c7ed2ef8ab6ab868743e201b4fa4f3f22315d Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Tue, 5 Jul 2022 15:12:14 -0700 Subject: [PATCH 794/878] Makefile.am: allow installing from separate build directory --- Makefile.am | 143 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 41 deletions(-) diff --git a/Makefile.am b/Makefile.am index ae39af39..1e94358d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -200,59 +200,120 @@ shairport_sync_mpris_test_client_SOURCES = shairport-sync-mpris-test-client.c shairport_sync_mpris_test_client_LDADD = lib_mpris_interface.a endif -install-exec-hook: -if BUILD_FOR_LINUX -DBUS_POLICY_DIR=$(DESTDIR)/etc/dbus-1/system.d -else -DBUS_POLICY_DIR=$(DESTDIR)$(sysconfdir)/dbus-1/system.d -endif if INSTALL_CONFIG_FILES - [ -e $(DESTDIR)$(sysconfdir) ] || mkdir $(DESTDIR)$(sysconfdir) - cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample - [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf + +CONFIG_FILE_INSTALL_TARGET = config-file-install-local +$(CONFIG_FILE_INSTALL_TARGET): scripts/shairport-sync.conf + install -d $(DESTDIR)$(sysconfdir) + install -m 0644 $< $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample + [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp $< $(DESTDIR)$(sysconfdir)/shairport-sync.conf + +if BUILD_FOR_LINUX +DBUS_POLICY_DIR = /etc/dbus-1/system.d +else +DBUS_POLICY_DIR = $(sysconfdir)/dbus-1/system.d +endif + if USE_DBUS - [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) + if INSTALL_CYGWIN_SERVICE - cp scripts/shairport-sync-dbus-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf +DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy-cygwin.conf else - cp scripts/shairport-sync-dbus-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf -endif -endif +DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy.conf +endif # INSTALL_CYGWIN_SERVICE + +DBUS_POLICY_INSTALL_TARGET = dbus-policy-install-local + +$(DBUS_POLICY_INSTALL_TARGET): $(DBUS_POLICY_FILE) + install -d $(DESTDIR)$(DBUS_POLICY_DIR) + install -m 0644 $< $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-dbus.conf + +endif # USE_DBUS + if USE_MPRIS - [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR) + if INSTALL_CYGWIN_SERVICE - cp scripts/shairport-sync-mpris-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf +MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy-cygwin.conf else - cp scripts/shairport-sync-mpris-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf -endif -endif -endif +MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy.conf +endif # INSTALL_CYGWIN_SERVICE + +MPRIS_POLICY_INSTALL_TARGET = mpris-policy-install-local + +$(MPRIS_POLICY_INSTALL_TARGET): $(MPRIS_POLICY_FILE) + install -d $(DESTDIR)$(DBUS_POLICY_DIR) + install -m 0644 $< $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-mpris.conf + +endif # USE_MPRIS + +endif # INSTALL_CONFIG_FILES + +INSTALL_GROUP_TARGET = install-group-local + +$(INSTALL_GROUP_TARGET): + getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null + +INSTALL_USER_TARGET = install-user-local + +$(INSTALL_USER_TARGET): $(INSTALL_GROUP_TARGET) + getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null + if INSTALL_SYSTEMV - getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null - getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null - [ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d - [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/ -endif + +INSTALL_SYSTEMV_TARGET = install-systemv-local + +$(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET) + install -d $(DESTDIR)$(sysconfdir)/init.d + [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || install -m 0644 $< $(DESTDIR)$(sysconfdir)/init.d + +endif # INSTALL_SYSTEMV + if INSTALL_SYSTEMD - getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null - getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync >/dev/null - [ -e $(DESTDIR)$(systemdsystemunitdir) ] || mkdir -p $(DESTDIR)$(systemdsystemunitdir) + if USE_AVAHI - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service-avahi $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service +SYSTEMD_SERVICE = shairport-sync.service-avahi else - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir) -endif -endif +SYSTEMD_SERVICE = shairport-sync.service +endif # USE_AVAHI + +INSTALL_SYSTEMD_TARGET = install-systemd-local + +$(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET) + install -d $(DESTDIR)$(systemdsystemunitdir) + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $< $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service + +endif # INSTALL_SYSTEMD + if INSTALL_FREEBSD_SERVICE - # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." + +# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..." +install-freebsd-user-local: pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup -n shairport-sync -g 801 > /dev/null 2>&1 pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1 - [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync - chown shairport-sync:shairport-sync /var/run/shairport-sync - # Always update the startup script -- this is different to the Linux systemd situation, where the startup script is untouched if it exists. - cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync - chmod 555 /usr/local/etc/rc.d/shairport_sync -endif + +INSTALL_FREEBSD_TARGET = install-freebsd-local + +$(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-local + install -d -o shairport-sync -g shairport-sync $(DESTDIR)/var/run/shairport-sync + install -d $(DESTDIR)/usr/local/etc/rc.d/ + install -m 0555 $< $(DESTDIR)/usr/local/etc/rc.d/shairport_sync + +endif # INSTALL_FREEBSD_SERVICE + if INSTALL_CYGWIN_SERVICE - [ -e /usr/local/bin/shairport-sync-config ] || cp scripts/shairport-sync-config /usr/local/bin -endif + +INSTALL_CYGWIN_TARGET = install-cygwin-local + +$(INSTALL_CYGWIN_TARGET): scripts/shairport-sync-config + install -d $(DESTDIR)/usr/local/bin + [ -e $(DESTDIR)/usr/local/bin/shairport-sync-config ] || install -m 0755 $< $(DESTDIR)/usr/local/bin/ + +endif # INSTALL_CYGWIN_SERVICE + +install-exec-hook: $(CONFIG_FILE_INSTALL_TARGET) \ + $(DBUS_POLICY_INSTALL_TARGET) \ + $(MPRIS_POLICY_INSTALL_TARGET) \ + $(INSTALL_SYSTEMV_TARGET) \ + $(INSTALL_SYSTEMD_TARGET) \ + $(INSTALL_FREEBSD_TARGET) \ + $(INSTALL_CYGWIN_TARGET) From 6a2ba5fd0f45919426c012c6abbcfd25fe41ec93 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:08:54 +0100 Subject: [PATCH 795/878] Minor debug message update. --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 1b24e77f..e7a88688 100644 --- a/player.c +++ b/player.c @@ -1295,7 +1295,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (do_wait == 0) if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! if (notified_buffer_empty == 0) { - debug(3, "Buffers exhausted."); + debug(1, "Connection %d: Buffer Empty", conn->connection_number); notified_buffer_empty = 1; // reset_input_flow_metrics(conn); // don't do a full flush parameters reset conn->initial_reference_time = 0; From 7bc480b3fc82ae64bf6b7658c2f4b9cac9caa9be Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 3 Aug 2022 12:53:06 +0100 Subject: [PATCH 796/878] Ensure player's IP is first in the perer list. Will be duplicated but that's okay. --- rtsp.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rtsp.c b/rtsp.c index 076a20d7..767e2804 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2439,6 +2439,12 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp 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)); plist_t addresses_array = NULL; plist_from_memory(req->content, req->contentlength, &addresses_array); @@ -2802,6 +2808,13 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) 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)); + + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); if (timing_peer_info) { // first, get the incoming plist. From 1ca2487b1cd359abe9689b0de8eaf6e29af6a5d1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 3 Aug 2022 12:54:46 +0100 Subject: [PATCH 797/878] Reset first_packet_timestamp if buffer goes empty. Reset anchor information if a clock control signal doesn't arrive in time in a realtime play. --- player.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/player.c b/player.c index e7a88688..7b2ce500 100644 --- a/player.c +++ b/player.c @@ -408,7 +408,7 @@ void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occup void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn) { - + // if it's original format, it has a valid seqno and must be decoded // otherwise, it can take the next seqno and doesn't need decoding. @@ -1031,7 +1031,6 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the // player if (conn->first_packet_timestamp == 0) { // if this is the very first packet - // debug(1,"First frame seen with timestamp..."); conn->first_packet_timestamp = curframe->given_timestamp; // we will keep buffering until we are // supposed to start playing this @@ -1066,7 +1065,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; if (lt < 100000000) { - debug(1, + debug(2, "Connection %d: Short lead time for first frame %" PRId64 ": %f seconds. Flushing 0.5 seconds", conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); @@ -1295,11 +1294,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (do_wait == 0) if ((conn->ab_synced != 0) && (conn->ab_read == conn->ab_write)) { // the buffer is empty! if (notified_buffer_empty == 0) { - debug(1, "Connection %d: Buffer Empty", conn->connection_number); + debug(2, "Connection %d: Buffer Empty", conn->connection_number); notified_buffer_empty = 1; // reset_input_flow_metrics(conn); // don't do a full flush parameters reset conn->initial_reference_time = 0; conn->initial_reference_timestamp = 0; + conn->first_packet_timestamp = 0; // make sure the first packet isn't late } do_wait = 1; } @@ -2574,6 +2574,10 @@ void *player_thread_func(void *arg) { // interpreted as a signed number, should yield the difference _and_ the ordering. sync_error = should_be_frame - will_be_frame; // this is done in int64_t form + + // int64_t t_ping = should_be_frame - conn->anchor_rtptime; + // if (t_ping < 0) + // debug(1, "Frame %" PRIu64 " is %" PRId64 " frames before anchor time %" PRIu64 ".", should_be_frame, -t_ping, conn->anchor_rtptime); // sign-extend the r-bit unsigned int calculation by treating it as an r-bit signed // integer @@ -2692,8 +2696,8 @@ void *player_thread_func(void *arg) { " frames (%f seconds), at frame: %" PRIu32 ".", sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); - debug(1, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", - frames_sent_for_play, actual_delay); + //debug(1, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", + // frames_sent_for_play, actual_delay); // the sync error is output frames, but we have to work out how many source frames // to drop there may be a multiple (the conn->output_sample_ratio) of output frames // per input frame... From 3cc1ec6abc5eb8d0f1a6c322585a5ec97a1406d5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 3 Aug 2022 12:55:59 +0100 Subject: [PATCH 798/878] reset anchor time if realtime control message is missing. --- rtp.c | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/rtp.c b/rtp.c index c43ef2b8..169a5f38 100644 --- a/rtp.c +++ b/rtp.c @@ -1274,8 +1274,16 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime, uint64_t networktime) { - // debug(1,"set_ptp_anchor_info: clock: %" PRIx64 ", rtptime: %" PRIu32 ", networktime: %" PRIx64 - // ".", clock_id, rtptime, networktime); + if ((conn->anchor_clock != 0) && (conn->anchor_clock == clock_id) && (conn->anchor_remote_info_is_valid != 0)) { + // check change in timing + int64_t time_difference = networktime - conn->anchor_time; + int32_t frame_difference = rtptime - conn->anchor_rtptime; + double time_difference_in_frames = (1.0 * time_difference * conn->input_rate) / 1000000000; + double frame_change = frame_difference - time_difference_in_frames; + debug(2,"set_ptp_anchor_info: clock: %" PRIx64 ", rtptime: %" PRIu32 ", networktime: %" PRIx64 ", frame adjustment: %7.3f.", clock_id, rtptime, networktime, frame_change); + } else { + debug(2,"set_ptp_anchor_info: clock: %" PRIx64 ", rtptime: %" PRIu32 ", networktime: %" PRIx64 ".", clock_id, rtptime, networktime); + } if (conn->anchor_clock != clock_id) { debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); } @@ -1719,6 +1727,13 @@ void *rtp_ap2_control_receiver(void *arg) { SOCKADDR from_sock_addr; socklen_t from_sock_addr_length = sizeof(SOCKADDR); memset(&from_sock_addr, 0, sizeof(SOCKADDR)); + + + struct timeval tv; + tv.tv_sec = 3; + tv.tv_usec = 0; + setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); + nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); @@ -1743,7 +1758,7 @@ void *rtp_ap2_control_receiver(void *arg) { } } // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); - + if (packet_number >= 2) { if ((config.diagnostic_drop_packet_fraction == 0.0) || (drand48() > config.diagnostic_drop_packet_fraction)) { // store the from_sock_addr if we haven't already done so @@ -1842,15 +1857,25 @@ void *rtp_ap2_control_receiver(void *arg) { free(packet_in_hex_cstring); } break; } + } else { debug(1, "AP2 Control Receiver -- dropping a packet."); } + } } } else if (nread == 0) { debug(1, "AP2 Control Receiver -- connection closed."); keep_going = 0; } else { - debug(1, "AP2 Control Receiver -- error %d receiving a packet.", errno); + if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { + if (conn->airplay_stream_type == realtime_stream) { + debug(2, "Connection %d: resetting anchor info", conn->connection_number); + reset_ptp_anchor_info(conn); + packet_number = 0; // start over in allowing the packet to set anchor information + } + } else { + debug(1, "AP2 Control Receiver -- error %d receiving a packet.", errno); + } } } debug(1, "AP2 Control RTP thread \"normal\" exit -- this can't happen. Hah!"); @@ -1900,7 +1925,8 @@ void *rtp_realtime_audio_receiver(void *arg) { uint8_t marker = (packet[1] & 0b1000000) >> 7; uint8_t payload_type = packet[1] & 0b01111111; */ - + // if (have_ptp_timing_information(conn)) { + if (1) { int32_t seqno = decipher_player_put_packet(packet + 2, nread - 2, conn); if (seqno >= 0) { if (last_seqno == -1) { @@ -1914,6 +1940,7 @@ void *rtp_realtime_audio_receiver(void *arg) { } else { debug(1, "Realtime Audio Receiver -- bad packet dropped."); } + } } else { debug(3, "Realtime Audio Receiver -- dropping a packet."); } From a72b5d7ca2d12a56219289e46151a60ae2bf13a7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 5 Aug 2022 14:24:03 +0100 Subject: [PATCH 799/878] Quieten a debug message. --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 7b2ce500..a76385b2 100644 --- a/player.c +++ b/player.c @@ -2414,7 +2414,7 @@ void *player_thread_func(void *arg) { double moving_average_correction = 0.0; double moving_average_insertions_plus_deletions = 0.0; if (number_of_statistics == 0) { - debug(1, "number_of_statistics is zero!"); + debug(2, "number_of_statistics is zero."); } else { moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; From f64793ee6e1e73f99ac146488f67fc8f67211822 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:17:17 +0100 Subject: [PATCH 800/878] Add (local) volume, not mediated through remote control. --- org.gnome.ShairportSync.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 10eb5477..2fb9c8e9 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -11,6 +11,7 @@ + From 6f7dc3d6705d0ae4037e2def2a9642270f1b3777 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:18:05 +0100 Subject: [PATCH 801/878] Remove redundant definition and add lock/unlock player. --- rtsp.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rtsp.h b/rtsp.h index 5222b8e2..b12c07ea 100644 --- a/rtsp.h +++ b/rtsp.h @@ -8,7 +8,8 @@ extern rtsp_conn_info **conns; void *rtsp_listen_loop(__attribute((unused)) void *arg); -void release_play_lock(rtsp_conn_info *conn); +void lock_player(); +void unlock_player(); // initialise and completely delete the metadata stuff From f9aa7fb81195a5cc9ef259709e0431360f05dc86 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:19:08 +0100 Subject: [PATCH 802/878] Simplifiy locking and unlocking the playing_conn and add dbus connection to changing the volume. --- rtsp.c | 62 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/rtsp.c b/rtsp.c index 767e2804..2a9d8aae 100644 --- a/rtsp.c +++ b/rtsp.c @@ -92,6 +92,11 @@ #include "ptp-utilities.h" #endif +#ifdef CONFIG_DBUS_INTERFACE +#include "dbus-service.h" +#endif + + #include "mdns.h" // mDNS advertisement strings @@ -497,44 +502,57 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { #endif +void lock_player() { + debug_mutex_lock(&playing_conn_lock, 1000000, 3); +} + +void unlock_player() { + debug_mutex_unlock(&playing_conn_lock, 3); +} + + int have_play_lock(rtsp_conn_info *conn) { int response = 0; - debug_mutex_lock(&playing_conn_lock, 1000000, 3); + lock_player(); if (playing_conn == conn) // this connection definitely has the play lock response = 1; - debug_mutex_unlock(&playing_conn_lock, 3); + unlock_player(); return response; } +/* // return 0 if the playing_conn isn't already locked by someone else and if // it belongs to the conn passed in. // remember to release it! -int try_to_hold_play_lock(rtsp_conn_info *conn) { +int try_to_get_and_lock_playing_conn(rtsp_conn_info **conn) { int response = -1; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { - if (playing_conn == conn) { - response = 0; - } else { - pthread_mutex_unlock(&playing_conn_lock); - } + response = 0; + *conn = playing_conn; } return response; } + void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { pthread_mutex_unlock(&playing_conn_lock); } +*/ + +// make conn no longer the playing_conn void release_play_lock(rtsp_conn_info *conn) { debug(2, "Connection %d: release play lock.", conn->connection_number); - debug_mutex_lock(&playing_conn_lock, 1000000, 3); + lock_player(); if (playing_conn == conn) { // if we have the player playing_conn = NULL; // let it go debug(2, "Connection %d: release play lock.", conn->connection_number); } - debug_mutex_unlock(&playing_conn_lock, 3); + unlock_player(); } + +// make conn the playing_conn, int get_play_lock(rtsp_conn_info *conn) { debug(2, "Connection %d: request play lock.", conn->connection_number); // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but @@ -547,7 +565,7 @@ int get_play_lock(rtsp_conn_info *conn) { // try to become the current playing_conn - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it + lock_player(); // don't let it change while we are looking at it... if (playing_conn == NULL) { playing_conn = conn; @@ -572,18 +590,18 @@ int get_play_lock(rtsp_conn_info *conn) { should_wait = 1; pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit } - debug_mutex_unlock(&playing_conn_lock, 3); + unlock_player(); if (should_wait) { int time_remaining = 3000000; // must be signed, as it could go negative... while ((time_remaining > 0) && (have_the_player == 0)) { - debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it + lock_player(); // get it if (playing_conn == NULL) { playing_conn = conn; have_the_player = 1; } - debug_mutex_unlock(&playing_conn_lock, 3); + unlock_player(); if (have_the_player == 0) { usleep(100000); @@ -3392,14 +3410,16 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, volume); // if we are playing, go ahead and change the volume - if (try_to_hold_play_lock(conn) == 0) { + lock_player(); + config.airplay_volume = volume; + if (playing_conn == conn) player_volume(volume, conn); - release_hold_on_play_lock(conn); - } else { - // otherwise use the volume as the initial setting for when/if the player starts - conn->initial_airplay_volume = volume; - conn->initial_airplay_volume_set = 1; + unlock_player(); +#ifdef CONFIG_DBUS_INTERFACE + if (dbus_service_is_running()) { + shairport_sync_set_volume(shairportSyncSkeleton, config.airplay_volume); } +#endif } else if (strncmp(cp, "progress: ", strlen("progress: ")) == 0) { // this can be sent even when metadata is not solicited @@ -4140,7 +4160,7 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(2, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { From a23884238596247c5eaf7c9740c71a3fa0595534 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:19:43 +0100 Subject: [PATCH 803/878] Simplify use of an initial volume figure. --- player.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/player.c b/player.c index a76385b2..1bb286c8 100644 --- a/player.c +++ b/player.c @@ -2096,8 +2096,6 @@ void *player_thread_func(void *arg) { pthread_setcancelstate(oldState, NULL); double initial_volume = config.airplay_volume; // default - if (conn->initial_airplay_volume_set) // if we have been given an initial volume - initial_volume = conn->initial_airplay_volume; // set the default volume to whatever it was before, as stored in the config airplay_volume debug(2, "Set initial volume to %f.", initial_volume); player_volume(initial_volume, conn); // will contain a cancellation point if asked to wait From 8b7e844e385cd8c47341e94f8c7b10d62b0844fa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:20:26 +0100 Subject: [PATCH 804/878] Add local volume control, not mediated by remote control. --- dbus-service.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/dbus-service.c b/dbus-service.c index 05dd9726..6b488bab 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -562,7 +562,7 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton, __attribute__((unused)) gpointer user_data) { gdouble dt = shairport_sync_get_drift_tolerance(skeleton); if ((dt >= 0.0) && (dt <= 2.0)) { - debug(1, ">> setting drift tolerance to %f seconds", dt); + debug(1, ">> setting drift tolerance to %f seconds.", dt); config.tolerance = dt; } else { debug(1, ">> invalid drift tolerance: %f seconds. Ignored.", dt); @@ -571,6 +571,25 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton, return TRUE; } +gboolean notify_volume_callback(ShairportSync *skeleton, + __attribute__((unused)) gpointer user_data) { + gdouble iv = shairport_sync_get_volume(skeleton); + if (((iv >= -30.0) && (iv <= 0.0)) || (iv == -144.0)) { + debug(1, ">> setting volume to %6.3f.", iv); + + lock_player(); + config.airplay_volume = iv; + if (playing_conn != NULL) + player_volume(iv, playing_conn); + unlock_player(); + + } else { + debug(1, ">> invalid volume: %f. Ignored.", iv); + shairport_sync_set_volume(skeleton, config.airplay_volume); + } + return TRUE; +} + gboolean notify_disable_standby_mode_callback(ShairportSync *skeleton, __attribute__((unused)) gpointer user_data) { char *th = (char *)shairport_sync_get_disable_standby_mode(skeleton); @@ -835,6 +854,8 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name G_CALLBACK(notify_loudness_threshold_callback), NULL); g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance", G_CALLBACK(notify_drift_tolerance_callback), NULL); + g_signal_connect(shairportSyncSkeleton, "notify::volume", + G_CALLBACK(notify_volume_callback), NULL); g_signal_connect(shairportSyncSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL); @@ -899,6 +920,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name shairport_sync_set_loudness_threshold(SHAIRPORT_SYNC(shairportSyncSkeleton), config.loudness_reference_volume_db); shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance); + shairport_sync_set_volume(SHAIRPORT_SYNC(shairportSyncSkeleton), config.airplay_volume); #ifdef CONFIG_APPLE_ALAC if (config.use_apple_decoder == 0) { From 720a271eecdcf9cbb750d5569288fd7533dd2f61 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:33:09 +0100 Subject: [PATCH 805/878] Add "Protocol" property -- AirPlay or AirPlay 2. --- dbus-service.c | 8 +++++++- org.gnome.ShairportSync.xml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dbus-service.c b/dbus-service.c index 6b488bab..e51330bf 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -1,6 +1,6 @@ /* * This file is part of Shairport Sync. - * Copyright (c) Mike Brady 2018 -- 2019 + * Copyright (c) Mike Brady 2018 -- 2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -1009,6 +1009,12 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name // NULL); #endif +#ifdef CONFIG_AIRPLAY_2 + shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2"); +#else + shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay"); +#endif + shairport_sync_set_version(SHAIRPORT_SYNC(shairportSyncSkeleton), PACKAGE_VERSION); char *vs = get_version_string(); shairport_sync_set_version_string(SHAIRPORT_SYNC(shairportSyncSkeleton), vs); diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 2fb9c8e9..efa00fd0 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -22,6 +22,7 @@ + From b81fb39a8025f7410a02e7f4e0f4f483bc9346b5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 7 Aug 2022 10:02:56 +0100 Subject: [PATCH 806/878] Add a new D-Bus method called "DropSession" to forcibly terminate the current play session. --- dbus-service.c | 13 +++++++++ org.gnome.ShairportSync.xml | 1 + rtsp.c | 53 ++++++++++++++++++++++--------------- rtsp.h | 3 +++ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/dbus-service.c b/dbus-service.c index e51330bf..c993223a 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -807,6 +807,16 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv return TRUE; } +static gboolean on_handle_drop_session(ShairportSync *skeleton, + GDBusMethodInvocation *invocation, + __attribute__((unused)) gpointer user_data) { + if (playing_conn != NULL) + debug(1, ">> stopping current play session"); + get_play_lock(NULL, 1); // stop any current session and don't replace it + shairport_sync_complete_drop_session(skeleton, invocation); + return TRUE; +} + static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { @@ -862,6 +872,9 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name g_signal_connect(shairportSyncSkeleton, "handle-remote-command", G_CALLBACK(on_handle_remote_command), NULL); + g_signal_connect(shairportSyncSkeleton, "handle-drop-session", + G_CALLBACK(on_handle_drop_session), NULL); + g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity", G_CALLBACK(notify_verbosity_callback), NULL); diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index efa00fd0..1051bab6 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -12,6 +12,7 @@ + diff --git a/rtsp.c b/rtsp.c index 2a9d8aae..de095412 100644 --- a/rtsp.c +++ b/rtsp.c @@ -542,19 +542,30 @@ void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { // make conn no longer the playing_conn void release_play_lock(rtsp_conn_info *conn) { - debug(2, "Connection %d: release play lock.", conn->connection_number); + if (conn != NULL) + debug(2, "Connection %d: release play lock.", conn->connection_number); + else + debug(2, "Release play lock."); lock_player(); if (playing_conn == conn) { // if we have the player + if (conn != NULL) + debug(2, "Connection %d: play lock released.", conn->connection_number); + else + debug(2, "Play lock released."); playing_conn = NULL; // let it go - debug(2, "Connection %d: release play lock.", conn->connection_number); } unlock_player(); } -// make conn the playing_conn, -int get_play_lock(rtsp_conn_info *conn) { - debug(2, "Connection %d: request play lock.", conn->connection_number); +// make conn the playing_conn, and kill the current session if permitted +int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption) { + if (conn != NULL) + debug(2, "Connection %d: request play lock.", conn->connection_number); + else if (playing_conn != NULL) + debug(2, "Connection %d: request release.", playing_conn->connection_number); + else + debug(2, "Request release of non-existent player."); // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but // interrupted an existing session int response = 0; @@ -572,19 +583,14 @@ int get_play_lock(rtsp_conn_info *conn) { have_the_player = 1; } else if (playing_conn == conn) { have_the_player = 1; - warn("Duplicate attempt to acquire the player by the same connection, by the look of it!"); + if (conn != NULL) + warn("Duplicate attempt to acquire the player by the same connection, by the look of it!"); } else if (playing_conn->stop) { debug(1, "Connection %d: Waiting for Connection %d to stop playing.", conn->connection_number, playing_conn->connection_number); should_wait = 1; -#ifdef CONFIG_AIRPLAY_2 - } else { // ignore the allow_session_interruption in AirPlay 2 -- it is always permissible, it - // seems -#else - } else if (config.allow_session_interruption == 1) { -#endif - debug(2, "Connection %d: Asking Connection %d to stop playing.", conn->connection_number, - playing_conn->connection_number); + } else if (allow_session_interruption != 0) { + debug(2, "Asking Connection %d to stop playing.", playing_conn->connection_number); playing_conn->stop = 1; interrupting_current_session = 1; should_wait = 1; @@ -608,18 +614,23 @@ int get_play_lock(rtsp_conn_info *conn) { time_remaining -= 100000; } } - if ((have_the_player == 1) && (interrupting_current_session == 1)) { - debug(2, "Connection %d: Got player lock", conn->connection_number); + if (conn != NULL) + debug(2, "Connection %d: Got player lock", conn->connection_number); + else + debug(2, "Player released."); response = 1; } else { - debug(1, "Connection %d: failed to get player lock after waiting.", conn->connection_number); + debug(2, "Connection %d: failed to get player lock after waiting.", conn->connection_number); response = -1; } } if ((have_the_player == 1) && (interrupting_current_session == 0)) { - debug(2, "Connection %d: Got player lock.", conn->connection_number); + if (conn != NULL) + debug(2, "Connection %d: Got player lock.", conn->connection_number); + else + debug(2, "Player released."); response = 0; } return response; @@ -2609,7 +2620,7 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag } else { teardown_phase_one(conn); // try to do phase one anyway teardown_phase_two(conn); - debug(2, "Connection %d: TEARDOWN phase two complete", conn->connection_number); + debug(1, "Connection %d: TEARDOWN phase two complete", conn->connection_number); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2751,7 +2762,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) send_ssnc_metadata('conn', conn->client_ip_string, strlen(conn->client_ip_string), 1); // before disconnecting an existing play #endif - get_play_lock(conn); + get_play_lock(conn, 1); // airplay 2 always allows interruption #ifdef CONFIG_METADATA send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); @@ -4269,7 +4280,7 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug(3, "Connection %d: ANNOUNCE", conn->connection_number); - int get_play_status = get_play_lock(conn); + int get_play_status = get_play_lock(conn, config.allow_session_interruption); if (get_play_status != -1) { debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); diff --git a/rtsp.h b/rtsp.h index b12c07ea..0725f664 100644 --- a/rtsp.h +++ b/rtsp.h @@ -11,6 +11,9 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg); void lock_player(); void unlock_player(); +// this can be used to forcibly stop a play session +int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption); + // initialise and completely delete the metadata stuff void metadata_init(void); From 4963c65a0e00af8c99c44869e4d278370fdcf081 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 7 Aug 2022 11:15:54 +0100 Subject: [PATCH 807/878] Fix the D-Bus quit method so that it works, duh. --- common.c | 4 ++-- common.h | 8 +++++++- dbus-service.c | 8 +++----- shairport.c | 11 ++++++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/common.c b/common.c index f8b7ae1a..06c22aa5 100644 --- a/common.c +++ b/common.c @@ -108,7 +108,7 @@ void set_alsa_out_dev(char *); #endif config_t config_file_stuff; -int emergency_exit; +int type_of_exit_cleanup; pthread_t main_thread_id; uint64_t ns_time_at_startup, ns_time_at_last_debug_message; @@ -478,7 +478,7 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { va_end(args); sps_log(LOG_ERR, "%s", b); pthread_setcancelstate(oldState, NULL); - emergency_exit = 1; + type_of_exit_cleanup = TOE_emergency; exit(EXIT_FAILURE); } diff --git a/common.h b/common.h index 321eeea2..ffacb9f1 100644 --- a/common.h +++ b/common.h @@ -34,6 +34,12 @@ typedef enum { } dbus_session_type; #endif +typedef enum { + TOE_normal, + TOE_emergency, + TOE_dbus // a dbus request was made -- don't wait for the dbus thread to exit +} type_of_exit_type; + #define sps_extra_code_output_stalled 32768 #define sps_extra_code_output_state_cannot_make_ready 32769 @@ -400,7 +406,7 @@ extern pthread_t main_thread_id; extern shairport_cfg config; extern config_t config_file_stuff; -extern int emergency_exit; +extern int type_of_exit_cleanup; // normal, emergency, dbus requested... int config_set_lookup_bool(config_t *cfg, char *where, int *dst); diff --git a/dbus-service.c b/dbus-service.c index c993223a..e72e5a9c 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -772,11 +772,9 @@ gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleto static gboolean on_handle_quit(ShairportSync *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) const gchar *command, __attribute__((unused)) gpointer user_data) { - debug(1, "quit requested (native interface)"); - if (main_thread_id) - debug(1, "Cancelling main thread results in %d.", pthread_cancel(main_thread_id)); - else - debug(1, "Main thread ID is NULL."); + debug(1, ">> quit requested"); + type_of_exit_cleanup = TOE_dbus; // request an exit cleanup that is compatible with dbus + exit(EXIT_SUCCESS); shairport_sync_complete_quit(skeleton, invocation); return TRUE; } diff --git a/shairport.c b/shairport.c index bbecc558..757e0a42 100644 --- a/shairport.c +++ b/shairport.c @@ -1536,7 +1536,7 @@ void exit_rtsp_listener() { void exit_function() { - if (emergency_exit == 0) { + if (type_of_exit_cleanup != TOE_emergency) { // the following is to ensure that if libdaemon has been included // that most of this code will be skipped when the parent process is exiting // exec @@ -1569,7 +1569,12 @@ void exit_function() { if (g_main_loop) { debug(2, "Stopping D-Bus Loop Thread"); g_main_loop_quit(g_main_loop); - pthread_join(dbus_thread, NULL); + + // If the request to exit has come from the D-Bus system, + // the D-Bus Loop Thread will not exit until the request is completed + // so don't wait for it + if (type_of_exit_cleanup != TOE_dbus) + pthread_join(dbus_thread, NULL); } #endif @@ -1743,7 +1748,7 @@ int main(int argc, char **argv) { setlogmask(LOG_UPTO(LOG_DEBUG)); openlog(NULL, 0, LOG_DAEMON); #endif - emergency_exit = 0; // what to do or skip in the exit_function + type_of_exit_cleanup = TOE_normal; // what kind of exit cleanup needed atexit(exit_function); // set defaults From 1f8210957744095c7daec10b8fcfbce7d48e4046 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Mon, 8 Aug 2022 09:56:25 +0100 Subject: [PATCH 808/878] Use printf in place of echo for portability to Mac OS. Update author --- Makefile.am | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile.am b/Makefile.am index e5674d2b..48653ac2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -36,11 +36,11 @@ endif if USE_GIT_VERSION common.c: gitversion.h gitversion.h: .git/index - echo "// Do not edit!" > gitversion.h - echo "// This file is automatically generated by 'git describe --dirty', if available." >> gitversion.h - echo -n " char git_version_string[] = \"" >> gitversion.h + printf "// Do not edit!\n" > gitversion.h + printf "// This file is automatically generated by 'git describe --dirty', if available.\n" >> gitversion.h + printf " char git_version_string[] = \"" >> gitversion.h git describe --dirty | tr -d '[[:space:]]' >> gitversion.h - echo "\";" >> gitversion.h + printf "\";\n" >> gitversion.h CLEANFILES += gitversion.h endif From bc7d122a9ed3a8e53d764d734753e0463192893d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Aug 2022 09:58:16 +0100 Subject: [PATCH 809/878] Clean us a few compilation errors. --- common.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/common.c b/common.c index f8b7ae1a..c4f9ac81 100644 --- a/common.c +++ b/common.c @@ -62,6 +62,9 @@ #endif #ifdef COMPILE_FOR_OSX +#include +#include +#include #include #include #include @@ -1220,7 +1223,6 @@ uint64_t get_monotonic_time_in_ns() { #ifdef COMPILE_FOR_OSX uint64_t time_now_mach; - uint64_t elapsedNano; static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; // this actually give you a monotonic clock @@ -1238,7 +1240,7 @@ uint64_t get_monotonic_time_in_ns() { } if (sTimebaseInfo.denom == 0) - die("could not initialise Mac timebase info in get_monotonic_time_in_ns().") + die("could not initialise Mac timebase info in get_monotonic_time_in_ns()."); // Do the maths. We hope that the multiplication doesn't // overflow; the price you pay for working in fixed point. @@ -1283,7 +1285,6 @@ uint64_t get_absolute_time_in_ns() { #ifdef COMPILE_FOR_OSX uint64_t time_now_mach; - uint64_t elapsedNano; static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; // this actually give you a monotonic clock @@ -1303,7 +1304,7 @@ uint64_t get_absolute_time_in_ns() { // overflow; the price you pay for working in fixed point. if (sTimebaseInfo.denom == 0) - die("could not initialise Mac timebase info in get_absolute_time_in_ns().") + die("could not initialise Mac timebase info in get_absolute_time_in_ns()."); // this gives us nanoseconds time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; @@ -1953,7 +1954,6 @@ int get_device_id(uint8_t *id, int int_length) { response = -1; } else { t = id; - int found = 0; for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { #ifdef AF_PACKET if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { @@ -1962,7 +1962,6 @@ int get_device_id(uint8_t *id, int int_length) { for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { *t++ = s->sll_addr[i]; } - found = 1; } } #else @@ -1975,7 +1974,6 @@ int get_device_id(uint8_t *id, int int_length) { debug(1, "char %d: \"%c\".", i, *s); *t++ = (uint8_t)*s++; } - found = 1; } } #endif From c458114c4ca1a016bbf2fcf4660846f848a3955a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Aug 2022 12:16:40 +0100 Subject: [PATCH 810/878] Add distinct flags for Darwin -- turn off deprecation warnings. --- Makefile.am | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile.am b/Makefile.am index 48653ac2..f16ae1d7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -19,6 +19,10 @@ noinst_LIBRARIES = shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c alac.c audio.c loudness.c activity_monitor.c +if BUILD_FOR_DARWIN + AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\" + AM_CFLAGS = -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\" +else if BUILD_FOR_FREEBSD AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\" AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\" @@ -31,6 +35,7 @@ else AM_CFLAGS = -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\" endif endif +endif # include information generated by 'git describe --dirty' if requested if USE_GIT_VERSION From 074457c8574897bea04ba896d8c6a62bd13f2b58 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Aug 2022 12:17:20 +0100 Subject: [PATCH 811/878] Add conditional test for Darwin. --- configure.ac | 1 + 1 file changed, 1 insertion(+) diff --git a/configure.ac b/configure.ac index 356d3d4c..333f186f 100644 --- a/configure.ac +++ b/configure.ac @@ -50,6 +50,7 @@ fi AM_CONDITIONAL([BUILD_FOR_LINUX], [test "x${with_os}" = xlinux ]) AM_CONDITIONAL([BUILD_FOR_FREEBSD], [test "x${with_os}" = xfreebsd ]) AM_CONDITIONAL([BUILD_FOR_OPENBSD], [test "x${with_os}" = xopenbsd ]) +AM_CONDITIONAL([BUILD_FOR_DARWIN], [test "x${with_os}" = xdarwin ]) ##### Some build systems are not fully using pkg-config, so we can use the flag ${with_pkg_config} on a case-by-case basis ##### to control how to deal with them From 54e228cbec988f1dfc3c785b0d69c0eb8c55de0c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Aug 2022 12:18:36 +0100 Subject: [PATCH 812/878] Accommodate but do not use extra texts and name for a secondary boujour message. --- mdns_dns_sd.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mdns_dns_sd.c b/mdns_dns_sd.c index 4952a9f0..01be15a1 100644 --- a/mdns_dns_sd.c +++ b/mdns_dns_sd.c @@ -33,8 +33,9 @@ static DNSServiceRef service; -static int mdns_dns_sd_register(char *apname, int port, - __attribute__((unused)) char **txt_records) { +static int mdns_dns_sd_register(char *ap1name, __attribute__((unused)) char *ap2name, int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { char *recordwithoutmetadata[] = {MDNS_RECORD_WITHOUT_METADATA, NULL}; #ifdef CONFIG_METADATA char *recordwithmetadata[] = {MDNS_RECORD_WITH_METADATA, NULL}; @@ -71,7 +72,7 @@ static int mdns_dns_sd_register(char *apname, int port, } DNSServiceErrorType error; - error = DNSServiceRegister(&service, 0, kDNSServiceInterfaceIndexAny, apname, config.regtype, "", + error = DNSServiceRegister(&service, 0, kDNSServiceInterfaceIndexAny, ap1name, config.regtype, "", NULL, htons((uint16_t)port), length, buf, NULL, NULL); free(buf); From de8df93471e70fc585670879b12a9539517c3ea0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 8 Aug 2022 12:19:04 +0100 Subject: [PATCH 813/878] Comment out an unused variable. --- rtp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtp.c b/rtp.c index 169a5f38..844f55fe 100644 --- a/rtp.c +++ b/rtp.c @@ -723,7 +723,7 @@ void *rtp_timing_receiver(void *arg) { // for getting mean and sd of return times int32_t stat_n = 0; double stat_mean = 0.0; - double stat_M2 = 0.0; + // double stat_M2 = 0.0; while (1) { nread = recv(conn->timing_socket, packet, sizeof(packet), 0); @@ -813,7 +813,7 @@ void *rtp_timing_receiver(void *arg) { stat_n += 1; double stat_delta = return_time - stat_mean; stat_mean += stat_delta / stat_n; - stat_M2 += stat_delta * (return_time - stat_mean); + // stat_M2 += stat_delta * (return_time - stat_mean); // debug(1, "Timing packet return time stats: current, mean and standard deviation over // %d packets: %.1f, %.1f, %.1f (nanoseconds).", // stat_n,return_time,stat_mean, sqrtf(stat_M2 / (stat_n - 1))); From 882d6c5b680e68ee35b17e891dc4c11905cf4732 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 12:34:17 +0100 Subject: [PATCH 814/878] Fix get_device_id to work property when using AF_LINK rather than AF_PACKET. Ensure only int_length bytes are copied to the device ID --- common.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/common.c b/common.c index c4f9ac81..2e582f63 100644 --- a/common.c +++ b/common.c @@ -1950,15 +1950,17 @@ int get_device_id(uint8_t *id, int int_length) { } if (getifaddrs(&ifaddr) == -1) { - debug(1, "getifaddrs"); response = -1; } else { t = id; - for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + int found = 0; + + for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) { #ifdef AF_PACKET if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; - if ((strcmp(ifa->ifa_name, "lo") != 0) && (found == 0)) { + if ((strcmp(ifa->ifa_name, "lo") != 0)) { + found = 1; for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { *t++ = s->sll_addr[i]; } @@ -1969,15 +1971,16 @@ int get_device_id(uint8_t *id, int int_length) { struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; if ((sdl) && (sdl->sdl_family == AF_LINK)) { if (sdl->sdl_type == IFT_ETHER) { - char *s = LLADDR(sdl); - for (i = 0; i < sdl->sdl_alen; i++) { - debug(1, "char %d: \"%c\".", i, *s); - *t++ = (uint8_t)*s++; + found = 1; + uint8_t *s = (uint8_t *)LLADDR(sdl); + for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) { + *t++ = *s++; } } } #endif #endif + } freeifaddrs(ifaddr); } From 919c4a41311de4c119325dd99cd362add7d66272 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 12:38:52 +0100 Subject: [PATCH 815/878] Clear the config record at the very very start, and not a little later. --- shairport.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shairport.c b/shairport.c index bbecc558..91bc4416 100644 --- a/shairport.c +++ b/shairport.c @@ -1693,6 +1693,7 @@ void termHandler(__attribute__((unused)) int k) { } int main(int argc, char **argv) { + memset(&config, 0, sizeof(config)); // also clears all strings, BTW /* Check if we are called with -V or --version parameter */ if (argc >= 2 && ((strcmp(argv[1], "-V") == 0) || (strcmp(argv[1], "--version") == 0))) { print_version(); @@ -1726,7 +1727,6 @@ int main(int argc, char **argv) { config.log_fd = -1; conns = NULL; // no connections active memset((void *)&main_thread_id, 0, sizeof(main_thread_id)); - memset(&config, 0, sizeof(config)); // also clears all strings, BTW ns_time_at_startup = get_absolute_time_in_ns(); ns_time_at_last_debug_message = ns_time_at_startup; // this is a bit weird, but necessary -- basename() may modify the argument passed in From f61278278a0933c2498af86530c3122b944ef003 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 12:39:23 +0100 Subject: [PATCH 816/878] Quieten a noisy debug message. --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 7b2ce500..76f5a3a1 100644 --- a/player.c +++ b/player.c @@ -2414,7 +2414,7 @@ void *player_thread_func(void *arg) { double moving_average_correction = 0.0; double moving_average_insertions_plus_deletions = 0.0; if (number_of_statistics == 0) { - debug(1, "number_of_statistics is zero!"); + debug(2, "number_of_statistics is zero!"); } else { moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics; moving_average_correction = (1.0 * tsum_of_corrections) / number_of_statistics; From a774a6fcb0bf7daa550efbc9d9e2f1f2d7898731 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:00:55 +0100 Subject: [PATCH 817/878] Strip off the last dot and everything to the right of it in the hostname when automatically generating a service name. --- shairport.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shairport.c b/shairport.c index 91bc4416..b4ad115b 100644 --- a/shairport.c +++ b/shairport.c @@ -1454,6 +1454,11 @@ int parse_options(int argc, char **argv) { // now, do the substitutions in the service name char hostname[100]; gethostname(hostname, 100); + + // strip off a terminating ., e.g. .local from the hostname + char *last_dot = strrchr(hostname,'.'); + if (last_dot != NULL) + *last_dot = '\0'; char *i0; if (raw_service_name == NULL) From dd197162f6224948b7657d6a43dcab5955fc6694 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:59:54 +0100 Subject: [PATCH 818/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index e0a21b67..634df2bb 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-454-ga774a6fc +==== +**Enhancement** +* Strip the trailing dot and everything after it in the hostname when using it as the basis for a service name. E.g. `freebsd.local` becomes `freebsd`. + +**Bug Fixes** +* Fix a few compilation bugs and portability issues, and turn off deprecation warnings when compiling for Mac OS. + Version 4.1-dev-439-gb7a11cae ==== Documentation update. Comments, corrections and suggestions welcome. From 77c9ff5de4cc9267a3236bb6883b78742569d7f2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:02:14 +0100 Subject: [PATCH 819/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 634df2bb..666071a8 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -6,6 +6,8 @@ Version 4.1-dev-454-ga774a6fc **Bug Fixes** * Fix a few compilation bugs and portability issues, and turn off deprecation warnings when compiling for Mac OS. +**Note:** After this update, you'll need to run `autoreconf -fi` and the `./configure...` steps again. + Version 4.1-dev-439-gb7a11cae ==== Documentation update. Comments, corrections and suggestions welcome. From 36dbd793c18f7677f2f7b0df639394d223a1517f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:28:23 +0100 Subject: [PATCH 820/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 666071a8..6d9c737c 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version 4.1-dev-466-g05dfcdfd +==== +**Enhancements** + +These are for advanced users. +* Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. +* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source. The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control available, it is preferable to "ask" the player to change the volume. +* Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control available, it is preferable to "ask" the player to stop playing. + Version 4.1-dev-454-ga774a6fc ==== **Enhancement** From cb03d5c68b9dc0a8e4c81f488d11aedb90635a66 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 20:52:08 +0100 Subject: [PATCH 821/878] Close the driver when play stops; reopen it when it restarts. Seems a lot more stable. Add a list of drivers to the help text. --- audio_ao.c | 170 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 63 deletions(-) diff --git a/audio_ao.c b/audio_ao.c index 40a94fdc..6e203709 100644 --- a/audio_ao.c +++ b/audio_ao.c @@ -1,6 +1,7 @@ /* * libao output driver. This file is part of Shairport. * Copyright (c) James Laird 2013 + * Copyright (c) Mike Brady 2014 -- 2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -32,98 +33,141 @@ #include ao_device *dev = NULL; +ao_option *ao_opts = NULL; +ao_sample_format fmt; +int driver = 0; static void help(void) { printf(" -d driver set the output driver\n" " -o name=value set an arbitrary ao option\n" " -i id shorthand for -o id=\n" " -n name shorthand for -o dev= -o dsp=\n"); + // get a list of drivers available + ao_initialize(); + int defaultDriver = ao_default_driver_id(); + if (defaultDriver == -1) { + printf(" No usable drivers available.\n"); + } else { + ao_info *defaultDriverInfo = ao_driver_info(defaultDriver); + int driver_count; + ao_info **driver_list = ao_driver_info_list(&driver_count); + int i = 0; + if (driver_count == 0) { + printf(" Driver list unavailable.\n"); + } else { + printf(" Drivers:\n"); + for (i = 0; i < driver_count; i++) { + ao_info *the_driver = driver_list[i]; + if (strcmp(the_driver->short_name, defaultDriverInfo->short_name) == 0) + printf(" \"%s\" (default)\n", the_driver->short_name); + else + printf(" \"%s\"\n", the_driver->short_name); + } + } + } + ao_shutdown(); } static int init(int argc, char **argv) { - // const char *str; - // int value; - // double dvalue; ao_initialize(); - int driver = ao_default_driver_id(); - ao_option *ao_opts = NULL; + driver = ao_default_driver_id(); + if (driver == -1) { + warn("libao can not find a usable driver!"); + } else { - // set up default values first + // set up default values first - config.audio_backend_buffer_desired_length = 1.0; - config.audio_backend_latency_offset = 0; + config.audio_backend_buffer_desired_length = 1.0; + config.audio_backend_latency_offset = 0; - // get settings from settings file first, allow them to be overridden by - // command line options + // get settings from settings file first, allow them to be overridden by + // command line options - // do the "general" audio options. Note, these options are in the "general" stanza! - parse_general_audio_options(); + // do the "general" audio options. Note, these options are in the "general" stanza! + parse_general_audio_options(); - optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour - argv--; // so we shift the arguments to satisfy getopt() - argc++; + optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour + argv--; // so we shift the arguments to satisfy getopt() + argc++; - // some platforms apparently require optreset = 1; - which? - int opt; - char *mid; - while ((opt = getopt(argc, argv, "d:i:n:o:")) > 0) { - switch (opt) { - case 'd': - driver = ao_driver_id(optarg); - if (driver < 0) - die("could not find ao driver %s", optarg); - break; - case 'i': - ao_append_option(&ao_opts, "id", optarg); - break; - case 'n': - ao_append_option(&ao_opts, "dev", optarg); - // Old libao versions (for example, 0.8.8) only support - // "dsp" instead of "dev". - ao_append_option(&ao_opts, "dsp", optarg); - break; - case 'o': - mid = strchr(optarg, '='); - if (!mid) - die("Expected an = in audio option %s", optarg); - *mid = 0; - ao_append_option(&ao_opts, optarg, mid + 1); - break; - default: - help(); - die("Invalid audio option -%c specified", opt); + // some platforms apparently require optreset = 1; - which? + int opt; + char *mid; + while ((opt = getopt(argc, argv, "d:i:n:o:")) > 0) { + switch (opt) { + case 'd': + driver = ao_driver_id(optarg); + if (driver < 0) + die("could not find ao driver %s", optarg); + break; + case 'i': + ao_append_option(&ao_opts, "id", optarg); + break; + case 'n': + ao_append_option(&ao_opts, "dev", optarg); + // Old libao versions (for example, 0.8.8) only support + // "dsp" instead of "dev". + ao_append_option(&ao_opts, "dsp", optarg); + break; + case 'o': + mid = strchr(optarg, '='); + if (!mid) + die("Expected an = in audio option %s", optarg); + *mid = 0; + ao_append_option(&ao_opts, optarg, mid + 1); + break; + default: + help(); + die("Invalid audio option -%c specified", opt); + } } + + if (optind < argc) + die("Invalid audio argument: %s", argv[optind]); + + memset(&fmt, 0, sizeof(fmt)); + + fmt.bits = 16; + fmt.rate = 44100; + fmt.channels = 2; + fmt.byte_format = AO_FMT_NATIVE; } - - if (optind < argc) - die("Invalid audio argument: %s", argv[optind]); - - ao_sample_format fmt; - memset(&fmt, 0, sizeof(fmt)); - - fmt.bits = 16; - fmt.rate = 44100; - fmt.channels = 2; - fmt.byte_format = AO_FMT_NATIVE; - - dev = ao_open_live(driver, &fmt, ao_opts); - - return dev ? 0 : 1; + return 0; } static void deinit(void) { - if (dev) + if (dev != NULL) ao_close(dev); dev = NULL; ao_shutdown(); } static void start(__attribute__((unused)) int sample_rate, - __attribute__((unused)) int sample_format) {} + __attribute__((unused)) int sample_format) { + // debug(1,"libao start"); +} -static int play(void *buf, int samples) { return ao_play(dev, buf, samples * 4); } +static int play(void *buf, int samples) { + int response = 0; + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + if (driver != -1) { + if (dev == NULL) + dev = ao_open_live(driver, &fmt, ao_opts); + if (dev != NULL) + response = ao_play(dev, buf, samples * 4); + } + pthread_setcancelstate(oldState, NULL); + return response; +} -static void stop(void) {} +static void stop(void) { + // debug(1,"libao stop"); + if (dev != NULL) { + ao_close(dev); + dev = NULL; + } +} audio_output audio_ao = {.name = "ao", .help = &help, From d374bf38250ece0268dab260f972413166b07a40 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:09:49 +0100 Subject: [PATCH 822/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 6d9c737c..c11eed92 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,10 @@ +Version 4.1-dev-468-gcb03d5c6 +==== +**Updates and Enhancements** +* The `development` branch now builds again on macOS with [brew](https://brew.sh). (Remember though, that Shairport Sync [will not function in AirPlay 2 mode on a Mac](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md)). +* The `libao` backend has been updated to address an issue with the system becoming unavailable. The biggest change was to close the driver as soon as playing stops, reopening it when playing restarts. +* The `libao` drivers are now listed in the help text. If available, the `alsa` driver is preferred over the `pulseaudio` driver. + Version 4.1-dev-466-g05dfcdfd ==== **Enhancements** From 67d9c4d029a8bd8ab7ecc3207be077e75cc36041 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:31:49 +0100 Subject: [PATCH 823/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c11eed92..beb73d7e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-468-gcb03d5c6 ==== **Updates and Enhancements** -* The `development` branch now builds again on macOS with [brew](https://brew.sh). (Remember though, that Shairport Sync [will not function in AirPlay 2 mode on a Mac](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md)). +* The `development` branch now builds again on macOS with [brew](https://brew.sh). (Remember, though, that Shairport Sync [will not function in AirPlay 2 mode on a Mac](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md)). * The `libao` backend has been updated to address an issue with the system becoming unavailable. The biggest change was to close the driver as soon as playing stops, reopening it when playing restarts. * The `libao` drivers are now listed in the help text. If available, the `alsa` driver is preferred over the `pulseaudio` driver. From 3fd0e251c2a1b04af2f1e75ea5900549c5273fe1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:33:00 +0100 Subject: [PATCH 824/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index beb73d7e..10c04468 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -3,7 +3,7 @@ Version 4.1-dev-468-gcb03d5c6 **Updates and Enhancements** * The `development` branch now builds again on macOS with [brew](https://brew.sh). (Remember, though, that Shairport Sync [will not function in AirPlay 2 mode on a Mac](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md)). * The `libao` backend has been updated to address an issue with the system becoming unavailable. The biggest change was to close the driver as soon as playing stops, reopening it when playing restarts. -* The `libao` drivers are now listed in the help text. If available, the `alsa` driver is preferred over the `pulseaudio` driver. +* The `libao` drivers are now listed in the help text. If available, it is suggested that the `alsa` driver should be used in preference to the `pulseaudio` driver. Version 4.1-dev-466-g05dfcdfd ==== From d1ead5dec8ee014801d6f7747c766758e18e4fde Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:34:07 +0100 Subject: [PATCH 825/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 10c04468..a8f1ff83 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -11,7 +11,7 @@ Version 4.1-dev-466-g05dfcdfd These are for advanced users. * Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. -* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source. The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control available, it is preferable to "ask" the player to change the volume. +* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control available, it is preferable to "ask" the player to change the volume. * Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control available, it is preferable to "ask" the player to stop playing. Version 4.1-dev-454-ga774a6fc From b90aa5454a4d24724f0541bf4063c4b99f2d4e36 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:34:51 +0100 Subject: [PATCH 826/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index a8f1ff83..feacd189 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -12,7 +12,7 @@ Version 4.1-dev-466-g05dfcdfd These are for advanced users. * Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. * Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control available, it is preferable to "ask" the player to change the volume. -* Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control available, it is preferable to "ask" the player to stop playing. +* Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control is available, it is preferable to use it to "ask" the player to stop playing. Version 4.1-dev-454-ga774a6fc ==== From 3f92fe737c770990e59b1410bd58e163c9914b35 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:35:33 +0100 Subject: [PATCH 827/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index feacd189..b1314636 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -11,7 +11,7 @@ Version 4.1-dev-466-g05dfcdfd These are for advanced users. * Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. -* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control available, it is preferable to "ask" the player to change the volume. +* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control is available, it is preferable to use it to "ask" the player to change the volume. * Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control is available, it is preferable to use it to "ask" the player to stop playing. Version 4.1-dev-454-ga774a6fc From e9c9a819e3e9550fd1978a6ab2979a140570ebd6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:36:58 +0100 Subject: [PATCH 828/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b1314636..c61248ce 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -11,8 +11,8 @@ Version 4.1-dev-466-g05dfcdfd These are for advanced users. * Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. -* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. If remote control is available, it is preferable to use it to "ask" the player to change the volume. -* Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. If remote control is available, it is preferable to use it to "ask" the player to stop playing. +* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. It is preferable to use remote control (if available) to "ask" the player to change the volume. +* Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. It is preferable to use remote control (if available) to "ask" the player to stop playing. Version 4.1-dev-454-ga774a6fc ==== From ccec515eb6df7e3bab7a79088a06b235515eb92c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:40:56 +0100 Subject: [PATCH 829/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index c61248ce..51426e84 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -3,7 +3,7 @@ Version 4.1-dev-468-gcb03d5c6 **Updates and Enhancements** * The `development` branch now builds again on macOS with [brew](https://brew.sh). (Remember, though, that Shairport Sync [will not function in AirPlay 2 mode on a Mac](https://github.com/mikebrady/shairport-sync/blob/development/AIRPLAY2.md)). * The `libao` backend has been updated to address an issue with the system becoming unavailable. The biggest change was to close the driver as soon as playing stops, reopening it when playing restarts. -* The `libao` drivers are now listed in the help text. If available, it is suggested that the `alsa` driver should be used in preference to the `pulseaudio` driver. +* The `libao` drivers are now listed in the help text. If available, it is suggested that the `alsa` driver be used in preference to the `pulseaudio` driver. Version 4.1-dev-466-g05dfcdfd ==== From 437048c5ff11758d1e0903015a59952edffe5cc2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:42:40 +0100 Subject: [PATCH 830/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 51426e84..f1f279ab 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -11,7 +11,7 @@ Version 4.1-dev-466-g05dfcdfd These are for advanced users. * Add a new `Protocol` property to the D-Bus interface. Values: `AirPlay` if built for AirPlay and `AirPlay 2` if build for AirPlay 2. -* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may not necessarity be the same. It is preferable to use remote control (if available) to "ask" the player to change the volume. +* Add a new `Volume` property to the D-Bus interface. This enables you to get and set the volume _locally_. Use this carefully: it means that you can set the volume of the player without changing it at the source (e.g. iTunes / Music). The source will not be informed of the change, so the volume it sets and and the actual volume may become different. It is preferable to use remote control (if available) to "ask" the player to change the volume. * Add a new `DropSession` method. This will immediately and forcibly terminate a playing session without informing the source. Use it carefully. It is preferable to use remote control (if available) to "ask" the player to stop playing. Version 4.1-dev-454-ga774a6fc From 6c1625e6f2f711171c3d7b0057cd5823fdf55328 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 10 Aug 2022 09:12:09 +0100 Subject: [PATCH 831/878] Make the register interfaces compatible with SPS 4.1. The secondary message information is ignored. --- mdns_external.c | 20 ++++++++++++-------- mdns_tinysvcmdns.c | 7 ++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mdns_external.c b/mdns_external.c index 17884f94..cf4d215a 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -80,15 +80,17 @@ static int fork_execvp(const char *file, char *const argv[]) { return response; } -static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port, - __attribute__((unused)) char **txt_records) { +static int mdns_external_avahi_register(char *ap1name, __attribute__((unused)) char *ap2name, + __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); char *argvwithoutmetadata[] = { - NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL}; + NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL}; #ifdef CONFIG_METADATA - char *argvwithmetadata[] = {NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA, + char *argvwithmetadata[] = {NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA, NULL}; #endif char **argv; @@ -120,16 +122,18 @@ static int mdns_external_avahi_register(char *apname, __attribute__((unused)) in return -1; } -static int mdns_external_dns_sd_register(char *apname, __attribute__((unused)) int port, - __attribute__((unused)) char **txt_records) { +static int mdns_external_dns_sd_register(char *ap1name, __attribute__((unused)) char *ap2name, + __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); char *argvwithoutmetadata[] = { - NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL}; + NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITHOUT_METADATA, NULL}; #ifdef CONFIG_METADATA - char *argvwithmetadata[] = {NULL, apname, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA, + char *argvwithmetadata[] = {NULL, ap1name, config.regtype, mdns_port, MDNS_RECORD_WITH_METADATA, NULL}; #endif diff --git a/mdns_tinysvcmdns.c b/mdns_tinysvcmdns.c index 619f9d31..516160da 100644 --- a/mdns_tinysvcmdns.c +++ b/mdns_tinysvcmdns.c @@ -38,8 +38,9 @@ static struct mdnsd *svr = NULL; -static int mdns_tinysvcmdns_register(char *apname, int port, - __attribute__((unused)) char **txt_records) { +static int mdns_tinysvcmdns_register(char *ap1name, __attribute__((unused)) char *ap2name, int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { struct ifaddrs *ifalist; struct ifaddrs *ifa; @@ -146,7 +147,7 @@ static int mdns_tinysvcmdns_register(char *apname, int port, strcat(extendedregtype, ".local"); struct mdns_service *svc = - mdnsd_register_svc(svr, apname, extendedregtype, port, NULL, + mdnsd_register_svc(svr, ap1name, extendedregtype, port, NULL, (const char **)txt); // TTL should be 75 minutes, i.e. 4500 seconds mdns_service_destroy(svc); From 01fc03a6c45b1185050449bc7ddd3a56f999de73 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 10 Aug 2022 10:04:58 +0100 Subject: [PATCH 832/878] Update sample dbus commands --- documents/sample dbus commands | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/documents/sample dbus commands b/documents/sample dbus commands index 5c0d2be1..12dc04e4 100644 --- a/documents/sample dbus commands +++ b/documents/sample dbus commands @@ -5,14 +5,6 @@ # Additionally, the following are examples of using the CLI command "dbus-send" to send D-Bus commands to Shairport Sync: -# Control the player using Remote Control. Remote Control commands are sent as requests to the player (iOS, iTunes, macOS Music, etc.). -# Different versions of the players implement different subsets of the following commands. iOS seems to implement most of them (the first nine in the list). -dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play -#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs - -# Set Airplay Volume using Remote Control. Airplay Volume is between -30.0 and 0.0 and maps linearly onto the slider, with -30.0 being lowest and 0.0 being highest. -dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.SetAirplayVolume double:-10.0 - # Get Log Verbosity dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync.Diagnostics string:Verbosity # Return Log Verbosity @@ -70,6 +62,30 @@ dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/Shair # Set Convolution Impulse Response File: dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:ConvolutionImpulseResponseFile variant:string:"/etc/shairport-sync/boom.wav" +# Get the Protocol Shairport Sync was built for -- AirPlay or AirPlay 2: +dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:Protocol + +# Get the current volume level. This returns a value between -30.0 and 0.0, which is linear on the UI volume control. It can also return -144.0 to indicate mute. +dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:Volume + +# Set the current volume level. This should be a value between -30.0 and 0.0. Set a value of -144.0 to mute. Note that all this is done locally on +# the Shairport Sync device itself. The volume control of audio source (e.g. iTunes / macOS Music / iOS) is not updated to reflect any changes. + +dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:Volume variant:double:-10.0 + +# Drop the current session immediately. Shairport Sync will immediately stop playing and drop the connection from the audio source. +# This may show up as an error at the source. + +dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.DropSession + +# Control the player using Remote Control. Remote Control commands are sent as requests to the player (iOS, iTunes, macOS Music, etc.). +# Different versions of the players implement different subsets of the following commands. iOS seems to implement most of them (the first nine in the list). +dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.Play +#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs + +# Set Airplay Volume using Remote Control. Airplay Volume is between -30.0 and 0.0 and maps linearly onto the slider, with -30.0 being lowest and 0.0 being highest. +dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.RemoteControl.SetAirplayVolume double:-10.0 + # AdvancedRemoteControl interface. # Some commands and properties are accessible only through the AdvancedRemoteControl interface. # However, support for this is very limited in recent implementations. From 9feb901004649cdf0e7f12f7d93e460e027a6be4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 12 Aug 2022 10:58:20 +0100 Subject: [PATCH 833/878] Fix a misleading comment --- shairport.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shairport.c b/shairport.c index 5eec4de5..40c821f2 100644 --- a/shairport.c +++ b/shairport.c @@ -560,7 +560,7 @@ int parse_options(int argc, char **argv) { // Set to NULL to work with transient pairing config.airplay_pin = NULL; - // use the start of the config.hw_addr and the PID to generate the default airplay_device_id + // use the MAC address placed in config.hw_addr to generate the default airplay_device_id uint64_t temporary_airplay_id = nctoh64(config.hw_addr); temporary_airplay_id = temporary_airplay_id >> 16; // we only use the first 6 bytes but have imported 8. From b871c31cda733bb3428275161e13308b22588249 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 15:47:24 +0100 Subject: [PATCH 834/878] Update CAR INSTALL.md --- CAR INSTALL.md | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 7dd87966..638c146a 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -1,5 +1,5 @@ -# Shairport Sync for Cars [Needs updating for Airplay 2] -If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. Your passengers can enjoy movies with the soundtrack on the car speakers. You can even listen to Siri's traffic directions on your car audio. +# Shairport Sync for Cars +If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. ## The Basic Idea @@ -14,10 +14,12 @@ Note that Android devices can not, so far, do this trick of using the two networ ## Example -In this example, a Raspberry Pi Zero W and a Pimoroni PHAT DAC are used. This combination has been tested for over two years. Please note that some of the details of setting up networks are specific to the version of Linux used -- Raspbian Stretch or later. +In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built to support AirPlay 2, but you will also be shown how to build it for "classic" AirPlay (aka AirPlay 1) if you prefer. BTW, the requirments for classic Airplay are less stringent: for classic AirPlay, a Pi Zero W is powerful enough, and Raspbian Stretch (Lite) or later is fine. + +Please note that some of the details of setting up networks are specific to the version of Linux used -- Rasberry Pi OS (Bullseye) Lite or later. ### Prepare the initial SD Image -* Download the latest version of Raspbian Lite -- Stretch Lite of 2018-03-13 at the time of writing -- and install it onto an SD Card. +* Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device. * Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. Both of these need a little modification. * Enable SSH service by creating a file called `ssh` on the `boot` drive. To do this, mount the drive and CD to its `boot` partition (since my username is `mike`, the drive is at `/media/mike/boot`): ``` @@ -44,23 +46,45 @@ The first thing to do on a Pi would be to use the `raspi-config` tool to expand # apt-get upgrade ``` -### Shairport Sync -First, install the packages needed by Shairport Sync: +### Build and Install +Let's get the tools and libraries for building and installing Shairport Sync (and NQPTP). + ``` -# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev +# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \ + libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd ``` -Next, download Shairport Sync, configure it, compile and install it: +If you are building classic Shairport Sync, the list of packages is shorter: +``` +# apt-get install --no-install-recommends build-essential git xmltoman autoconf automake libtool \ + libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev +``` + +#### NQPTP +Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync. + +Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp). + +#### Shairport Sync +Download Shairport Sync, check out the `development` branch and configure, compile and install it. + +* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync. + ``` $ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync +$ git checkout development $ autoreconf -fi -$ ./configure --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-soxr --with-systemd +$ ./configure --sysconfdir=/etc --with-alsa \ + --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 $ make -$ sudo make install +# make install ``` -*Do not* enable Shairport Sync to automatically start at boot time -- startup is organised differently. +The `autoreconf` step may take quite a while – please be patient! -Third, finish by configuring Shairport Sync. +**Note:** *Do not* enable Shairport Sync to start automatically at boot time -- later on in this installation, we will arrange for it to start after the network has been set up. + +### Configure Shairport Sync Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: ``` // Sample Configuration File for Shairport Sync for Car Audio with a Pimoroni PHAT From 248a9903d42b2ee0b3a4100a6b088469065a12d2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 15:49:36 +0100 Subject: [PATCH 835/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 638c146a..0c5c4e05 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -14,7 +14,7 @@ Note that Android devices can not, so far, do this trick of using the two networ ## Example -In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built to support AirPlay 2, but you will also be shown how to build it for "classic" AirPlay (aka AirPlay 1) if you prefer. BTW, the requirments for classic Airplay are less stringent: for classic AirPlay, a Pi Zero W is powerful enough, and Raspbian Stretch (Lite) or later is fine. +In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you will also see how to build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer. The requirments for classic Airplay are less stringent: a Pi Zero W is powerful enough, and Raspbian Stretch (Lite) or later is fine. Please note that some of the details of setting up networks are specific to the version of Linux used -- Rasberry Pi OS (Bullseye) Lite or later. From 8de767fc365bba6de1c161f8bb391fc6a170b429 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 15:52:12 +0100 Subject: [PATCH 836/878] Update CAR INSTALL.md --- CAR INSTALL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 0c5c4e05..6bebe638 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -5,9 +5,9 @@ If your car audio has an AUX input, you can get AirPlay in your car using Shairp The basic idea is to use a small Linux computer to create an isolated WiFi network for the car and to run Shairport Sync on it to provide an AirPlay service. The audio goes via a DAC to the AUX input of your car audio system. -The car WiFi network you create is isolated and local to your car, and since it isn't connected to the Internet, you don't really need to secure it with a password. Likewise, you don't really have to use a password to connect to the AirPlay service. +The car WiFi network you create is isolated and local to your car, and since it isn't connected to the Internet, you don't really need to secure it with a password. -When an iPhone or an iPad with cellular capability is connected to an isolated WiFi network like this, it can use its cellular connection to access the Internet. +When an iPhone or an iPad with cellular capability is connected to an isolated WiFi network, it can use its cellular connection to access the Internet. This means it can connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and play the audio through the car network to the AirPlay service provided by Shairport Sync. Note that Android devices can not, so far, do this trick of using the two networks simultaneously. From 4473bc329d3bf299fb87845fec513d5158eb2892 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:22:19 +0100 Subject: [PATCH 837/878] Update CAR INSTALL.md --- CAR INSTALL.md | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 6bebe638..83130db0 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -19,28 +19,18 @@ In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shair Please note that some of the details of setting up networks are specific to the version of Linux used -- Rasberry Pi OS (Bullseye) Lite or later. ### Prepare the initial SD Image -* Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device. -* Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. Both of these need a little modification. -* Enable SSH service by creating a file called `ssh` on the `boot` drive. To do this, mount the drive and CD to its `boot` partition (since my username is `mike`, the drive is at `/media/mike/boot`): -``` -$ touch ssh -``` -* Also in the `boot` drive, edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: +* Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device. +* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager`. You can specify a wireless network that the Pi will connect to it when it boots up. Later on the Pi will be configured to start its own isolated network. +* Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. +* CD to the `boot` drive (since my username is `mike`, it will be at `/media/mike/boot`): +* Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: ``` dtoverlay=hifiberry-dac ``` -* Next, some modifications need to be done to the `rootfs` drive to make the Pi connect to your main WiFi network. (This is a temporary measure to enable you to connect the Pi to your main network so that you can do all the software installation and updating of the software necessary. Later, the Pi will be configured to start its own isolated network.) On the `rootfs` drive, edit the file `etc/wpa_supplicant/wpa_supplicant.conf` (you'll need root privileges) and add the name and password of your main WiFi network (substitute your own network name and password in, but keep the quotation marks): -``` -network={ - ssid="Network Name" - psk="Password" -} - -``` -Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at `raspberrypi.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. +Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname yoou gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. ### Boot, Configure, Update -The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. It might be useful to change the `hostname` too. Next, do the usual update and upgrade: +The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade: ``` # apt-get update # apt-get upgrade From 3f7806f5c78ea1062aeb57ea8f0d919ddfd1dff5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:23:19 +0100 Subject: [PATCH 838/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 83130db0..f004748a 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -27,7 +27,7 @@ Please note that some of the details of setting up networks are specific to the ``` dtoverlay=hifiberry-dac ``` -Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname yoou gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. + Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname you gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. ### Boot, Configure, Update The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade: From e3197adc039fd0f5ec854660d5b38453088c4069 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:23:56 +0100 Subject: [PATCH 839/878] Update CAR INSTALL.md --- CAR INSTALL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index f004748a..4ceb24ce 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -27,7 +27,8 @@ Please note that some of the details of setting up networks are specific to the ``` dtoverlay=hifiberry-dac ``` - Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname you gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. + + Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname you gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. ### Boot, Configure, Update The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade: From 3f2367db8bf95e590b023de81c4a0f1f80c8fce2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:28:20 +0100 Subject: [PATCH 840/878] Update CAR INSTALL.md --- CAR INSTALL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 4ceb24ce..b8e8360f 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -24,9 +24,9 @@ Please note that some of the details of setting up networks are specific to the * Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. * CD to the `boot` drive (since my username is `mike`, it will be at `/media/mike/boot`): * Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: -``` -dtoverlay=hifiberry-dac -``` + ``` + dtoverlay=hifiberry-dac + ``` Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname you gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. From aa83a7ad399267dccc26888c1d27bb88e4de3c6d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:30:05 +0100 Subject: [PATCH 841/878] Update CAR INSTALL.md --- CAR INSTALL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index b8e8360f..cd76b613 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -28,7 +28,10 @@ Please note that some of the details of setting up networks are specific to the dtoverlay=hifiberry-dac ``` - Close the file and carefully dismount and eject the two drives. Remove the SD card from the Linux machine, insert it into the Pi and reboot. After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the hostname you gave it appended with `.local`, e.g. if the hostname is `bmw` then try `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. +* Close the file and carefully dismount and eject the two drives. +* Remove the SD card from the Linux machine, insert it into the Pi and reboot. + +After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the `.local`, e.g. if the hostname is `bmw` then use `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. ### Boot, Configure, Update The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade: From a9a5bde4f5956db12b47cb084fb31b1937fe85e4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:27:07 +0100 Subject: [PATCH 842/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index cd76b613..3db3f41b 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -20,7 +20,7 @@ Please note that some of the details of setting up networks are specific to the ### Prepare the initial SD Image * Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device. -* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager`. You can specify a wireless network that the Pi will connect to it when it boots up. Later on the Pi will be configured to start its own isolated network. +* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager` to set hostname, enable SSH and provide a username and password to use while building the system. Similarly, you can specify a wireless network the Pi will connect to while building the system. Later on the Pi will be configured to start its own isolated network. * Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. * CD to the `boot` drive (since my username is `mike`, it will be at `/media/mike/boot`): * Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: From e312c0cda29d78d1624b73fbf66d519a04d9cf74 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:43:21 +0100 Subject: [PATCH 843/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 3db3f41b..ee258e58 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -57,7 +57,7 @@ If you are building classic Shairport Sync, the list of packages is shorter: #### NQPTP Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync. -Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp). +Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp) following the guide for Linux. #### Shairport Sync Download Shairport Sync, check out the `development` branch and configure, compile and install it. From 08de8a40842fdb780f0480b773930644ef1b2582 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:03:45 +0100 Subject: [PATCH 844/878] Update CAR INSTALL.md --- CAR INSTALL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index ee258e58..bd2de1fa 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -128,8 +128,8 @@ ssid=BMW # Use the 2.4GHz band hw_mode=g -# Use channel 6 -channel=9 +# Use channel 6 or some other channel of your choice +channel=6 # Enable 802.11n ieee80211n=1 @@ -160,6 +160,7 @@ ignore_broadcast_ssid=0 # Use AES, instead of TKIP #rsn_pairwise=CCMP + ``` #### Configure DHCP server @@ -201,6 +202,7 @@ Configure the startup sequence by adding commands to `/etc/rc.local` to start `h /bin/systemctl start shairport-sync exit 0 + ``` As you can see, the effect of these commands is to start the WiFi transmitter, give the base station the IP address `10.0.10.1`, start a DHCP server and finally start the Shairport Sync service. From 30510e72f1793b737c17fa23a86c6890be0a7cd3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:25:53 +0100 Subject: [PATCH 845/878] Remove a very noisy and redundant debug message. --- audio_alsa.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 4ee19bcc..93c25689 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1456,9 +1456,9 @@ int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, // delay_temp = 0; ret = 0; } - } else { - debug(1, "alsa_handle is NULL in standard_delay_and_status."); - } + } // else { + // debug(1, "alsa_handle is NULL in standard_delay_and_status."); + // } stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes From 174556f05486393a9f2b4ef80e144f5b9b545dc3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:26:35 +0100 Subject: [PATCH 846/878] Remove a few causes of "possibly uninitialised" warnings. --- player.c | 4 ++-- rtp.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/player.c b/player.c index 5f124680..15ee9671 100644 --- a/player.c +++ b/player.c @@ -1746,8 +1746,8 @@ void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; uint64_t previous_frames_played; - uint64_t previous_raw_measurement_time; - uint64_t previous_corrected_measurement_time; + uint64_t previous_raw_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning + uint64_t previous_corrected_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning int previous_frames_played_valid = 0; // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); diff --git a/rtp.c b/rtp.c index 844f55fe..247f1689 100644 --- a/rtp.c +++ b/rtp.c @@ -2362,7 +2362,7 @@ void *rtp_buffered_audio_processor(void *arg) { uint8_t *pcm_audio = NULL; // the S16 output unsigned char *data_to_process; ssize_t data_remaining; - uint32_t seq_no; // audio packet number + uint32_t seq_no = 0; // audio packet number. Initialised to avoid a "possibly uninitialised" warning. // uint32_t previous_seq_no = 0; int new_buffer_needed = 0; ssize_t nread; @@ -2380,10 +2380,10 @@ void *rtp_buffered_audio_processor(void *arg) { uint64_t blocks_read_since_flush = 0; int flush_requested = 0; - uint32_t timestamp; + uint32_t timestamp = 0; // initialised to avoid a "possibly uninitialised" warning. int streaming_has_started = 0; int play_enabled = 0; - uint32_t flush_from_timestamp; + uint32_t flush_from_timestamp = 0; // initialised to avoid a "possibly uninitialised" warning. double requested_lead_time = 0.0; // normal lead time minimum -- maybe it should be about 0.1 // wait until our timing information is valid From d34db2a4b683cb6571ff0dbb84a9f5b263493f8f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:41:36 +0100 Subject: [PATCH 847/878] Update CAR INSTALL.md --- CAR INSTALL.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index bd2de1fa..166eb9f0 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -1,5 +1,5 @@ # Shairport Sync for Cars -If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. +If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone or an iPad with cellular capability can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. ## The Basic Idea @@ -14,24 +14,24 @@ Note that Android devices can not, so far, do this trick of using the two networ ## Example -In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you will also see how to build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer. The requirments for classic Airplay are less stringent: a Pi Zero W is powerful enough, and Raspbian Stretch (Lite) or later is fine. +In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you can build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer. A Pi Zero W is powerful enough for classic AirPlay. Please note that some of the details of setting up networks are specific to the version of Linux used -- Rasberry Pi OS (Bullseye) Lite or later. ### Prepare the initial SD Image * Download the latest version of Raspberry Pi OS (Lite) -- Bullseye (Lite) of 2022-04-04 at the time of writing -- and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device. -* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager` to set hostname, enable SSH and provide a username and password to use while building the system. Similarly, you can specify a wireless network the Pi will connect to while building the system. Later on the Pi will be configured to start its own isolated network. -* Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. -* CD to the `boot` drive (since my username is `mike`, it will be at `/media/mike/boot`): -* Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: - ``` - dtoverlay=hifiberry-dac - ``` - -* Close the file and carefully dismount and eject the two drives. +* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager` to set hostname, enable SSH and provide a username and password to use while building the system. Similarly, you can specify a wireless network the Pi will connect to while building the system. Later on the Pi will be configured to start its own isolated network. +* The next few steps are to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used. If you do not need to add an overlay, skip these steps. + * Mount the card on a Linux machine. Two drives should appear -- a `boot` drive and a `rootfs` drive. + * `cd` to the `boot` drive (since my username is `mike`, it will be `$ cd /media/mike/boot`). + * Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added: + ``` + dtoverlay=hifiberry-dac + ``` + * Close the file and carefully dismount and eject the two drives. *Be sure to dismount and eject the drives properly; otherwise they may be corrupted.* * Remove the SD card from the Linux machine, insert it into the Pi and reboot. -After a short time, the Pi should appear on your network and you can SSH into it. To check that it has appeared on the network, try to ping it at the `.local`, e.g. if the hostname is `bmw` then use `bmw.local`. It may take a minute or so to appear. Once it has appeared on your network you can SSH into it and configure it. +After a short time, the Pi should appear on your network -- it may take a minute or so. To check, try to `ping` it at the `.local`, e.g. if the hostname is `bmw` then use `$ ping bmw.local`. Once it has appeared, you can SSH into it and configure it. ### Boot, Configure, Update The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade: From 3c717b96b7f03206a07682365e6892a182182b5d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:47:14 +0100 Subject: [PATCH 848/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index f1f279ab..87c89659 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-494-gd34db2a4 +==== +* The [CAR INSTALL.md](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) guide has been updated for AirPlay 2 and for Raspberry Pi OS (Bullseye). +* A few noisy debug messages have been removed or quietened. + Version 4.1-dev-468-gcb03d5c6 ==== **Updates and Enhancements** From d6e2b91a83eae3673ad45ebead70d2b32f78b234 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:15:31 +0100 Subject: [PATCH 849/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 87c89659..df34badd 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-494-gd34db2a4 ==== * The [CAR INSTALL.md](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) guide has been updated for AirPlay 2 and for Raspberry Pi OS (Bullseye). -* A few noisy debug messages have been removed or quietened. +* A few noisy debug messages have been removed or quietened. Thanks to [David Leibovic](https://github.com/dasl-) for [the report](https://github.com/mikebrady/shairport-sync/issues/1513). Version 4.1-dev-468-gcb03d5c6 ==== From fcf2434f47ede40a5b80510284d5e47a762c7257 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:48:00 +0100 Subject: [PATCH 850/878] Update CAR INSTALL.md --- CAR INSTALL.md | 49 +++++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 166eb9f0..19bd8acc 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -116,51 +116,32 @@ Disable both of these services from starting at boot time (this is because we wi #### Configure HostAPD Configure `hostapd` by creating `/etc/hostapd/hostapd.conf` with the following contents which will set up an open network with the name BMW. You might wish to change the name: ``` -# This is the name of the WiFi interface we configured above +# Thanks to https://wiki.gentoo.org/wiki/Hostapd#802.11b.2Fg.2Fn_triple_AP + +# The interface used by the AP interface=wlan0 -# Use the nl80211 driver with the brcmfmac driver -driver=nl80211 - -# This is the name of the network -- yours might be different +# This is the name of the network -- yours may be different ssid=BMW -# Use the 2.4GHz band +# "g" simply means 2.4GHz band hw_mode=g -# Use channel 6 or some other channel of your choice -channel=6 +# Channel to use +channel=11 -# Enable 802.11n +# Limit the frequencies used to those allowed in the country +ieee80211d=1 + +# The country code +country_code=IE + +# Enable 802.11n support ieee80211n=1 -# Enable WMM +# QoS support, also required for full speed on 802.11n/ac/ax wmm_enabled=1 -# Enable 40MHz channels with 20ns guard interval -#ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40] - -# Accept all MAC addresses -macaddr_acl=0 - -# Use WPA authentication -#auth_algs=1 - -# Require clients to know the network name -ignore_broadcast_ssid=0 - -# Use WPA2 -#wpa=2 - -# Use a pre-shared key -#wpa_key_mgmt=WPA-PSK - -# The network passphrase -#wpa_passphrase=none - -# Use AES, instead of TKIP -#rsn_pairwise=CCMP - ``` #### Configure DHCP server From 0e77c117b8d6fa52d27553bda8426b87c39637df Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 12:07:16 +0100 Subject: [PATCH 851/878] Update CAR INSTALL.md --- CAR INSTALL.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 19bd8acc..165a7823 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -3,12 +3,7 @@ If your car audio has an AUX input, you can get AirPlay in your car using Shairp ## The Basic Idea -The basic idea is to use a small Linux computer to create an isolated WiFi network for the car and to run Shairport Sync on it to provide an AirPlay service. The audio goes via a DAC to the AUX input of your car audio system. - -The car WiFi network you create is isolated and local to your car, and since it isn't connected to the Internet, you don't really need to secure it with a password. - -When an iPhone or an iPad with cellular capability is connected to an isolated WiFi network, it can use its cellular connection to access the Internet. -This means it can connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and play the audio through the car network to the AirPlay service provided by Shairport Sync. +The basic idea is to use a small Linux computer to create an isolated WiFi network (a "car network") and run Shairport Sync on the car network to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and send AirPlay audio through the car network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's DAC which is connected to the AUX input of your car audio. Note that Android devices can not, so far, do this trick of using the two networks simultaneously. @@ -143,6 +138,8 @@ ieee80211n=1 wmm_enabled=1 ``` +Note that, since car network is isolated from the Internet, you don't really need to secure it with a password. + #### Configure DHCP server First, replace the contents of `/etc/dhcp/dhcpd.conf` with this: From 0b2ebd2576f04a08e8abb78bb4b09de17aceeffc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 16 Aug 2022 12:08:01 +0100 Subject: [PATCH 852/878] Update CAR INSTALL.md --- CAR INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 165a7823..1d49ea64 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -3,7 +3,7 @@ If your car audio has an AUX input, you can get AirPlay in your car using Shairp ## The Basic Idea -The basic idea is to use a small Linux computer to create an isolated WiFi network (a "car network") and run Shairport Sync on the car network to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and send AirPlay audio through the car network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's DAC which is connected to the AUX input of your car audio. +The basic idea is to use a small Linux computer to create an isolated WiFi network (a "car network") and run Shairport Sync on it to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and send AirPlay audio through the car network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's DAC which is connected to the AUX input of your car audio. Note that Android devices can not, so far, do this trick of using the two networks simultaneously. From 5f92164c4c6c8e5d612db2a5277b7c02b17890f8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 19 Aug 2022 12:25:36 +0100 Subject: [PATCH 853/878] Quieten a bogus warning. --- player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player.c b/player.c index 15ee9671..e8d5dc1e 100644 --- a/player.c +++ b/player.c @@ -1745,7 +1745,7 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; - uint64_t previous_frames_played; + 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 uint64_t previous_corrected_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning int previous_frames_played_valid = 0; From 457cc48f3ab791dc63283bf979b8f88837a7deb7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 19 Aug 2022 21:04:40 +0100 Subject: [PATCH 854/878] Update AIRPLAY2.md --- AIRPLAY2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index d741f6ad..86c5a57b 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -1,5 +1,5 @@ # AirPlay 2 -Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs. +Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs from macOS 10.15 (Catalina) onwards, HomePod minis and Apple TVs. ## What Works - AirPlay 2 audio for iOS, HomePod mini, AppleTV and Mac players. From 352a981be96e53ff294496da10ceffe7b3eceffe Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 19 Aug 2022 21:07:09 +0100 Subject: [PATCH 855/878] Update AIRPLAY2.md --- AIRPLAY2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 86c5a57b..3b9bc7dc 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -14,6 +14,7 @@ Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs f ## What Does Not Work - No AirPlay 2 for Windows iTunes. - Remote control facilities are not implemented. +- AirPlay 2 from macOS prior to 10.15 (Catalina) is not supported. ## General Shairport Sync uses a companion application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP") From 232d3074db86a8302f60149d05987e41043a9efa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 19 Aug 2022 21:11:46 +0100 Subject: [PATCH 856/878] Update TROUBLESHOOTING.md --- TROUBLESHOOTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 791629da..73e9e7b9 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -108,6 +108,7 @@ You have installed Shairport Sync successfully, the daemon is running, you can s You have to allow connections to your Shairport Sync device from remote devices. To do so, after re-enabling UFW (see last step of the previous section), enter the following commands in shell: ``` +sudo ufw allow 319:320/udp sudo ufw allow 3689/tcp sudo ufw allow 5353 sudo ufw allow 5000/tcp @@ -115,6 +116,7 @@ sudo ufw allow 7000/tcp sudo ufw allow 6000:6009/udp sudo ufw allow 35000:65535/udp ``` +Ports 319 and 320 need to be opened for AirPlay 2 only. You can check UFW config by typing `sudo ufw status` in shell. Please make sure that UFW is active, especially if you have deactivated it previously for testing purposes. Check out the [ufw man pages](http://manpages.ubuntu.com/manpages/man8/ufw.8.html) for more. From 8629fb5c82818064a1ab4a6ecea754296b97c6d3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 19 Aug 2022 22:21:51 +0100 Subject: [PATCH 857/878] Update README.md --- ADVANCED TOPICS/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index c681e1ff..48e9fd62 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -5,7 +5,7 @@ Here you will find links to some advanced features and things you can do with Sh * [Metadata](Metadata.md). * [Events](Events.md). * [Statistics](Statistics.md). -* Setting up an [MQTT](MQTT.md) system. +* Setting up an [MQTT](../MQTT.md) system. * [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). * [Car Installation](CAR%20INSTALL.md) – build an isolated WiFi network containing a Shairport Sync player on a Raspberry Pi. Suitable for a car radio with an `aux` input or for the stereo in that broadband-free holiday cottage. From e887ccd8de4a58079fe8f3fb7e5271a20de281a2 Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Sat, 20 Aug 2022 00:02:54 +0100 Subject: [PATCH 858/878] s6 improvements and docker build changes --- .dockerignore | 4 +- .github/ISSUE_TEMPLATE/Bug Report.yaml | 37 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++++ .github/workflows/docker-build-on-push.yaml | 16 ++++++-- .github/workflows/docker-build-on-tag.yaml | 8 ++-- .gitignore | 2 +- README.md | 2 +- docker/Dockerfile | 24 ++++++------ docker/README.md | 2 +- docker/{airplay1 => classic}/Dockerfile | 6 +-- docker/{airplay1 => classic}/README.md | 2 +- docker/{airplay1 => classic}/start.sh | 0 docker/etc/s6-overlay/s6-rc.d/01-dbus/finish | 2 + docker/etc/s6-overlay/s6-rc.d/01-dbus/run | 3 +- docker/etc/s6-overlay/s6-rc.d/02-avahi/finish | 2 + docker/etc/s6-overlay/s6-rc.d/02-avahi/run | 17 ++++----- docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish | 2 + docker/etc/s6-overlay/s6-rc.d/03-nqptp/run | 3 +- .../etc/s6-overlay/s6-rc.d/startup/script.sh | 2 + docker/etc/s6-overlay/s6-rc.d/startup/type | 1 + docker/etc/s6-overlay/s6-rc.d/startup/up | 1 + .../s6-rc.d/user/contents.d/startup | 0 22 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/Bug Report.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml rename docker/{airplay1 => classic}/Dockerfile (97%) rename docker/{airplay1 => classic}/README.md (73%) rename docker/{airplay1 => classic}/start.sh (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/01-dbus/finish create mode 100644 docker/etc/s6-overlay/s6-rc.d/02-avahi/finish create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish create mode 100644 docker/etc/s6-overlay/s6-rc.d/startup/script.sh create mode 100644 docker/etc/s6-overlay/s6-rc.d/startup/type create mode 100644 docker/etc/s6-overlay/s6-rc.d/startup/up create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup diff --git a/.dockerignore b/.dockerignore index 98c4e3ee..f5e8e83a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ .github -documents \ No newline at end of file +documents +docker/Dockerfile +docker/classic/Dockerfile \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yaml b/.github/ISSUE_TEMPLATE/Bug Report.yaml new file mode 100644 index 00000000..11e2f3a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug Report.yaml @@ -0,0 +1,37 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also, what did you expect to happen? + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: version + attributes: + label: Version? + description: What version of Shairport Sync are you running? (`shairport-sync -V`) + validations: + required: true + - type: checkboxes + id: checked-current-issues + attributes: + label: Check previous issues + description: Please check previous issues (including closed ones) for duplicates. + options: + - label: Confirm + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..08744522 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions? + url: https://github.com/mikebrady/shairport-sync/discussions/categories/q-a + about: Please ask and answer questions here. + - name: Feature Requests + url: https://github.com/mikebrady/shairport-sync/discussions/categories/ideas + about: Please submit feature requests/enhancements here. \ No newline at end of file diff --git a/.github/workflows/docker-build-on-push.yaml b/.github/workflows/docker-build-on-push.yaml index 072e9218..dca048e2 100644 --- a/.github/workflows/docker-build-on-push.yaml +++ b/.github/workflows/docker-build-on-push.yaml @@ -1,5 +1,5 @@ # Builds & pushes a docker image when a commit is made to one of the branches specified below. -# Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-airplay1' +# Tag pattern: 'unstable-[BRANCH NAME]' & 'unstable-[BRANCH NAME]-classic' name: Build and push docker image based on commit to specified branches. @@ -43,6 +43,14 @@ jobs: username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + - name: Set tag start (part 1). + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'master' }} + run: echo "IMAGE_TAG_BASE=unstable" >> $GITHUB_ENV + + - name: Set tag start (part 2). + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'master' }} + run: echo "IMAGE_TAG_BASE=unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}" >> $GITHUB_ENV + - name: Build and push uses: docker/build-push-action@v2 with: @@ -50,7 +58,7 @@ jobs: file: ./docker/Dockerfile platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }} + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }} build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} @@ -59,9 +67,9 @@ jobs: uses: docker/build-push-action@v2 with: context: ./ - file: ./docker/airplay1/Dockerfile + file: ./docker/classic/Dockerfile platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_IMAGE_NAME }}:unstable-${{ env.SHAIRPORT_SYNC_BRANCH }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }}-classic build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.github/workflows/docker-build-on-tag.yaml b/.github/workflows/docker-build-on-tag.yaml index d345bd89..6762a9bd 100644 --- a/.github/workflows/docker-build-on-tag.yaml +++ b/.github/workflows/docker-build-on-tag.yaml @@ -2,7 +2,7 @@ # It seems this yaml has to exist on the branch the tag refers to. # The following docker tags are created & pushed: -# [RELEASE TAG], [RELEASE TAG]-airplay1, +# [RELEASE TAG], [RELEASE TAG]-classic, name: Build and push docker images on releases. @@ -65,13 +65,13 @@ jobs: SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} - - name: Build and push (AirPlay 1) + - name: Build and push (Classic) uses: docker/build-push-action@v2 with: context: ./ - file: ./docker/airplay1/Dockerfile + file: ./docker/classic/Dockerfile platforms: ${{ env.DOCKER_PLATFORMS }} push: true - tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}-airplay1 + tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}-classic build-args: | SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 52c49f30..096527a9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ Makefile.in aclocal.m4 autom4te.cache compile -config.* +/config.* configure depcomp install-sh diff --git a/README.md b/README.md index 01e05666..704c8f3e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Shairport Sync does not support AirPlay video or photo streaming. # Quick Start * A building guide is available [here](BUILD.md). -* A Docker image is available on the [Docker Hub](https://hub.docker.com/repository/docker/mikebrady/shairport-sync). +* A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). * Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md). * Runtime settings are documented [here](scripts/shairport-sync.conf). * Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). diff --git a/docker/Dockerfile b/docker/Dockerfile index 2a465a38..cb2df177 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,7 +35,7 @@ RUN apk -U add \ ##### ALAC ##### RUN git clone https://github.com/mikebrady/alac WORKDIR /alac -RUN autoreconf -fi +RUN autoreconf -i RUN ./configure RUN make RUN make install @@ -46,7 +46,7 @@ WORKDIR / RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp RUN git checkout "$NQPTP_BRANCH" -RUN autoreconf -fi +RUN autoreconf -i RUN ./configure RUN make RUN make install @@ -57,18 +57,19 @@ WORKDIR / WORKDIR /shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" -RUN autoreconf -fi -RUN ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-airplay-2 \ +WORKDIR /shairport-sync/build +RUN autoreconf -i ../ +RUN ../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-airplay-2 \ --with-metadata --with-dummy --with-pipe --with-dbus-interface \ --with-stdout --with-mpris-interface --with-mqtt-client \ --with-apple-alac --with-convolution -RUN make -j -RUN make install +RUN make -j $(nproc) +RUN DESTDIR=install make install WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-3.0.0.2 +FROM crazymax/alpine-s6:3.12-3.1.1.2 RUN apk -U add \ alsa-lib \ @@ -88,13 +89,14 @@ RUN apk -U add \ libgcrypt # Copy build files. -COPY --from=builder /usr/local/bin/shairport-sync /usr/local/bin/shairport-sync +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 /etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ -COPY --from=builder /etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ +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/ COPY ./docker/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d +RUN chmod +x /etc/s6-overlay/s6-rc.d/startup/script.sh # Create non-root user for running the container -- running as the user 'shairport-sync' also allows # Shairport Sync to provide the D-Bus and MPRIS interfaces within the container @@ -110,4 +112,4 @@ RUN rm -rf /lib/apk/db/* ENTRYPOINT [ "/init" ] -CMD /usr/local/bin/shairport-sync -vu --statistics \ No newline at end of file +CMD [ "/usr/local/bin/shairport-sync", "-v", "--statistics" ] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 408215fe..12b8f3bb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -44,7 +44,7 @@ docker buildx build --platform linux/arm/v7 -f ./docker/Dockerfile --build-arg S ### AirPlay 1 Only -The AirPlay1 only dockerfile is in the `airplay1` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image built via the AirPlay 2 branch has missing functionality, e.g. it does not work with multiple instances on the same hardware; does not support iTunes for Windows etc. +The AirPlay 1 only dockerfile is in the `classic` folder. This also includes the `start.sh` script used by the container. Please note that the AirPlay 1 image built via the AirPlay 2 branch has missing functionality, e.g. it does not work with multiple instances on the same hardware; does not support iTunes for Windows etc. ### GitHub Action Builds diff --git a/docker/airplay1/Dockerfile b/docker/classic/Dockerfile similarity index 97% rename from docker/airplay1/Dockerfile rename to docker/classic/Dockerfile index 6276f08d..a2c33af5 100644 --- a/docker/airplay1/Dockerfile +++ b/docker/classic/Dockerfile @@ -41,7 +41,7 @@ COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-alac /usr/local/lib/pkgconfig/alac.pc /usr/local/lib/pkgconfig/alac.pc COPY --from=builder-alac /usr/local/include /usr/local/include -WORKDIR shairport-sync +WORKDIR /shairport-sync COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" RUN autoreconf -fi @@ -80,7 +80,7 @@ RUN apk -U add \ libgcc \ libgc++ -RUN rm -rf /lib/apk/db/* +RUN rm -rf /lib/apk/db/* COPY --from=builder-alac /usr/local/lib/libalac.* /usr/local/lib/ COPY --from=builder-sps /etc/shairport-sync* /etc/ @@ -97,7 +97,7 @@ RUN adduser -D shairport-sync -G shairport-sync # Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio -COPY ./docker/airplay1/start.sh / +COPY ./docker/classic/start.sh / RUN chmod +x /start.sh ENTRYPOINT [ "/start.sh" ] diff --git a/docker/airplay1/README.md b/docker/classic/README.md similarity index 73% rename from docker/airplay1/README.md rename to docker/classic/README.md index f8335d76..4dd3bf76 100644 --- a/docker/airplay1/README.md +++ b/docker/classic/README.md @@ -1,3 +1,3 @@ -# AirPlay 1 Only Docker Image +# Classic (AirPlay 1 Only) Docker Image See the [Shairport Sync Docker Hub Repo](https://hub.docker.com/r/mikebrady/shairport-sync) for available tags. diff --git a/docker/airplay1/start.sh b/docker/classic/start.sh similarity index 100% rename from docker/airplay1/start.sh rename to docker/classic/start.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/finish b/docker/etc/s6-overlay/s6-rc.d/01-dbus/finish new file mode 100644 index 00000000..a8222fb0 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-dbus/finish @@ -0,0 +1,2 @@ +#!/command/execlineb -S0 +/run/s6/basedir/bin/halt \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run b/docker/etc/s6-overlay/s6-rc.d/01-dbus/run index 096e093a..8757e5d5 100644 --- a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run +++ b/docker/etc/s6-overlay/s6-rc.d/01-dbus/run @@ -1,3 +1,4 @@ -#!/bin/sh +#!/command/with-contenv sh rm -rf /var/run/dbus.pid +echo "Starting dbus" exec dbus-daemon --system --nofork \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/finish b/docker/etc/s6-overlay/s6-rc.d/02-avahi/finish new file mode 100644 index 00000000..a8222fb0 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-avahi/finish @@ -0,0 +1,2 @@ +#!/command/execlineb -S0 +/run/s6/basedir/bin/halt \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run index e1cdd1c8..fc509297 100644 --- a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run +++ b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run @@ -1,9 +1,8 @@ -#!/bin/sh -avahi-daemon --check -if [ $? -eq 0 ]; then - echo "Reloading avahi-daemon." - exec avahi-daemon --no-chroot --reload -else - echo "Starting avahi-daemon." - exec avahi-daemon --no-chroot -fi \ No newline at end of file +#!/command/with-contenv sh +sleep 5 +while [ ! -f /var/run/dbus.pid ]; do + "dbus is not running, sleeping for 5 seconds before trying to start avahi" + sleep 5 +done +echo "Starting avahi" +exec avahi-daemon --no-chroot \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish new file mode 100644 index 00000000..a8222fb0 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish @@ -0,0 +1,2 @@ +#!/command/execlineb -S0 +/run/s6/basedir/bin/halt \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run index 8b07c68f..7af2c601 100644 --- a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run +++ b/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run @@ -1,2 +1,3 @@ -#!/bin/sh +#!/command/with-contenv sh +echo "Starting nqptp" exec /usr/local/bin/nqptp -v \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/script.sh b/docker/etc/s6-overlay/s6-rc.d/startup/script.sh new file mode 100644 index 00000000..c975a403 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/startup/script.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "STARTING - $(date)" \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/type b/docker/etc/s6-overlay/s6-rc.d/startup/type new file mode 100644 index 00000000..3d92b15f --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/startup/type @@ -0,0 +1 @@ +oneshot \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/up b/docker/etc/s6-overlay/s6-rc.d/startup/up new file mode 100644 index 00000000..0c609ec6 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/startup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/startup/script.sh \ No newline at end of file diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup new file mode 100644 index 00000000..e69de29b From cf5f3ddd8516aed5ba4dfd0dfd214e59f8936e7e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:27:13 +0100 Subject: [PATCH 859/878] Update README.md --- ADVANCED TOPICS/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index 48e9fd62..f2090549 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -7,7 +7,7 @@ Here you will find links to some advanced features and things you can do with Sh * [Statistics](Statistics.md). * Setting up an [MQTT](../MQTT.md) system. * [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). -* [Car Installation](CAR%20INSTALL.md) +* [Car Installation](../CAR%20INSTALL.md) – build an isolated WiFi network containing a Shairport Sync player on a Raspberry Pi. Suitable for a car radio with an `aux` input or for the stereo in that broadband-free holiday cottage. The configuration file -- which contains lots of documentation -- is on your system. By default, the sample configuration file is From b56d2d39cfe7c2c74ee29f6102025776ab58dcab Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 23 Aug 2022 21:49:00 +0100 Subject: [PATCH 860/878] Add avahi-tools --- docker/Dockerfile | 1 + docker/classic/Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index cb2df177..a34d2c50 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -79,6 +79,7 @@ RUN apk -U add \ mbedtls \ soxr \ avahi \ + avahi-tools \ libconfig \ libsndfile \ mosquitto \ diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index a2c33af5..0556cd94 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -73,6 +73,7 @@ RUN apk -U add \ mbedtls \ soxr \ avahi \ + avahi-tools \ libconfig \ libsndfile \ mosquitto-libs \ From b77e06be9e5e15537e2117374029e97eedf47ada Mon Sep 17 00:00:00 2001 From: Charles <32498151+charlesomer@users.noreply.github.com> Date: Tue, 23 Aug 2022 22:05:35 +0100 Subject: [PATCH 861/878] Update readme --- docker/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/README.md b/docker/README.md index 12b8f3bb..acabc123 100644 --- a/docker/README.md +++ b/docker/README.md @@ -21,10 +21,11 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ### Options -Any options you add to the command above will be passed to Shairport Sync. Here is an example: +You can change the default commands passed to Shairport Sync. Here is an example: ``` $ docker run -d --restart unless-stopped --net host --device /dev/snd \ - mikebrady/shairport-sync -a DenSystem -- -d hw:0 -c PCM + mikebrady/shairport-sync:unstable-development shairport-sync -v \ + --statistics -a DenSystem -d hw:0 -c PCM ``` This will sent audio to alsa hardware device `hw:0` and make use of the that device's mixer control called `PCM`. The service will be visible as `DenSystem` on the network. From a617e607cb3f73d0e0e6bac42ce7349fdb2e467b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 31 Aug 2022 12:55:42 +0100 Subject: [PATCH 862/878] Wait for a longer period for a realtime control packet before concluding that the sender is gone. Also, don't exit if the packet is empty. Also check minimum packet size. --- rtp.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/rtp.c b/rtp.c index 247f1689..d215b8a4 100644 --- a/rtp.c +++ b/rtp.c @@ -1730,7 +1730,7 @@ void *rtp_ap2_control_receiver(void *arg) { struct timeval tv; - tv.tv_sec = 3; + tv.tv_sec = 7; // wait this many seconds for a packet, which should come every two seconds tv.tv_usec = 0; setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); @@ -1738,10 +1738,11 @@ void *rtp_ap2_control_receiver(void *arg) { nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); uint64_t time_now = get_absolute_time_in_ns(); - int64_t time_since_start = time_now - start_time; + + // debug(1,"Connection %d: AP2 Control Packet received.", conn->connection_number); - if (nread > 0) { + if (nread >= 28) { // must have at least 28 bytes for the timing information if ((time_since_start < 2000000) && ((packet[0] & 0x10) == 0)) { debug(1, "Dropping what looks like a (non-sentinel) packet left over from a previous session " @@ -1863,18 +1864,19 @@ void *rtp_ap2_control_receiver(void *arg) { } } } - } else if (nread == 0) { - debug(1, "AP2 Control Receiver -- connection closed."); - keep_going = 0; } else { - if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { - if (conn->airplay_stream_type == realtime_stream) { - debug(2, "Connection %d: resetting anchor info", conn->connection_number); - reset_ptp_anchor_info(conn); - packet_number = 0; // start over in allowing the packet to set anchor information + if (nread == -1) { + if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { + if (conn->airplay_stream_type == realtime_stream) { + debug(2, "Connection %d: no control packets for the last 10 seconds -- resetting anchor info", conn->connection_number); + reset_ptp_anchor_info(conn); + packet_number = 0; // start over in allowing the packet to set anchor information + } + } else { + debug(2, "Connection %d: AP2 Control Receiver -- error %d receiving a packet.", conn->connection_number, errno); } } else { - debug(1, "AP2 Control Receiver -- error %d receiving a packet.", errno); + debug(2, "Connection %d: AP2 Control Receiver -- malformed packet, %d bytes long.", conn->connection_number, nread); } } } From 344fa8f8d65dc4934db72666e321e974f43bdd1d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 31 Aug 2022 13:09:44 +0100 Subject: [PATCH 863/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index df34badd..838e2776 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-516-ga617e607 +==== +**Updates and Enhancements** +* The Docker image has been enhanced. Thanks, as always, to [Charles Omer](https://github.com/charlesomer). More changes are on the way here. + +**Bug Fixes** +* With Realtime Streams, if even one timing control packet was missing, Shairport Sync would work as if the sender had disappeared and cause a playback interruption. This has been modified so that it takes at least three missing control packets in a row to conclude that the sender was gone. Thanks to [Devid Leibovic](https://github.com/dasl-) for a [very thorough investigation](https://github.com/mikebrady/shairport-sync/issues/1515) of this issue. + Version 4.1-dev-494-gd34db2a4 ==== * The [CAR INSTALL.md](https://github.com/mikebrady/shairport-sync/blob/development/CAR%20INSTALL.md) guide has been updated for AirPlay 2 and for Raspberry Pi OS (Bullseye). @@ -10,6 +18,9 @@ Version 4.1-dev-468-gcb03d5c6 * The `libao` backend has been updated to address an issue with the system becoming unavailable. The biggest change was to close the driver as soon as playing stops, reopening it when playing restarts. * The `libao` drivers are now listed in the help text. If available, it is suggested that the `alsa` driver be used in preference to the `pulseaudio` driver. +**Bug Fixes** +* The classic AirPlay build now works properly again with `tinysvcmdns` and `dns_sd`. + Version 4.1-dev-466-g05dfcdfd ==== **Enhancements** From 7cbf302666fa09a4322fde3e7489284c5a60e4fb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:09:08 +0100 Subject: [PATCH 864/878] Use pkg-config to find gcrypt and remove a few commented-out lines. --- configure.ac | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index 2283689c..fc12ce99 100644 --- a/configure.ac +++ b/configure.ac @@ -416,13 +416,10 @@ if test "x$with_airplay_2" = "xyes" ; then ]) ]) PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - AC_CHECK_LIB([gcrypt], [gcry_control], [], [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]) -# PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) -# AC_CHECK_LIB([sodium], [sodium_init], ,[AC_MSG_ERROR([AirPlay 2 support requires libsodium -- libsodium-dev suggested])]) + PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) AC_CHECK_LIB([avutil],[av_malloc], [], [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_encoder], [], [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])]) From 6767bde989e1a9b0f7e3b8665c672ac5345c036d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:49:16 +0100 Subject: [PATCH 865/878] Add AC_CHECK_LIB for gcrypt in case PKG_CHECK_MODULES fails to find it. --- configure.ac | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index fc12ce99..ac965045 100644 --- a/configure.ac +++ b/configure.ac @@ -416,8 +416,13 @@ if test "x$with_airplay_2" = "xyes" ; then ]) ]) PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libgcrypt -- libgcrypt-dev suggested)]) - AC_CHECK_LIB([avutil],[av_malloc], [], [AC_MSG_ERROR([Airplay 2 support requires libavutil -- libavutil-dev suggested])]) + PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[ + AC_CHECK_LIB([gcrypt], [gcry_control], [], [ + AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested]) + ]) + ]) + 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([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])]) From 1092a8c19bd3ea0a03d7599abc5ce36be0de5fde Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 10 Sep 2022 10:30:43 +0100 Subject: [PATCH 866/878] Fix compilation and installation under FreeBSD -- changes to allow building in a separate directory broke the FreeBSD build process. --- Makefile.am | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Makefile.am b/Makefile.am index 09fb9692..36bb4d02 100644 --- a/Makefile.am +++ b/Makefile.am @@ -140,12 +140,10 @@ shairport_sync_LDADD += lib_pair_ap.a lib_pair_ap_a_SOURCES = pair_ap/pair.c pair_ap/pair_fruit.c pair_ap/pair_homekit.c pair_ap/pair-tlv.c noinst_LIBRARIES += lib_pair_ap.a plist_xml_strings.h: plists/get_info_response.xml - echo "// Do not edit!" > plist_xml_strings.h - echo "// This file is automatically generated from files in the plists folder." >> plist_xml_strings.h - echo "" >> plist_xml_strings.h - xxd -i $< >> plist_xml_strings.h - echo "" >> plist_xml_strings.h - sed -e 's/[^ ]*plists_get_info_response_xml/plists_get_info_response_xml/g' -i plist_xml_strings.h + printf "// Do not edit!\n" > plist_xml_strings.h + printf "// This file is automatically generated from files in the plists folder.\n\n" >> plist_xml_strings.h + xxd -i $(top_srcdir)/plists/get_info_response.xml - | sed -e 's/[^ ]*plists_get_info_response_xml/plists_get_info_response_xml/g' >> plist_xml_strings.h + printf "\n" >> plist_xml_strings.h plist_xml_strings.c: plist_xml_strings.h touch plist_xml_strings.c @@ -166,8 +164,10 @@ noinst_HEADERS += $(BUILT_SOURCES) # Correctly clean the generated headers, but keep the xml description CLEANFILES += $(BUILT_SOURCES) + + dbus-interface.c: org.gnome.ShairportSync.xml - gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $< + gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $(top_srcdir)/org.gnome.ShairportSync.xml dbus-interface.h: dbus-interface.c touch dbus-interface.h endif @@ -184,7 +184,7 @@ noinst_HEADERS += $(BUILT_SOURCES) CLEANFILES += $(BUILT_SOURCES) mpris-interface.c: org.mpris.MediaPlayer2.xml - gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $< + gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $(top_srcdir)/org.mpris.MediaPlayer2.xml mpris-interface.h: mpris-interface.c touch mpris-interface.h endif @@ -210,8 +210,8 @@ if INSTALL_CONFIG_FILES CONFIG_FILE_INSTALL_TARGET = config-file-install-local $(CONFIG_FILE_INSTALL_TARGET): scripts/shairport-sync.conf install -d $(DESTDIR)$(sysconfdir) - install -m 0644 $< $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample - [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp $< $(DESTDIR)$(sysconfdir)/shairport-sync.conf + install -m 0644 $(top_srcdir)/scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample + [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf if BUILD_FOR_LINUX DBUS_POLICY_DIR = /etc/dbus-1/system.d @@ -231,7 +231,7 @@ DBUS_POLICY_INSTALL_TARGET = dbus-policy-install-local $(DBUS_POLICY_INSTALL_TARGET): $(DBUS_POLICY_FILE) install -d $(DESTDIR)$(DBUS_POLICY_DIR) - install -m 0644 $< $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-dbus.conf + install -m 0644 $(top_srcdir)/$(DBUS_POLICY_FILE) $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-dbus.conf endif # USE_DBUS @@ -247,7 +247,7 @@ MPRIS_POLICY_INSTALL_TARGET = mpris-policy-install-local $(MPRIS_POLICY_INSTALL_TARGET): $(MPRIS_POLICY_FILE) install -d $(DESTDIR)$(DBUS_POLICY_DIR) - install -m 0644 $< $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-mpris.conf + install -m 0644 $(top_srcdir)/$(MPRIS_POLICY_FILE) $(DESTDIR)$(DBUS_POLICY_DIR)/shairport-sync-mpris.conf endif # USE_MPRIS @@ -269,7 +269,7 @@ INSTALL_SYSTEMV_TARGET = install-systemv-local $(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET) install -d $(DESTDIR)$(sysconfdir)/init.d - [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || install -m 0644 $< $(DESTDIR)$(sysconfdir)/init.d + [ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || install -m 0644 $(top_srcdir)/scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d endif # INSTALL_SYSTEMV @@ -285,7 +285,7 @@ INSTALL_SYSTEMD_TARGET = install-systemd-local $(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET) install -d $(DESTDIR)$(systemdsystemunitdir) - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $< $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $(top_srcdir)/cripts/$(SYSTEMD_SERVICE) $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service endif # INSTALL_SYSTEMD @@ -301,7 +301,7 @@ INSTALL_FREEBSD_TARGET = install-freebsd-local $(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-local install -d -o shairport-sync -g shairport-sync $(DESTDIR)/var/run/shairport-sync install -d $(DESTDIR)/usr/local/etc/rc.d/ - install -m 0555 $< $(DESTDIR)/usr/local/etc/rc.d/shairport_sync + install -m 0555 $(top_srcdir)/scripts/shairport-sync.freebsd $(DESTDIR)/usr/local/etc/rc.d/shairport_sync endif # INSTALL_FREEBSD_SERVICE @@ -311,7 +311,7 @@ INSTALL_CYGWIN_TARGET = install-cygwin-local $(INSTALL_CYGWIN_TARGET): scripts/shairport-sync-config install -d $(DESTDIR)/usr/local/bin - [ -e $(DESTDIR)/usr/local/bin/shairport-sync-config ] || install -m 0755 $< $(DESTDIR)/usr/local/bin/ + [ -e $(DESTDIR)/usr/local/bin/shairport-sync-config ] || install -m 0755 $(top_srcdir)/scripts/shairport-sync-config $(DESTDIR)/usr/local/bin/ endif # INSTALL_CYGWIN_SERVICE From 15fcded7110d0aa4fa4bce5c031eba1ef9655b0c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 10 Sep 2022 10:45:08 +0100 Subject: [PATCH 867/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 838e2776..1f192912 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-520-g1092a8c1 +==== +**Enhancements** +* It is now possible to build Shairport Sync in a separate directory, thanks mainly to the work [#1493](https://github.com/mikebrady/shairport-sync/pull/1493) and [#1500](https://github.com/mikebrady/shairport-sync/pull/1500) of [Lukas Rusak](https://github.com/lrusak). A number of subsequent changes were necessary to make it work for FreeBSD. +* Shairport Sync should build on piCore. Thanks to [jackaroo](https://github.com/94jackaroo) for [reporting](https://github.com/mikebrady/shairport-sync/pull/1528) and checking. + Version 4.1-dev-516-ga617e607 ==== **Updates and Enhancements** From 14bda21b686cda449f60fbe810751e41397a7175 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 10 Sep 2022 13:49:33 +0100 Subject: [PATCH 868/878] Add a reference to the source directory. --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 36bb4d02..648b3f72 100644 --- a/Makefile.am +++ b/Makefile.am @@ -211,7 +211,7 @@ CONFIG_FILE_INSTALL_TARGET = config-file-install-local $(CONFIG_FILE_INSTALL_TARGET): scripts/shairport-sync.conf install -d $(DESTDIR)$(sysconfdir) install -m 0644 $(top_srcdir)/scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample - [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf + [ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp $(top_srcdir)/scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf if BUILD_FOR_LINUX DBUS_POLICY_DIR = /etc/dbus-1/system.d From 09f7656c3f5e325cd9ae843dd66e2fc599cbbaea Mon Sep 17 00:00:00 2001 From: Isioma Nnodum Date: Sat, 10 Sep 2022 12:16:54 -0400 Subject: [PATCH 869/878] Corrected path to scripts directory in INSTALL_SYSTEMD_TARGET --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 648b3f72..666047ea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -285,7 +285,7 @@ INSTALL_SYSTEMD_TARGET = install-systemd-local $(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET) install -d $(DESTDIR)$(systemdsystemunitdir) - [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $(top_srcdir)/cripts/$(SYSTEMD_SERVICE) $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service + [ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 $(top_srcdir)/scripts/$(SYSTEMD_SERVICE) $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service endif # INSTALL_SYSTEMD From b178e0e0a5dc631e267920cd84e909b3f14a4285 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 10 Sep 2022 18:48:44 +0100 Subject: [PATCH 870/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 1f192912..02cba47f 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-535-gdafd1726 +==== +**Bug Fix** +* Fix yet another bug in the Makefile.am. Thanks to [Isioma Nnodum](https://github.com/mikebrady/shairport-sync/issues?q=is%3Apr+author%3Aicompuiz) for finding and [fixing](https://github.com/mikebrady/shairport-sync/pull/1536) it. + Version 4.1-dev-520-g1092a8c1 ==== **Enhancements** From fd880056fb47c3d426f25b319f2662f3b3e3fbaa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:19:52 +0100 Subject: [PATCH 871/878] Squashed commit of the following: Add three parameters to the backend play() function call -- (1) a flag indicating whether the samples are timed or not. If timed, (2) the timestamp and (3) the local time at which the first frame should be heard. Update missing pipewire library message. Makefile.am fix. Update the SHM version and fix the SHM name so that it works with FreeBSD. Remove redundant (?) AC_HEADER_STDC check. Fix compilation and installation under FreeBSD -- changes to allow building in a separate directory broke the FreeBSD build process. Add AC_CHECK_LIB for gcrypt in case PKG_CHECK_MODULES fails to find it. Find gcrypt using pkg-config Add code to detect when the RTSP channel goes idle for a period, and use it to reset clocks and timings, etc. so that SPS resumes correctly where the source has gone to sleep while playing a realtime stream and has subsequently woken up. Don't exit if a UDP Clock Control packet is empty. Also check minimum packet size. Remove some very experimental code which may cause memory and port leaks. Strip file path from the filename used in debug messages, information messages, warnings and fatal error messages. BB fix to Makefile.am -- overwriting the DBUS stuff with MPRIS definitions, duh. Fix some problems building on FreeBSD and tidy up the use of the "sed" editor. Fix some problems building on picore. Fix an uninitialised variable start using the "nqptp" SMI --- activity_monitor.c | 13 +- audio.h | 15 +- audio_alsa.c | 160 ++--- audio_ao.c | 4 +- audio_dummy.c | 23 +- audio_jack.c | 92 ++- audio_pa.c | 4 +- audio_pipe.c | 4 +- audio_pw.c | 10 +- audio_sndio.c | 112 +--- audio_soundio.c | 4 +- audio_stdout.c | 4 +- common.c | 60 +- common.h | 2 +- configure.ac | 9 +- dacp.c | 3 +- dbus-service.c | 17 +- mdns_avahi.c | 9 +- mdns_external.c | 12 +- mdns_tinysvcmdns.c | 4 +- nqptp-shm-structures.h | 4 +- player.c | 276 +++++--- player.h | 7 +- ptp-utilities.c | 4 +- ptp-utilities.h | 1 + rtp.c | 1420 ++++++++++++++++++++-------------------- rtsp.c | 145 ++-- shairport.c | 26 +- 28 files changed, 1292 insertions(+), 1152 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 4bda32e2..0de7d341 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 @@ -121,18 +122,16 @@ void activity_monitor_signify_activity(int active) { // So, if the active end procedure is on a timer, it will be executed when the // timeout occurs and the "blocking" status is ignored. - + if ((state == am_inactive) && (player_state == ps_active)) { state = am_active; pthread_mutex_unlock(&activity_monitor_mutex); - going_active( - config.cmd_blocking); + going_active(config.cmd_blocking); } else if ((state == am_active) && (player_state == ps_inactive) && (config.active_state_timeout == 0.0)) { state = am_inactive; pthread_mutex_unlock(&activity_monitor_mutex); - going_inactive( - config.cmd_blocking); + going_inactive(config.cmd_blocking); } else { pthread_mutex_unlock(&activity_monitor_mutex); } @@ -179,7 +178,7 @@ void *activity_monitor_thread_code(void *arg) { // debug(1,"am_state: am_active"); while (player_state != ps_inactive) pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex); - + // if it's not already am_inactive, the it should be beginning to time out... if (state != am_inactive) { state = am_timing_out; diff --git a/audio.h b/audio.h index 5ff14195..583b2bbb 100644 --- a/audio.h +++ b/audio.h @@ -4,6 +4,19 @@ #include #include +// clang-format off +// Play samples provided may be from the source, in which case they will be timed +// or they may be generated by Shairport Sync, in which case they will not be timed. + +// Typically these would be samples of silence, which may be dithered, sent during the lead-in to +// the start of the material, or inserted instead of a missing frame, or after a flush. +// clang-format on + +typedef enum { + play_samples_are_untimed = 0, // typically the samples are (possibly dithered) silence + play_samples_are_timed, // timed and numbered. +} play_samples_type; + typedef struct { double current_volume_dB; int32_t minimum_volume_dB; @@ -24,7 +37,7 @@ typedef struct { void (*start)(int sample_rate, int sample_format); // block of samples - int (*play)(void *buf, int samples); + int (*play)(void *buf, int samples, int sample_type, uint32_t timestamp, uint64_t playtime); void (*stop)(void); // may be null if no implemented diff --git a/audio_alsa.c b/audio_alsa.c index 93c25689..fc53ee65 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -52,32 +52,34 @@ typedef struct { int frame_size; } format_record; -static void help(void); -static int init(int argc, char **argv); -static void deinit(void); -static void start(int i_sample_rate, int i_sample_format); -static int play(void *buf, int samples); -static void stop(void); -static void flush(void); -int delay(long *the_delay); -int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, - uint64_t *frames_sent_to_dac); - -void *alsa_buffer_monitor_thread_code(void *arg); - -static void volume(double vol); -void do_volume(double vol); -int prepare(void); -int do_play(void *buf, int samples); - -static void parameters(audio_parameters *info); -int mute(int do_mute); // returns true if it actually is allowed to use the mute -static double set_volume; -static int output_method_signalled = 0; // for reporting whether it's using mmap or not +int output_method_signalled = 0; // for reporting whether it's using mmap or not int delay_type_notified = -1; // for controlling the reporting of whether the output device can do // precision delays (e.g. alsa->pulsaudio virtual devices can't) int use_monotonic_clock = 0; // this value will be set when the hardware is initialised +static void help(void); +static int init(int argc, char **argv); +static void deinit(void); +static void start(int i_sample_rate, int i_sample_format); +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime); +static void stop(void); +static void flush(void); +static int delay(long *the_delay); +static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, + uint64_t *the_delay, uint64_t *frames_sent_to_dac); + +static void *alsa_buffer_monitor_thread_code(void *arg); + +static void volume(double vol); +static void do_volume(double vol); +static int prepare(void); +static int do_play(void *buf, int samples); + +static void parameters(audio_parameters *info); +static int mute(int do_mute); // returns true if it actually is allowed to use the mute +static double set_volume; audio_output audio_alsa = { .name = "alsa", .help = &help, @@ -98,8 +100,8 @@ audio_output audio_alsa = { .volume = NULL, // a function will be provided if it can do hardware volume .parameters = NULL}; // a function will be provided if it can do hardware volume -static pthread_mutex_t alsa_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_mutex_t alsa_mixer_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t alsa_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t alsa_mixer_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_t alsa_buffer_monitor_thread; @@ -121,7 +123,7 @@ long stall_monitor_frame_count; // set to delay at start of time, increm uint64_t stall_monitor_error_threshold; // if the time is longer than this, it's // an error -static snd_output_t *output = NULL; +snd_output_t *output = NULL; int frame_size; // in bytes for interleaved stereo int alsa_device_initialised; // boolean to ensure the initialisation is only @@ -133,25 +135,25 @@ yndk_type precision_delay_available_status = snd_pcm_t *alsa_handle = NULL; int alsa_handle_status = ENODEV; // if alsa_handle is NULL, this should say why with a unix error code -static snd_pcm_hw_params_t *alsa_params = NULL; -static snd_pcm_sw_params_t *alsa_swparams = NULL; -static snd_ctl_t *ctl = NULL; -static snd_ctl_elem_id_t *elem_id = NULL; -static snd_mixer_t *alsa_mix_handle = NULL; -static snd_mixer_elem_t *alsa_mix_elem = NULL; -static snd_mixer_selem_id_t *alsa_mix_sid = NULL; -static long alsa_mix_minv, alsa_mix_maxv; -static long alsa_mix_mindb, alsa_mix_maxdb; +snd_pcm_hw_params_t *alsa_params = NULL; +snd_pcm_sw_params_t *alsa_swparams = NULL; +snd_ctl_t *ctl = NULL; +snd_ctl_elem_id_t *elem_id = NULL; +snd_mixer_t *alsa_mix_handle = NULL; +snd_mixer_elem_t *alsa_mix_elem = NULL; +snd_mixer_selem_id_t *alsa_mix_sid = NULL; +long alsa_mix_minv, alsa_mix_maxv; +long alsa_mix_mindb, alsa_mix_maxdb; -static char *alsa_out_dev = "default"; -static char *alsa_mix_dev = NULL; -static char *alsa_mix_ctrl = NULL; -static int alsa_mix_index = 0; -static int has_softvol = 0; +char *alsa_out_dev = "default"; +char *alsa_mix_dev = NULL; +char *alsa_mix_ctrl = NULL; +int alsa_mix_index = 0; +int has_softvol = 0; int64_t dither_random_number_store = 0; -static int volume_set_request = 0; // set when an external request is made to set the volume. +int volume_set_request = 0; // set when an external request is made to set the volume. int mixer_volume_setting_gives_mute = 0; // set when it is discovered that // particular mixer volume setting @@ -164,10 +166,10 @@ int volume_based_mute_is_active = // use this to allow the use of snd_pcm_writei or snd_pcm_mmap_writei snd_pcm_sframes_t (*alsa_pcm_write)(snd_pcm_t *, const void *, snd_pcm_uframes_t) = snd_pcm_writei; -int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, - yndk_type *using_update_timestamps); -int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, - yndk_type *using_update_timestamps); +static int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, + yndk_type *using_update_timestamps); +static int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, + yndk_type *using_update_timestamps); // use this to allow the use of standard or precision delay calculations, with standard the, uh, // standard. @@ -182,7 +184,7 @@ int (*delay_and_status)(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, // If you want it to check again, set precision_delay_available_status to YNDK_DONT_KNOW // first. -int precision_delay_available() { +static int precision_delay_available() { if (precision_delay_available_status == YNDK_DONT_KNOW) { // this is very crude -- if the device is a hardware device, then it's assumed the delay is // precise @@ -247,16 +249,16 @@ int precision_delay_available() { int alsa_characteristics_already_listed = 0; -static snd_pcm_uframes_t period_size_requested, buffer_size_requested; -static int set_period_size_request, set_buffer_size_request; +snd_pcm_uframes_t period_size_requested, buffer_size_requested; +int set_period_size_request, set_buffer_size_request; -static uint64_t frames_sent_for_playing; +uint64_t frames_sent_for_playing; // set to true if there has been a discontinuity between the last reported frames_sent_for_playing // and the present reported frames_sent_for_playing // Note that it will be set when the device is opened, as any previous figures for // frames_sent_for_playing (which Shairport Sync might hold) would be invalid. -static int frames_sent_break_occurred; +int frames_sent_break_occurred; static void help(void) { printf(" -d output-device set the output device, default is \"default\".\n" @@ -270,10 +272,10 @@ static void help(void) { debug(2, "error %d executing a script to list alsa hardware device names", r); } -void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } +void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } // ugh -- not static! // assuming pthread cancellation is disabled -int open_mixer() { +static int open_mixer() { int response = 0; if (alsa_mix_ctrl != NULL) { debug(3, "Open Mixer"); @@ -317,7 +319,7 @@ int open_mixer() { } // assuming pthread cancellation is disabled -void close_mixer() { +static void close_mixer() { if (alsa_mix_handle) { snd_mixer_close(alsa_mix_handle); alsa_mix_handle = NULL; @@ -325,7 +327,7 @@ void close_mixer() { } // assuming pthread cancellation is disabled -void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { +static void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0) != 0) { debug(1, "Can't set playback volume accurately to %f dB.", vol); if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, -1) != 0) @@ -386,7 +388,7 @@ sps_format_t auto_format_check_sequence[] = { // assuming pthread cancellation is disabled // if do_auto_setting is true and auto format or auto speed has been requested, // select the settings as appropriate and store them -int actual_open_alsa_device(int do_auto_setup) { +static int actual_open_alsa_device(int do_auto_setup) { // the alsa mutex is already acquired when this is called const snd_pcm_uframes_t minimal_buffer_headroom = 352 * 2; // we accept this much headroom in the hardware buffer, but we'll @@ -867,7 +869,7 @@ int actual_open_alsa_device(int do_auto_setup) { return 0; } -int open_alsa_device(int do_auto_setup) { +static int open_alsa_device(int do_auto_setup) { int result; int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable @@ -876,7 +878,7 @@ int open_alsa_device(int do_auto_setup) { return result; } -int prepare_mixer() { +static int prepare_mixer() { int response = 0; // do any alsa device initialisation (general case) // at present, this is only needed if a hardware mixer is being used @@ -991,7 +993,7 @@ int prepare_mixer() { return response; } -int alsa_device_init() { return prepare_mixer(); } +static int alsa_device_init() { return prepare_mixer(); } static int init(int argc, char **argv) { // for debugging @@ -1383,7 +1385,7 @@ static void deinit(void) { pthread_setcancelstate(oldState, NULL); } -int set_mute_state() { +static int set_mute_state() { int response = 1; // some problem expected, e.g. no mixer or not allowed to use it or disconnected int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable @@ -1436,8 +1438,8 @@ static void start(__attribute__((unused)) int i_sample_rate, } } -int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, - yndk_type *using_update_timestamps) { +static int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, + yndk_type *using_update_timestamps) { int ret = alsa_handle_status; if (using_update_timestamps) *using_update_timestamps = YNDK_NO; @@ -1470,8 +1472,8 @@ int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, return ret; } -int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, - yndk_type *using_update_timestamps) { +static int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, + yndk_type *using_update_timestamps) { snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED; snd_pcm_sframes_t delay_temp = 0; if (using_update_timestamps) @@ -1635,7 +1637,7 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, return ret; } -int delay(long *the_delay) { +static int delay(long *the_delay) { // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or // SND_PCM_STATE_PREPARED // or SND_PCM_STATE_DRAINING @@ -1667,8 +1669,8 @@ int delay(long *the_delay) { return ret; } -int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, - uint64_t *frames_sent_to_dac) { +static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, + uint64_t *the_delay, uint64_t *frames_sent_to_dac) { // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or // SND_PCM_STATE_PREPARED // or SND_PCM_STATE_DRAINING. @@ -1709,7 +1711,7 @@ int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, return ret; } /* -int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { +static int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { // elapsed_time is in nanoseconds int response = 0; // zero means okay if (measurement_data_is_valid) { @@ -1724,7 +1726,7 @@ int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { } */ -int do_play(void *buf, int samples) { +static int do_play(void *buf, int samples) { // assuming the alsa_mutex has been acquired // debug(3,"audio_alsa play called."); int oldState; @@ -1809,7 +1811,7 @@ int do_play(void *buf, int samples) { return ret; } -int do_open(int do_auto_setup) { +static int do_open(int do_auto_setup) { int ret = 0; if (alsa_backend_state != abm_disconnected) debug(1, "alsa: do_open() -- opening the output device when it is already " @@ -1838,7 +1840,7 @@ int do_open(int do_auto_setup) { return ret; } -int do_close() { +static int do_close() { debug(2, "alsa: do_close()"); if (alsa_backend_state == abm_disconnected) debug(1, "alsa: do_close() -- closing the output device when it is already " @@ -1863,7 +1865,7 @@ int do_close() { return derr; } -int sub_flush() { +static int sub_flush() { if (alsa_backend_state == abm_disconnected) debug(1, "alsa: do_flush() -- closing the output device when it is already " "disconnected"); @@ -1882,7 +1884,9 @@ int sub_flush() { return derr; } -int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { // play() will change the state of the alsa_backend_mode to abm_playing // also, if the present alsa_backend_state is abm_disconnected, then first the @@ -1919,7 +1923,7 @@ int play(void *buf, int samples) { return ret; } -int prepare(void) { +static int prepare(void) { // this will leave the DAC open / connected. int ret = 0; @@ -1973,8 +1977,8 @@ static void parameters(audio_parameters *info) { info->maximum_volume_dB = alsa_mix_maxdb; } -void do_volume(double vol) { // caller is assumed to have the alsa_mutex when - // using this function +static void do_volume(double vol) { // caller is assumed to have the alsa_mutex when + // using this function debug(3, "Setting volume db to %f.", vol); int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable @@ -2014,7 +2018,7 @@ void do_volume(double vol) { // caller is assumed to have the alsa_mutex when pthread_setcancelstate(oldState, NULL); } -void volume(double vol) { +static void volume(double vol) { volume_set_request = 1; // an external request has been made to set the volume do_volume(vol); } @@ -2038,7 +2042,7 @@ linear_volume; } */ -int mute(int mute_state_requested) { // these would be for external reasons, not +static int mute(int mute_state_requested) { // these would be for external reasons, not // because of the // state of the backend. mute_requested_externally = mute_state_requested; // request a mute for external reasons @@ -2046,13 +2050,13 @@ int mute(int mute_state_requested) { // these would be for extern return set_mute_state(); } /* -void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void +static void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void *arg) { debug(1, "alsa: alsa_buffer_monitor_thread_cleanup_function called."); } */ -void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { +static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { int frame_count = 0; int error_count = 0; int error_detected = 0; diff --git a/audio_ao.c b/audio_ao.c index 6e203709..64299659 100644 --- a/audio_ao.c +++ b/audio_ao.c @@ -147,7 +147,9 @@ static void start(__attribute__((unused)) int sample_rate, // debug(1,"libao start"); } -static int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { int response = 0; int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable diff --git a/audio_dummy.c b/audio_dummy.c index 70ea755e..4ec1d29d 100644 --- a/audio_dummy.c +++ b/audio_dummy.c @@ -4,7 +4,7 @@ * All rights reserved. * * Modifications for audio synchronisation - * and related work, copyright (c) Mike Brady 2014 + * and related work, copyright (c) Mike Brady 2014 -- 2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -30,6 +30,7 @@ #include "audio.h" #include "common.h" +#include // for PRId64 and friends #include #include #include @@ -41,8 +42,26 @@ static void deinit(void) {} static void start(int sample_rate, __attribute__((unused)) int sample_format) { debug(1, "dummy audio output started at %d frames per second.", sample_rate); } +// clang-format off +// Here is a brief explanation of the parameters: +// sample_type is 'play_samples_are_untimed' or 'play_samples_are_timed' defined in audio.h. +// untimed samples are typically silence used by Shairport Sync itself to set up synchronisation or to fill for missing packets of audio -- the timestamp and playtime are zero. +// timed samples come from the client. +// timestamp is the RTP timestamp given to the first frame by the client +// playtime is when the first frame should to be played. It is given in "local time". +// local time is CLOCK_MONOTONIC_RAW if available, or CLOCK_MONOTONIC otherwise, expressed in nanoseconds as an unsigned 64-bit integer (See get_absolute_time_in_ns() in common.c) +// clang-format on + +static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples, + int sample_type, uint32_t timestamp, uint64_t playtime) { + + // sample code using some of the extra information + if (sample_type == play_samples_are_timed) { + int64_t lead_time = playtime - get_absolute_time_in_ns(); + debug(1, "leadtime for frame %u is %" PRId64 " nanoseconds, i.e. %f seconds.", timestamp, + lead_time, 0.000000001 * lead_time); + } -static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples) { return 0; } diff --git a/audio_jack.c b/audio_jack.c index e87e16a3..4218d245 100644 --- a/audio_jack.c +++ b/audio_jack.c @@ -1,6 +1,6 @@ /* * jack output driver. This file is part of Shairport Sync. - * Copyright (c) 2019 Mike Brady , + * Copyright (c) 2019 -- 2022 Mike Brady <4265913+mikebrady@users.noreply.github.com>, * Jörn Nettingsmeier * * All rights reserved. @@ -40,49 +40,25 @@ typedef jack_default_audio_sample_t sample_t; #define jack_sample_size sizeof(sample_t) // Two-channel, 32bit audio: -static const int bytes_per_frame = NPORTS * jack_sample_size; +const int bytes_per_frame = NPORTS * jack_sample_size; -static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER; - -int jack_init(int, char **); -void jack_deinit(void); -void jack_start(int, int); -int play(void *, int); -void jack_stop(void); -int jack_delay(long *); -void jack_flush(void); - -audio_output audio_jack = {.name = "jack", - .help = NULL, - .init = &jack_init, - .deinit = &jack_deinit, - .prepare = NULL, - .start = &jack_start, - .stop = NULL, - .is_running = NULL, - .flush = &jack_flush, - .delay = &jack_delay, - .stats = NULL, - .play = &play, - .volume = NULL, - .parameters = NULL, - .mute = NULL}; +pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER; // This also affects deinterlacing. // So make it exactly the number of incoming audio channels! -static jack_port_t *port[NPORTS]; -static const char *port_name[NPORTS] = {"out_L", "out_R"}; +jack_port_t *port[NPORTS]; +const char *port_name[NPORTS] = {"out_L", "out_R"}; -static jack_client_t *client; -static jack_nframes_t sample_rate; -static jack_nframes_t jack_latency; +jack_client_t *client; +jack_nframes_t sample_rate; +jack_nframes_t jack_latency; -static jack_ringbuffer_t *jackbuf; -static int flush_please = 0; +jack_ringbuffer_t *jackbuf; +int flush_please = 0; -static jack_latency_range_t latest_latency_range[NPORTS]; -static int64_t time_of_latest_transfer; +jack_latency_range_t latest_latency_range[NPORTS]; +int64_t time_of_latest_transfer; #ifdef CONFIG_SOXR typedef struct soxr_quality { @@ -90,9 +66,9 @@ typedef struct soxr_quality { const char *name; } soxr_quality_t; -static soxr_quality_t soxr_quality_table[] = {{SOXR_VHQ, "very high"}, {SOXR_HQ, "high"}, - {SOXR_MQ, "medium"}, {SOXR_LQ, "low"}, - {SOXR_QQ, "quick"}, {-1, NULL}}; +soxr_quality_t soxr_quality_table[] = {{SOXR_VHQ, "very high"}, {SOXR_HQ, "high"}, + {SOXR_MQ, "medium"}, {SOXR_LQ, "low"}, + {SOXR_QQ, "quick"}, {-1, NULL}}; static int parse_soxr_quality_name(const char *name) { for (soxr_quality_t *s = soxr_quality_table; s->name != NULL; ++s) { @@ -103,9 +79,9 @@ static int parse_soxr_quality_name(const char *name) { return -1; } -static soxr_t soxr = NULL; -static soxr_quality_spec_t quality_spec; -static soxr_io_spec_t io_spec; +soxr_t soxr = NULL; +soxr_quality_spec_t quality_spec; +soxr_io_spec_t io_spec; #endif static inline sample_t sample_conv(short sample) { @@ -204,7 +180,7 @@ static void error(const char *desc) { warn("JACK error: \"%s\"", desc); } // This is the function JACK will call in case of a non-critical event in the library. static void info(const char *desc) { inform("JACK information: \"%s\"", desc); } -int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { +static int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { int i; int bufsz = -1; config.audio_backend_latency_offset = 0; @@ -332,7 +308,7 @@ int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **a return 0; } -void jack_deinit() { +static void jack_deinit() { pthread_mutex_lock(&client_mutex); if (jack_deactivate(client)) warn("Error deactivating jack client"); @@ -348,7 +324,7 @@ void jack_deinit() { #endif } -void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) { +static void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) { // Nothing to do, JACK client has already been set up at jack_init(). // Also, we have no say over the sample rate or sample format of JACK, // We convert the 16bit samples to float, and die if the sample rate is != 44k1 without soxr. @@ -367,13 +343,13 @@ void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) #endif } -void jack_flush() { +static void jack_flush() { debug(2, "Only the consumer can safely flush a lock-free ringbuffer. Asking the" " process callback to do it..."); flush_please = 1; } -int jack_delay(long *the_delay) { +static int jack_delay(long *the_delay) { // Semantics change: we now look at the last transfer into the lock-free // ringbuffer, not into the jack buffers directly (because locking those would // violate real-time constraints). On average, that should lead to just a @@ -399,7 +375,9 @@ int jack_delay(long *the_delay) { return 0; } -int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { jack_ringbuffer_data_t v[2] = {0}; size_t i, j, c; jack_nframes_t thisbuf; @@ -445,3 +423,19 @@ int play(void *buf, int samples) { } return 0; } + +audio_output audio_jack = {.name = "jack", + .help = NULL, + .init = &jack_init, + .deinit = &jack_deinit, + .prepare = NULL, + .start = &jack_start, + .stop = NULL, + .is_running = NULL, + .flush = &jack_flush, + .delay = &jack_delay, + .stats = NULL, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL}; diff --git a/audio_pa.c b/audio_pa.c index 9d37bc60..c91b76f6 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -223,7 +223,9 @@ static void start(__attribute__((unused)) int sample_rate, pa_threaded_mainloop_unlock(mainloop); } -static int play(void *buf, int samples) { +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; diff --git a/audio_pipe.c b/audio_pipe.c index fcece96b..83b9666f 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -66,7 +66,9 @@ static void start(__attribute__((unused)) int sample_rate, } } -static int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { // if the file is not open, try to open it. char errorstring[1024]; if (fd == -1) { diff --git a/audio_pw.c b/audio_pw.c index 016df41d..4271ba09 100644 --- a/audio_pw.c +++ b/audio_pw.c @@ -1,6 +1,6 @@ /* * Asynchronous Pipewire Backend. This file is part of Shairport Sync. - * Copyright (c) Shairport Sync 2021 + * Copyright (c) Shairport Sync 2021--2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -452,8 +452,10 @@ static void flush() { pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); } -static int play(void *buf, int samples) { - struct pw_buffer *pw_buffer; +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { + struct pw_buffer *pw_buffer = NULL; struct spa_buffer *spa_buffer; struct spa_data *spa_data; int ret; @@ -522,4 +524,4 @@ audio_output audio_pw = {.name = "pw", .play = &play, .volume = NULL, .parameters = NULL, - .mute = NULL}; + .mute = NULL}; \ No newline at end of file diff --git a/audio_sndio.c b/audio_sndio.c index 998290e2..a369bfb5 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -28,42 +28,12 @@ #include #include -static void help(void); -static int init(int, char **); -static void onmove_cb(void *, int); -static void deinit(void); -static void start(int, int); -static int play(void *, int); -static void stop(void); -static void onmove_cb(void *, int); -static int delay(long *); -static void flush(void); -int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, - uint64_t *frames_sent_to_dac); - -audio_output audio_sndio = {.name = "sndio", - .help = &help, - .init = &init, - .deinit = &deinit, - .prepare = NULL, - .start = &start, - .stop = &stop, - .is_running = NULL, - .flush = &flush, - .delay = &delay, - .stats = NULL, - .play = &play, - .volume = NULL, - .parameters = NULL, - .mute = NULL}; - static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER; static struct sio_hdl *hdl; static int framesize; static size_t played; static size_t written; uint64_t time_of_last_onmove_cb; -uint64_t corrected_time_of_last_onmove_cb; int at_least_one_onmove_cb_seen; struct sio_par par; @@ -90,18 +60,12 @@ static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, S static void help() { printf(" -d output-device set the output device [default*|...]\n"); } -uint64_t get_uptime_in_ns() { - uint64_t time_now_ns; - struct timespec tn; - clock_gettime(CLOCK_UPTIME_PRECISE, &tn); - uint64_t tnnsec = tn.tv_sec; - tnnsec = tnnsec * 1000000000; - uint64_t tnjnsec = tn.tv_nsec; - time_now_ns = tnnsec + tnjnsec; - return time_now_ns; +void onmove_cb(__attribute__((unused)) void *arg, int delta) { + time_of_last_onmove_cb = get_absolute_time_in_ns(); + at_least_one_onmove_cb_seen = 1; + played += delta; } - static int init(int argc, char **argv) { int found, opt, round, rate, bufsz; unsigned int i; @@ -136,7 +100,7 @@ static int init(int argc, char **argv) { devname = SIO_DEVANY; if (config_lookup_int(config.cfg, "sndio.rate", &rate)) { if (rate % 44100 == 0 && rate >= 44100 && rate <= 352800) { - par.rate = rate; + par.rate = rate; } else { die("sndio: output rate must be a multiple of 44100 and 44100 <= rate <= " "352800"); @@ -188,15 +152,15 @@ static int init(int argc, char **argv) { pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1); // pthread_mutex_lock(&sndio_mutex); debug(1, "sndio: output device name is \"%s\".", devname); - debug(1, "sndio: rate: %u.",par.rate); - debug(1, "sndio: bits: %u.",par.bits); + debug(1, "sndio: rate: %u.", par.rate); + debug(1, "sndio: bits: %u.", par.bits); hdl = sio_open(devname, SIO_PLAY, 0); if (!hdl) die("sndio: cannot open audio device"); written = played = 0; - time_of_last_onmove_cb = 0; + time_of_last_onmove_cb = 0; at_least_one_onmove_cb_seen = 0; for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) { @@ -265,7 +229,9 @@ static void start(__attribute__((unused)) int sample_rate, pthread_cleanup_pop(1); // unlock the mutex } -static int play(void *buf, int frames) { +static int play(void *buf, int frames, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { if (frames > 0) { // pthread_mutex_lock(&sndio_mutex); pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1); @@ -285,20 +251,13 @@ static void stop() { pthread_cleanup_pop(1); // unlock the mutex } -static void onmove_cb(__attribute__((unused)) void *arg, int delta) { - time_of_last_onmove_cb = get_uptime_in_ns(); // this is not (?) adjusted ("disciplined") by NTP - corrected_time_of_last_onmove_cb = get_monotonic_time_in_ns(); // this is ("disciplined") by NTP - at_least_one_onmove_cb_seen = 1; - played += delta; -} - int get_delay(long *delay) { int response = 0; size_t estimated_extra_frames_output = 0; if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made // calculate the difference in time between now and when the last callback occurred, // and use it to estimate the frames that would have been output - uint64_t time_difference = get_uptime_in_ns() - time_of_last_onmove_cb; + uint64_t time_difference = get_absolute_time_in_ns() - time_of_last_onmove_cb; uint64_t frame_difference = (time_difference * par.rate) / 1000000000; estimated_extra_frames_output = frame_difference; // sanity check -- total estimate can not exceed frames written. @@ -332,35 +291,18 @@ static void flush() { pthread_cleanup_pop(1); // unlock the mutex } -/* -// this doesn't seem to be very accurate, so not using it. -int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay, - uint64_t *frames_sent_to_dac) { - // returns 0 if the device is in a valid state - // returns the actual delay if running or 0 if prepared in *the_delay - // returns the present estimated value of frames played - // otherwise return a non-zero value - int ret = 0; - *the_delay = 0; - - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - long my_delay = 0; // initialised to stop a compiler warning - pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1); - *raw_measurement_time = time_of_last_onmove_cb; - *corrected_measurement_time = corrected_time_of_last_onmove_cb; // this is ("disciplined") by NTP - get_delay(&my_delay); - ret = frames_sent_break_occurred; // will be zero unless an error like an underrun occurred - frames_sent_break_occurred = 0; // reset it. - if (frames_sent_to_dac != NULL) - *frames_sent_to_dac = frames_sent_for_playing; - pthread_cleanup_pop(1); // unlock the mutex - pthread_setcancelstate(oldState, NULL); - uint64_t hd = my_delay; // note: snd_pcm_sframes_t is a long - *the_delay = hd; - if (at_least_one_onmove_cb_seen == 0) - ret = 1; - return ret; -} -*/ - +audio_output audio_sndio = {.name = "sndio", + .help = &help, + .init = &init, + .deinit = &deinit, + .prepare = NULL, + .start = &start, + .stop = &stop, + .is_running = NULL, + .flush = &flush, + .delay = &delay, + .stats = NULL, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL}; diff --git a/audio_soundio.c b/audio_soundio.c index ccf44bf7..83347b5b 100644 --- a/audio_soundio.c +++ b/audio_soundio.c @@ -164,7 +164,9 @@ static void start(int sample_rate, int sample_format) { debug(1, "libsoundio output started\n"); } -static int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { // int err; int free_bytes = soundio_ring_buffer_free_count(ring_buffer); int written_bytes = 0; diff --git a/audio_stdout.c b/audio_stdout.c index 61fdc744..61d91bfa 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -43,7 +43,9 @@ static void start(__attribute__((unused)) int sample_rate, fd = STDOUT_FILENO; } -static int play(void *buf, int samples) { +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { char errorstring[1024]; int warned = 0; int rc = write(fd, buf, samples * 4); diff --git a/common.c b/common.c index e522a681..dbbbef60 100644 --- a/common.c +++ b/common.c @@ -62,12 +62,12 @@ #endif #ifdef COMPILE_FOR_OSX -#include -#include -#include #include #include #include +#include +#include +#include #endif #ifdef CONFIG_OPENSSL @@ -455,9 +455,10 @@ char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss return insertion_point; } -void _die(const char *filename, const int linenumber, const char *format, ...) { +void _die(const char *thefilename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + char b[16384]; b[0] = 0; char *s; @@ -468,9 +469,12 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); + char *basec = strdup(thefilename); + char *filename = basename(basec); s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " *fatal error: "); + free(basec); } else { strncpy(b, "fatal error: ", sizeof(b)); s = b + strlen(b); @@ -485,7 +489,7 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { exit(EXIT_FAILURE); } -void _warn(const char *filename, const int linenumber, const char *format, ...) { +void _warn(const char *thefilename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); char b[16384]; @@ -498,9 +502,12 @@ void _warn(const char *filename, const int linenumber, const char *format, ...) uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); + char *basec = strdup(thefilename); + char *filename = basename(basec); s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " *warning: "); + free(basec); } else { strncpy(b, "warning: ", sizeof(b)); s = b + strlen(b); @@ -513,11 +520,12 @@ void _warn(const char *filename, const int linenumber, const char *format, ...) pthread_setcancelstate(oldState, NULL); } -void _debug(const char *filename, const int linenumber, int level, const char *format, ...) { +void _debug(const char *thefilename, const int linenumber, int level, const char *format, ...) { if (level > debuglev) return; int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + char b[16384]; b[0] = 0; pthread_mutex_lock(&debug_timing_lock); @@ -526,9 +534,12 @@ void _debug(const char *filename, const int linenumber, int level, const char *f uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); + char *basec = strdup(thefilename); + char *filename = basename(basec); char *s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " "); + free(basec); va_list args; va_start(args, format); vsnprintf(s, sizeof(b) - (s - b), format, args); @@ -537,7 +548,7 @@ void _debug(const char *filename, const int linenumber, int level, const char *f pthread_setcancelstate(oldState, NULL); } -void _inform(const char *filename, const int linenumber, const char *format, ...) { +void _inform(const char *thefilename, const int linenumber, const char *format, ...) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); char b[16384]; @@ -550,9 +561,12 @@ void _inform(const char *filename, const int linenumber, const char *format, ... uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); + char *basec = strdup(thefilename); + char *filename = basename(basec); s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " "); + free(basec); } else { s = b; } @@ -1242,11 +1256,11 @@ uint64_t get_monotonic_time_in_ns() { if (sTimebaseInfo.denom == 0) die("could not initialise Mac timebase info in get_monotonic_time_in_ns()."); - // Do the maths. We hope that the multiplication doesn't - // overflow; the price you pay for working in fixed point. + // Do the maths. We hope that the multiplication doesn't + // overflow; the price you pay for working in fixed point. - // this gives us nanoseconds - time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; #endif return time_now_ns; @@ -1306,8 +1320,8 @@ uint64_t get_absolute_time_in_ns() { if (sTimebaseInfo.denom == 0) die("could not initialise Mac timebase info in get_absolute_time_in_ns()."); - // this gives us nanoseconds - time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; #endif return time_now_ns; @@ -1520,21 +1534,20 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char return pthread_mutex_lock(mutex); int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - if (debuglevel != 0) + if (debuglevel != 0) _debug(filename, line, 3, "mutex_lock \"%s\".", mutexname); // only if you really ask for it! int result = sps_pthread_mutex_timedlock(mutex, dally_time); if (result == ETIMEDOUT) { - _debug(filename, line, debuglevel, - "mutex_lock \"%s\" failed to lock after %f ms -- now waiting unconditionally to lock it.", - mutexname, dally_time * 1E-3); + _debug( + filename, line, debuglevel, + "mutex_lock \"%s\" failed to lock after %f ms -- now waiting unconditionally to lock it.", + mutexname, dally_time * 1E-3); result = pthread_mutex_lock(mutex); if (result == 0) - _debug(filename, line, debuglevel, - " ...mutex_lock \"%s\" locked successfully.", - mutexname); + _debug(filename, line, debuglevel, " ...mutex_lock \"%s\" locked successfully.", mutexname); else - _debug(filename, line, debuglevel, - " ...mutex_lock \"%s\" exited with error code: %u", mutexname, result); + _debug(filename, line, debuglevel, " ...mutex_lock \"%s\" exited with error code: %u", + mutexname, result); } pthread_setcancelstate(oldState, NULL); return result; @@ -1954,7 +1967,7 @@ int get_device_id(uint8_t *id, int int_length) { } else { t = id; int found = 0; - + for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) { #ifdef AF_PACKET if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { @@ -1980,7 +1993,6 @@ int get_device_id(uint8_t *id, int int_length) { } #endif #endif - } freeifaddrs(ifaddr); } diff --git a/common.h b/common.h index ffacb9f1..0d5ae2e7 100644 --- a/common.h +++ b/common.h @@ -37,7 +37,7 @@ typedef enum { typedef enum { TOE_normal, TOE_emergency, - TOE_dbus // a dbus request was made -- don't wait for the dbus thread to exit + TOE_dbus // a dbus request was made -- don't wait for the dbus thread to exit } type_of_exit_type; #define sps_extra_code_output_stalled 32768 diff --git a/configure.ac b/configure.ac index ac965045..b0f53cd9 100644 --- a/configure.ac +++ b/configure.ac @@ -75,21 +75,18 @@ fi AC_ARG_WITH([dummy],[AS_HELP_STRING([--with-dummy],[include the dummy audio back end])]) if test "x$with_dummy" = "xyes" ; then - AC_MSG_RESULT(include the dummy audio back end) AC_DEFINE([CONFIG_DUMMY], 1, [Include a fake audio backend.]) fi AM_CONDITIONAL([USE_DUMMY], [test "x$with_dummy" = "xyes" ]) AC_ARG_WITH([stdout],[AS_HELP_STRING([--with-stdout],[include the stdout audio back end])]) if test "x$with_stdout" = "xyes" ; then - AC_MSG_RESULT(include the stdout audio back end) AC_DEFINE([CONFIG_STDOUT], 1, [Include an audio backend to output to standard output (stdout).]) fi AM_CONDITIONAL([USE_STDOUT], [test "x$with_stdout" = "xyes"]) AC_ARG_WITH([pipe],[AS_HELP_STRING([--with-pipe],[include the pipe audio back end])]) if test "x$with_pipe" = "xyes" ; then - AC_MSG_RESULT(include the pipe audio back end) AC_DEFINE([CONFIG_PIPE], 1, [Include an audio backend to output to a unix pipe.]) fi AM_CONDITIONAL([USE_PIPE], [test "x$with_pipe" = "xyes" ]) @@ -320,9 +317,9 @@ if test "x$with_pw" = "xyes" ; then PKG_CHECK_MODULES( [PIPEWIRE], [libpipewire-0.3 >= 0.3.24], [CFLAGS="${PIPEWIRE_CFLAGS} ${CFLAGS} -Wno-missing-field-initializers" LIBS="${PIPEWIRE_LIBS} ${LIBS}"], - [AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library!)]) + [AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!)]) else - AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library.)) + AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!)) fi fi AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"]) @@ -409,7 +406,6 @@ 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_MSG_RESULT(>>Include libraries required 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!) @@ -456,7 +452,6 @@ fi AM_CONDITIONAL([HAVE_XMLTOMAN], [test -n "$XMLTOMAN"]) # Checks for header files. -AC_HEADER_STDC AC_CHECK_HEADERS([getopt_long.h]) AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h mach/mach.h memory.h netdb.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h sys/time.h syslog.h unistd.h]) diff --git a/dacp.c b/dacp.c index 1355fdc6..73164ae2 100644 --- a/dacp.c +++ b/dacp.c @@ -206,7 +206,8 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { pthread_cleanup_push(addrinfo_cleanup, (void *)&res); // only do this one at a time -- not sure it is necessary, but better safe than sorry - // int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command, 1); + // int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command, + // 1); int mutex_reply = debug_mutex_lock(&dacp_conversation_lock, 2000000, 1); // int mutex_reply = pthread_mutex_lock(&dacp_conversation_lock); if (mutex_reply == 0) { diff --git a/dbus-service.c b/dbus-service.c index c68aac9b..c461e12a 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -572,17 +572,17 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton, } gboolean notify_volume_callback(ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { gdouble iv = shairport_sync_get_volume(skeleton); if (((iv >= -30.0) && (iv <= 0.0)) || (iv == -144.0)) { debug(2, ">> setting volume to %7.4f.", iv); - + lock_player(); config.airplay_volume = iv; if (playing_conn != NULL) player_volume(iv, playing_conn); unlock_player(); - + } else { debug(1, ">> invalid volume: %f. Ignored.", iv); shairport_sync_set_volume(skeleton, config.airplay_volume); @@ -805,8 +805,7 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv return TRUE; } -static gboolean on_handle_drop_session(ShairportSync *skeleton, - GDBusMethodInvocation *invocation, +static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvocation *invocation, __attribute__((unused)) gpointer user_data) { if (playing_conn != NULL) debug(1, ">> stopping current play session"); @@ -862,16 +861,16 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name G_CALLBACK(notify_loudness_threshold_callback), NULL); g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance", G_CALLBACK(notify_drift_tolerance_callback), NULL); - g_signal_connect(shairportSyncSkeleton, "notify::volume", - G_CALLBACK(notify_volume_callback), NULL); + g_signal_connect(shairportSyncSkeleton, "notify::volume", G_CALLBACK(notify_volume_callback), + NULL); g_signal_connect(shairportSyncSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL); g_signal_connect(shairportSyncSkeleton, "handle-remote-command", G_CALLBACK(on_handle_remote_command), NULL); - g_signal_connect(shairportSyncSkeleton, "handle-drop-session", - G_CALLBACK(on_handle_drop_session), NULL); + g_signal_connect(shairportSyncSkeleton, "handle-drop-session", G_CALLBACK(on_handle_drop_session), + NULL); g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity", G_CALLBACK(notify_verbosity_callback), NULL); diff --git a/mdns_avahi.c b/mdns_avahi.c index 6f66181d..c154ec60 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -47,11 +47,10 @@ #include #include - void threaded_poll_unlock(void *arg) { avahi_threaded_poll_unlock((AvahiThreadedPoll *)arg); } -#define pthread_avahi_threaded_poll_lock_and_push(mu) \ - avahi_threaded_poll_lock(mu); \ +#define pthread_avahi_threaded_poll_lock_and_push(mu) \ + avahi_threaded_poll_lock(mu); \ pthread_cleanup_push(threaded_poll_unlock, (void *)mu) #define check_avahi_response(debugLevelArg, veryUnLikelyArgumentName) \ @@ -347,8 +346,8 @@ static int avahi_update(char **txt_records, char **secondary_txt_records) { else selected_interface = AVAHI_IF_UNSPEC; pthread_avahi_threaded_poll_lock_and_push(tpoll); - //avahi_threaded_poll_lock(tpoll); - //pthread_cleanup_push(threaded_poll_unlock, (void *)tpoll) + // avahi_threaded_poll_lock(tpoll); + // pthread_cleanup_push(threaded_poll_unlock, (void *)tpoll) if (txt_records != NULL) { if (text_record_string_list) diff --git a/mdns_external.c b/mdns_external.c index cf4d215a..9d4bff1c 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -81,9 +81,9 @@ static int fork_execvp(const char *file, char *const argv[]) { } static int mdns_external_avahi_register(char *ap1name, __attribute__((unused)) char *ap2name, - __attribute__((unused)) int port, - __attribute__((unused)) char **txt_records, - __attribute__((unused)) char **secondary_txt_records) { + __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); @@ -123,9 +123,9 @@ static int mdns_external_avahi_register(char *ap1name, __attribute__((unused)) c } static int mdns_external_dns_sd_register(char *ap1name, __attribute__((unused)) char *ap2name, - __attribute__((unused)) int port, - __attribute__((unused)) char **txt_records, - __attribute__((unused)) char **secondary_txt_records) { + __attribute__((unused)) int port, + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { char mdns_port[6]; snprintf(mdns_port, sizeof(mdns_port), "%d", config.port); diff --git a/mdns_tinysvcmdns.c b/mdns_tinysvcmdns.c index 516160da..b11de208 100644 --- a/mdns_tinysvcmdns.c +++ b/mdns_tinysvcmdns.c @@ -39,8 +39,8 @@ static struct mdnsd *svr = NULL; static int mdns_tinysvcmdns_register(char *ap1name, __attribute__((unused)) char *ap2name, int port, - __attribute__((unused)) char **txt_records, - __attribute__((unused)) char **secondary_txt_records) { + __attribute__((unused)) char **txt_records, + __attribute__((unused)) char **secondary_txt_records) { struct ifaddrs *ifalist; struct ifaddrs *ifa; diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index f3861b44..3dbe0c90 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -20,7 +20,9 @@ #ifndef NQPTP_SHM_STRUCTURES_H #define NQPTP_SHM_STRUCTURES_H -#define NQPTP_SHM_STRUCTURES_VERSION 7 +#define NQPTP_INTERFACE_NAME "/nqptp" + +#define NQPTP_SHM_STRUCTURES_VERSION 8 #define NQPTP_CONTROL_PORT 9000 // The control port expects a UDP packet with the first space-delimited string diff --git a/player.c b/player.c index e8d5dc1e..68a3b5bf 100644 --- a/player.c +++ b/player.c @@ -4,7 +4,7 @@ * All rights reserved. * * Modifications for audio synchronisation, AirPlay 2 - * and related work, copyright (c) Mike Brady 2014 -- 2021 + * and related work, copyright (c) Mike Brady 2014 -- 2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -119,7 +119,7 @@ int32_t modulo_32_offset(uint32_t from, uint32_t to) { return to - from; } void do_flush(uint32_t timestamp, rtsp_conn_info *conn); -static void ab_resync(rtsp_conn_info *conn) { +void ab_resync(rtsp_conn_info *conn) { int i; for (i = 0; i < BUFFER_FRAMES; i++) { conn->audio_buffer[i].ready = 0; @@ -326,7 +326,7 @@ static int init_alac_decoder(int32_t fmtp[12], rtsp_conn_info *conn) { // We are going to go on that basis - // clang-format on + // clang-format on alac_file *alac; @@ -408,7 +408,7 @@ void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occup void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn) { - + // if it's original format, it has a valid seqno and must be decoded // otherwise, it can take the next seqno and doesn't need decoding. @@ -897,6 +897,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->flush_rtp_timestamp = 0; debug_mutex_unlock(&conn->flush_mutex, 0); } + debug_mutex_lock(&conn->flush_mutex, 1000, 0); pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); if (conn->flush_requested == 1) { @@ -921,40 +922,92 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); if ((firstPacket != NULL) && (firstPacket->ready)) { - // discard flushes more than 10 seconds into the future -- they are probably bogus uint32_t first_frame_in_buffer = firstPacket->given_timestamp; - int32_t offset_from_first_frame = - (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); - if (offset_from_first_frame > (int)conn->input_rate * 10) { - debug( - 1, - "flush request: sanity check -- flush frame %u is too far into the future from " - "the first frame %u -- discarded.", - conn->flush_rtp_timestamp, first_frame_in_buffer); - drop_request = 1; - } else { - if ((lastPacket != NULL) && (lastPacket->ready)) { - // we have enough information to check if the flush is needed or can be discarded - uint32_t last_frame_in_buffer = - lastPacket->given_timestamp + lastPacket->length - 1; - // now we have to work out if the flush frame is in the buffer - // if it is later than the end of the buffer, flush everything and keep the - // request active. if it is in the buffer, we need to flush part of the buffer. - // Actually we flush the entire buffer and drop the request. if it is before the - // buffer, no flush is needed. Drop the request. - if (offset_from_first_frame > 0) { - int32_t offset_to_last_frame = - (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); - if (offset_to_last_frame >= 0) { + int32_t offset_from_first_frame = conn->flush_rtp_timestamp - first_frame_in_buffer; + if ((lastPacket != NULL) && (lastPacket->ready)) { + // we have enough information to check if the flush is needed or can be discarded + uint32_t last_frame_in_buffer = + lastPacket->given_timestamp + lastPacket->length - 1; + + // clang-format off + // Now we have to work out if the flush frame is in the buffer. + + // If it is later than the end of the buffer, flush everything and keep the + // request active. + + // If it is in the buffer, we need to flush part of the buffer. + // (Actually we flush the entire buffer and drop the request.) + + // If it is before the buffer, no flush is needed. Drop the request. + // clang-format on + + if (offset_from_first_frame > 0) { + int32_t offset_to_last_frame = last_frame_in_buffer - conn->flush_rtp_timestamp; + if (offset_to_last_frame >= 0) { + debug(2, + "flush request: flush frame %u active -- buffer contains %u frames, from " + "%u to %u.", + conn->flush_rtp_timestamp, + last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, + last_frame_in_buffer); + + // We need to drop all complete frames leading up to the frame containing + // the flush request frame. + int32_t offset_to_flush_frame = 0; + abuf_t *current_packet = NULL; + do { + current_packet = conn->audio_buffer + BUFIDX(conn->ab_read); + if (current_packet != NULL) { + uint32_t last_frame_in_current_packet = + current_packet->given_timestamp + current_packet->length - 1; + offset_to_flush_frame = + conn->flush_rtp_timestamp - last_frame_in_current_packet; + if (offset_to_flush_frame > 0) { + debug(2, + "flush to %u request: flush buffer %u, from " + "%u to %u. ab_write is: %u.", + conn->flush_rtp_timestamp, conn->ab_read, + current_packet->given_timestamp, + current_packet->given_timestamp + current_packet->length - 1, + conn->ab_write); + conn->ab_read++; + } + } else { + debug(1, "NULL current_packet"); + } + } while ((current_packet == NULL) || (offset_to_flush_frame > 0)); + // now remove any frames from the buffer that are before the flush frame itself. + int32_t frames_to_remove = + conn->flush_rtp_timestamp - current_packet->given_timestamp; + if (frames_to_remove > 0) { + debug(2, "%u frames to remove from current buffer", frames_to_remove); + void *dest = (void *)current_packet->data; + void *source = dest + conn->input_bytes_per_frame * frames_to_remove; + size_t frames_remaining = (current_packet->length - frames_to_remove); + memmove(dest, source, frames_remaining * conn->input_bytes_per_frame); + current_packet->given_timestamp = conn->flush_rtp_timestamp; + current_packet->length = frames_remaining; + } + debug( + 2, + "flush request: flush frame %u complete -- buffer contains %u frames, from " + "%u to %u -- flushed to %u in buffer %u, with %u frames remaining.", + conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, + first_frame_in_buffer, last_frame_in_buffer, + current_packet->given_timestamp, conn->ab_read, + last_frame_in_buffer - current_packet->given_timestamp + 1); + drop_request = 1; + } else { + if (conn->flush_rtp_timestamp == last_frame_in_buffer + 1) { debug( 2, - "flush request: flush frame %u active -- buffer contains %u frames, from " + "flush request: flush frame %u completed -- buffer contained %u frames, " + "from " "%u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); drop_request = 1; - flush_needed = 1; } else { debug(2, "flush request: flush frame %u pending -- buffer contains %u frames, " @@ -963,18 +1016,17 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); - flush_needed = 1; } - } else { - debug(2, - "flush request: flush frame %u expired -- buffer contains %u frames, " - "from %u " - "to %u", - conn->flush_rtp_timestamp, - last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, - last_frame_in_buffer); - drop_request = 1; + flush_needed = 1; } + } else { + debug(2, + "flush request: flush frame %u expired -- buffer contains %u frames, " + "from %u " + "to %u", + conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, + first_frame_in_buffer, last_frame_in_buffer); + drop_request = 1; } } } @@ -999,14 +1051,56 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { dac_delay = 0; } if (drop_request) { - debug(2, "flush request: request dropped."); conn->flush_requested = 0; conn->flush_rtp_timestamp = 0; conn->flush_output_flushed = 0; } pthread_cleanup_pop(1); // unlock the conn->flush_mutex + + // skip out-of-date frames, and even more if we haven't seen the first frame + int out_of_date = 1; + uint32_t should_be_frame; + + uint64_t time_to_aim_for = local_time_now; + uint64_t desired_lead_time = 120000000; + if (conn->first_packet_timestamp == 0) + time_to_aim_for = time_to_aim_for + desired_lead_time; + + while ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0) && (out_of_date != 0)) { + abuf_t *thePacket = conn->audio_buffer + BUFIDX(conn->ab_read); + if ((thePacket != NULL) && (thePacket->ready)) { + local_time_to_frame(time_to_aim_for, &should_be_frame, conn); + // debug(1,"should_be frame is %u.",should_be_frame); + int32_t frame_difference = thePacket->given_timestamp - should_be_frame; + if (frame_difference < 0) { + debug(2, "Dropping out of date packet %u with timestamp %u. Lead time is %f seconds.", + conn->ab_read, thePacket->given_timestamp, + frame_difference * 1.0 / 44100.0 + desired_lead_time * 0.000000001); + conn->ab_read++; + } else { + if (conn->first_packet_timestamp == 0) + debug(2, "Accepting packet %u with timestamp %u. Lead time is %f seconds.", + conn->ab_read, thePacket->given_timestamp, + frame_difference * 1.0 / 44100.0 + desired_lead_time * 0.000000001); + out_of_date = 0; + } + } else { + debug(2, "Packet %u empty or not ready.", conn->ab_read); + conn->ab_read++; + } + } + if (conn->ab_synced) { curframe = conn->audio_buffer + BUFIDX(conn->ab_read); + if (curframe != NULL) { + uint64_t should_be_time; + frame_to_local_time(curframe->given_timestamp, &should_be_time, conn); + int64_t time_difference = should_be_time - local_time_now; + debug(3, "Check packet from buffer %u, timestamp %u, %f seconds ahead.", conn->ab_read, + curframe->given_timestamp, 0.000000001 * time_difference); + } else { + debug(3, "Check packet from buffer %u, empty.", conn->ab_read); + } if ((conn->ab_read != conn->ab_write) && (curframe->ready)) { // it could be synced and empty, under @@ -1034,13 +1128,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->first_packet_timestamp = curframe->given_timestamp; // we will keep buffering until we are // supposed to start playing this -#ifdef CONFIG_METADATA - // say we have started receiving frames here - debug(2, "pffr"); - send_ssnc_metadata( - 'pffr', NULL, 0, - 0); // "first frame received", but don't wait if the queue is locked -#endif + // Here, calculate when we should start playing. We need to know when to allow the // packets to be sent to the player. @@ -1064,24 +1152,16 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int64_t lt = conn->first_packet_time_to_play - local_time_now; - if (lt < 100000000) { - debug(2, - "Connection %d: Short lead time for first frame %" PRId64 - ": %f seconds. Flushing 0.5 seconds", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); - do_flush(conn->first_packet_timestamp + 5 * 4410, conn); - } else { - debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", - conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); - } - /* - int64_t lateness = local_time_now - conn->first_packet_time_to_play; - if (lateness > 0) { - debug(1, "First packet is %" PRId64 " nanoseconds late! Flushing 0.5 - seconds", lateness); - - } - */ + // can't be too late because we skipped late packets already, FLW. + debug(2, "Connection %d: Lead time for first frame %" PRId64 ": %f seconds.", + conn->connection_number, conn->first_packet_timestamp, lt * 0.000000001); +#ifdef CONFIG_METADATA + // say we have started receiving frames here + debug(2, "pffr"); + send_ssnc_metadata( + 'pffr', NULL, 0, + 0); // "first frame received", but don't wait if the queue is locked +#endif } if (conn->first_packet_time_to_play != 0) { @@ -1168,7 +1248,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->previous_random_number = generate_zero_frames( silence, fs, config.output_format, conn->enable_dither, conn->previous_random_number); - config.output->play(silence, fs); + config.output->play(silence, fs, play_samples_are_untimed, 0, 0); debug(3, "Sent %" PRId64 " frames of silence", fs); free(silence); have_sent_prefiller_silence = 1; @@ -1213,7 +1293,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->previous_random_number = generate_zero_frames(silence, fs, config.output_format, conn->enable_dither, conn->previous_random_number); - config.output->play(silence, fs); + config.output->play(silence, fs, play_samples_are_untimed, 0, 0); free(silence); } frame_gap -= fs; @@ -1299,7 +1379,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // reset_input_flow_metrics(conn); // don't do a full flush parameters reset conn->initial_reference_time = 0; conn->initial_reference_timestamp = 0; - conn->first_packet_timestamp = 0; // make sure the first packet isn't late + conn->first_packet_timestamp = 0; // make sure the first packet isn't late } do_wait = 1; } @@ -1311,9 +1391,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (conn->input_rate == 0) die("input_rate is zero -- should never happen!"); uint64_t time_to_wait_for_wakeup_ns = - 1000000000 / conn->input_rate; // this is time period of one frame + 1000000000 / conn->input_rate; // this is time period of one frame time_to_wait_for_wakeup_ns *= 12 * 352; // two full 352-frame packets - time_to_wait_for_wakeup_ns /= 3; // two thirds of a packet time + time_to_wait_for_wakeup_ns /= 3; // two thirds of a packet time #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns; @@ -1580,7 +1660,7 @@ int ap2_realtime_synced_stream_statistics_print_profile[] = {2, 2, 2, 0, 2, 1, int ap2_realtime_nosync_stream_statistics_print_profile[] = {2, 0, 0, 0, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0}; int ap2_realtime_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 0, 2, 1, 1, 2, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0}; -int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 2, 2, 0, 0}; +int ap2_buffered_synced_stream_statistics_print_profile[] = {2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 2, 2, 0, 0}; int ap2_buffered_nosync_stream_statistics_print_profile[] = {2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}; int ap2_buffered_nodelay_stream_statistics_print_profile[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}; // clang-format on @@ -1644,8 +1724,8 @@ void player_thread_cleanup_handler(void *arg) { ":%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); + 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 ":%02" PRId64 ".", @@ -1746,8 +1826,10 @@ void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; 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 - uint64_t previous_corrected_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning + uint64_t previous_raw_measurement_time = + 0; // initialised to avoid a "possibly uninitialised" warning + uint64_t previous_corrected_measurement_time = + 0; // initialised to avoid a "possibly uninitialised" warning int previous_frames_played_valid = 0; // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg); @@ -2146,7 +2228,8 @@ void *player_thread_func(void *arg) { conn->previous_random_number = generate_zero_frames( silence, conn->max_frames_per_packet * conn->output_sample_ratio, config.output_format, conn->enable_dither, conn->previous_random_number); - config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio); + config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio, + play_samples_are_untimed, 0, 0); free(silence); } } else if (conn->play_number_after_flush < 10) { @@ -2168,7 +2251,8 @@ void *player_thread_func(void *arg) { conn->previous_random_number = generate_zero_frames( silence, conn->max_frames_per_packet * conn->output_sample_ratio, config.output_format, conn->enable_dither, conn->previous_random_number); - config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio); + config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio, + play_samples_are_untimed, 0, 0); free(silence); } } else { @@ -2458,7 +2542,8 @@ void *player_thread_func(void *arg) { #endif statistics_item("Nominal FPS", "%*.2f", 11, conn->remote_frame_rate); statistics_item("Received FPS", "%*.2f", 12, conn->input_frame_rate); - // only make the next two columns appear if we are getting stats information from the back end + // only make the next two columns appear if we are getting stats information from + // the back end if (config.output->stats) { if (conn->frame_rate_valid) { statistics_item("Output FPS (r)", "%*.2f", 14, conn->raw_frame_rate); @@ -2543,7 +2628,8 @@ void *player_thread_func(void *arg) { conn->connection_number); } } else { - if (resp != -EBUSY) // delay errors can be reported if the device is (hopefully temporarily) busy + if (resp != -EBUSY) // delay errors can be reported if the device is (hopefully + // temporarily) busy debug(1, "Delay error %d when checking running latency.", resp); } } @@ -2572,10 +2658,11 @@ void *player_thread_func(void *arg) { // interpreted as a signed number, should yield the difference _and_ the ordering. sync_error = should_be_frame - will_be_frame; // this is done in int64_t form - + // int64_t t_ping = should_be_frame - conn->anchor_rtptime; // if (t_ping < 0) - // debug(1, "Frame %" PRIu64 " is %" PRId64 " frames before anchor time %" PRIu64 ".", should_be_frame, -t_ping, conn->anchor_rtptime); + // debug(1, "Frame %" PRIu64 " is %" PRId64 " frames before anchor time %" PRIu64 ".", + // should_be_frame, -t_ping, conn->anchor_rtptime); // sign-extend the r-bit unsigned int calculation by treating it as an r-bit signed // integer @@ -2628,7 +2715,8 @@ void *player_thread_func(void *arg) { "final sync adjustment: %" PRId64 " silent frames added with a bias of %" PRId64 " frames.", -sync_error, first_frame_early_bias); - config.output->play(final_adjustment_silence, final_adjustment_length_sized); + config.output->play(final_adjustment_silence, final_adjustment_length_sized, + play_samples_are_untimed, 0, 0); free(final_adjustment_silence); } else { warn("Failed to allocate memory for a final_adjustment_silence buffer of %d " @@ -2690,11 +2778,12 @@ void *player_thread_func(void *arg) { (int64_t)(config.resyncthreshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { debug(1, - "Large positive sync error of %" PRId64 + "Large positive (i.e. late) sync error of %" PRId64 " frames (%f seconds), at frame: %" PRIu32 ".", sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); - //debug(1, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", + // debug(1, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " + // frames.", // frames_sent_for_play, actual_delay); // the sync error is output frames, but we have to work out how many source frames // to drop there may be a multiple (the conn->output_sample_ratio) of output frames @@ -2714,11 +2803,11 @@ void *player_thread_func(void *arg) { } else if ((sync_error < 0) && ((-sync_error) > filler_length)) { debug(1, - "Large negative sync error of %" PRId64 + "Large negative (i.e. early) sync error of %" PRId64 " frames (%f seconds), at frame: %" PRIu32 ".", sync_error, (sync_error * 1.0) / config.output_rate, inframe->given_timestamp); - debug(1, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", + debug(3, "%" PRId64 " frames sent to DAC. DAC buffer contains %" PRId64 " frames.", frames_sent_for_play, actual_delay); int64_t silence_length = -sync_error; if (silence_length > (filler_length * 5)) @@ -2732,7 +2821,8 @@ void *player_thread_func(void *arg) { conn->enable_dither, conn->previous_random_number); debug(2, "Play a silence of %d frames.", silence_length_sized); - config.output->play(long_silence, silence_length_sized); + config.output->play(long_silence, silence_length_sized, play_samples_are_untimed, + 0, 0); free(long_silence); } else { warn("Failed to allocate memory for a long_silence buffer of %d frames for a " @@ -2917,7 +3007,10 @@ void *player_thread_func(void *arg) { generate_zero_frames(conn->outbuf, play_samples, config.output_format, conn->enable_dither, conn->previous_random_number); } - config.output->play(conn->outbuf, play_samples); + 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); } } @@ -2962,7 +3055,10 @@ void *player_thread_func(void *arg) { generate_zero_frames(conn->outbuf, play_samples, config.output_format, conn->enable_dither, conn->previous_random_number); } - config.output->play(conn->outbuf, play_samples); // remove the (short*)! + 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); } } diff --git a/player.h b/player.h index 54d648dd..1c18567a 100644 --- a/player.h +++ b/player.h @@ -128,7 +128,6 @@ typedef struct { int is_encrypted; } pair_cipher_bundle; // cipher context and buffers - typedef struct { struct pair_setup_context *setup_ctx; struct pair_verify_context *verify_ctx; @@ -151,6 +150,7 @@ typedef struct flush_request_t { typedef struct { int connection_number; // for debug ID purposes, nothing else... int resend_interval; // this is really just for debugging + int rtsp_link_is_idle; // if true, this indicates if the client asleep char *UserAgent; // free this on teardown int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency int latency_warning_issued; @@ -287,6 +287,9 @@ typedef struct { // this is what connects an rtp timestamp to the remote time + int udp_clock_is_initialised; + int udp_clock_sender_is_initialised; + int anchor_remote_info_is_valid; // these can be modified if the master clock changes over time @@ -426,6 +429,8 @@ void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occup int32_t modulo_32_offset(uint32_t from, uint32_t to); +void ab_resync(rtsp_conn_info *conn); + int player_prepare_to_play(rtsp_conn_info *conn); int player_play(rtsp_conn_info *conn); int player_stop(rtsp_conn_info *conn); diff --git a/ptp-utilities.c b/ptp-utilities.c index c7d88aa7..559b62ea 100644 --- a/ptp-utilities.c +++ b/ptp-utilities.c @@ -39,12 +39,10 @@ #endif #define __STDC_FORMAT_MACROS #include "common.h" +#include "ptp-utilities.h" #include #include -#include "nqptp-shm-structures.h" -#include "ptp-utilities.h" - int shm_fd; void *mapped_addr = NULL; diff --git a/ptp-utilities.h b/ptp-utilities.h index 86d2b499..761a5cae 100644 --- a/ptp-utilities.h +++ b/ptp-utilities.h @@ -27,6 +27,7 @@ #define __PTP_UTILITIES_H #include "config.h" +#include "nqptp-shm-structures.h" #include int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint64_t *raw_offset, diff --git a/rtp.c b/rtp.c index d215b8a4..ce1376c4 100644 --- a/rtp.c +++ b/rtp.c @@ -333,228 +333,242 @@ void *rtp_control_receiver(void *arg) { ssize_t nread; while (1) { nread = recv(conn->control_socket, packet, sizeof(packet), 0); + if (conn->rtsp_link_is_idle == 0) { + if (nread >= 0) { + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { - if (nread >= 0) { + ssize_t plen = nread; + if (packet[1] == 0xd4) { // sync data + // clang-format off + /* + // the following stanza is for debugging only -- normally commented out. + { + char obf[4096]; + char *obfp = obf; + int obfc; + for (obfc = 0; obfc < plen; obfc++) { + snprintf(obfp, 3, "%02X", packet[obfc]); + obfp += 2; + }; + *obfp = 0; - if ((config.diagnostic_drop_packet_fraction == 0.0) || - (drand48() > config.diagnostic_drop_packet_fraction)) { - ssize_t plen = nread; - if (packet[1] == 0xd4) { // sync data - /* - // the following stanza is for debugging only -- normally commented out. - { - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc = 0; obfc < plen; obfc++) { - snprintf(obfp, 3, "%02X", packet[obfc]); - obfp += 2; - }; - *obfp = 0; - - - // get raw timestamp information - // I think that a good way to understand these timestamps is that - // (1) the rtlt below is the timestamp of the frame that should be playing at the - // client-time specified in the packet if there was no delay - // and (2) that the rt below is the timestamp of the frame that should be playing - // at the client-time specified in the packet on this device taking account of - // the delay - // Thus, (3) the latency can be calculated by subtracting the second from the - // first. - // There must be more to it -- there something missing. - - // In addition, it seems that if the value of the short represented by the second - // pair of bytes in the packet is 7 - // then an extra time lag is expected to be added, presumably by - // the AirPort Express. - - // Best guess is that this delay is 11,025 frames. - - uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency - uint32_t rt = nctohl(&packet[16]); // raw timestamp - - uint32_t fl = nctohs(&packet[2]); // - - debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, - giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); - //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " - respectively.",monotonic_timestamp(rt, conn),monotonic_timestamp(rtlt, conn)); - } - */ - if (conn->local_to_remote_time_difference) { // need a time packet to be interchanged - // first... - uint64_t ps, pn; + // get raw timestamp information + // I think that a good way to understand these timestamps is that + // (1) the rtlt below is the timestamp of the frame that should be playing at the + // client-time specified in the packet if there was no delay + // and (2) that the rt below is the timestamp of the frame that should be playing + // at the client-time specified in the packet on this device taking account of + // the delay + // Thus, (3) the latency can be calculated by subtracting the second from the + // first. + // There must be more to it -- there something missing. - ps = nctohl(&packet[8]); - ps = ps * 1000000000; // this many nanoseconds from the whole seconds - pn = nctohl(&packet[12]); - pn = pn * 1000000000; - pn = pn >> 32; // this many nanoseconds from the fractional part - remote_time_of_sync = ps + pn; + // In addition, it seems that if the value of the short represented by the second + // pair of bytes in the packet is 7 + // then an extra time lag is expected to be added, presumably by + // the AirPort Express. - // debug(1,"Remote Sync Time: " PRIu64 "",remote_time_of_sync); + // Best guess is that this delay is 11,025 frames. - sync_rtp_timestamp = nctohl(&packet[16]); - uint32_t rtp_timestamp_less_latency = nctohl(&packet[4]); + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency + uint32_t rt = nctohl(&packet[16]); // raw timestamp - // debug(1,"Sync timestamp is %u.",ntohl(*((uint32_t *)&packet[16]))); + uint32_t fl = nctohs(&packet[2]); // - if (config.userSuppliedLatency) { - if (config.userSuppliedLatency != conn->latency) { - debug(1, "Using the user-supplied latency: %" PRIu32 ".", - config.userSuppliedLatency); - } - conn->latency = config.userSuppliedLatency; - } else { + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, + giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); + //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " + respectively.",monotonic_timestamp(rt, conn),monotonic_timestamp(rtlt, conn)); + } + */ + // clang-format off + if (conn->local_to_remote_time_difference) { // need a time packet to be interchanged + // first... + uint64_t ps, pn; - // It seems that the second pair of bytes in the packet indicate whether a fixed - // delay of 11,025 frames should be added -- iTunes set this field to 7 and - // AirPlay sets it to 4. + ps = nctohl(&packet[8]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[12]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + remote_time_of_sync = ps + pn; - // However, on older versions of AirPlay, the 11,025 frames seem to be necessary too + // debug(1,"Remote Sync Time: " PRIu64 "",remote_time_of_sync); - // The value of 11,025 (0.25 seconds) is a guess based on the "Audio-Latency" - // parameter - // returned by an AE. + sync_rtp_timestamp = nctohl(&packet[16]); + uint32_t rtp_timestamp_less_latency = nctohl(&packet[4]); - // Sigh, it would be nice to have a published protocol... + // debug(1,"Sync timestamp is %u.",ntohl(*((uint32_t *)&packet[16]))); - uint16_t flags = nctohs(&packet[2]); - uint32_t la = sync_rtp_timestamp - rtp_timestamp_less_latency; // note, this might - // loop around in - // modulo. Not sure if - // you'll get an error! - // debug(1, "Latency from the sync packet is %" PRIu32 " frames.", la); - - if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) || - ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) { - la += config.fixedLatencyOffset; - // debug(1, "Latency offset by %" PRIu32" frames due to the source flags and version - // giving a latency of %" PRIu32 " frames.", config.fixedLatencyOffset, la); - } - if ((conn->maximum_latency) && (conn->maximum_latency < la)) - la = conn->maximum_latency; - if ((conn->minimum_latency) && (conn->minimum_latency > la)) - la = conn->minimum_latency; - - const uint32_t max_frames = ((3 * BUFFER_FRAMES * 352) / 4) - 11025; - - if (la > max_frames) { - warn("An out-of-range latency request of %" PRIu32 - " frames was ignored. Must be %" PRIu32 - " frames or less (44,100 frames per second). " - "Latency remains at %" PRIu32 " frames.", - la, max_frames, conn->latency); + if (config.userSuppliedLatency) { + if (config.userSuppliedLatency != conn->latency) { + debug(1, "Using the user-supplied latency: %" PRIu32 ".", + config.userSuppliedLatency); + } + conn->latency = config.userSuppliedLatency; } else { - // here we have the latency but it does not yet account for the - // audio_backend_latency_offset - int32_t latency_offset = - (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + // It seems that the second pair of bytes in the packet indicate whether a fixed + // delay of 11,025 frames should be added -- iTunes set this field to 7 and + // AirPlay sets it to 4. - // debug(1,"latency offset is %" PRId32 ", input rate is %u", latency_offset, - // conn->input_rate); - int32_t adjusted_latency = latency_offset + (int32_t)la; - if ((adjusted_latency < 0) || - (adjusted_latency > - (int32_t)(conn->max_frames_per_packet * - (BUFFER_FRAMES - config.minimum_free_buffer_headroom)))) - warn("audio_backend_latency_offset out of range -- ignored."); - else - la = adjusted_latency; + // However, on older versions of AirPlay, the 11,025 frames seem to be necessary too - if (la != conn->latency) { - conn->latency = la; - debug(2, - "New latency: %" PRIu32 ", sync latency: %" PRIu32 - ", minimum latency: %" PRIu32 ", maximum " - "latency: %" PRIu32 ", fixed offset: %" PRIu32 - ", audio_backend_latency_offset: %f.", - conn->latency, sync_rtp_timestamp - rtp_timestamp_less_latency, - conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset, - config.audio_backend_latency_offset); + // The value of 11,025 (0.25 seconds) is a guess based on the "Audio-Latency" + // parameter + // returned by an AE. + + // Sigh, it would be nice to have a published protocol... + + uint16_t flags = nctohs(&packet[2]); + uint32_t la = sync_rtp_timestamp - rtp_timestamp_less_latency; // note, this might + // loop around in + // modulo. Not sure if + // you'll get an error! + // debug(1, "Latency from the sync packet is %" PRIu32 " frames.", la); + + if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) || + ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) { + la += config.fixedLatencyOffset; + // debug(1, "Latency offset by %" PRIu32" frames due to the source flags and version + // giving a latency of %" PRIu32 " frames.", config.fixedLatencyOffset, la); + } + if ((conn->maximum_latency) && (conn->maximum_latency < la)) + la = conn->maximum_latency; + if ((conn->minimum_latency) && (conn->minimum_latency > la)) + la = conn->minimum_latency; + + const uint32_t max_frames = ((3 * BUFFER_FRAMES * 352) / 4) - 11025; + + if (la > max_frames) { + warn("An out-of-range latency request of %" PRIu32 + " frames was ignored. Must be %" PRIu32 + " frames or less (44,100 frames per second). " + "Latency remains at %" PRIu32 " frames.", + la, max_frames, conn->latency); + } else { + + // here we have the latency but it does not yet account for the + // audio_backend_latency_offset + int32_t latency_offset = + (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + + // debug(1,"latency offset is %" PRId32 ", input rate is %u", latency_offset, + // conn->input_rate); + int32_t adjusted_latency = latency_offset + (int32_t)la; + if ((adjusted_latency < 0) || + (adjusted_latency > + (int32_t)(conn->max_frames_per_packet * + (BUFFER_FRAMES - config.minimum_free_buffer_headroom)))) + warn("audio_backend_latency_offset out of range -- ignored."); + else + la = adjusted_latency; + + if (la != conn->latency) { + conn->latency = la; + debug(2, + "New latency: %" PRIu32 ", sync latency: %" PRIu32 + ", minimum latency: %" PRIu32 ", maximum " + "latency: %" PRIu32 ", fixed offset: %" PRIu32 + ", audio_backend_latency_offset: %f.", + conn->latency, sync_rtp_timestamp - rtp_timestamp_less_latency, + conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset, + config.audio_backend_latency_offset); + } } } - } - // here, we apply the latency to the sync_rtp_timestamp + // here, we apply the latency to the sync_rtp_timestamp - sync_rtp_timestamp = sync_rtp_timestamp - conn->latency; + sync_rtp_timestamp = sync_rtp_timestamp - conn->latency; - debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); + debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); - if (conn->initial_reference_time == 0) { - if (conn->packet_count_since_flush > 0) { - conn->initial_reference_time = remote_time_of_sync; - conn->initial_reference_timestamp = sync_rtp_timestamp; - } - } else { - uint64_t remote_frame_time_interval = - conn->anchor_time - - conn->initial_reference_time; // here, this should never be zero - if (remote_frame_time_interval) { - conn->remote_frame_rate = - (1.0E9 * (conn->anchor_rtptime - conn->initial_reference_timestamp)) / - remote_frame_time_interval; + if (conn->initial_reference_time == 0) { + if (conn->packet_count_since_flush > 0) { + conn->initial_reference_time = remote_time_of_sync; + conn->initial_reference_timestamp = sync_rtp_timestamp; + } } else { - conn->remote_frame_rate = 0.0; // use as a flag. + uint64_t remote_frame_time_interval = + conn->anchor_time - + conn->initial_reference_time; // here, this should never be zero + if (remote_frame_time_interval) { + conn->remote_frame_rate = + (1.0E9 * (conn->anchor_rtptime - conn->initial_reference_timestamp)) / + remote_frame_time_interval; + } else { + conn->remote_frame_rate = 0.0; // use as a flag. + } } + + // this is for debugging + uint64_t old_remote_reference_time = conn->anchor_time; + uint32_t old_reference_timestamp = conn->anchor_rtptime; + // int64_t old_latency_delayed_timestamp = conn->latency_delayed_timestamp; + if (conn->anchor_remote_info_is_valid != 0) { + int64_t time_difference = remote_time_of_sync - conn->anchor_time; + int32_t frame_difference = sync_rtp_timestamp - conn->anchor_rtptime; + double time_difference_in_frames = (1.0 * time_difference * conn->input_rate) / 1000000000; + double frame_change = frame_difference - time_difference_in_frames; + debug(2,"AP1 control thread: set_ntp_anchor_info: rtptime: %" PRIu32 ", networktime: %" PRIx64 ", frame adjustment: %7.3f.", sync_rtp_timestamp, remote_time_of_sync, frame_change); + } else { + debug(2,"AP1 control thread: set_ntp_anchor_info: rtptime: %" PRIu32 ", networktime: %" PRIx64 ".", sync_rtp_timestamp, remote_time_of_sync); + } + + conn->anchor_time = remote_time_of_sync; + // conn->reference_timestamp_time = + // remote_time_of_sync - local_to_remote_time_difference_now(conn); + conn->anchor_rtptime = sync_rtp_timestamp; + conn->anchor_remote_info_is_valid = 1; + + + conn->latency_delayed_timestamp = rtp_timestamp_less_latency; + debug_mutex_unlock(&conn->reference_time_mutex, 0); + + conn->reference_to_previous_time_difference = + remote_time_of_sync - old_remote_reference_time; + if (old_reference_timestamp == 0) + conn->reference_to_previous_frame_difference = 0; + else + conn->reference_to_previous_frame_difference = + sync_rtp_timestamp - old_reference_timestamp; + } else { + debug(2, "Sync packet received before we got a timing packet back."); } + } else if (packet[1] == 0xd6) { // resent audio data in the control path -- whaale only? + pktp = packet + 4; + plen -= 4; + seq_t seqno = ntohs(*(uint16_t *)(pktp + 2)); + debug(3, "Control Receiver -- Retransmitted Audio Data Packet %u received.", seqno); - // this is for debugging - uint64_t old_remote_reference_time = conn->anchor_time; - uint32_t old_reference_timestamp = conn->anchor_rtptime; - // int64_t old_latency_delayed_timestamp = conn->latency_delayed_timestamp; + uint32_t actual_timestamp = ntohl(*(uint32_t *)(pktp + 4)); - conn->anchor_time = remote_time_of_sync; - // conn->reference_timestamp_time = - // remote_time_of_sync - local_to_remote_time_difference_now(conn); - conn->anchor_rtptime = sync_rtp_timestamp; - conn->latency_delayed_timestamp = rtp_timestamp_less_latency; - debug_mutex_unlock(&conn->reference_time_mutex, 0); + pktp += 12; + plen -= 12; - conn->reference_to_previous_time_difference = - remote_time_of_sync - old_remote_reference_time; - if (old_reference_timestamp == 0) - conn->reference_to_previous_frame_difference = 0; - else - conn->reference_to_previous_frame_difference = - sync_rtp_timestamp - old_reference_timestamp; - } else { - debug(2, "Sync packet received before we got a timing packet back."); - } - } else if (packet[1] == 0xd6) { // resent audio data in the control path -- whaale only? - pktp = packet + 4; - plen -= 4; - seq_t seqno = ntohs(*(uint16_t *)(pktp + 2)); - debug(3, "Control Receiver -- Retransmitted Audio Data Packet %u received.", seqno); - - uint32_t actual_timestamp = ntohl(*(uint32_t *)(pktp + 4)); - - pktp += 12; - plen -= 12; - - // check if packet contains enough content to be reasonable - if (plen >= 16) { - player_put_packet(1, seqno, actual_timestamp, pktp, plen, - conn); // the '1' means is original format - continue; - } else { - debug(3, "Too-short retransmitted audio packet received in control port, ignored."); - } - } else - debug(1, "Control Receiver -- Unknown RTP packet of type 0x%02X length %d, ignored.", - packet[1], nread); + // check if packet contains enough content to be reasonable + if (plen >= 16) { + player_put_packet(1, seqno, actual_timestamp, pktp, plen, + conn); // the '1' means is original format + continue; + } else { + debug(3, "Too-short retransmitted audio packet received in control port, ignored."); + } + } else + debug(1, "Control Receiver -- Unknown RTP packet of type 0x%02X length %d, ignored.", + packet[1], nread); + } else { + debug(3, "Control Receiver -- dropping a packet to simulate a bad network."); + } } else { - debug(3, "Control Receiver -- dropping a packet to simulate a bad network."); - } - } else { - char em[1024]; - strerror_r(errno, em, sizeof(em)); - debug(1, "Control Receiver -- error %d receiving a packet: \"%s\".", errno, em); + char em[1024]; + strerror_r(errno, em, sizeof(em)); + debug(1, "Control Receiver -- error %d receiving a packet: \"%s\".", errno, em); + } } } debug(1, "Control RTP thread \"normal\" exit -- this can't happen. Hah!"); @@ -591,42 +605,51 @@ void *rtp_timing_sender(void *arg) { conn->time_ping_count = 0; while (1) { - // debug(1,"Send a timing request"); - - if (!conn->rtp_running) - debug(1, "rtp_timing_sender called without active stream in RTSP conversation thread %d!", - conn->connection_number); - - // debug(1, "Requesting ntp timestamp exchange."); - - req.filler = 0; - req.origin = req.receive = req.transmit = 0; - - conn->departure_time = get_absolute_time_in_ns(); - socklen_t msgsize = sizeof(struct sockaddr_in); -#ifdef AF_INET6 - if (conn->rtp_client_timing_socket.SAFAMILY == AF_INET6) { - msgsize = sizeof(struct sockaddr_in6); - } -#endif - if ((config.diagnostic_drop_packet_fraction == 0.0) || - (drand48() > config.diagnostic_drop_packet_fraction)) { - if (sendto(conn->timing_socket, &req, sizeof(req), 0, - (struct sockaddr *)&conn->rtp_client_timing_socket, msgsize) == -1) { - char em[1024]; - strerror_r(errno, em, sizeof(em)); - debug(1, "Error %d using send-to to the timing socket: \"%s\".", errno, em); + if (conn->rtsp_link_is_idle == 0) { + if (conn->udp_clock_sender_is_initialised == 0) { + request_number = 0; + conn->udp_clock_sender_is_initialised = 1; + debug(2,"AP1 clock sender thread: initialised."); } + // debug(1,"Send a timing request"); + + if (!conn->rtp_running) + debug(1, "rtp_timing_sender called without active stream in RTSP conversation thread %d!", + conn->connection_number); + + // debug(1, "Requesting ntp timestamp exchange."); + + req.filler = 0; + req.origin = req.receive = req.transmit = 0; + + conn->departure_time = get_absolute_time_in_ns(); + socklen_t msgsize = sizeof(struct sockaddr_in); + #ifdef AF_INET6 + if (conn->rtp_client_timing_socket.SAFAMILY == AF_INET6) { + msgsize = sizeof(struct sockaddr_in6); + } + #endif + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + if (sendto(conn->timing_socket, &req, sizeof(req), 0, + (struct sockaddr *)&conn->rtp_client_timing_socket, msgsize) == -1) { + char em[1024]; + strerror_r(errno, em, sizeof(em)); + debug(1, "Error %d using send-to to the timing socket: \"%s\".", errno, em); + } + } else { + debug(3, "Timing Sender Thread -- dropping outgoing packet to simulate bad network."); + } + + request_number++; + + if (request_number <= 3) + usleep(300000); // these are thread cancellation points + else + usleep(3000000); } else { - debug(3, "Timing Sender Thread -- dropping outgoing packet to simulate bad network."); + usleep(100000); // wait until sleep is over } - - request_number++; - - if (request_number <= 6) - usleep(300000); // these are thread cancellation points - else - usleep(3000000); } debug(3, "rtp_timing_sender thread interrupted. This should never happen."); pthread_cleanup_pop(0); // don't execute anything here. @@ -682,8 +705,6 @@ void *rtp_timing_receiver(void *arg) { uint64_t distant_receive_time, distant_transmit_time, arrival_time, return_time; local_to_remote_time_jitter = 0; local_to_remote_time_jitter_count = 0; - // uint64_t first_remote_time = 0; - // uint64_t first_local_time = 0; uint64_t first_local_to_remote_time_difference = 0; @@ -727,222 +748,234 @@ void *rtp_timing_receiver(void *arg) { while (1) { nread = recv(conn->timing_socket, packet, sizeof(packet), 0); + if (conn->rtsp_link_is_idle == 0) { + if (conn->udp_clock_is_initialised == 0) { + debug(2,"AP1 clock receiver thread: initialised."); + local_to_remote_time_jitter = 0; + local_to_remote_time_jitter_count = 0; - if (nread >= 0) { + first_local_to_remote_time_difference = 0; - if ((config.diagnostic_drop_packet_fraction == 0.0) || - (drand48() > config.diagnostic_drop_packet_fraction)) { - arrival_time = get_absolute_time_in_ns(); + sequence_number = 0; + stat_n = 0; + stat_mean = 0.0; + conn->udp_clock_is_initialised = 1; + } + if (nread >= 0) { + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + arrival_time = get_absolute_time_in_ns(); - // ssize_t plen = nread; - // debug(1,"Packet Received on Timing Port."); - if (packet[1] == 0xd3) { // timing reply + // ssize_t plen = nread; + // debug(1,"Packet Received on Timing Port."); + if (packet[1] == 0xd3) { // timing reply - return_time = arrival_time - conn->departure_time; - debug(2, "clock synchronisation request: return time is %8.3f milliseconds.", - 0.000001 * return_time); + return_time = arrival_time - conn->departure_time; + debug(2, "clock synchronisation request: return time is %8.3f milliseconds.", + 0.000001 * return_time); - if (return_time < 200000000) { // must be less than 0.2 seconds - // distant_receive_time = - // ((uint64_t)ntohl(*((uint32_t*)&packet[16])))<<32+ntohl(*((uint32_t*)&packet[20])); + if (return_time < 200000000) { // must be less than 0.2 seconds + // distant_receive_time = + // ((uint64_t)ntohl(*((uint32_t*)&packet[16])))<<32+ntohl(*((uint32_t*)&packet[20])); - uint64_t ps, pn; + uint64_t ps, pn; - ps = nctohl(&packet[16]); - ps = ps * 1000000000; // this many nanoseconds from the whole seconds - pn = nctohl(&packet[20]); - pn = pn * 1000000000; - pn = pn >> 32; // this many nanoseconds from the fractional part - distant_receive_time = ps + pn; + ps = nctohl(&packet[16]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[20]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + distant_receive_time = ps + pn; - // distant_transmit_time = - // ((uint64_t)ntohl(*((uint32_t*)&packet[24])))<<32+ntohl(*((uint32_t*)&packet[28])); + // distant_transmit_time = + // ((uint64_t)ntohl(*((uint32_t*)&packet[24])))<<32+ntohl(*((uint32_t*)&packet[28])); - ps = nctohl(&packet[24]); - ps = ps * 1000000000; // this many nanoseconds from the whole seconds - pn = nctohl(&packet[28]); - pn = pn * 1000000000; - pn = pn >> 32; // this many nanoseconds from the fractional part - distant_transmit_time = ps + pn; + ps = nctohl(&packet[24]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[28]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + distant_transmit_time = ps + pn; - uint64_t remote_processing_time = 0; + uint64_t remote_processing_time = 0; - if (distant_transmit_time >= distant_receive_time) - remote_processing_time = distant_transmit_time - distant_receive_time; - else { - debug(1, "Yikes: distant_transmit_time is before distant_receive_time; remote " - "processing time set to zero."); - } - // debug(1,"Return trip time: %" PRIu64 " nS, remote processing time: %" PRIu64 " - // nS.",return_time, remote_processing_time); - - if (remote_processing_time < return_time) - return_time -= remote_processing_time; - else - debug(1, "Remote processing time greater than return time -- ignored."); - - int cc; - // debug(1, "time ping history is %d entries.", time_ping_history); - for (cc = time_ping_history - 1; cc > 0; cc--) { - conn->time_pings[cc] = conn->time_pings[cc - 1]; - // if ((conn->time_ping_count) && (conn->time_ping_count < 10)) - // conn->time_pings[cc].dispersion = - // conn->time_pings[cc].dispersion * pow(2.14, - // 1.0/conn->time_ping_count); - if (conn->time_pings[cc].dispersion > UINT64_MAX / dispersion_factor) - debug(1, "dispersion factor is too large at %" PRIu64 "."); - else - conn->time_pings[cc].dispersion = - (conn->time_pings[cc].dispersion * dispersion_factor) / - 100; // make the dispersions 'age' by this rational factor - } - // these are used for doing a least squares calculation to get the drift - conn->time_pings[0].local_time = arrival_time; - conn->time_pings[0].remote_time = distant_transmit_time + return_time / 2; - conn->time_pings[0].sequence_number = sequence_number++; - conn->time_pings[0].chosen = 0; - conn->time_pings[0].dispersion = return_time; - if (conn->time_ping_count < time_ping_history) - conn->time_ping_count++; - - // here, calculate the mean and standard deviation of the return times - - // mean and variance calculations from "online_variance" algorithm at - // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm - - stat_n += 1; - double stat_delta = return_time - stat_mean; - stat_mean += stat_delta / stat_n; - // stat_M2 += stat_delta * (return_time - stat_mean); - // debug(1, "Timing packet return time stats: current, mean and standard deviation over - // %d packets: %.1f, %.1f, %.1f (nanoseconds).", - // stat_n,return_time,stat_mean, sqrtf(stat_M2 / (stat_n - 1))); - - // here, pick the record with the least dispersion, and record that it's been chosen - - // uint64_t local_time_chosen = arrival_time; - // uint64_t remote_time_chosen = distant_transmit_time; - // now pick the timestamp with the lowest dispersion - uint64_t rt = conn->time_pings[0].remote_time; - uint64_t lt = conn->time_pings[0].local_time; - uint64_t tld = conn->time_pings[0].dispersion; - int chosen = 0; - for (cc = 1; cc < conn->time_ping_count; cc++) - if (conn->time_pings[cc].dispersion < tld) { - chosen = cc; - rt = conn->time_pings[cc].remote_time; - lt = conn->time_pings[cc].local_time; - tld = conn->time_pings[cc].dispersion; - // local_time_chosen = conn->time_pings[cc].local_time; - // remote_time_chosen = conn->time_pings[cc].remote_time; - } - // debug(1,"Record %d has the lowest dispersion with %0.2f us - // dispersion.",chosen,1.0*((tld * 1000000) >> 32)); - conn->time_pings[chosen].chosen = 1; // record the fact that it has been used for timing - - conn->local_to_remote_time_difference = - rt - lt; // make this the new local-to-remote-time-difference - conn->local_to_remote_time_difference_measurement_time = lt; // done at this time. - - if (first_local_to_remote_time_difference == 0) { - first_local_to_remote_time_difference = conn->local_to_remote_time_difference; - // first_local_to_remote_time_difference_time = get_absolute_time_in_fp(); - } - - // here, let's try to use the timing pings that were selected because of their short - // return times to - // estimate a figure for drift between the local clock (x) and the remote clock (y) - - // if we plug in a local interval, we will get back what that is in remote time - - // calculate the line of best fit for relating the local time and the remote time - // we will calculate the slope, which is the drift - // see https://www.varsitytutors.com/hotmath/hotmath_help/topics/line-of-best-fit - - uint64_t y_bar = 0; // remote timestamp average - uint64_t x_bar = 0; // local timestamp average - int sample_count = 0; - - // approximate time in seconds to let the system settle down - const int settling_time = 60; - // number of points to have for calculating a valid drift - const int sample_point_minimum = 8; - for (cc = 0; cc < conn->time_ping_count; cc++) - if ((conn->time_pings[cc].chosen) && - (conn->time_pings[cc].sequence_number > - (settling_time / 3))) { // wait for a approximate settling time - // have to scale them down so that the sum, possibly over - // every term in the array, doesn't overflow - y_bar += (conn->time_pings[cc].remote_time >> time_ping_history_power_of_two); - x_bar += (conn->time_pings[cc].local_time >> time_ping_history_power_of_two); - sample_count++; - } - conn->local_to_remote_time_gradient_sample_count = sample_count; - if (sample_count > sample_point_minimum) { - y_bar = y_bar / sample_count; - x_bar = x_bar / sample_count; - - int64_t xid, yid; - double mtl, mbl; - mtl = 0; - mbl = 0; - for (cc = 0; cc < conn->time_ping_count; cc++) - if ((conn->time_pings[cc].chosen) && - (conn->time_pings[cc].sequence_number > (settling_time / 3))) { - - uint64_t slt = conn->time_pings[cc].local_time >> time_ping_history_power_of_two; - if (slt > x_bar) - xid = slt - x_bar; - else - xid = -(x_bar - slt); - - uint64_t srt = conn->time_pings[cc].remote_time >> time_ping_history_power_of_two; - if (srt > y_bar) - yid = srt - y_bar; - else - yid = -(y_bar - srt); - - mtl = mtl + (1.0 * xid) * yid; - mbl = mbl + (1.0 * xid) * xid; - } - if (mbl) - conn->local_to_remote_time_gradient = mtl / mbl; + if (distant_transmit_time >= distant_receive_time) + remote_processing_time = distant_transmit_time - distant_receive_time; else { - // conn->local_to_remote_time_gradient = 1.0; - debug(1, "mbl is zero. Drift remains at %.2f ppm.", - (conn->local_to_remote_time_gradient - 1.0) * 1000000); + debug(1, "Yikes: distant_transmit_time is before distant_receive_time; remote " + "processing time set to zero."); } + // debug(1,"Return trip time: %" PRIu64 " nS, remote processing time: %" PRIu64 " + // nS.",return_time, remote_processing_time); - // scale the numbers back up - uint64_t ybf = y_bar << time_ping_history_power_of_two; - uint64_t xbf = x_bar << time_ping_history_power_of_two; + if (remote_processing_time < return_time) + return_time -= remote_processing_time; + else + debug(1, "Remote processing time greater than return time -- ignored."); + + int cc; + // debug(1, "time ping history is %d entries.", time_ping_history); + for (cc = time_ping_history - 1; cc > 0; cc--) { + conn->time_pings[cc] = conn->time_pings[cc - 1]; + // if ((conn->time_ping_count) && (conn->time_ping_count < 10)) + // conn->time_pings[cc].dispersion = + // conn->time_pings[cc].dispersion * pow(2.14, + // 1.0/conn->time_ping_count); + if (conn->time_pings[cc].dispersion > UINT64_MAX / dispersion_factor) + debug(1, "dispersion factor is too large at %" PRIu64 "."); + else + conn->time_pings[cc].dispersion = + (conn->time_pings[cc].dispersion * dispersion_factor) / + 100; // make the dispersions 'age' by this rational factor + } + // these are used for doing a least squares calculation to get the drift + conn->time_pings[0].local_time = arrival_time; + conn->time_pings[0].remote_time = distant_transmit_time + return_time / 2; + conn->time_pings[0].sequence_number = sequence_number++; + conn->time_pings[0].chosen = 0; + conn->time_pings[0].dispersion = return_time; + if (conn->time_ping_count < time_ping_history) + conn->time_ping_count++; + + // here, calculate the mean and standard deviation of the return times + + // mean and variance calculations from "online_variance" algorithm at + // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm + + stat_n += 1; + double stat_delta = return_time - stat_mean; + stat_mean += stat_delta / stat_n; + // stat_M2 += stat_delta * (return_time - stat_mean); + // debug(1, "Timing packet return time stats: current, mean and standard deviation over + // %d packets: %.1f, %.1f, %.1f (nanoseconds).", + // stat_n,return_time,stat_mean, sqrtf(stat_M2 / (stat_n - 1))); + + // here, pick the record with the least dispersion, and record that it's been chosen + + // uint64_t local_time_chosen = arrival_time; + // uint64_t remote_time_chosen = distant_transmit_time; + // now pick the timestamp with the lowest dispersion + uint64_t rt = conn->time_pings[0].remote_time; + uint64_t lt = conn->time_pings[0].local_time; + uint64_t tld = conn->time_pings[0].dispersion; + int chosen = 0; + for (cc = 1; cc < conn->time_ping_count; cc++) + if (conn->time_pings[cc].dispersion < tld) { + chosen = cc; + rt = conn->time_pings[cc].remote_time; + lt = conn->time_pings[cc].local_time; + tld = conn->time_pings[cc].dispersion; + // local_time_chosen = conn->time_pings[cc].local_time; + // remote_time_chosen = conn->time_pings[cc].remote_time; + } + // debug(1,"Record %d has the lowest dispersion with %0.2f us + // dispersion.",chosen,1.0*((tld * 1000000) >> 32)); + conn->time_pings[chosen].chosen = 1; // record the fact that it has been used for timing conn->local_to_remote_time_difference = - ybf - xbf; // make this the new local-to-remote-time-difference - conn->local_to_remote_time_difference_measurement_time = xbf; + rt - lt; // make this the new local-to-remote-time-difference + conn->local_to_remote_time_difference_measurement_time = lt; // done at this time. + if (first_local_to_remote_time_difference == 0) { + first_local_to_remote_time_difference = conn->local_to_remote_time_difference; + // first_local_to_remote_time_difference_time = get_absolute_time_in_fp(); + } + + // here, let's try to use the timing pings that were selected because of their short + // return times to + // estimate a figure for drift between the local clock (x) and the remote clock (y) + + // if we plug in a local interval, we will get back what that is in remote time + + // calculate the line of best fit for relating the local time and the remote time + // we will calculate the slope, which is the drift + // see https://www.varsitytutors.com/hotmath/hotmath_help/topics/line-of-best-fit + + uint64_t y_bar = 0; // remote timestamp average + uint64_t x_bar = 0; // local timestamp average + int sample_count = 0; + + // approximate time in seconds to let the system settle down + const int settling_time = 60; + // number of points to have for calculating a valid drift + const int sample_point_minimum = 8; + for (cc = 0; cc < conn->time_ping_count; cc++) + if ((conn->time_pings[cc].chosen) && + (conn->time_pings[cc].sequence_number > + (settling_time / 3))) { // wait for a approximate settling time + // have to scale them down so that the sum, possibly over + // every term in the array, doesn't overflow + y_bar += (conn->time_pings[cc].remote_time >> time_ping_history_power_of_two); + x_bar += (conn->time_pings[cc].local_time >> time_ping_history_power_of_two); + sample_count++; + } + conn->local_to_remote_time_gradient_sample_count = sample_count; + if (sample_count > sample_point_minimum) { + y_bar = y_bar / sample_count; + x_bar = x_bar / sample_count; + + int64_t xid, yid; + double mtl, mbl; + mtl = 0; + mbl = 0; + for (cc = 0; cc < conn->time_ping_count; cc++) + if ((conn->time_pings[cc].chosen) && + (conn->time_pings[cc].sequence_number > (settling_time / 3))) { + + uint64_t slt = conn->time_pings[cc].local_time >> time_ping_history_power_of_two; + if (slt > x_bar) + xid = slt - x_bar; + else + xid = -(x_bar - slt); + + uint64_t srt = conn->time_pings[cc].remote_time >> time_ping_history_power_of_two; + if (srt > y_bar) + yid = srt - y_bar; + else + yid = -(y_bar - srt); + + mtl = mtl + (1.0 * xid) * yid; + mbl = mbl + (1.0 * xid) * xid; + } + if (mbl) + conn->local_to_remote_time_gradient = mtl / mbl; + else { + // conn->local_to_remote_time_gradient = 1.0; + debug(1, "mbl is zero. Drift remains at %.2f ppm.", + (conn->local_to_remote_time_gradient - 1.0) * 1000000); + } + + // scale the numbers back up + uint64_t ybf = y_bar << time_ping_history_power_of_two; + uint64_t xbf = x_bar << time_ping_history_power_of_two; + + conn->local_to_remote_time_difference = + ybf - xbf; // make this the new local-to-remote-time-difference + conn->local_to_remote_time_difference_measurement_time = xbf; + + } else { + debug(3, "not enough samples to estimate drift -- remaining at %.2f ppm.", + (conn->local_to_remote_time_gradient - 1.0) * 1000000); + // conn->local_to_remote_time_gradient = 1.0; + } + // debug(1,"local to remote time gradient is %12.2f ppm, based on %d + // samples.",conn->local_to_remote_time_gradient*1000000,sample_count); + // debug(1,"ntp set offset and measurement time"); // iin PTP terms, this is the local-to-network offset and the local measurement time } else { - debug(3, "not enough samples to estimate drift -- remaining at %.2f ppm.", - (conn->local_to_remote_time_gradient - 1.0) * 1000000); - // conn->local_to_remote_time_gradient = 1.0; + debug(1, + "Time ping turnaround time: %" PRIu64 + " ns -- it looks like a timing ping was lost.", + return_time); } - // debug(1,"local to remote time gradient is %12.2f ppm, based on %d - // samples.",conn->local_to_remote_time_gradient*1000000,sample_count); - } else { - debug(1, - "Time ping turnaround time: %" PRIu64 - " ns -- it looks like a timing ping was lost.", - return_time); + debug(1, "Timing port -- Unknown RTP packet of type 0x%02X length %d.", packet[1], nread); } } else { - debug(1, "Timing port -- Unknown RTP packet of type 0x%02X length %d.", packet[1], nread); + debug(3, "Timing Receiver Thread -- dropping incoming packet to simulate a bad network."); } } else { - debug(3, "Timing Receiver Thread -- dropping incoming packet to simulate a bad network."); + debug(1, "Timing receiver -- error receiving a packet."); } - } else { - debug(1, "Timing receiver -- error receiving a packet."); } } @@ -1070,111 +1103,70 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport void reset_ntp_anchor_info(rtsp_conn_info *conn) { debug_mutex_lock(&conn->reference_time_mutex, 1000, 1); + conn->anchor_remote_info_is_valid = 0; conn->anchor_rtptime = 0; conn->anchor_time = 0; debug_mutex_unlock(&conn->reference_time_mutex, 3); } -int have_ntp_timestamp_timing_information(rtsp_conn_info *conn) { - if (conn->anchor_rtptime == 0) - return 0; - else +int have_ntp_timing_information(rtsp_conn_info *conn) { + if (conn->anchor_remote_info_is_valid != 0) return 1; -} - -// set this to zero to use the rates supplied by the sources, which might not always be completely -// right... -const int use_nominal_rate = 0; // specify whether to use the nominal input rate, usually 44100 fps - -int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_conn_info *conn) { - int result = 1; - if (conn->input_rate == 0) - die("conn->input_rate is zero!"); - uint32_t fs = conn->input_rate; - *frames = fs; // default value to return - *time = 1000000000; // default value to return - if ((conn->initial_reference_time) && (conn->initial_reference_timestamp)) { - // uint32_t local_frames = conn->reference_timestamp - conn->initial_reference_timestamp; - uint32_t local_frames = - modulo_32_offset(conn->initial_reference_timestamp, conn->anchor_rtptime); - uint64_t local_time = conn->anchor_time - conn->initial_reference_time; - if ((local_frames == 0) || (local_time == 0) || (use_nominal_rate)) { - result = 1; - } else { - double calculated_frame_rate = conn->input_rate; - if (local_time) - calculated_frame_rate = (1.0E9 * local_frames) / local_time; - else - debug(1, "sanitised_source_rate_information: local_time is zero"); - if ((local_time == 0) || ((calculated_frame_rate / conn->input_rate) > 1.002) || - ((calculated_frame_rate / conn->input_rate) < 0.998)) { - debug(3, "input frame rate out of bounds at %.2f fps.", calculated_frame_rate); - result = 1; - } else { - *frames = local_frames; - if (local_frames == 0) - die("local_frames is zero!"); - *time = local_time; - if (local_time == 0) - die("local_time is zero!"); - result = 0; - } - } - } - return result; -} - -void set_ntp_anchor_info(rtsp_conn_info *conn, uint32_t rtptime, uint64_t networktime) { - conn->anchor_remote_info_is_valid = 1; - conn->anchor_rtptime = rtptime; - conn->anchor_time = networktime; + else + return 0; } // the timestamp is a timestamp calculated at the input rate // the reference timestamps are denominated in terms of the input rate int frame_to_ntp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) { + // a zero result is good + if (conn->anchor_remote_info_is_valid == 0) + debug(1,"no anchor information"); debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); - int result = 0; - uint64_t time_difference; - uint32_t frame_difference; - result = sanitised_source_rate_information(&frame_difference, &time_difference, conn); - - uint64_t remote_time_of_timestamp; - int32_t timestamp_interval = timestamp - conn->anchor_rtptime; - int64_t timestamp_interval_time = timestamp_interval; - timestamp_interval_time = timestamp_interval_time * time_difference; - timestamp_interval_time = - timestamp_interval_time / frame_difference; // this is the nominal time, based on the - // fps specified between current and - // previous sync frame. - remote_time_of_timestamp = - conn->anchor_time + timestamp_interval_time; // based on the reference timestamp time - // plus the time interval calculated based - // on the specified fps. - *time = remote_time_of_timestamp - local_to_remote_time_difference_now(conn); + int result = -1; + if (conn->anchor_remote_info_is_valid != 0) { + uint64_t remote_time_of_timestamp; + int32_t timestamp_interval = timestamp - conn->anchor_rtptime; + int64_t timestamp_interval_time = timestamp_interval; + timestamp_interval_time = timestamp_interval_time * 1000000000; + timestamp_interval_time = + timestamp_interval_time / 44100; // this is the nominal time, based on the + // fps specified between current and + // previous sync frame. + remote_time_of_timestamp = + conn->anchor_time + timestamp_interval_time; // based on the reference timestamp time + // plus the time interval calculated based + // on the specified fps. + if (time != NULL) + *time = remote_time_of_timestamp - local_to_remote_time_difference_now(conn); + result = 0; + } debug_mutex_unlock(&conn->reference_time_mutex, 0); return result; } int local_ntp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { + // a zero result is good debug_mutex_lock(&conn->reference_time_mutex, 1000, 0); - int result = 0; - uint64_t time_difference; - uint32_t frame_difference; - result = sanitised_source_rate_information(&frame_difference, &time_difference, conn); - // first, get from [local] time to remote time. - uint64_t remote_time = time + local_to_remote_time_difference_now(conn); - // next, get the remote time interval from the remote_time to the reference time - // here, we calculate the time interval, in terms of remote time - int64_t offset = remote_time - conn->anchor_time; - // now, convert the remote time interval into frames using the frame rate we have observed or - // which has been nominated - int64_t frame_interval = 0; - frame_interval = (offset * frame_difference) / time_difference; - int32_t frame_interval_32 = frame_interval; - uint32_t new_frame = conn->anchor_rtptime + frame_interval_32; - *frame = new_frame; + int result = -1; + if (conn->anchor_remote_info_is_valid != 0) { + // first, get from [local] time to remote time. + uint64_t remote_time = time + local_to_remote_time_difference_now(conn); + // next, get the remote time interval from the remote_time to the reference time + // here, we calculate the time interval, in terms of remote time + int64_t offset = remote_time - conn->anchor_time; + // now, convert the remote time interval into frames using the frame rate we have observed or + // which has been nominated + int64_t frame_interval = 0; + frame_interval = (offset * 44100) / 1000000000; + int32_t frame_interval_32 = frame_interval; + uint32_t new_frame = conn->anchor_rtptime + frame_interval_32; + // debug(1,"frame is %u.", new_frame); + if (frame != NULL) + *frame = new_frame; + result = 0; + } debug_mutex_unlock(&conn->reference_time_mutex, 0); return result; } @@ -1338,71 +1330,74 @@ void reset_ptp_anchor_info(rtsp_conn_info *conn) { int long_time_notifcation_done = 0; int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, uint64_t *anchorLocalTime) { - uint64_t actual_clock_id, actual_time_of_sample, actual_offset, start_of_mastership; - int response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, - &start_of_mastership); - - if (response == clock_ok) { - uint64_t time_now = get_absolute_time_in_ns(); - int64_t time_since_sample = time_now - actual_time_of_sample; - if (time_since_sample > 300000000000) { - if (long_time_notifcation_done == 0) { - debug(1, "The last PTP timing sample is pretty old: %f seconds.", - 0.000000001 * time_since_sample); - long_time_notifcation_done = 1; - } - } else if ((time_since_sample < 2000000000) && (long_time_notifcation_done != 0)) { - debug(1, "The last PTP timing sample is no longer too old: %f seconds.", - 0.000000001 * time_since_sample); - long_time_notifcation_done = 0; - } - - if (conn->anchor_remote_info_is_valid != - 0) { // i.e. if we have anchor clock ID and anchor time / rtptime - - if (actual_clock_id == conn->anchor_clock) { - conn->last_anchor_rtptime = conn->anchor_rtptime; - conn->last_anchor_local_time = conn->anchor_time - actual_offset; - conn->last_anchor_time_of_update = time_now; - if (conn->last_anchor_info_is_valid == 0) - conn->last_anchor_validity_start_time = start_of_mastership; - conn->last_anchor_info_is_valid = 1; - } else { - debug(3, "Current master clock %" PRIx64 " and anchor_clock %" PRIx64 " are different", - actual_clock_id, conn->anchor_clock); - // the anchor clock and the actual clock are different - - if (conn->last_anchor_info_is_valid != 0) { - - int64_t time_since_last_update = - get_absolute_time_in_ns() - conn->last_anchor_time_of_update; - if (time_since_last_update > 5000000000) { - int64_t duration_of_mastership = time_now - start_of_mastership; - debug(2, - "Connection %d: Master clock has changed to %" PRIx64 - ". History: %.3f milliseconds.", - conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); - - // Now, the thing is that while the anchor clock and master clock for a - // buffered session start off the same, - // the master clock can change without the anchor clock changing. - // SPS gives the new master clock time to settle down and then - // calculates the appropriate offset to it by - // calculating back from the local anchor information and the new clock's - // advertised offset. - - conn->anchor_time = conn->last_anchor_local_time + actual_offset; - conn->anchor_clock = actual_clock_id; - - } - - } else { - response = clock_not_valid; // no current clock information and no previous clock info + int response = clock_not_valid; + uint64_t actual_clock_id; + if (conn->rtsp_link_is_idle == 0) { + uint64_t actual_time_of_sample, actual_offset, start_of_mastership; + response = ptp_get_clock_info(&actual_clock_id, &actual_time_of_sample, &actual_offset, + &start_of_mastership); + if (response == clock_ok) { + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_sample = time_now - actual_time_of_sample; + if (time_since_sample > 300000000000) { + if (long_time_notifcation_done == 0) { + debug(1, "The last PTP timing sample is pretty old: %f seconds.", + 0.000000001 * time_since_sample); + long_time_notifcation_done = 1; } + } else if ((time_since_sample < 2000000000) && (long_time_notifcation_done != 0)) { + debug(1, "The last PTP timing sample is no longer too old: %f seconds.", + 0.000000001 * time_since_sample); + long_time_notifcation_done = 0; + } + + if (conn->anchor_remote_info_is_valid != + 0) { // i.e. if we have anchor clock ID and anchor time / rtptime + + if (actual_clock_id == conn->anchor_clock) { + conn->last_anchor_rtptime = conn->anchor_rtptime; + conn->last_anchor_local_time = conn->anchor_time - actual_offset; + conn->last_anchor_time_of_update = time_now; + if (conn->last_anchor_info_is_valid == 0) + conn->last_anchor_validity_start_time = start_of_mastership; + conn->last_anchor_info_is_valid = 1; + } else { + debug(3, "Current master clock %" PRIx64 " and anchor_clock %" PRIx64 " are different", + actual_clock_id, conn->anchor_clock); + // the anchor clock and the actual clock are different + + if (conn->last_anchor_info_is_valid != 0) { + + int64_t time_since_last_update = + get_absolute_time_in_ns() - conn->last_anchor_time_of_update; + if (time_since_last_update > 5000000000) { + int64_t duration_of_mastership = time_now - start_of_mastership; + debug(2, + "Connection %d: Master clock has changed to %" PRIx64 + ". History: %.3f milliseconds.", + conn->connection_number, actual_clock_id, 0.000001 * duration_of_mastership); + + // Now, the thing is that while the anchor clock and master clock for a + // buffered session start off the same, + // the master clock can change without the anchor clock changing. + // SPS gives the new master clock time to settle down and then + // calculates the appropriate offset to it by + // calculating back from the local anchor information and the new clock's + // advertised offset. + + conn->anchor_time = conn->last_anchor_local_time + actual_offset; + conn->anchor_clock = actual_clock_id; + + } + + } else { + response = clock_not_valid; // no current clock information and no previous clock info + } + } + } else { + // debug(1, "anchor_remote_info_is_valid not valid"); + response = clock_no_anchor_info; // no anchor information } - } else { - // debug(1, "anchor_remote_info_is_valid not valid"); - response = clock_no_anchor_info; // no anchor information } } @@ -1443,7 +1438,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, debug(1, "Connection %d: NQPTP clock is not synchronised.", conn->connection_number); break; case clock_not_valid: - debug(1, "Connection %d: NQPTP clock information is not valid.", conn->connection_number); + debug(2, "Connection %d: NQPTP clock information is not valid.", conn->connection_number); break; default: debug(1, "Connection %d: NQPTP clock reports an unrecognised status: %u.", @@ -1485,7 +1480,7 @@ int frame_to_ptp_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info * *time = ltime; result = 0; } else { - debug(3, "frame_to_local_time can't get anchor local time information"); + debug(3, "frame_to_ptp_local_time can't get anchor local time information"); } return result; } @@ -1504,7 +1499,7 @@ int local_ptp_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn *frame = lframe; result = 0; } else { - debug(3, "local_time_to_frame can't get anchor local time information"); + debug(3, "local_ptp_time_to_frame can't get anchor local time information"); } return result; } @@ -1706,6 +1701,7 @@ int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread, if (new_payload_length > max_int) debug(1, "Madly long payload length!"); int plen = new_payload_length; // + // debug(1," Write packet to buffer %d, timestamp %u.", sequence_number, timestamp); player_put_packet(1, sequence_number, timestamp, m, plen, conn); // the '1' means is original format return sequence_number; @@ -1728,155 +1724,157 @@ void *rtp_ap2_control_receiver(void *arg) { socklen_t from_sock_addr_length = sizeof(SOCKADDR); memset(&from_sock_addr, 0, sizeof(SOCKADDR)); - - struct timeval tv; - tv.tv_sec = 7; // wait this many seconds for a packet, which should come every two seconds - tv.tv_usec = 0; - setsockopt(conn->ap2_control_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); - - nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0, (struct sockaddr *)&from_sock_addr, &from_sock_addr_length); uint64_t time_now = get_absolute_time_in_ns(); int64_t time_since_start = time_now - start_time; - // debug(1,"Connection %d: AP2 Control Packet received.", conn->connection_number); - - if (nread >= 28) { // must have at least 28 bytes for the timing information - if ((time_since_start < 2000000) && ((packet[0] & 0x10) == 0)) { - debug(1, - "Dropping what looks like a (non-sentinel) packet left over from a previous session " - "at %f ms.", - 0.000001 * time_since_start); - } else { - packet_number++; - - if (packet_number == 1) { - if ((packet[0] & 0x10) != 0) { - debug(2, "First packet is a sentinel packet."); - } else { - debug(2, "First packet is a not a sentinel packet!"); - } - } - // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); - if (packet_number >= 2) { - if ((config.diagnostic_drop_packet_fraction == 0.0) || - (drand48() > config.diagnostic_drop_packet_fraction)) { - // store the from_sock_addr if we haven't already done so - // v remember to zero this when you're finished! - if (conn->ap2_remote_control_socket_addr_length == 0) { - memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length); - conn->ap2_remote_control_socket_addr_length = from_sock_addr_length; - } - switch (packet[1]) { - case 215: // code 215, effectively an anchoring announcement - { - // struct timespec tnr; - // clock_gettime(CLOCK_REALTIME, &tnr); - // uint64_t local_realtime_now = timespec_to_ns(&tnr); - - /* - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc=0;obfcinput_rate); - // the actual latency is the notified latency plus the fixed latency + the added latency - - int32_t net_latency = - notified_latency + 11035 + - added_latency; // this is the latency between incoming frames and the DAC - net_latency = net_latency - - (int32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); - // debug(1, "Net latency is %d frames.", net_latency); - - if (net_latency <= 0) { - if (conn->latency_warning_issued == 0) { - warn("The stream latency (%f seconds) it too short to accommodate an offset of %f " - "seconds and a backend buffer of %f seconds.", - ((notified_latency + 11035) * 1.0) / conn->input_rate, - config.audio_backend_latency_offset, - config.audio_backend_buffer_desired_length); - warn("(FYI the stream latency needed would be %f seconds.)", - config.audio_backend_buffer_desired_length - - config.audio_backend_latency_offset); - conn->latency_warning_issued = 1; - } - conn->latency = notified_latency + 11035; - } else { - conn->latency = notified_latency + 11035 + added_latency; - } - - set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, - remote_packet_time_ns); - if (conn->anchor_clock != clock_id) { - debug(2, "Connection %d: Change Anchor Clock: %" PRIx64 ".", conn->connection_number, - clock_id); - } - - } break; - case 0xd6: - // six bytes in is the sequence number at the start of the encrypted audio packet - // returns the sequence number but we're not really interested - decipher_player_put_packet(packet + 6, nread - 6, conn); - break; - default: { - char *packet_in_hex_cstring = - debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards - debug( - 1, - "AP2 Control Receiver Packet of first byte 0x%02X, type 0x%02X length %d received: " - "\"%s\".", - packet[0], packet[1], nread, packet_in_hex_cstring); - free(packet_in_hex_cstring); - } break; - } - - } else { - debug(1, "AP2 Control Receiver -- dropping a packet."); - } - } + if (conn->rtsp_link_is_idle == 0) { + if (conn->udp_clock_is_initialised == 0) { + packet_number = 0; + conn->udp_clock_is_initialised = 1; + debug(1,"AP2 Realtime Clock receiver initialised."); } - } else { - if (nread == -1) { - if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { - if (conn->airplay_stream_type == realtime_stream) { - debug(2, "Connection %d: no control packets for the last 10 seconds -- resetting anchor info", conn->connection_number); - reset_ptp_anchor_info(conn); - packet_number = 0; // start over in allowing the packet to set anchor information - } + + // debug(1,"Connection %d: AP2 Control Packet received.", conn->connection_number); + + if (nread >= 28) { // must have at least 28 bytes for the timing information + if ((time_since_start < 2000000) && ((packet[0] & 0x10) == 0)) { + debug(1, + "Dropping what looks like a (non-sentinel) packet left over from a previous session " + "at %f ms.", + 0.000001 * time_since_start); } else { - debug(2, "Connection %d: AP2 Control Receiver -- error %d receiving a packet.", conn->connection_number, errno); + packet_number++; + // debug(1,"AP2 Packet %" PRIu64 ".", packet_number); + + if (packet_number == 1) { + if ((packet[0] & 0x10) != 0) { + debug(2, "First packet is a sentinel packet."); + } else { + debug(2, "First packet is a not a sentinel packet!"); + } + } + // debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]); + // you might want to set this higher to specify how many initial timings to ignore + if (packet_number >= 1) { + if ((config.diagnostic_drop_packet_fraction == 0.0) || + (drand48() > config.diagnostic_drop_packet_fraction)) { + // store the from_sock_addr if we haven't already done so + // v remember to zero this when you're finished! + if (conn->ap2_remote_control_socket_addr_length == 0) { + memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length); + conn->ap2_remote_control_socket_addr_length = from_sock_addr_length; + } + switch (packet[1]) { + case 215: // code 215, effectively an anchoring announcement + { + // struct timespec tnr; + // clock_gettime(CLOCK_REALTIME, &tnr); + // uint64_t local_realtime_now = timespec_to_ns(&tnr); + + /* + char obf[4096]; + char *obfp = obf; + int obfc; + for (obfc=0;obfcinput_rate); + // the actual latency is the notified latency plus the fixed latency + the added latency + + int32_t net_latency = + notified_latency + 11035 + + added_latency; // this is the latency between incoming frames and the DAC + net_latency = net_latency - + (int32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); + // debug(1, "Net latency is %d frames.", net_latency); + + if (net_latency <= 0) { + if (conn->latency_warning_issued == 0) { + warn("The stream latency (%f seconds) it too short to accommodate an offset of %f " + "seconds and a backend buffer of %f seconds.", + ((notified_latency + 11035) * 1.0) / conn->input_rate, + config.audio_backend_latency_offset, + config.audio_backend_buffer_desired_length); + warn("(FYI the stream latency needed would be %f seconds.)", + config.audio_backend_buffer_desired_length - + config.audio_backend_latency_offset); + conn->latency_warning_issued = 1; + } + conn->latency = notified_latency + 11035; + } else { + conn->latency = notified_latency + 11035 + added_latency; + } + + set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, + remote_packet_time_ns); + if (conn->anchor_clock != clock_id) { + debug(2, "Connection %d: Change Anchor Clock: %" PRIx64 ".", conn->connection_number, + clock_id); + } + + } break; + case 0xd6: + // six bytes in is the sequence number at the start of the encrypted audio packet + // returns the sequence number but we're not really interested + decipher_player_put_packet(packet + 6, nread - 6, conn); + break; + default: { + char *packet_in_hex_cstring = + debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards + debug( + 1, + "AP2 Control Receiver Packet of first byte 0x%02X, type 0x%02X length %d received: " + "\"%s\".", + packet[0], packet[1], nread, packet_in_hex_cstring); + free(packet_in_hex_cstring); + } break; + } + } else { + debug(1, "AP2 Control Receiver -- dropping a packet."); + } + } } } else { - debug(2, "Connection %d: AP2 Control Receiver -- malformed packet, %d bytes long.", conn->connection_number, nread); + if (nread == -1) { + if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { + if (conn->airplay_stream_type == realtime_stream) { + debug(1, "Connection %d: no control packets for the last 7 seconds -- resetting anchor info", conn->connection_number); + reset_ptp_anchor_info(conn); + packet_number = 0; // start over in allowing the packet to set anchor information + } + } else { + debug(2, "Connection %d: AP2 Control Receiver -- error %d receiving a packet.", conn->connection_number, errno); + } + } else { + debug(2, "Connection %d: AP2 Control Receiver -- malformed packet, %d bytes long.", conn->connection_number, nread); + } } } } @@ -2927,7 +2925,7 @@ int have_timestamp_timing_information(rtsp_conn_info *conn) { if (conn->timing_type == ts_ptp) return have_ptp_timing_information(conn); else - return have_ntp_timestamp_timing_information(conn); + return have_ntp_timing_information(conn); } #else @@ -2943,6 +2941,6 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { void reset_anchor_info(rtsp_conn_info *conn) { reset_ntp_anchor_info(conn); } int have_timestamp_timing_information(rtsp_conn_info *conn) { - return have_ntp_timestamp_timing_information(conn); + return have_ntp_timing_information(conn); } #endif diff --git a/rtsp.c b/rtsp.c index 09bd4d58..8cc36beb 100644 --- a/rtsp.c +++ b/rtsp.c @@ -96,7 +96,6 @@ #include "dbus-service.h" #endif - #include "mdns.h" // mDNS advertisement strings @@ -502,14 +501,9 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { #endif -void lock_player() { - debug_mutex_lock(&playing_conn_lock, 1000000, 3); -} - -void unlock_player() { - debug_mutex_unlock(&playing_conn_lock, 3); -} +void lock_player() { debug_mutex_lock(&playing_conn_lock, 1000000, 3); } +void unlock_player() { debug_mutex_unlock(&playing_conn_lock, 3); } int have_play_lock(rtsp_conn_info *conn) { int response = 0; @@ -552,12 +546,11 @@ void release_play_lock(rtsp_conn_info *conn) { debug(2, "Connection %d: play lock released.", conn->connection_number); else debug(2, "Play lock released."); - playing_conn = NULL; // let it go + playing_conn = NULL; // let it go } unlock_player(); } - // make conn the playing_conn, and kill the current session if permitted int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption) { if (conn != NULL) @@ -630,7 +623,7 @@ int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption) { if (conn != NULL) debug(2, "Connection %d: Got player lock.", conn->connection_number); else - debug(2, "Player released."); + debug(2, "Player released."); response = 0; } return response; @@ -1220,7 +1213,18 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou */ #endif -ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { +ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time, void *buf, + size_t count) { + struct timeval tv; + tv.tv_sec = wait_time / 1000000000; // seconds + tv.tv_usec = (wait_time % 1000000000) / 1000; // microseconds + if (setsockopt(conn->fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) != 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "could not set time limit on timed_read_from_rtsp_connection -- error %d \"%s\".", + errno, errorstring); + } + #ifdef CONFIG_AIRPLAY_2 if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; @@ -1233,6 +1237,34 @@ ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) #endif } +ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { + // first try to read with a timeout, to see if there is any traffic... + ssize_t response = timed_read_from_rtsp_connection(conn, 4000000000L, buf, count); + if ((response == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { + if (conn->rtsp_link_is_idle == 0) { + conn->rtsp_link_is_idle = 1; +#ifdef CONFIG_AIRPLAY_2 + conn->last_anchor_info_is_valid = 0; +#endif + conn->anchor_remote_info_is_valid = 0; + conn->udp_clock_sender_is_initialised = 0; + conn->udp_clock_is_initialised = 0; + debug(1, "Connection %d: RTSP connection is idle.", conn->connection_number); + } + response = timed_read_from_rtsp_connection(conn, 0, buf, count); + } + if (conn->rtsp_link_is_idle == 1) { + conn->local_to_remote_time_difference_measurement_time = 0; + conn->local_to_remote_time_difference = 0; + conn->rtsp_link_is_idle = 0; + conn->first_packet_timestamp = 0; + conn->input_frame_rate_starting_point_is_valid = 0; + ab_resync(conn); + debug(1, "Connection %d: RTSP connection traffic has resumed.", conn->connection_number); + } + return response; +} + enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) { *the_packet = NULL; // need this for error handling @@ -1418,6 +1450,7 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { {403, "Unauthorized"}, {404, "Not Found"}, {451, "Unavailable"}, + {456, "Header Field Not Valid for Resource"}, {500, "Internal Server Error"}, {501, "Not Implemented"}}; // 451 is really "Unavailable For Legal Reasons"! @@ -1482,7 +1515,8 @@ int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { #ifdef CONFIG_AIRPLAY_2 ssize_t reply; if (conn->ap2_pairing_context.control_cipher_bundle.is_encrypted) { - reply = write_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, pkt, p - pkt); + reply = + write_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, pkt, p - pkt); } else { reply = write(conn->fd, pkt, p - pkt); } @@ -1909,7 +1943,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); conn->ap2_rate = rate; if ((rate & 1) != 0) { - debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); + debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", + conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); conn->ap2_play_enabled = 1; } else { @@ -2313,6 +2348,9 @@ void handle_configure(rtsp_conn_info *conn __attribute__((unused)), void handle_feedback(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); if (conn->airplay_stream_category == remote_control_stream) { plist_t array_plist = plist_new_array(); @@ -2349,7 +2387,9 @@ void handle_feedback(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { - debug_log_rtsp_message(3, "POST /command", req); + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); if (rtsp_message_contains_plist(req)) { plist_t command_dict = NULL; plist_from_memory(req->content, req->contentlength, &command_dict); @@ -2468,12 +2508,11 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp 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, " ", 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)); + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); plist_t addresses_array = NULL; plist_from_memory(req->content, req->contentlength, &addresses_array); @@ -2542,32 +2581,36 @@ void teardown_phase_two(rtsp_conn_info *conn) { // we are being asked to disconnect // this can be called more than once on the same connection -- // by the player itself but also by the play seesion being killed - debug(2, "Connection %d: TEARDOWN a %s connection.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); + debug(2, "Connection %d: TEARDOWN %s connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == remote_control_stream) { if (conn->rtp_data_thread) { - debug(2, "Connection %d (RC): TEARDOWN Delete Data Thread.", conn->connection_number); + debug(2, "Connection %d: TEARDOWN %s Delete Data Thread.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); pthread_cancel(*conn->rtp_data_thread); pthread_join(*conn->rtp_data_thread, NULL); free(conn->rtp_data_thread); conn->rtp_data_thread = NULL; } - debug(2, "Connection %d: TEARDOWN Close Data Socket.", conn->connection_number); if (conn->data_socket) { + debug(2, "Connection %d: TEARDOWN %s Close Data Socket.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); close(conn->data_socket); conn->data_socket = 0; } } if (conn->rtp_event_thread) { - debug(2, "Connection %d: TEARDOWN Delete Event Thread.", conn->connection_number); + debug(2, "Connection %d: TEARDOWN %s Delete Event Thread.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); pthread_cancel(*conn->rtp_event_thread); pthread_join(*conn->rtp_event_thread, NULL); free(conn->rtp_event_thread); conn->rtp_event_thread = NULL; } - debug(2, "Connection %d: TEARDOWN Close Event Socket.", conn->connection_number); if (conn->event_socket) { + debug(2, "Connection %d: TEARDOWN %s Close Event Socket.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); close(conn->event_socket); conn->event_socket = 0; } @@ -2611,16 +2654,20 @@ void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_messag plist_t streams = plist_dict_get_item(messagePlist, "streams"); if (streams) { - debug(2, "Connection %d: TEARDOWN a %s.", conn->connection_number, + debug(2, "Connection %d: TEARDOWN %s Close the stream.", conn->connection_number, get_category_string(conn->airplay_stream_category)); // we are being asked to close a stream teardown_phase_one(conn); plist_free(streams); - debug(2, "Connection %d: TEARDOWN phase one complete", conn->connection_number); + debug(2, "Connection %d: TEARDOWN %s Close the stream complete", conn->connection_number, + get_category_string(conn->airplay_stream_category)); } else { + debug(2, "Connection %d: TEARDOWN %s Close the connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); teardown_phase_one(conn); // try to do phase one anyway teardown_phase_two(conn); - debug(2, "Connection %d: TEARDOWN phase two complete", conn->connection_number); + debug(2, "Connection %d: TEARDOWN %s Close the connection complete", conn->connection_number, + get_category_string(conn->airplay_stream_category)); } //} else { // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", @@ -2672,7 +2719,6 @@ void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message } void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - // TODO -- don't know what this is for in AP2 debug_log_rtsp_message(2, "FLUSH request", req); debug(3, "Connection %d: FLUSH", conn->connection_number); char *p = NULL; @@ -2698,11 +2744,7 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); #endif -// hack -- ignore it for airplay 2 -#ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_type != ap_2) -#endif - player_flush(rtptime, conn); // will not crash even it there is no player thread. + player_flush(rtptime, conn); // will not crash even it there is no player thread. resp->respcode = 200; } else { @@ -2839,10 +2881,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // 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)); + 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)); - + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); if (timing_peer_info) { @@ -2988,7 +3029,11 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d SETUP (RC): TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); if (conn->rtp_event_thread != NULL) - debug(1, "previous rtp_event_thread allocation not freed, it seems."); + debug(1, + "Connection %d SETUP (RC): previous rtp_event_thread allocation not freed, it " + "seems.", + conn->connection_number); + conn->rtp_event_thread = malloc(sizeof(pthread_t)); if (conn->rtp_event_thread == NULL) die("Couldn't allocate space for pthread_t"); @@ -3209,8 +3254,10 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) 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.", conn->connection_number); - + debug(2, "Connection %d (RC): SETUP: Remote Control Stream received from %s.", + conn->connection_number, conn->client_ip_string); + debug_log_rtsp_message(2, "Remote Control Stream SETUP incoming message", req); + /* // get a port to use as an data port // bind a new TCP port and get a socket conn->local_data_port = 0; // any port @@ -3226,10 +3273,9 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(1, "Connection %d SETUP (RC): TCP Remote Control data port opened: %u.", conn->connection_number, conn->local_data_port); if (conn->rtp_data_thread != NULL) - debug(1, "previous rtp_data_thread allocation not freed, it seems."); - conn->rtp_data_thread = malloc(sizeof(pthread_t)); - if (conn->rtp_data_thread == NULL) - die("Couldn't allocate space for pthread_t"); + debug(1, "Connection %d SETUP (RC): previous rtp_data_thread allocation not freed, it + seems.", conn->connection_number); conn->rtp_data_thread = malloc(sizeof(pthread_t)); if + (conn->rtp_data_thread == NULL) die("Couldn't allocate space for pthread_t"); pthread_create(conn->rtp_data_thread, NULL, &rtp_data_receiver, (void *)conn); @@ -3241,6 +3287,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t coreResponseArray = plist_new_array(); plist_array_append_item(coreResponseArray, coreResponseDict); plist_dict_set_item(setupResponsePlist, "streams", coreResponseArray); + */ resp->respcode = 200; } else { debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", @@ -3430,7 +3477,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, if (dbus_service_is_running()) { shairport_sync_set_volume(shairportSyncSkeleton, config.airplay_volume); } -#endif +#endif } else if (strncmp(cp, "progress: ", strlen("progress: ")) == 0) { // this can be sent even when metadata is not solicited @@ -4171,7 +4218,7 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + debug(2, "Connection %d: Current volume (%.6f) requested", conn->connection_number, config.airplay_volume); char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { @@ -4401,11 +4448,13 @@ 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 { + } else if ((paesiv != NULL) && (prsaaeskey != NULL)){ conn->stream.encrypted = 1; // debug(1,"Encrypted session requested"); + } else { + warn("Invalid Announce message -- missing paesiv or prsaaeskey."); + goto out; } - if (conn->stream.encrypted) { int len, keylen; uint8_t *aesiv = base64_dec(paesiv, &len); diff --git a/shairport.c b/shairport.c index 40c821f2..9d1657f0 100644 --- a/shairport.c +++ b/shairport.c @@ -159,7 +159,7 @@ int has_fltp_capable_aac_decoder(void) { const enum AVSampleFormat *p = codec->sample_fmts; if (p != NULL) { while ((has_capability == 0) && (*p != AV_SAMPLE_FMT_NONE)) { - if (*p == AV_SAMPLE_FMT_FLTP) + if (*p == AV_SAMPLE_FMT_FLTP) has_capability = 1; p++; } @@ -1379,7 +1379,7 @@ int parse_options(int argc, char **argv) { config.appName, temporary_airplay_id); // debug(1, "smi name: \"%s\"", shared_memory_interface_name); - config.nqptp_shared_memory_interface_name = strdup(shared_memory_interface_name); + config.nqptp_shared_memory_interface_name = strdup(NQPTP_INTERFACE_NAME); char apids[6 * 2 + 5 + 1]; // six pairs of digits, 5 colons and a NUL apids[6 * 2 + 5] = 0; // NUL termination @@ -1454,9 +1454,9 @@ int parse_options(int argc, char **argv) { // now, do the substitutions in the service name char hostname[100]; gethostname(hostname, 100); - + // strip off a terminating ., e.g. .local from the hostname - char *last_dot = strrchr(hostname,'.'); + char *last_dot = strrchr(hostname, '.'); if (last_dot != NULL) *last_dot = '\0'; @@ -1574,10 +1574,10 @@ void exit_function() { if (g_main_loop) { debug(2, "Stopping D-Bus Loop Thread"); g_main_loop_quit(g_main_loop); - - // If the request to exit has come from the D-Bus system, + + // If the request to exit has come from the D-Bus system, // the D-Bus Loop Thread will not exit until the request is completed - // so don't wait for it + // so don't wait for it if (type_of_exit_cleanup != TOE_dbus) pthread_join(dbus_thread, NULL); } @@ -2023,10 +2023,10 @@ int main(int argc, char **argv) { apfh = apfh >> 32; uint32_t apf32 = apf; uint32_t apfh32 = apfh; - debug(1, "startup in Airplay 2 mode with features 0x%" PRIx32 ",0x%" PRIx32 " on device \"%s\".", + debug(1, "startup in AirPlay 2 mode, with features 0x%" PRIx32 ",0x%" PRIx32 " on device \"%s\".", apf32, apfh32, config.airplay_device_id); #else - debug(1, "startup in Airplay 1 mode."); + debug(1, "startup in classic Airplay (aka \"AirPlay 1\") mode."); #endif // control-c (SIGINT) cleanly @@ -2180,8 +2180,9 @@ int main(int argc, char **argv) { debug(1, "mdns backend \"%s\".", strnull(config.mdns_name)); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); debug(1, "interpolation setting is \"%s\".", - config.packet_stuffing == ST_basic ? "basic" - : config.packet_stuffing == ST_soxr ? "soxr" : "auto"); + config.packet_stuffing == ST_basic ? "basic" + : config.packet_stuffing == ST_soxr ? "soxr" + : "auto"); debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold); debug(1, "resync time is %f seconds.", config.resyncthreshold); debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption); @@ -2338,7 +2339,8 @@ int main(int argc, char **argv) { ptp_send_control_message_string("T"); // get nqptp to create the named shm interface usleep(ptp_wait_interval_us); ptp_check_times++; - } while ((ptp_shm_interface_open() != 0) && (ptp_check_times < (10000000 / ptp_wait_interval_us))); + } while ((ptp_shm_interface_open() != 0) && + (ptp_check_times < (10000000 / ptp_wait_interval_us))); if (ptp_shm_interface_open() != 0) { die("Can't access NQPTP! Is it installed and running?"); From 793ec4192b37f750d30fc7967d75fd6ef724d605 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:55:02 +0100 Subject: [PATCH 872/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 02cba47f..01e94e22 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-526-gfd880056 +==== +***Pesky Changes You Can't Ignore*** + +* **Important**. Please update and re-enable NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to reflect changes in NQPTP's operation. + +Apart from moving to the new SMI iinterface for NQPTP, this update consists of many small enhancements and bug fixes. + Version 4.1-dev-535-gdafd1726 ==== **Bug Fix** From baee246ed1f24ae48fe29502e41b0118262fbb11 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:55:29 +0100 Subject: [PATCH 873/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 01e94e22..cbacbbd4 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -4,7 +4,7 @@ Version 4.1-dev-526-gfd880056 * **Important**. Please update and re-enable NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to reflect changes in NQPTP's operation. -Apart from moving to the new SMI iinterface for NQPTP, this update consists of many small enhancements and bug fixes. +Apart from moving to the new SMI iinterface for NQPTP, this update consists of many small enhancements and bug fixes. (Yeah, and the version number really did go backwards...) Version 4.1-dev-535-gdafd1726 ==== From 08e3f7df131ca699c463a58d389f07ada06bac1b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:55:56 +0100 Subject: [PATCH 874/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index cbacbbd4..91004cad 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -4,7 +4,7 @@ Version 4.1-dev-526-gfd880056 * **Important**. Please update and re-enable NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to reflect changes in NQPTP's operation. -Apart from moving to the new SMI iinterface for NQPTP, this update consists of many small enhancements and bug fixes. (Yeah, and the version number really did go backwards...) +Apart from moving to the new SMI interface for NQPTP, this update consists of many small enhancements and bug fixes. (Yeah, and the version number really did go backwards...) Version 4.1-dev-535-gdafd1726 ==== From 03d442f1c3d390ead33fea88cd47982302c474fd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:56:20 +0100 Subject: [PATCH 875/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 91004cad..9986bf37 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -4,7 +4,7 @@ Version 4.1-dev-526-gfd880056 * **Important**. Please update and re-enable NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to reflect changes in NQPTP's operation. -Apart from moving to the new SMI interface for NQPTP, this update consists of many small enhancements and bug fixes. (Yeah, and the version number really did go backwards...) +Apart from moving to the new SMI interface for NQPTP, this update consists of many small enhancements and bug fixes. (Yeah, and the build number really did go backwards...) Version 4.1-dev-535-gdafd1726 ==== From 114c39d8a9717012550ca62c5d1e2ef1145da9dd Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 13 Sep 2022 09:34:42 +0100 Subject: [PATCH 876/878] Add a few debug messages to watch the RTSP connection. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index 8cc36beb..4c05e394 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1297,7 +1297,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes if (nread == 0) { // a blocking read that returns zero means eof -- implies connection closed - debug(3, "Connection %d: -- connection closed.", conn->connection_number); + debug(1, "Connection %d: connection closed.", conn->connection_number); reply = rtsp_read_request_response_channel_closed; goto shutdown; } @@ -5407,7 +5407,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(2, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(1, "Connection %d: new 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_start_time = get_absolute_time_in_ns(); From 8dfebea2228d0d5f7f042fcba69a6afc601a50c1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:09:33 +0100 Subject: [PATCH 877/878] Make the RTSP idle checker a bit more resilient -- wait for the full timeout even if interrupted. Also signal to NQPTP when to stop listening and when to start w.r.t. idle and when a session stop. --- rtsp.c | 102 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/rtsp.c b/rtsp.c index 4c05e394..3477bbe8 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3,7 +3,7 @@ * Copyright (c) James Laird 2013 * Modifications associated with audio synchronization, multithreading and - * metadata handling copyright (c) Mike Brady 2014-2021 + * metadata handling copyright (c) Mike Brady 2014-2022 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -1215,52 +1215,92 @@ static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t cou ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time, void *buf, size_t count) { - struct timeval tv; - tv.tv_sec = wait_time / 1000000000; // seconds - tv.tv_usec = (wait_time % 1000000000) / 1000; // microseconds - if (setsockopt(conn->fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) != 0) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "could not set time limit on timed_read_from_rtsp_connection -- error %d \"%s\".", - errno, errorstring); - } + // note: a wait time of zero means wait forever + 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();; + time_to_wait_to = time_to_wait_to + wait_time; + + // 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(); + } + do { + struct timeval tv; + tv.tv_sec = remaining_time / 1000000000; // seconds + tv.tv_usec = (remaining_time % 1000000000) / 1000; // microseconds + if (setsockopt(conn->fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) != 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "could not set time limit on timed_read_from_rtsp_connection -- error %d \"%s\".", + errno, errorstring); + } #ifdef CONFIG_AIRPLAY_2 - if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { - conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; - return read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); - } else { - return read(conn->fd, buf, count); - } + 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); + } else { + result = read(conn->fd, buf, count); + } #else - return read(conn->fd, buf, count); + result = read(conn->fd, buf, count); #endif + 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)); + + } else { + debug(1,"Connection %d: attempt to read from a closed RTSP connection.",conn->connection_number); + } + return result; } +#ifdef CONFIG_AIRPLAY_2 +void set_client_as_ptp_clock(rtsp_conn_info *conn) { + char timing_list_message[4096] = ""; + strncat(timing_list_message, "T ", 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)); + ptp_send_control_message_string(timing_list_message); +} + +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) { // first try to read with a timeout, to see if there is any traffic... ssize_t response = timed_read_from_rtsp_connection(conn, 4000000000L, buf, count); if ((response == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { if (conn->rtsp_link_is_idle == 0) { + debug(1, "Connection %d: RTSP connection is idle.", conn->connection_number); conn->rtsp_link_is_idle = 1; #ifdef CONFIG_AIRPLAY_2 conn->last_anchor_info_is_valid = 0; + clear_ptp_clock(); #endif conn->anchor_remote_info_is_valid = 0; conn->udp_clock_sender_is_initialised = 0; conn->udp_clock_is_initialised = 0; - debug(1, "Connection %d: RTSP connection is idle.", conn->connection_number); } response = timed_read_from_rtsp_connection(conn, 0, buf, count); } if (conn->rtsp_link_is_idle == 1) { + debug(1, "Connection %d: RTSP connection traffic has resumed.", conn->connection_number); conn->local_to_remote_time_difference_measurement_time = 0; conn->local_to_remote_time_difference = 0; conn->rtsp_link_is_idle = 0; conn->first_packet_timestamp = 0; conn->input_frame_rate_starting_point_is_valid = 0; ab_resync(conn); - debug(1, "Connection %d: RTSP connection traffic has resumed.", conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 + set_client_as_ptp_clock(conn); +#endif } return response; } @@ -1297,7 +1337,12 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes if (nread == 0) { // a blocking read that returns zero means eof -- implies connection closed - debug(1, "Connection %d: connection closed.", conn->connection_number); + + 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; reply = rtsp_read_request_response_channel_closed; goto shutdown; } @@ -1621,6 +1666,7 @@ 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); @@ -2504,7 +2550,7 @@ 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; @@ -2534,6 +2580,8 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp ptp_send_control_message_string(timing_list_message); } plist_free(addresses_array); +*/ + //set_client_as_ptp_clock(conn); resp->respcode = 200; } #endif @@ -2566,7 +2614,7 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * void teardown_phase_one(rtsp_conn_info *conn) { // this can be called more than once on the same connection -- - // by the player itself but also by the play seesion being killed + // by the player itself but also by the play session being killed if (conn->player_thread) { player_stop(conn); // this nulls the player_thread activity_monitor_signify_activity(0); // inactive, and should be after command_stop() @@ -2637,6 +2685,7 @@ void teardown_phase_two(rtsp_conn_info *conn) { conn->dacp_active_remote = NULL; } release_play_lock(conn); + clear_ptp_clock(); } } @@ -2846,7 +2895,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (ptp_shm_interface_open() != 0) // it should be open already, but just in case it isn't... die("Can not access the NQPTP service. Has it stopped running?"); - ptp_send_control_message_string("T"); // remove all previous history + // clear_ptp_clock(); debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); if (groupUUID) { @@ -2956,7 +3005,8 @@ 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)); plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); @@ -4902,7 +4952,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { close(conn->fd); debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); - debug(2, "Connection %d: terminating connection from %s:%u to self at %s:%u.", + 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, conn->self_ip_string, conn->self_rtsp_port); } @@ -5407,7 +5457,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(1, "Connection %d: new connection from %s:%u to self at %s:%u.", + debug(1, "Connection %d: New 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_start_time = get_absolute_time_in_ns(); From 694c9f5ad6e1d0779fce89b838463b96569f684d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:20:30 +0100 Subject: [PATCH 878/878] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 9986bf37..e0ac0d8f 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-532-g8dfebea2 +==== +**Enhancements** +* Improve the response of Shairport Sync when a Mac is woken up from sleep and continues playing e.g. a YouTube video. Shairport Sync should resume play in most circumstances. (BTW, the HomePod mini does not resume in these circumstances.) + +**Bug Fix** +* Make the RTSP idle checker a bit more resilient -- wait for the full timeout even if interrupted by e.g. `SIGINT`. Also signal to NQPTP when to stop listening when a session goes idle or stops. + Version 4.1-dev-526-gfd880056 ==== ***Pesky Changes You Can't Ignore***