diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 9029b462..ae2ef389 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -23,16 +23,17 @@ for timing and synchronisation in AirPlay 2. NQPTP must run as `root` and must h Lossless and High Definition Lossless material is transcoded to AAC before it reaches Shairport Sync. ## What You Need -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 recommended. +AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. A system with the power of a Raspberry Pi 2 or Raspberry Pi Zero 2 W, or better, is recommended. 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. * 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. +* Shairport Sync will not run in AirPlay 2 mode 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](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. +- Multiple instances of the AirPlay 2 version of Shairport Sync can not be hosted on the same system. It seems that AirPlay 2 clients are confused by having multiple AirPlay 2 players at the same IP addresses. ## Guides * A building guide is available at [BUILD.md](BUILD.md). diff --git a/BUILD.md b/BUILD.md index 58f5f35a..3d5bc4a9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -39,7 +39,7 @@ Okay, now let's get the tools and libraries for building and installing Shairpor # 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 xxd + 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: ``` @@ -54,7 +54,7 @@ For AirPlay 2 operation, _before you install the libraries_, please ensure the y # 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 libuuid-devel vim-common \ + ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-dev libuuid-devel vim-common \ alsa-lib-devel ``` If you are building classic Shairport Sync, the list of packages is shorter: @@ -121,7 +121,7 @@ Download, install, enable and start NQPTP from [here](https://github.com/mikebra ### Shairport Sync #### Build and Install -Download Shairport Sync, configure, compile and install it. Before executing the commands, please note the following: +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. @@ -130,6 +130,7 @@ Download Shairport Sync, configure, compile and install it. Before executing the ``` $ 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 diff --git a/Makefile.am b/Makefile.am index 1dc654cc..bc6710d8 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/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index d6436fdf..2bccf048 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -116,6 +116,15 @@ 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: +**Classic AirPlay:** +``` +sudo ufw allow 3689/tcp +sudo ufw allow 5353 +sudo ufw allow 5000/tcp +sudo ufw allow 6000:6009/udp +``` + +**AirPlay 2:** ``` sudo ufw allow 319:320/udp sudo ufw allow 3689/tcp @@ -123,9 +132,11 @@ 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 +sudo ufw allow 32768:60999/udp +sudo ufw allow 32768:60999/tcp ``` -Ports 319 and 320 need to be opened for AirPlay 2 only. + +The range `32768:60999` represents the [ephemeral port range](https://en.m.wikipedia.org/wiki/Ephemeral_port) which seems to be standardised on those values. 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. diff --git a/activity_monitor.c b/activity_monitor.c index 0de7d341..896a79f9 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -56,8 +56,7 @@ 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 @@ -71,17 +70,12 @@ void going_active(int block) { #endif if (config.disable_standby_mode == disable_standby_auto) { -#ifdef CONFIG_DBUS_INTERFACE - if (dbus_service_is_running()) - shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE); -#endif config.keep_dac_busy = 1; } } void going_inactive(int block) { - // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : - // "out"); + // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : "out"); if (config.cmd_active_stop) command_execute(config.cmd_active_stop, "", block); #ifdef CONFIG_METADATA @@ -95,10 +89,6 @@ void going_inactive(int block) { #endif if (config.disable_standby_mode == disable_standby_auto) { -#ifdef CONFIG_DBUS_INTERFACE - if (dbus_service_is_running()) - shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE); -#endif config.keep_dac_busy = 0; } } diff --git a/audio_dummy.c b/audio_dummy.c index 4ec1d29d..1ea68ff0 100644 --- a/audio_dummy.c +++ b/audio_dummy.c @@ -58,7 +58,7 @@ static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int s // 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, + debug(2, "leadtime for frame %u is %" PRId64 " nanoseconds, i.e. %f seconds.", timestamp, lead_time, 0.000000001 * lead_time); } diff --git a/configure.ac b/configure.ac index 477030db..b0f53cd9 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.1-rc0], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.1-dev], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE([subdir-objects]) AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -412,6 +412,11 @@ 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_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}"],[ diff --git a/rtsp.c b/rtsp.c index 76395df9..62da82e5 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1830,92 +1830,95 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message 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); - } + if (messagePlist != NULL) { + 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); + } - item = plist_dict_get_item(messagePlist, "flushFromTS"); - if (item == NULL) { - if (flushFromValid != 0) - debug(1, "flushFromSeq without flushFromTS!"); - else - debug(2, "Can't find a flushFromTS"); - } else { - plist_get_uint_val(item, &flushFromTS); + item = plist_dict_get_item(messagePlist, "flushFromTS"); + if (item == NULL) { + if (flushFromValid != 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 { + 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); + // 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) - debug(1, "flushFromTS without flushFromSeq!"); - debug(2, "flushFromTS is %" PRId64 ".", flushFromTS); + conn->ap2_play_enabled = 0; + + // 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(2, "Deferred Flush Requested"); + else + debug(2, "Immediate Flush Requested"); + + plist_free(messagePlist); + // display_all_flush_requests(conn); } - 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); - // 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; - - // 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(2, "Deferred Flush Requested"); - else - debug(2, "Immediate Flush Requested"); - - // display_all_flush_requests(conn); - resp->respcode = 200; } @@ -2003,6 +2006,10 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message activity_monitor_signify_activity(0); conn->ap2_play_enabled = 0; reset_anchor_info(conn); + if (config.output->stop) { + debug(2, "Connection %d: Stop the output backend.", conn->connection_number); + config.output->stop(); + } } pthread_cleanup_pop(1); // unlock the conn->flush_mutex } @@ -2218,7 +2225,7 @@ void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *re 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, + debug(2, "Connection %d: handle_pair-setup Content-Length %d", conn->connection_number, req->contentlength); if (!conn->ap2_pairing_context.setup_ctx) { @@ -2819,6 +2826,7 @@ static void check_and_send_plist_metadata(plist_t messagePlist, const char *plis plist_get_string_val(item, &value); if (value != NULL) { send_metadata('ssnc', metadata_code, value, strlen(value), NULL, 0); + free(value); } } } @@ -4169,19 +4177,22 @@ void metadata_stop(void) { int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, int block) { - // parameters: type, code, pointer to data or NULL, length of data or NULL, - // the rtsp_message or - // NULL + // clang-format off + // parameters: + // type, + // code, + // pointer to data or NULL, + // length of data or NULL, + // the rtsp_message or NULL, + // the rtsp_message is sent for 'core' messages, because it contains the data - // and must not be - // freed until the data has been read. So, it is passed to send_metadata to be - // retained, - // sent to the thread where metadata is processed and released (and probably - // freed). + // and must not be freed until the data has been read. + // So, it is passed to send_metadata to be retained, sent to the thread where metadata + // is processed and released (and probably freed). // The rtsp_message is also sent for certain non-'core' messages. - // The reading of the parameters is a bit complex + // The reading of the parameters is a bit complex: // If the rtsp_message field is non-null, then it represents an rtsp_message // and the data pointer is assumed to point to something within it. // The reference counter of the rtsp_message is incremented here and @@ -4191,8 +4202,10 @@ int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char * // If the rtsp_message is NULL, then if the pointer is non-null then the data it // points to, of the length specified, is memcpy'd and passed to the metadata // handler. The handler should free it when done. + // If the rtsp_message is NULL and the pointer is also NULL, nothing further // is done. + // clang-format on metadata_package pack; pack.type = type; diff --git a/shairport.c b/shairport.c index aec61bc7..9f3ce855 100644 --- a/shairport.c +++ b/shairport.c @@ -45,6 +45,7 @@ #ifdef CONFIG_AIRPLAY_2 #include "ptp-utilities.h" +#include #include #include #include @@ -570,7 +571,7 @@ int parse_options(int argc, char **argv) { uuid_t binuuid; uuid_generate_random(binuuid); - char *uuid = malloc(UUID_STR_LEN); + char *uuid = malloc(UUID_STR_LEN + 1); // leave space for the NUL at the end // Produces a UUID string at uuid consisting of lower-case letters uuid_unparse_lower(binuuid, uuid); config.airplay_pi = uuid; @@ -1307,7 +1308,6 @@ int parse_options(int argc, char **argv) { // now, do the command line options again, but this time do them fully -- it's a unix convention // that command line // arguments have precedence over configuration file settings. - optind = argc; for (j = 0; j < argc; j++) if (strcmp(argv[j], "--") == 0) @@ -1652,7 +1652,6 @@ void exit_function() { } } #endif - if (config.cfg) config_destroy(config.cfg); if (config.appName) @@ -2103,6 +2102,28 @@ int main(int argc, char **argv) { 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 */