From 4ca5a15de2d53c69e6c3c23b0440c27978bb91df Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 8 Dec 2022 09:37:59 +0000 Subject: [PATCH 01/85] Update TROUBLESHOOTING.md --- TROUBLESHOOTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 174a16fc..b20d898f 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -66,7 +66,10 @@ There are some more details in some the closed issues on this repository. ### VPNs -To see the AirPlay service Shairport Sync provides, your devices must be on the same subnet as Shairport Sync. If, say, a device such as an iPhone is on a VPN and Shairport Sync is not, or vice-versa, then even though the devices might be physically on the same network, they are effectively on separate networks due to the VPN and the AirPlay service will not be accessible to the device. So, when you are troubleshooting, look out for VPN issues. +To see the AirPlay service Shairport Sync provides, your devices must be on the same subnet as Shairport Sync. If, say, a device such as an iPhone is on a VPN and Shairport Sync is not, or vice-versa, then even though the devices might be physically on the same network, they are effectively on separate networks due to the VPN and the AirPlay service will not be accessible to the device. So, when you are troubleshooting, look out for VPN issues. + +### Ubiquiti Routers +Ubiquiti router settings occasionally cause services that use Boujour/Zeroconf -- including Shairport Sync -- not to appear on the network. (AirPrint is another example of a service that can be affected by this problem.) [This link](https://community.ui.com/questions/Issues-with-Bonjour-on-UDM-Pro/6db37e3f-73cf-4aaf-8dba-8a196fd0e2b1) to a Ubiquiti community discussion might be helpful. ### 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). From 3cdb8b790c6af59a7e39c2ab29e54efb4f0684c6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 10 Dec 2022 11:57:19 +0000 Subject: [PATCH 02/85] Send B/E/P signals to NQPTP to tell it when SPS is playing. The clock should not be sleeping when SPS is playing or paused. --- player.c | 3 +++ rtsp.c | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/player.c b/player.c index 61b4e5ff..b920971c 100644 --- a/player.c +++ b/player.c @@ -3565,6 +3565,9 @@ int player_stop(rtsp_conn_info *conn) { // debuglev = 3; debug(3, "player_stop"); if (conn->player_thread) { +#ifdef CONFIG_AIRPLAY_2 + ptp_send_control_message_string("E"); // signify play is "E"nding +#endif debug(3, "player_thread cancel..."); pthread_cancel(*conn->player_thread); debug(3, "player_thread join..."); diff --git a/rtsp.c b/rtsp.c index c68feb43..abebb645 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1276,6 +1276,7 @@ void set_client_as_ptp_clock(rtsp_conn_info *conn) { 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); + // the clock will be active for a few seconds after this. } void clear_ptp_clock() { ptp_send_control_message_string("T"); } @@ -2003,6 +2004,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) { + ptp_send_control_message_string("B"); // signify play is "B"eginning or resuming debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); @@ -2013,6 +2015,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message conn->ap2_play_enabled = 1; } else { debug(2, "Connection %d: Stop playing.", conn->connection_number); + ptp_send_control_message_string("P"); // signify play is "P"ausing conn->ap2_play_enabled = 0; activity_monitor_signify_activity(0); reset_anchor_info(conn); @@ -3161,6 +3164,8 @@ 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) { + ptp_send_control_message_string("B"); // signify play is "B"eginning + // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); From 12586cd82ee81e3d0cd26b55ead436335f4cf397 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 13 Dec 2022 09:29:58 +0000 Subject: [PATCH 03/85] Bump the SHM Interface number to note new commands. --- nqptp-shm-structures.h | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 3dbe0c90..251d06c1 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -22,26 +22,36 @@ #define NQPTP_INTERFACE_NAME "/nqptp" -#define NQPTP_SHM_STRUCTURES_VERSION 8 +#define NQPTP_SHM_STRUCTURES_VERSION 9 #define NQPTP_CONTROL_PORT 9000 -// 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 control port expects a UDP packet with the first character being a command letter +// and the rest being any arguments, the whole not to exceed 4096 characters. +// The "T" command, must followed by nothing or by +// a space and a space-delimited list of IPv4 or IPv6 numbers. // 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. +// previous list. The first IP number is the clock that NQPTP will listen to. +// The remaining IP address are the addresses of all the timing peers. The timing peers +// are not used in this version of NQPTP. // 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. +// The "B" command is a message that the client -- which generates the clock -- +// is about to start playing. +// NQPTP uses it to determine that the clock is active and will not sleep. +// The "E" command signifies that the client has stopped playing and that +// the clock may shortly sleep. +// The "P" command signifies that SPS has paused play (buffered audio only). +// The clock seems to stay running in this state. + +// When the clock is active, it is assumed that any decreases in the offset +// between the local and remote clocks are due to delays in the network. +// NQPTP smooths the offset by clamping any decreases to a small value. +// In this way, it can follow clock drift but ignore network delays. + +// When the clock is inactive, it can stop running. This causes the offset to decrease. +// NQPTP clock smoothing would treat this as a network delay, causing true sync to be lost. +// To avoid this, when the clock goes from inactive to active, +// NQPTP resets clock smoothing to the new offset. + #include #include From db155abb64e49ea7b01ed98c9f47da26447af864 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:12:01 +0000 Subject: [PATCH 04/85] Danger note. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 9a65b3ec..8b181e91 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ Metadata such as artist information and cover art can be requested and provided Shairport Sync does not support AirPlay video or photo streaming. +# Danger +This branch of the Shairport Sync repository is very likely to be buggy and can change very rapidly and without warning. The `development` branch is more stable, although it too may contains bugs. The `master` branch is the most stable. + +The `danger` branch version of Shairport Sync may only be compatible with the `danger` branch of NQPTP. + # Quick Start * A building guide is available [here](BUILD.md). * A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). From 51b9c5989c2850118d4ec29e4403161a372c7fe2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:17:24 +0000 Subject: [PATCH 05/85] Update BUILD.md --- BUILD.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index f6dc3ad3..6594a482 100644 --- a/BUILD.md +++ b/BUILD.md @@ -7,6 +7,11 @@ Overall, you'll be building and installing two programs – Shairport Sync itsel 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`. +# Danger +This branch of the Shairport Sync repository is very likely to be buggy and can change very rapidly and without warning. The `development` branch is more stable, although it too may contains bugs. The `master` branch is the most stable. + +The `danger` branch version of Shairport Sync may only be compatible with the `danger` branch of NQPTP. + ## 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: @@ -118,11 +123,11 @@ 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 the `danger` branch of NQPTP from [here](https://github.com/mikebrady/nqptp/blob/danger/README.md). ### 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: +Download Shairport Sync, check out the `danger` 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. @@ -131,7 +136,7 @@ Download Shairport Sync, check out the `development` branch and configure, compi ``` $ git clone https://github.com/mikebrady/shairport-sync.git $ cd shairport-sync -$ git checkout development +$ git checkout danger $ autoreconf -fi $ ./configure --sysconfdir=/etc --with-alsa \ --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2 From 14b3fc4de86451445a535954b0994f1a232105c4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:30:22 +0000 Subject: [PATCH 06/85] Fix endless error messages. --- audio_stdout.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/audio_stdout.c b/audio_stdout.c index 61d91bfa..2a54f072 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -37,21 +37,23 @@ #include static int fd = -1; +static int warned = 0; + static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { fd = STDOUT_FILENO; + warned = 0; } 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); if ((rc < 0) && (warned == 0)) { strerror_r(errno, (char *)errorstring, 1024); - warn("Error %d writing to stdout: \"%s\".", errno, errorstring); + warn("Error %d writing to stdout (fd: %d): \"%s\".", errno, fd, errorstring); warned = 1; } return rc; @@ -69,6 +71,7 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * // get settings from settings file // do the "general" audio options. Note, these options are in the "general" stanza! parse_general_audio_options(); + return 0; } From 74c093351a85711820974570becee7611cf6a821 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:33:28 +0000 Subject: [PATCH 07/85] Update check_ap2_systemd_full.yml Run on the `danger` branch too. --- .github/workflows/check_ap2_systemd_full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 780dd3a4..858b2af5 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -2,7 +2,7 @@ name: Full configuration (but without apple-alac) for systemd. on: push: - branches: [ "development" ] + branches: [ "development", "danger" ] jobs: build: From 527edf6aa5f55928c34523aff095c5a3d9872c6f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:33:57 +0000 Subject: [PATCH 08/85] Update check_classic_systemd_full.yml Run on the `danger` branch too. --- .github/workflows/check_classic_systemd_full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index bbc7c02e..06744a03 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -2,7 +2,7 @@ name: Full classic configuration (but without apple-alac) for systemd, using a b on: push: - branches: [ "development" ] + branches: [ "development", "danger" ] jobs: build: From b72e2c649db924693a4b27b63b9e905172cecbf2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:35:37 +0000 Subject: [PATCH 09/85] Update check_classic_mac_basic.yml Run on the `danger` branch too. --- .github/workflows/check_classic_mac_basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index 2dc4e715..d41d0fc8 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -2,7 +2,7 @@ name: Basic libao configuration for macOS with BREW -- classic only, because mac on: push: - branches: [ "development" ] + branches: [ "development", "danger" ] jobs: build: From e518891b298e3aa7e3ea36c025333aa31a17009a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 19 Dec 2022 09:12:23 +0000 Subject: [PATCH 10/85] Shorten the keepalive timeout time to 15 seconds. --- rtsp.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rtsp.c b/rtsp.c index abebb645..02c6c975 100644 --- a/rtsp.c +++ b/rtsp.c @@ -5503,12 +5503,12 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. - // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up - // an ETIMEOUT error is returned if the keepalive check fails + // Turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up + // An ETIMEOUT error is returned if the keepalive check fails - int keepAliveIdleTime = 35; // wait this many seconds before checking for a dropped client + int keepAliveIdleTime = 10; // wait this many seconds before checking for a dropped client int keepAliveCount = 5; // check this many times - int keepAliveInterval = 5; // wait this many seconds between checks + int keepAliveInterval = 1; // wait this many seconds between checks #if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX From d6536a8e798c73c85fb5eb1e6c3f1d57e0dff4a7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Dec 2022 18:36:33 +0000 Subject: [PATCH 11/85] Call the unfixable error if the output device can't be opened or disappears. Clang-format. --- audio_alsa.c | 39 +++++++++++++++++++++++-------- audio_stdout.c | 3 +-- common.h | 6 +++-- dbus-service.c | 3 ++- metadata_hub.c | 3 ++- metadata_hub.h | 8 +++---- nqptp-shm-structures.h | 3 +-- player.c | 32 +++++++++++++------------ player.h | 3 +-- rtsp.c | 53 +++++++++++++++++++++--------------------- 10 files changed, 87 insertions(+), 66 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 9a37f3f6..3c8cebd0 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -444,13 +444,19 @@ static int actual_open_alsa_device(int do_auto_setup) { 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]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, - alsa_out_dev); + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; + char messageString[1024]; + messageString[0] = '\0'; + snprintf(messageString, sizeof(messageString), "output_device_error %d", -ret); + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, messageString, 1); + } else { + die("an unrecoverable error, \"output_device_error %d\", has been " + "detected.", -ret); + } + } } alsa_handle_status = ret; frames_sent_break_occurred = 1; @@ -1795,10 +1801,23 @@ static int do_play(void *buf, int samples) { } } } else { - char errorstring[1024]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret, - (char *)errorstring, samples); + + if (ret == -ENODEV) { + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, "output_device_error 19", 1); + } else { + die("an unrecoverable error, \"output_device_error 19\", has been " + "detected."); + } + } + } else { + char errorstring[1024]; + strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); + debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret, + (char *)errorstring, samples); + } } } } diff --git a/audio_stdout.c b/audio_stdout.c index 2a54f072..ef9df61d 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -39,11 +39,10 @@ static int fd = -1; static int warned = 0; - static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { fd = STDOUT_FILENO; - warned = 0; + warned = 0; } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, diff --git a/common.h b/common.h index 639c1c0f..66c02ca6 100644 --- a/common.h +++ b/common.h @@ -37,7 +37,8 @@ typedef enum { typedef enum { TOE_normal, TOE_emergency, - TOE_dbus // a request was made on a D-Bus interface (the native D-Bus or MPRIS interfaces)-- don't wait for the dbus thread to exit + TOE_dbus // a request was made on a D-Bus interface (the native D-Bus or MPRIS interfaces)-- don't + // wait for the dbus thread to exit } type_of_exit_type; #define sps_extra_code_output_stalled 32768 @@ -160,7 +161,7 @@ typedef struct { int mqtt_enable_remote; char *mqtt_empty_payload_substitute; #endif - uint8_t ap1_prefix[6]; + uint8_t ap1_prefix[6]; uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; int udp_port_base; @@ -313,6 +314,7 @@ typedef struct { char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist char *nqptp_shared_memory_interface_name; // client name for nqptp service #endif + int unfixable_error_reported; // only report once. } shairport_cfg; // accessors to config for multi-thread access diff --git a/dbus-service.c b/dbus-service.c index 4cfc8254..70792248 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -111,7 +111,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) th = shairport_sync_get_first_frame_position(shairportSyncSkeleton); if ((th == NULL) || (strcasecmp(th, argc->first_frame_position_string) != 0)) { // debug(1, "First frame position string should be changed"); - shairport_sync_set_first_frame_position(shairportSyncSkeleton, argc->first_frame_position_string); + shairport_sync_set_first_frame_position(shairportSyncSkeleton, + argc->first_frame_position_string); } } diff --git a/metadata_hub.c b/metadata_hub.c index e39752fe..796e0927 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -598,7 +598,8 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin if (string_update(&metadata_store.first_frame_position_string, &metadata_store.first_frame_position_string_changed, cs)) { changed = 1; - debug(2, "MH First Frame Position String set to: \"%s\"", metadata_store.first_frame_position_string); + debug(2, "MH First Frame Position String set to: \"%s\"", + metadata_store.first_frame_position_string); } free(cs); break; diff --git a/metadata_hub.h b/metadata_hub.h index 8235c6e4..94b3acaa 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -48,7 +48,7 @@ typedef struct metadata_bundle { char *server_ip; // IP number used by Shairport Sync int server_ip_changed; - + char *stream_type; // Realtime or Buffered int stream_type_changed; @@ -79,7 +79,7 @@ typedef struct metadata_bundle { char *cover_art_pathname; int cover_art_pathname_changed; - + uint64_t item_id; // seems to be a track ID -- see itemid in DACP.c int item_id_changed; int item_id_is_valid; @@ -89,11 +89,10 @@ typedef struct metadata_bundle { int item_composite_id_changed; int item_composite_id_is_valid; - int song_data_kind; int song_data_kind_changed; int song_data_kind_is_valid; - + char *track_name; int track_name_changed; @@ -139,7 +138,6 @@ typedef struct metadata_bundle { uint32_t songtime_in_milliseconds; int songtime_in_milliseconds_changed; int songtime_in_milliseconds_is_valid; - // end diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 251d06c1..5982a730 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -50,8 +50,7 @@ // When the clock is inactive, it can stop running. This causes the offset to decrease. // NQPTP clock smoothing would treat this as a network delay, causing true sync to be lost. // To avoid this, when the clock goes from inactive to active, -// NQPTP resets clock smoothing to the new offset. - +// NQPTP resets clock smoothing to the new offset. #include #include diff --git a/player.c b/player.c index b920971c..0898feda 100644 --- a/player.c +++ b/player.c @@ -106,7 +106,7 @@ int64_t first_frame_early_bias = 8; // default buffer size // needs to be a power of 2 because of the way BUFIDX(seqno) works -//#define BUFFER_FRAMES 512 +// #define BUFFER_FRAMES 512 #define MAX_PACKET 2048 // DAC buffer occupancy stuff @@ -531,9 +531,9 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta (uint64_t)(config.resend_control_first_check_time * (uint64_t)1000000000); uint64_t resend_repeat_interval = (uint64_t)(config.resend_control_check_interval_time * (uint64_t)1000000000); - uint64_t minimum_remaining_time = (uint64_t)( - (config.resend_control_last_check_time + config.audio_backend_buffer_desired_length) * - (uint64_t)1000000000); + uint64_t minimum_remaining_time = (uint64_t)((config.resend_control_last_check_time + + config.audio_backend_buffer_desired_length) * + (uint64_t)1000000000); uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)1000000000); latency_time = latency_time / (uint64_t)conn->input_rate; @@ -1271,14 +1271,13 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } else { if (resp == sps_extra_code_output_stalled) { - if (conn->unfixable_error_reported == 0) { - conn->unfixable_error_reported = 1; + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; if (config.cmd_unfixable) { command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { die("an unrecoverable error, \"output_device_stalled\", has been " - "detected.", - conn->connection_number); + "detected."); } } } else { @@ -1838,7 +1837,8 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; #ifdef CONFIG_METADATA - uint64_t time_of_last_metadata_progress_update = 0; // the assignment is to stop a compiler warning... + uint64_t time_of_last_metadata_progress_update = + 0; // the assignment is to stop a compiler warning... #endif uint64_t previous_frames_played = 0; // initialised to avoid a "possibly uninitialised" warning uint64_t previous_raw_measurement_time = @@ -1982,15 +1982,16 @@ void *player_thread_func(void *arg) { debug(3, "Output frame bytes is %d.", conn->output_bytes_per_frame); - conn->dac_buffer_queue_minimum_length = (uint64_t)( - config.audio_backend_buffer_interpolation_threshold_in_seconds * config.output_rate); + conn->dac_buffer_queue_minimum_length = + (uint64_t)(config.audio_backend_buffer_interpolation_threshold_in_seconds * + config.output_rate); debug(3, "dac_buffer_queue_minimum_length is %" PRIu64 " frames.", conn->dac_buffer_queue_minimum_length); conn->session_corrections = 0; conn->connection_state_to_output = get_requested_connection_state_to_output(); // this is about half a minute -//#define trend_interval 3758 +// #define trend_interval 3758 // this is about 8 seconds #define trend_interval 1003 @@ -2629,8 +2630,8 @@ void *player_thread_func(void *arg) { } else { current_delay = 0; if ((resp == sps_extra_code_output_stalled) && - (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; + (config.unfixable_error_reported == 0)) { + config.unfixable_error_reported = 1; if (config.cmd_unfixable) { warn("Connection %d: An unfixable error has been detected -- output device is " "stalled. Executing the " @@ -2780,7 +2781,8 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- Classic AirPlay (\"AirPlay 1\").", + inform("Connection %d: Playback started at frame %" PRId64 + " -- Classic AirPlay (\"AirPlay 1\").", conn->connection_number, inframe->given_timestamp); #endif } diff --git a/player.h b/player.h index 1c18567a..14e352ff 100644 --- a/player.h +++ b/player.h @@ -168,8 +168,7 @@ typedef struct { volatile int stop; volatile int running; volatile uint64_t watchdog_bark_time; - volatile int watchdog_barks; // number of times the watchdog has timed out and done something - int unfixable_error_reported; // set when an unfixable error command has been executed. + volatile int watchdog_barks; // number of times the watchdog has timed out and done something uint64_t playstart; uint64_t connection_start_time; // the time the device is selected, which could be a long time diff --git a/rtsp.c b/rtsp.c index 02c6c975..ac3ce652 100644 --- a/rtsp.c +++ b/rtsp.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -47,7 +48,6 @@ #include #include #include -#include #include @@ -665,8 +665,8 @@ void *player_watchdog_thread_code(void *arg) { conn->stop = 1; pthread_cancel(conn->thread); } else if (conn->watchdog_barks == 3) { - if ((config.cmd_unfixable) && (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; + if ((config.cmd_unfixable) && (config.unfixable_error_reported == 0)) { + config.unfixable_error_reported = 1; command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); } else { die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", @@ -1223,10 +1223,10 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time uint64_t time_to_wait_to = get_absolute_time_in_ns(); ; time_to_wait_to = time_to_wait_to + wait_time; - + int flags = 1; if (setsockopt(conn->fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { - debug(1,"can't enable keepalive checking on the RTSP socket"); + debug(1, "can't enable keepalive checking on the RTSP socket"); } // remaining_time will be zero if wait_time is zero @@ -1356,7 +1356,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes // Note: the socket will be closed when the thread exits goto shutdown; } - + // An ETIMEDOUT error usually means keepalive has failed. if (nread < 0) { @@ -2070,7 +2070,7 @@ struct pairings { uint8_t public_key[32]; struct pairings *next; -} * pairings; +} *pairings; static struct pairings *pairing_find(const char *device_id) { for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { @@ -3165,7 +3165,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { ptp_send_control_message_string("B"); // signify play is "B"eginning - + // get stream[0] plist_t stream0 = plist_array_get_item(streams, 0); @@ -5502,39 +5502,40 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. - - // Turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up - // An ETIMEOUT error is returned if the keepalive check fails + + // Turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds + // before giving up An ETIMEOUT error is returned if the keepalive check fails int keepAliveIdleTime = 10; // wait this many seconds before checking for a dropped client - int keepAliveCount = 5; // check this many times - int keepAliveInterval = 1; // wait this many seconds between checks - + int keepAliveCount = 5; // check this many times + int keepAliveInterval = 1; // wait this many seconds between checks #if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX - #define SOL_OPTION IPPROTO_TCP +#define SOL_OPTION IPPROTO_TCP #else - #define SOL_OPTION SOL_TCP +#define SOL_OPTION SOL_TCP #endif #ifdef COMPILE_FOR_OSX - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE #else - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE #endif - - if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { - debug(1,"can't set the keepidle wait time"); + if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, + (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { + debug(1, "can't set the keepidle wait time"); } - if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, sizeof(keepAliveCount))) { - debug(1,"can't set the keepidle missing count"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, + sizeof(keepAliveCount))) { + debug(1, "can't set the keepidle missing count"); } - if (setsockopt(conn->fd, SOL_OPTION , TCP_KEEPINTVL, (void *)&keepAliveInterval, sizeof(keepAliveInterval))) { - debug(1,"can't set the keepidle missing count interval"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPINTVL, (void *)&keepAliveInterval, + sizeof(keepAliveInterval))) { + debug(1, "can't set the keepidle missing count interval"); }; - + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; From f4c9c0bcd8bdc49b2fde8d20274e8e4475007ff7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Dec 2022 18:49:10 +0000 Subject: [PATCH 12/85] Move unfixable_error_reported flag from conn to config. --- rtsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index c68feb43..2104caca 100644 --- a/rtsp.c +++ b/rtsp.c @@ -665,8 +665,8 @@ void *player_watchdog_thread_code(void *arg) { conn->stop = 1; pthread_cancel(conn->thread); } else if (conn->watchdog_barks == 3) { - if ((config.cmd_unfixable) && (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; + if ((config.cmd_unfixable) && (config.unfixable_error_reported == 0)) { + config.unfixable_error_reported = 1; command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); } else { die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", From 5bc072eb9f79026997103388932177419674dda5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Dec 2022 19:21:16 +0000 Subject: [PATCH 13/85] small layout edit to shairport-sync.conf --- scripts/shairport-sync.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 660d2b2a..61a56684 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -81,7 +81,8 @@ sessioncontrol = // run_this_before_play_begins = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line // run_this_after_play_ends = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line -// run_this_if_an_unfixable_error_is_detected = "/full/path/to/application and args"; // if a problem occurs that can't be cleared by Shairport Sync itself, hook a program on here to deal with it. An error code-string is passed as the last argument. +// run_this_if_an_unfixable_error_is_detected = "/full/path/to/application and args"; // if a problem occurs that can't be cleared by Shairport Sync itself, hook a program on here to deal with it. +// An error code-string is passed as the last argument. // Many of these "unfixable" problems are caused by malfunctioning output devices, and sometimes it is necessary to restart the whole device to clear the problem. // You could hook on a program to do this automatically, but beware -- the device may then power off and restart without warning! // wait_for_completion = "no"; // set to "yes" to get Shairport Sync to wait until the "run_this..." applications have terminated before continuing From ae5910e2e7c34f8b3bf2c2703b4cb03a7719211d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Dec 2022 19:31:01 +0000 Subject: [PATCH 14/85] make the error message one single identifier by linking the error number to the message with an underscore. --- audio_alsa.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 3c8cebd0..608696ce 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -449,11 +449,11 @@ static int actual_open_alsa_device(int do_auto_setup) { config.unfixable_error_reported = 1; char messageString[1024]; messageString[0] = '\0'; - snprintf(messageString, sizeof(messageString), "output_device_error %d", -ret); + snprintf(messageString, sizeof(messageString), "output_device_error_%d", -ret); if (config.cmd_unfixable) { command_execute(config.cmd_unfixable, messageString, 1); } else { - die("an unrecoverable error, \"output_device_error %d\", has been " + die("an unrecoverable error, \"output_device_error_%d\", has been " "detected.", -ret); } } @@ -1806,9 +1806,9 @@ static int do_play(void *buf, int samples) { if (config.unfixable_error_reported == 0) { config.unfixable_error_reported = 1; if (config.cmd_unfixable) { - command_execute(config.cmd_unfixable, "output_device_error 19", 1); + command_execute(config.cmd_unfixable, "output_device_error_19", 1); } else { - die("an unrecoverable error, \"output_device_error 19\", has been " + die("an unrecoverable error, \"output_device_error_19\", has been " "detected."); } } From dcc994ab7767f8f98e2e895717a09b8ee1674b71 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 31 Dec 2022 10:13:01 +0000 Subject: [PATCH 15/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index a63adc5a..62e79f44 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version 4.1-dev-843-gae5910e2 +==== +**Bug Fix** +* Stop endless error messages from the `stdout` backend. + +**Enhancements** +* Call the unfixable error handler if the output device can't be opened or generates errors while being used. If no program is attached to the `run_this_if_an_unfixable_error_is_detected` hook in the `sessioncontrol` section of the configuration file, the program will exit instead. +* Shorten the amount of time to wait after a client disappears before terminating a play session. + Version 4.1-dev-824-g8e5393ec ==== **Enhancements** From 76387f8291a6f36bdb0feaf1c2aad1ea36c24f1d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 31 Dec 2022 17:16:02 +0000 Subject: [PATCH 16/85] Be more careful checking for ENODEV conditions in audio_alsa.c. Still a few places where it is not done... --- audio_alsa.c | 190 ++++++++++++++++++--------------------------------- player.c | 11 ++- rtsp.c | 45 ++++++------ 3 files changed, 96 insertions(+), 150 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 608696ce..4fb4049c 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -166,6 +166,22 @@ 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; +void handle_unfixable_error(int errorCode) { + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; + char messageString[1024]; + messageString[0] = '\0'; + snprintf(messageString, sizeof(messageString), "output_device_error_%d", errorCode); + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, messageString, 1); + } else { + die("An unrecoverable error, \"output_device_error_%d\", has been " + "detected. Doing an emergency exit, as no run_this_if_an_unfixable_error_is_detected program.", + errorCode); + } + } +} + 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, @@ -277,40 +293,33 @@ static void help(void) { void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } // ugh -- not static! // assuming pthread cancellation is disabled +// returns zero of all is okay, a Unx error code if there's a problem static int open_mixer() { int response = 0; if (alsa_mix_ctrl != NULL) { debug(3, "Open Mixer"); - int ret = 0; snd_mixer_selem_id_alloca(&alsa_mix_sid); snd_mixer_selem_id_set_index(alsa_mix_sid, alsa_mix_index); snd_mixer_selem_id_set_name(alsa_mix_sid, alsa_mix_ctrl); - if ((snd_mixer_open(&alsa_mix_handle, 0)) < 0) { + if ((response = snd_mixer_open(&alsa_mix_handle, 0)) < 0) { debug(1, "Failed to open mixer"); - response = -1; } else { debug(3, "Mixer device name is \"%s\".", alsa_mix_dev); - if ((snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) { + if ((response = snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) { debug(1, "Failed to attach mixer"); - response = -2; } else { - if ((snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) { + if ((response = snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) { debug(1, "Failed to register mixer element"); - response = -3; } else { - ret = snd_mixer_load(alsa_mix_handle); - if (ret < 0) { + if ((response = snd_mixer_load(alsa_mix_handle)) < 0) { debug(1, "Failed to load mixer element"); - response = -4; } else { 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\",%d.", alsa_mix_ctrl, alsa_mix_index); - response = -5; - } else { - response = 1; // we found a hardware mixer and successfully opened it + response = -ENXIO; // don't let this be ENODEV! } } } @@ -321,21 +330,25 @@ static int open_mixer() { } // assuming pthread cancellation is disabled -static void close_mixer() { +static int close_mixer() { + int ret = 0; if (alsa_mix_handle) { - snd_mixer_close(alsa_mix_handle); + ret = snd_mixer_close(alsa_mix_handle); alsa_mix_handle = NULL; } + return ret; } // assuming pthread cancellation is disabled -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) { +static int do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { + int response = 0; + if ((response = 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) - if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 1) != 0) + if ((response = snd_mixer_selem_set_playback_dB_all(mix_elem, vol, -1)) != 0) + if ((response = snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 1)) != 0) debug(1, "Could not set playback dB volume on the mixer."); } + return response; } // This array is a sequence of the output rates to be tried if automatic speed selection is @@ -444,19 +457,6 @@ static int actual_open_alsa_device(int do_auto_setup) { 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 (config.unfixable_error_reported == 0) { - config.unfixable_error_reported = 1; - char messageString[1024]; - messageString[0] = '\0'; - snprintf(messageString, sizeof(messageString), "output_device_error_%d", -ret); - if (config.cmd_unfixable) { - command_execute(config.cmd_unfixable, messageString, 1); - } else { - die("an unrecoverable error, \"output_device_error_%d\", has been " - "detected.", -ret); - } - } } alsa_handle_status = ret; frames_sent_break_occurred = 1; @@ -906,11 +906,11 @@ static int prepare_mixer() { // Now, start trying to initialise the alsa device with the settings // obtained pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1); - if (open_mixer() == 1) { + if (open_mixer() == 0) { if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, &alsa_mix_maxv) < - 0) + 0) { debug(1, "Can't read mixer's [linear] min and max volumes."); - else { + } else { if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb, &alsa_mix_maxdb) == 0) { @@ -940,14 +940,12 @@ static int prepare_mixer() { "a dB volume scale.", alsa_mix_ctrl); - if (snd_ctl_open(&ctl, alsa_mix_dev, 0) < 0) { + if ((response = snd_ctl_open(&ctl, alsa_mix_dev, 0)) < 0) { warn("Cannot open control \"%s\"", alsa_mix_dev); - response = -1; } - if (snd_ctl_elem_id_malloc(&elem_id) < 0) { + if ((response = snd_ctl_elem_id_malloc(&elem_id)) < 0) { debug(1, "Cannot allocate memory for control \"%s\"", alsa_mix_dev); elem_id = NULL; - response = -2; } else { snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(elem_id, alsa_mix_ctrl); @@ -965,22 +963,6 @@ static int prepare_mixer() { debug(1, "Cannot get the dB range from the volume control \"%s\"", alsa_mix_ctrl); } } - /* - debug(1, "Min and max volumes are %d and - %d.",alsa_mix_minv,alsa_mix_maxv); - alsa_mix_maxdb = 0; - if ((alsa_mix_maxv!=0) && (alsa_mix_minv!=0)) - alsa_mix_mindb = - -20*100*(log10(alsa_mix_maxv*1.0)-log10(alsa_mix_minv*1.0)); - else if (alsa_mix_maxv!=0) - alsa_mix_mindb = -20*100*log10(alsa_mix_maxv*1.0); - audio_alsa.volume = &linear_volume; // insert the linear volume - function - audio_alsa.parameters = ¶meters; // likewise the parameters - stuff - debug(1,"Max and min dB calculated are %d and - %d.",alsa_mix_maxdb,alsa_mix_mindb); - */ } } if (((config.alsa_use_hardware_mute == 1) && @@ -992,7 +974,8 @@ static int prepare_mixer() { } else { // debug(1, "Has mixer but not using hardware mute."); } - close_mixer(); + if (response == 0) + response = close_mixer(); } debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex pthread_cleanup_pop(0); @@ -1399,7 +1382,7 @@ static int set_mute_state() { pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 10000, 0); if ((alsa_backend_state != abm_disconnected) && (config.alsa_use_hardware_mute == 1) && - (open_mixer() == 1)) { + (open_mixer() == 0)) { response = 0; // okay if actually using the mute facility debug(2, "alsa: actually set_mute_state"); int mute = 0; @@ -1718,36 +1701,20 @@ static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement *the_delay = hd; return ret; } -/* -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) { - *elapsed_time = measurement_time - measurement_start_time; - *frames_played = frames_played_at_measurement_time - frames_played_at_measurement_start_time; - } else { - *elapsed_time = 0; - *frames_played = 0; - response = -1; - } - return response; -} -*/ static int do_play(void *buf, int samples) { // assuming the alsa_mutex has been acquired - // debug(3,"audio_alsa play called."); - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + int ret = 0; + if ((samples != 0) && (buf != NULL)) { - snd_pcm_state_t state; - snd_pcm_sframes_t my_delay; - int ret = delay_and_status(&state, &my_delay, NULL); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - if (ret == 0) { // will be non-zero if an error or a stall - - if ((samples != 0) && (buf != NULL)) { + snd_pcm_state_t state; + snd_pcm_sframes_t my_delay; + ret = delay_and_status(&state, &my_delay, NULL); + if (ret == 0) { // will be non-zero if an error or a stall // just check the state of the DAC if ((state != SND_PCM_STATE_PREPARED) && (state != SND_PCM_STATE_RUNNING) && @@ -1784,51 +1751,27 @@ static int do_play(void *buf, int samples) { 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); - if (tret < 0) { - warn("alsa: can't recover from SND_PCM_STATE_XRUN: %s.", snd_strerror(tret)); - } + ret = snd_pcm_recover(alsa_handle, ret, 1); } else if (ret == -ESTRPIPE) { /* suspended */ - debug(1, "alsa: suspended while writing %d samples to alsa device.", samples); - int tret; - while ((tret = snd_pcm_resume(alsa_handle)) == -EAGAIN) { - sleep(1); /* wait until the suspend flag is released */ - if (tret < 0) { - warn("alsa: can't recover from SND_PCM_STATE_SUSPENDED state, " - "snd_pcm_prepare() " - "failed: %s.", - snd_strerror(tret)); - } - } - } else { - - if (ret == -ENODEV) { - if (config.unfixable_error_reported == 0) { - config.unfixable_error_reported = 1; - if (config.cmd_unfixable) { - command_execute(config.cmd_unfixable, "output_device_error_19", 1); - } else { - die("an unrecoverable error, \"output_device_error_19\", has been " - "detected."); - } - } - } else { - char errorstring[1024]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret, - (char *)errorstring, samples); - } + if (state != prior_state) + debug(1, "alsa: suspended while writing %d samples to alsa device.", samples); + if ((ret = snd_pcm_resume(alsa_handle)) == -ENOSYS) + ret = snd_pcm_prepare(alsa_handle); + } else if (ret >= 0) { + debug(1, "alsa: only %d of %d samples output.", ret, samples); } } } - } else { - debug(1, - "alsa: device status returns fault status %d and SND_PCM_STATE_* " - "%d for play.", - ret, state); + pthread_setcancelstate(oldState, NULL); + if (ret < 0) { + char errorstring[1024]; + strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); + debug(1, "alsa: SND_PCM_STATE_* %d, error %d (\"%s\") writing %d samples to alsa device.", + state, ret, (char *)errorstring, samples); + } + if (ret == -ENODEV) // if the device isn't there... + handle_unfixable_error(-ret); } - - pthread_setcancelstate(oldState, NULL); return ret; } @@ -1854,6 +1797,9 @@ static int do_open(int do_auto_setup) { // any previously-reported frame count frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. + } else { + if (ret == -ENODEV) // if the device isn't there... + handle_unfixable_error(-ret); } } else { debug(1, "alsa: do_open() -- output device already open."); @@ -2006,7 +1952,7 @@ static void do_volume(double vol) { // caller is assumed to have the alsa_mutex pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable set_volume = vol; pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1); - if (volume_set_request && (open_mixer() == 1)) { + if (volume_set_request && (open_mixer() == 0)) { if (has_softvol) { if (ctl && elem_id) { snd_ctl_elem_value_t *value; diff --git a/player.c b/player.c index 4cf4a66e..e526f66e 100644 --- a/player.c +++ b/player.c @@ -531,9 +531,9 @@ void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timesta (uint64_t)(config.resend_control_first_check_time * (uint64_t)1000000000); uint64_t resend_repeat_interval = (uint64_t)(config.resend_control_check_interval_time * (uint64_t)1000000000); - uint64_t minimum_remaining_time = (uint64_t)((config.resend_control_last_check_time + - config.audio_backend_buffer_desired_length) * - (uint64_t)1000000000); + uint64_t minimum_remaining_time = (uint64_t)( + (config.resend_control_last_check_time + config.audio_backend_buffer_desired_length) * + (uint64_t)1000000000); uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)1000000000); latency_time = latency_time / (uint64_t)conn->input_rate; @@ -1982,9 +1982,8 @@ void *player_thread_func(void *arg) { debug(3, "Output frame bytes is %d.", conn->output_bytes_per_frame); - conn->dac_buffer_queue_minimum_length = - (uint64_t)(config.audio_backend_buffer_interpolation_threshold_in_seconds * - config.output_rate); + conn->dac_buffer_queue_minimum_length = (uint64_t)( + config.audio_backend_buffer_interpolation_threshold_in_seconds * config.output_rate); debug(3, "dac_buffer_queue_minimum_length is %" PRIu64 " frames.", conn->dac_buffer_queue_minimum_length); diff --git a/rtsp.c b/rtsp.c index 2104caca..aa95e2d5 100644 --- a/rtsp.c +++ b/rtsp.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -47,7 +48,6 @@ #include #include #include -#include #include @@ -1223,10 +1223,10 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time uint64_t time_to_wait_to = get_absolute_time_in_ns(); ; time_to_wait_to = time_to_wait_to + wait_time; - + int flags = 1; if (setsockopt(conn->fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { - debug(1,"can't enable keepalive checking on the RTSP socket"); + debug(1, "can't enable keepalive checking on the RTSP socket"); } // remaining_time will be zero if wait_time is zero @@ -1355,7 +1355,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes // Note: the socket will be closed when the thread exits goto shutdown; } - + // An ETIMEDOUT error usually means keepalive has failed. if (nread < 0) { @@ -5497,39 +5497,40 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. - - // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up - // an ETIMEOUT error is returned if the keepalive check fails + + // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds + // before giving up an ETIMEOUT error is returned if the keepalive check fails int keepAliveIdleTime = 35; // wait this many seconds before checking for a dropped client - int keepAliveCount = 5; // check this many times - int keepAliveInterval = 5; // wait this many seconds between checks - + int keepAliveCount = 5; // check this many times + int keepAliveInterval = 5; // wait this many seconds between checks #if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX - #define SOL_OPTION IPPROTO_TCP +#define SOL_OPTION IPPROTO_TCP #else - #define SOL_OPTION SOL_TCP +#define SOL_OPTION SOL_TCP #endif #ifdef COMPILE_FOR_OSX - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE #else - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE #endif - - if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { - debug(1,"can't set the keepidle wait time"); + if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, + (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { + debug(1, "can't set the keepidle wait time"); } - if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, sizeof(keepAliveCount))) { - debug(1,"can't set the keepidle missing count"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, + sizeof(keepAliveCount))) { + debug(1, "can't set the keepidle missing count"); } - if (setsockopt(conn->fd, SOL_OPTION , TCP_KEEPINTVL, (void *)&keepAliveInterval, sizeof(keepAliveInterval))) { - debug(1,"can't set the keepidle missing count interval"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPINTVL, (void *)&keepAliveInterval, + sizeof(keepAliveInterval))) { + debug(1, "can't set the keepidle missing count interval"); }; - + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; From b5bb2fef38c6a448fb66f8ef0eb2952c551879f1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 31 Dec 2022 17:57:25 +0000 Subject: [PATCH 17/85] Capture where the device was never there (ENOENT), as opposed to when it was there but disappeared (ENODEV). --- audio_alsa.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 4fb4049c..bf8d6ada 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1769,7 +1769,7 @@ static int do_play(void *buf, int samples) { debug(1, "alsa: SND_PCM_STATE_* %d, error %d (\"%s\") writing %d samples to alsa device.", state, ret, (char *)errorstring, samples); } - if (ret == -ENODEV) // if the device isn't there... + if ((ret == -ENOENT) || (ret == -ENODEV)) // if the device isn't there... handle_unfixable_error(-ret); } return ret; @@ -1798,7 +1798,7 @@ static int do_open(int do_auto_setup) { frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. } else { - if (ret == -ENODEV) // if the device isn't there... + if ((ret == -ENOENT) || (ret == -ENODEV)) // if the device isn't there... handle_unfixable_error(-ret); } } else { From 5ab76c2548f4cadc31133ac134126dbb5e9bef5e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 31 Dec 2022 18:14:14 +0000 Subject: [PATCH 18/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 62e79f44..a3b0fbd5 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version 4.1-dev-846-gb5bb2fef +==== +* Improvements to the detection of a missing or non-existent output device. Look for `ENODEV` and `ENOENT` errors when opening or playing to an output device. + Version 4.1-dev-843-gae5910e2 ==== **Bug Fix** From 20dca3c542f5a4db0c20a1d278ff4b3f911fa375 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 13 Jan 2023 14:33:30 +0000 Subject: [PATCH 19/85] Clean up the PulseAudio backend to open a stream once only. --- audio_pa.c | 310 +++++++++++++++++++++-------------------------------- 1 file changed, 123 insertions(+), 187 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index 8f92ad39..d2478fe7 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -66,8 +66,72 @@ void stream_state_cb(pa_stream *s, void *mainloop); void stream_success_cb(pa_stream *stream, int success, void *userdata); void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata); -static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { +static void connect_stream() { + debug(1, "connect_stream"); + uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here + // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); + pa_threaded_mainloop_lock(mainloop); + // Create a playback stream + pa_sample_spec sample_specifications; + sample_specifications.format = FORMAT; + sample_specifications.rate = RATE; + sample_specifications.channels = 2; + + pa_channel_map map; + pa_channel_map_init_stereo(&map); + + stream = pa_stream_new(context, "Playback", &sample_specifications, &map); + pa_stream_set_state_callback(stream, stream_state_cb, mainloop); + pa_stream_set_write_callback(stream, stream_write_cb, mainloop); + // pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop); + + // recommended settings, i.e. server uses sensible values + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t)-1; + buffer_attr.tlength = buffer_size_in_bytes; + buffer_attr.prebuf = (uint32_t)0; + buffer_attr.minreq = (uint32_t)-1; + + // Settings copied as per the chromium browser source + pa_stream_flags_t stream_flags; + stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | + // PA_STREAM_AUTO_TIMING_UPDATE; + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; + + int connect_result; + + if (config.pa_sink) { + // Connect stream to the sink specified in the config + connect_result = + pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL); + } else { + // Connect stream to the default audio output sink + connect_result = + pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL); + } + + if (connect_result != 0) + die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".", + pa_strerror(pa_context_errno(context))); + + // Wait for the stream to be ready + for (;;) { + pa_stream_state_t stream_state = pa_stream_get_state(stream); + if (!PA_STREAM_IS_GOOD(stream_state)) + die("stream state is no longer good while waiting for stream to become ready -- the error " + "message is \"%s\".", + pa_strerror(pa_context_errno(context))); + if (stream_state == PA_STREAM_READY) + break; + pa_threaded_mainloop_wait(mainloop); + } + + pa_threaded_mainloop_unlock(mainloop); +} + +static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { + debug(1, "pa_init"); // set up default values first config.audio_backend_buffer_desired_length = 0.35; config.audio_backend_buffer_interpolation_threshold_in_seconds = @@ -148,132 +212,48 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * } pa_threaded_mainloop_unlock(mainloop); - return 0; } static void deinit(void) { + pa_stream_disconnect(stream); pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); debug(1, "pa deinit done"); } -static void do_start() { - // debug(1, "pa_start"); - uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here - // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); - - pa_threaded_mainloop_lock(mainloop); - // Create a playback stream - pa_sample_spec sample_specifications; - sample_specifications.format = FORMAT; - sample_specifications.rate = RATE; - sample_specifications.channels = 2; - - pa_channel_map map; - pa_channel_map_init_stereo(&map); - - stream = pa_stream_new(context, "Playback", &sample_specifications, &map); - pa_stream_set_state_callback(stream, stream_state_cb, mainloop); - pa_stream_set_write_callback(stream, stream_write_cb, mainloop); - // pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop); - - // recommended settings, i.e. server uses sensible values - pa_buffer_attr buffer_attr; - buffer_attr.maxlength = (uint32_t)-1; - buffer_attr.tlength = buffer_size_in_bytes; - buffer_attr.prebuf = (uint32_t)0; - buffer_attr.minreq = (uint32_t)-1; - - // Settings copied as per the chromium browser source - pa_stream_flags_t stream_flags; - stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | - // PA_STREAM_AUTO_TIMING_UPDATE; - PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; - - int connect_result; - - if (config.pa_sink) { - // Connect stream to the sink specified in the config - connect_result = - pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL); - } else { - // Connect stream to the default audio output sink - connect_result = - pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL); - } - - if (connect_result != 0) - die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".", - pa_strerror(pa_context_errno(context))); - - // Wait for the stream to be ready - for (;;) { - pa_stream_state_t stream_state = pa_stream_get_state(stream); - if (!PA_STREAM_IS_GOOD(stream_state)) - die("stream state is no longer good while waiting for stream to become ready -- the error " - "message is \"%s\".", - pa_strerror(pa_context_errno(context))); - if (stream_state == PA_STREAM_READY) - break; - pa_threaded_mainloop_wait(mainloop); - } - - pa_threaded_mainloop_unlock(mainloop); -} - static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - do_start(); -} - -static int stream_is_open() { - int response = 0; // default to no - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl >= 0) - response = 1; - return response; + debug(1, "pa_start"); } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint32_t timestamp, __attribute__((unused)) uint64_t playtime) { // debug(1,"pa_play of %d samples.",samples); - if (stream_is_open() == 0) { - // debug(1,"pa open stream before play."); - do_start(); - } - if (stream_is_open() != 0) { - // copy the samples into the queue - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; - } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } + // copy the samples into the queue + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); } else { - // debug(2, "could not open pa stream for play"); + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); } return 0; } @@ -282,68 +262,52 @@ int pa_delay(long *the_delay) { // debug(1,"pa_delay"); int reply = -ENODEV; long result = 0; - if (stream_is_open() == 0) { - // debug(1,"pa open stream before delay."); - do_start(); - } - if (stream_is_open() != 0) { - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); - reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; - } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; - } + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; } else { - // debug(2, "could not open pa stream for delay"); + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; } *the_delay = result; return reply; } void flush(void) { - // debug(1,"Flush."); - if (stream_is_open() != 0) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; + debug(1, "pa_flush."); + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; } static void stop(void) { - // debug(1,"Stop."); - if (stream_is_open() != 0) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; - - // debug(1,"pa stop"); - pa_stream_disconnect(stream); + debug(1, "pa_stop."); + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; } audio_output audio_pa = {.name = "pa", @@ -454,34 +418,6 @@ void stream_write_cb(pa_stream *stream, size_t requested_bytes, // %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(stream)); } -void alt_stream_write_cb(pa_stream *stream, size_t requested_bytes, - __attribute__((unused)) void *userdata) { - // debug(1, "***Bytes requested bytes %d.", requested_bytes); - size_t bytes_remaining = requested_bytes; - while (bytes_remaining > 0) { - uint8_t *buffer = NULL; - size_t bytes_to_fill = 44100; - size_t i; - - if (bytes_to_fill > bytes_remaining) - bytes_to_fill = bytes_remaining; - - pa_stream_begin_write(stream, (void **)&buffer, &bytes_to_fill); - if (buffer) { - for (i = 0; i < bytes_to_fill; i += 2) { - buffer[i] = (i % 100) * 40 / 100 + 44; - buffer[i + 1] = (i % 100) * 40 / 100 + 44; - } - } else { - die("buffer not allocated in alt_stream_write_cb."); - } - - pa_stream_write(stream, buffer, bytes_to_fill, NULL, 0LL, PA_SEEK_RELATIVE); - - bytes_remaining -= bytes_to_fill; - } -} - void stream_success_cb(__attribute__((unused)) pa_stream *stream, __attribute__((unused)) int success, __attribute__((unused)) void *userdata) { From 99ca92998e72ca7980df2bb5b5bb04f4beb97515 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:08:51 +0000 Subject: [PATCH 20/85] Gremlin fix and comment tidy-up --- audio_pa.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index d2478fe7..6299c411 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -93,10 +93,8 @@ static void connect_stream() { buffer_attr.prebuf = (uint32_t)0; buffer_attr.minreq = (uint32_t)-1; - // Settings copied as per the chromium browser source pa_stream_flags_t stream_flags; stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | - // PA_STREAM_AUTO_TIMING_UPDATE; PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; int connect_result; @@ -212,6 +210,7 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * } pa_threaded_mainloop_unlock(mainloop); + connect_stream(); return 0; } From c2e4ab29f12385d029158c1643f1a16ec80d3fa7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:19:46 +0000 Subject: [PATCH 21/85] Quieten some debug messages --- audio_pa.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index 6299c411..6ebde181 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -218,12 +218,12 @@ static void deinit(void) { pa_stream_disconnect(stream); pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); - debug(1, "pa deinit done"); + // debug(1, "pa deinit done"); } static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - debug(1, "pa_start"); + // debug(1, "pa_start"); } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, @@ -281,7 +281,7 @@ int pa_delay(long *the_delay) { } void flush(void) { - debug(1, "pa_flush."); + // debug(1, "pa_flush."); pa_threaded_mainloop_lock(mainloop); if (pa_stream_is_corked(stream) == 0) { // debug(1,"Flush and cork for flush."); @@ -295,7 +295,7 @@ void flush(void) { } static void stop(void) { - debug(1, "pa_stop."); + // debug(1, "pa_stop."); // Cork the stream so it will stop playing pa_threaded_mainloop_lock(mainloop); if (pa_stream_is_corked(stream) == 0) { From 7d243492fd5600cb67abcd0a248e97e96f15b353 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 13 Jan 2023 18:03:26 +0000 Subject: [PATCH 22/85] Don't report an ENODEV condition as an error when getting a delay. --- player.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player.c b/player.c index e526f66e..15fa80a0 100644 --- a/player.c +++ b/player.c @@ -2645,8 +2645,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) && (resp != -ENODEV)) // delay and not-there errors can be reported if the device is (hopefully + // temporarily) busy or unavailable debug(1, "Delay error %d when checking running latency.", resp); } } From 0ac1c0d8b515ec33ac1c9780a6625b66b8b1431d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 13 Jan 2023 18:06:34 +0000 Subject: [PATCH 23/85] Check to see if the stream is suspended when accessing it or sending data. --- audio_pa.c | 156 +++++++++++++++++++++++++---------------------------- 1 file changed, 72 insertions(+), 84 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index 6ebde181..ffcee930 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -1,6 +1,6 @@ /* * Asynchronous PulseAudio Backend. This file is part of Shairport Sync. - * Copyright (c) Mike Brady 2017 + * Copyright (c) Mike Brady 2017 -- 2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -67,7 +67,7 @@ void stream_success_cb(pa_stream *stream, int success, void *userdata); void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata); static void connect_stream() { - debug(1, "connect_stream"); + // debug(1, "connect_stream"); uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); @@ -129,7 +129,7 @@ static void connect_stream() { } static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { - debug(1, "pa_init"); + // debug(1, "pa_init"); // set up default values first config.audio_backend_buffer_desired_length = 0.35; config.audio_backend_buffer_interpolation_threshold_in_seconds = @@ -231,50 +231,59 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint64_t playtime) { // debug(1,"pa_play of %d samples.",samples); // copy the samples into the queue - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + int ret = pa_stream_is_suspended(stream); + if (ret == 0) { + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + } else { + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); + } + } else if (ret == 1) { + ret = -ENODEV; } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } - return 0; + return ret; } int pa_delay(long *the_delay) { // debug(1,"pa_delay"); - int reply = -ENODEV; long result = 0; - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); + int reply = pa_stream_is_suspended(stream); + if (reply == 0) { + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; + } else { + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; + } + } else if (reply == 1) { reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; - } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; } *the_delay = result; return reply; @@ -282,13 +291,15 @@ int pa_delay(long *the_delay) { void flush(void) { // debug(1, "pa_flush."); - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); + if (pa_stream_is_suspended(stream) == 0) { + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); } - pa_threaded_mainloop_unlock(mainloop); audio_toq = audio_eoq = audio_lmb; audio_umb = audio_lmb + audio_size; audio_occupancy = 0; @@ -297,13 +308,15 @@ void flush(void) { static void stop(void) { // debug(1, "pa_stop."); // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); + if (pa_stream_is_suspended(stream) == 0) { + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); } - pa_threaded_mainloop_unlock(mainloop); audio_toq = audio_eoq = audio_lmb; audio_umb = audio_lmb + audio_size; audio_occupancy = 0; @@ -326,50 +339,23 @@ audio_output audio_pa = {.name = "pa", .mute = NULL}; void context_state_cb(__attribute__((unused)) pa_context *context, void *mainloop) { + // debug(1,"context_state_cb called."); pa_threaded_mainloop_signal(mainloop, 0); } void stream_state_cb(__attribute__((unused)) pa_stream *s, void *mainloop) { + // debug(1,"stream_state_cb called."); pa_threaded_mainloop_signal(mainloop, 0); } void stream_write_cb(pa_stream *stream, size_t requested_bytes, __attribute__((unused)) void *userdata) { - - /* - // play with timing information - const struct pa_timing_info *ti = pa_stream_get_timing_info(stream); - if ((ti == NULL) || (ti->write_index_corrupt)) { - debug(2, "Timing info invalid"); - } else { - struct timeval time_now; - - pa_gettimeofday(&time_now); - - uint64_t time_now_fp = ((uint64_t)time_now.tv_sec << 32) + - ((uint64_t)time_now.tv_usec << 32) / 1000000; // types okay - uint64_t time_of_ti_fp = ((uint64_t)(ti->timestamp.tv_sec) << 32) + - ((uint64_t)(ti->timestamp.tv_usec) << 32) / 1000000; // types okay - - if (time_now_fp >= time_of_ti_fp) { - uint64_t estimate_age = ((time_now_fp - time_of_ti_fp) * 1000000) >> 32; - uint64_t bytes_in_buffer = ti->write_index - ti->read_index; - pa_usec_t microseconds_to_write_buffer = (bytes_in_buffer * 1000000) / (44100 * 2 * 2); - pa_usec_t ea = (pa_usec_t)estimate_age; - pa_usec_t pa_latency = ti->sink_usec + ti->transport_usec + microseconds_to_write_buffer; - pa_usec_t estimated_latency = pa_latency - estimate_age; - // debug(1,"Estimated latency is %d microseconds.",estimated_latency); - - // } else { - // debug(1, "Time now is earlier than time of timing information"); - } - } - */ int bytes_to_transfer = requested_bytes; int bytes_transferred = 0; uint8_t *buffer = NULL; - while ((bytes_to_transfer > 0) && (audio_occupancy > 0)) { + while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && + (pa_stream_is_suspended(stream) == 0)) { size_t bytes_we_can_transfer = bytes_to_transfer; if (audio_occupancy < bytes_we_can_transfer) { // debug(1, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy, @@ -412,7 +398,9 @@ void stream_write_cb(pa_stream *stream, size_t requested_bytes, bytes_to_transfer -= bytes_we_can_transfer; // debug(1,"audio_toq is %llx",audio_toq); } - + // if (pa_stream_is_suspended(stream) != 0) { + // debug(1,"stream suspended while sending audio..."); + // } // debug(1,"<< Date: Fri, 13 Jan 2023 20:41:27 +0000 Subject: [PATCH 24/85] Stop using the pa_stream_is_suspended, but check writing errors more carefully. --- audio_pa.c | 184 +++++++++++++++++++++++++---------------------------- 1 file changed, 86 insertions(+), 98 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index ffcee930..0bcc5310 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -231,59 +231,50 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint64_t playtime) { // debug(1,"pa_play of %d samples.",samples); // copy the samples into the queue - int ret = pa_stream_is_suspended(stream); - if (ret == 0) { - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; - } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } - } else if (ret == 1) { - ret = -ENODEV; + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + } else { + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; } - return ret; + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); + } + return 0; } int pa_delay(long *the_delay) { // debug(1,"pa_delay"); long result = 0; - int reply = pa_stream_is_suspended(stream); - if (reply == 0) { - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); - reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; - } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; - } - } else if (reply == 1) { + int reply = 0; + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; + } else { + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; } *the_delay = result; return reply; @@ -291,35 +282,31 @@ int pa_delay(long *the_delay) { void flush(void) { // debug(1, "pa_flush."); - if (pa_stream_is_suspended(stream) == 0) { - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } audio_toq = audio_eoq = audio_lmb; audio_umb = audio_lmb + audio_size; audio_occupancy = 0; + pa_threaded_mainloop_unlock(mainloop); } static void stop(void) { // debug(1, "pa_stop."); // Cork the stream so it will stop playing - if (pa_stream_is_suspended(stream) == 0) { - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } audio_toq = audio_eoq = audio_lmb; audio_umb = audio_lmb + audio_size; audio_occupancy = 0; + pa_threaded_mainloop_unlock(mainloop); } audio_output audio_pa = {.name = "pa", @@ -353,13 +340,14 @@ void stream_write_cb(pa_stream *stream, size_t requested_bytes, int bytes_to_transfer = requested_bytes; int bytes_transferred = 0; uint8_t *buffer = NULL; - - while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && - (pa_stream_is_suspended(stream) == 0)) { + int ret = 0; + while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && (ret == 0)) { + if (pa_stream_is_suspended(stream)) + debug(1, "stream is suspended"); size_t bytes_we_can_transfer = bytes_to_transfer; if (audio_occupancy < bytes_we_can_transfer) { // debug(1, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy, - // bytes_we_can_transfer); + // bytes_we_can_transfer); pa_stream_cork(stream, 1, stream_success_cb, mainloop); // debug(1, "Corked"); bytes_we_can_transfer = audio_occupancy; @@ -367,40 +355,40 @@ void stream_write_cb(pa_stream *stream, size_t requested_bytes, // bytes we can transfer will never be greater than the bytes available - pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer); - if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) { - // the bytes are all in a row in the audo buffer - memcpy(buffer, audio_toq, bytes_we_can_transfer); - audio_toq += bytes_we_can_transfer; - // lock - pthread_mutex_lock(&buffer_mutex); - audio_occupancy -= bytes_we_can_transfer; - pthread_mutex_unlock(&buffer_mutex); - // unlock - pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); - bytes_transferred += bytes_we_can_transfer; - } else { - // the bytes are in two places in the audio buffer - size_t first_portion_to_write = audio_umb - audio_toq; - if (first_portion_to_write != 0) - memcpy(buffer, audio_toq, first_portion_to_write); - uint8_t *new_buffer = buffer + first_portion_to_write; - memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write); - pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); - bytes_transferred += bytes_we_can_transfer; - audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; - // lock - pthread_mutex_lock(&buffer_mutex); - audio_occupancy -= bytes_we_can_transfer; - pthread_mutex_unlock(&buffer_mutex); - // unlock + ret = pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer); + if ((ret == 0) && (buffer != NULL)) { + if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) { + // the bytes are all in a row in the audo buffer + memcpy(buffer, audio_toq, bytes_we_can_transfer); + audio_toq += bytes_we_can_transfer; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + } else { + // the bytes are in two places in the audio buffer + size_t first_portion_to_write = audio_umb - audio_toq; + if (first_portion_to_write != 0) + memcpy(buffer, audio_toq, first_portion_to_write); + uint8_t *new_buffer = buffer + first_portion_to_write; + memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write); + ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + } + bytes_to_transfer -= bytes_we_can_transfer; } - bytes_to_transfer -= bytes_we_can_transfer; - // debug(1,"audio_toq is %llx",audio_toq); } - // if (pa_stream_is_suspended(stream) != 0) { - // debug(1,"stream suspended while sending audio..."); - // } + if (ret != 0) + debug(1, "error writing to pa buffer"); // debug(1,"<< Date: Fri, 13 Jan 2023 20:46:54 +0000 Subject: [PATCH 25/85] tidy up a few small things. --- audio_pa.c | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index 0bcc5310..3370cfed 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -1,6 +1,6 @@ /* * Asynchronous PulseAudio Backend. This file is part of Shairport Sync. - * Copyright (c) Mike Brady 2017 -- 2023 + * Copyright (c) Mike Brady 2017-2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -45,14 +45,6 @@ static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; -/* -static struct { - char *server; - char *sink; - char *service_name; -} pulse_options = {.server = NULL, .sink = NULL, .service_name = NULL}; -*/ - pa_threaded_mainloop *mainloop; pa_mainloop_api *mainloop_api; pa_context *context; From 28f54af281ce805f8b36eea719f4dcfce8fe33bb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 14 Jan 2023 12:48:18 +0000 Subject: [PATCH 26/85] Add a new setting: resync_recovery_time to give the systems a little time to restart. Clean up the tolerance and resync threshold times. --- common.h | 6 +++--- player.c | 17 ++++++++--------- scripts/shairport-sync.conf | 2 +- shairport.c | 33 ++++++++++++++++++++------------- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/common.h b/common.h index 66c02ca6..93f04ec6 100644 --- a/common.h +++ b/common.h @@ -171,9 +171,9 @@ typedef struct { int volume_max_db; int no_sync; // disable synchronisation, even if it's available int no_mmap; // disable use of mmap-based output, even if it's available - double resyncthreshold; // if it get's out of whack my more than this number of seconds, resync. - // Zero means never - // resync. + double resync_threshold; // if it gets out of whack by more than this number of seconds, do a resync. + // if zero, never do a resync. + double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames up to the resync_recovery_time int allow_session_interruption; int timeout; // while in play mode, exit if no packets of audio come in for more than this number // of seconds . Zero means never exit. diff --git a/player.c b/player.c index 15fa80a0..293bde26 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 -- 2022 + * and related work, copyright (c) Mike Brady 2014 -- 2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -2791,8 +2791,8 @@ void *player_thread_func(void *arg) { abs_sync_error = -abs_sync_error; if ((config.no_sync == 0) && (inframe->given_timestamp != 0) && - (config.resyncthreshold > 0.0) && - (abs_sync_error > config.resyncthreshold * config.output_rate)) { + (config.resync_threshold > 0.0) && + (abs_sync_error > config.resync_threshold * config.output_rate)) { sync_error_out_of_bounds++; } else { sync_error_out_of_bounds = 0; @@ -2815,7 +2815,7 @@ void *player_thread_func(void *arg) { } int64_t filler_length = - (int64_t)(config.resyncthreshold * config.output_rate); // number of samples + (int64_t)(config.resync_threshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { debug(1, "Large positive (i.e. late) sync error of %" PRId64 @@ -2831,9 +2831,8 @@ void *player_thread_func(void *arg) { 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); + // drop some extra frames to give the pipeline a chance to recover + int64_t extra_frames_to_drop = (int64_t)(conn->input_rate * config.resync_recovery_time); source_frames_to_drop += extra_frames_to_drop; uint32_t frames_to_drop = source_frames_to_drop; @@ -3086,8 +3085,8 @@ void *player_thread_func(void *arg) { // timestamp of zero means an inserted silent frame in place of a missing frame /* if ((config.no_sync == 0) && (inframe->timestamp != 0) && - && (config.resyncthreshold > 0.0) && - (abs_sync_error > config.resyncthreshold * config.output_rate)) { + && (config.resync_threshold > 0.0) && + (abs_sync_error > config.resync_threshold * config.output_rate)) { sync_error_out_of_bounds++; // debug(1,"Sync error out of bounds: Error: %lld; previous error: %lld; DAC: %lld; // timestamp: %llx, time now diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 61a56684..918ad377 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -28,7 +28,7 @@ general = // 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 - +// resync_recovery_time_in_seconds = 0.100; // allow this extra time to recover after a late resync. Increase the value, possibly to 0.5, in a virtual machine. // playback_mode = "stereo"; // This can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo". // alac_decoder = "hammerton"; // This can be "hammerton" or "apple". This advanced setting allows you to choose // the original Shairport decoder by David Hammerton or the Apple Lossless Audio Codec (ALAC) decoder written by Apple. diff --git a/shairport.c b/shairport.c index 96fdcbec..b5ecd030 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--2022 + * Modifications and additions (c) Mike Brady 2014--2023 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -343,8 +343,8 @@ int parse_options(int argc, char **argv) { char *stuffing = NULL; /* used for picking up the stuffing option */ signed char c; /* used for argument parsing */ // int i = 0; /* used for tracking options */ - int fResyncthreshold = (int)(config.resyncthreshold * 44100); - int fTolerance = (int)(config.tolerance * 44100); + int resync_threshold_in_frames = 0; + int tolerance_in_frames = 0; poptContext optCon; /* context for parsing command-line options */ struct poptOption optionsTable[] = { {"verbose", 'v', POPT_ARG_NONE, NULL, 'v', NULL, NULL}, @@ -365,10 +365,10 @@ int parse_options(int argc, char **argv) { {"mdns", 'm', POPT_ARG_STRING, &config.mdns_name, 0, NULL, NULL}, {"latency", 'L', POPT_ARG_INT, &config.userSuppliedLatency, 0, NULL, NULL}, {"stuffing", 'S', POPT_ARG_STRING, &stuffing, 'S', NULL, NULL}, - {"resync", 'r', POPT_ARG_INT, &fResyncthreshold, 0, NULL, NULL}, + {"resync", 'r', POPT_ARG_INT, &resync_threshold_in_frames, 'r', NULL, NULL}, {"timeout", 't', POPT_ARG_INT, &config.timeout, 't', NULL, NULL}, {"password", 0, POPT_ARG_STRING, &config.password, 0, NULL, NULL}, - {"tolerance", 'z', POPT_ARG_INT, &fTolerance, 0, NULL, NULL}, + {"tolerance", 'z', POPT_ARG_INT, &tolerance_in_frames, 'z', NULL, NULL}, {"use-stderr", 'u', POPT_ARG_NONE, NULL, 'u', NULL, NULL}, {"log-to-syslog", 0, POPT_ARG_NONE, &log_to_syslog_selected, 0, NULL, NULL}, #ifdef CONFIG_METADATA @@ -424,10 +424,12 @@ int parse_options(int argc, char **argv) { "automatically received from forkedDaapd"); break; case 'r': + config.resync_threshold = (resync_threshold_in_frames * 1.0) / 44100; inform("Warning: the option -r or --resync is deprecated. Please use the " "\"resync_threshold_in_seconds\" setting in the config file instead."); break; case 'z': + config.tolerance = (tolerance_in_frames * 1.0) / 44100; inform("Warning: the option --tolerance is deprecated. Please use the " "\"drift_tolerance_in_seconds\" setting in the config file instead."); break; @@ -462,8 +464,6 @@ int parse_options(int argc, char **argv) { }; #endif - config.resyncthreshold = 1.0 * fResyncthreshold / 44100; - 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 = -24.0; // if no volume is ever set, default to initial default value if @@ -692,7 +692,7 @@ int parse_options(int argc, char **argv) { if (config_lookup_int(config.cfg, "general.resync_threshold", &value)) { inform("The resync_threshold setting is deprecated. Use " "resync_threshold_in_seconds instead"); - config.resyncthreshold = 1.0 * value / 44100; + config.resync_threshold = 1.0 * value / 44100; } /* Get the drift tolerance setting. */ @@ -701,7 +701,11 @@ int parse_options(int argc, char **argv) { /* Get the resync setting. */ if (config_lookup_float(config.cfg, "general.resync_threshold_in_seconds", &dvalue)) - config.resyncthreshold = dvalue; + config.resync_threshold = dvalue; + + /* Get the resync recovery time setting. */ + if (config_lookup_float(config.cfg, "general.resync_recovery_time_in_seconds", &dvalue)) + config.resync_recovery_time = dvalue; /* Get the verbosity setting. */ if (config_lookup_int(config.cfg, "general.log_verbosity", &value)) { @@ -1983,11 +1987,13 @@ int main(int argc, char **argv) { 1; // by default, log the file and line of the originating message config.debugger_show_relative_time = 1; // by default, log the time back to the previous debug message - config.resyncthreshold = 0.05; // 50 ms config.timeout = 120; // this number of seconds to wait for [more] audio before switching to idle. - config.tolerance = - 0.002; // this number of seconds of timing error before attempting to correct it. config.buffer_start_fill = 220; + + config.resync_threshold = 0.050; // default + config.resync_recovery_time = 0.1; // drop this amount of frames following the resync delay. + config.tolerance = 0.002; + #ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog @@ -2372,7 +2378,8 @@ int main(int argc, char **argv) { : 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, "resync time is %f seconds.", config.resync_threshold); + debug(1, "resync recovery time is %f seconds.", config.resync_recovery_time); 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); From d2ca8c84a415c85b3625afcb4e2e0e654d8d8783 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 14 Jan 2023 12:53:20 +0000 Subject: [PATCH 27/85] Clang format --- common.h | 11 ++++++----- player.c | 8 +++++--- shairport.c | 12 +++++------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/common.h b/common.h index 93f04ec6..298bdbd0 100644 --- a/common.h +++ b/common.h @@ -169,11 +169,12 @@ typedef struct { int ignore_volume_control; int volume_max_db_set; // set to 1 if a maximum volume db has been set int volume_max_db; - int no_sync; // disable synchronisation, even if it's available - int no_mmap; // disable use of mmap-based output, even if it's available - double resync_threshold; // if it gets out of whack by more than this number of seconds, do a resync. - // if zero, never do a resync. - double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames up to the resync_recovery_time + int no_sync; // disable synchronisation, even if it's available + int no_mmap; // disable use of mmap-based output, even if it's available + double resync_threshold; // if it gets out of whack by more than this number of seconds, do a + // resync. if zero, never do a resync. + double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames + // up to the resync_recovery_time int allow_session_interruption; int timeout; // while in play mode, exit if no packets of audio come in for more than this number // of seconds . Zero means never exit. diff --git a/player.c b/player.c index 293bde26..21107ecb 100644 --- a/player.c +++ b/player.c @@ -2645,8 +2645,9 @@ void *player_thread_func(void *arg) { conn->connection_number); } } else { - if ((resp != -EBUSY) && (resp != -ENODEV)) // delay and not-there errors can be reported if the device is (hopefully - // temporarily) busy or unavailable + if ((resp != -EBUSY) && + (resp != -ENODEV)) // delay and not-there errors can be reported if the device + // is (hopefully temporarily) busy or unavailable debug(1, "Delay error %d when checking running latency.", resp); } } @@ -2832,7 +2833,8 @@ void *player_thread_func(void *arg) { source_frames_to_drop = source_frames_to_drop / conn->output_sample_ratio; // drop some extra frames to give the pipeline a chance to recover - int64_t extra_frames_to_drop = (int64_t)(conn->input_rate * config.resync_recovery_time); + int64_t extra_frames_to_drop = + (int64_t)(conn->input_rate * config.resync_recovery_time); source_frames_to_drop += extra_frames_to_drop; uint32_t frames_to_drop = source_frames_to_drop; diff --git a/shairport.c b/shairport.c index b5ecd030..4b2129cc 100644 --- a/shairport.c +++ b/shairport.c @@ -1986,15 +1986,14 @@ int main(int argc, char **argv) { config.debugger_show_file_and_line = 1; // by default, log the file and line of the originating message config.debugger_show_relative_time = - 1; // by default, log the time back to the previous debug message + 1; // by default, log the time back to the previous debug message config.timeout = 120; // this number of seconds to wait for [more] audio before switching to idle. config.buffer_start_fill = 220; - - config.resync_threshold = 0.050; // default + + config.resync_threshold = 0.050; // default config.resync_recovery_time = 0.1; // drop this amount of frames following the resync delay. config.tolerance = 0.002; - #ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog config.port = 7000; @@ -2374,9 +2373,8 @@ 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.resync_threshold); debug(1, "resync recovery time is %f seconds.", config.resync_recovery_time); From 8201903a4e5f32919a985dc6e5b375f4b19b69db Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:41:50 +0000 Subject: [PATCH 28/85] Add code to try to save your ears by handling a situation where the player volume was set very high some time ago and you have forgotten. In that situation currenctly, when you play something, it will come out at the last volume set on the device, which will be that very high volume. This could happen, for example, the morning after a loud party :). The new code works as follows: If nothing has been played for a period, say a few hours, and the volume is higher than a threshold, new connections that ask for the current volume setting will be offered the default (low) volume rather than the current (very high) one. Most players, including the HomePod mini, iOS and iPadOS use this to set their initial volume. The Mac Music app retains its own setting, and the Mac System Sounds preference sometimes seems to take its setting from the Music app, but at least you can see the settings they will apply. Existing connections are unaffected, so this facility should be unobtrusive. --- common.c | 15 --------------- common.h | 21 ++++++++++++--------- dbus-service.c | 9 ++++++--- player.c | 47 ++++++++++++++++++++++++++++++++++++++++++++--- player.h | 6 ++++-- rtsp.c | 31 ++++++++++++++++++++++++++----- shairport.c | 20 +++++++++++++++----- 7 files changed, 107 insertions(+), 42 deletions(-) diff --git a/common.c b/common.c index 63ea1cd4..d10a2fad 100644 --- a/common.c +++ b/common.c @@ -240,21 +240,6 @@ void log_to_syslog() { shairport_cfg config; -// accessors for multi-thread-access fields in the conn structure - -double get_config_airplay_volume() { - config_lock; - double v = config.airplay_volume; - config_unlock; - return v; -} - -void set_config_airplay_volume(double v) { - config_lock; - config.airplay_volume = v; - config_unlock; -} - volatile int debuglev = 0; sigset_t pselect_sigset; diff --git a/common.h b/common.h index 298bdbd0..e152db68 100644 --- a/common.h +++ b/common.h @@ -122,7 +122,14 @@ typedef struct { config_t *cfg; int endianness; double airplay_volume; // stored here for reloading when necessary - char *appName; // normally the app is called shairport-syn, but it may be symlinked + double default_airplay_volume; + double high_threshold_airplay_volume; + uint64_t last_access_to_volume_info_time; + int limit_to_high_volume_threshold_time_in_minutes; // revert to the high threshold volume level + // if the existing volume level exceeds this + // and hasn't been used for this amount of + // time (0 means never revert) + char *appName; // normally the app is called shairport-syn, but it may be symlinked char *password; char *service_name; // the name for the shairport service, e.g. "Shairport Sync Version %v running // on host %h" @@ -169,10 +176,10 @@ typedef struct { int ignore_volume_control; int volume_max_db_set; // set to 1 if a maximum volume db has been set int volume_max_db; - int no_sync; // disable synchronisation, even if it's available - int no_mmap; // disable use of mmap-based output, even if it's available - double resync_threshold; // if it gets out of whack by more than this number of seconds, do a - // resync. if zero, never do a resync. + int no_sync; // disable synchronisation, even if it's available + int no_mmap; // disable use of mmap-based output, even if it's available + double resync_threshold; // if it gets out of whack by more than this number of seconds, do a + // resync. if zero, never do a resync. double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames // up to the resync_recovery_time int allow_session_interruption; @@ -318,10 +325,6 @@ typedef struct { int unfixable_error_reported; // only report once. } 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 uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t diff --git a/dbus-service.c b/dbus-service.c index 70792248..6a3b544f 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -636,11 +636,14 @@ gboolean notify_volume_callback(ShairportSync *skeleton, debug(2, ">> setting volume to %7.4f.", iv); lock_player(); - config.airplay_volume = iv; - if (playing_conn != NULL) + if (playing_conn != NULL) { player_volume(iv, playing_conn); + playing_conn->own_airplay_volume = iv; + playing_conn->own_airplay_volume_set = 1; + } unlock_player(); - + config.airplay_volume = iv; + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); } else { debug(1, ">> invalid volume: %f. Ignored.", iv); shairport_sync_set_volume(skeleton, config.airplay_volume); diff --git a/player.c b/player.c index 21107ecb..db7b8ae0 100644 --- a/player.c +++ b/player.c @@ -1702,6 +1702,41 @@ void statistics_item(const char *heading, const char *format, ...) { statistics_column++; } +double suggested_volume(rtsp_conn_info *conn) { + double response = config.airplay_volume; + if (conn) { + if (conn->own_airplay_volume_set != 0) { + response = conn->own_airplay_volume; + } else if (config.airplay_volume > config.high_threshold_airplay_volume) { + int64_t volume_validity_time = config.limit_to_high_volume_threshold_time_in_minutes; + // zero means never check the volume + if (volume_validity_time != 0) { + // If the volume is higher than the high volume threshold + // and enough time has gone past, suggest the default volume. + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_last_access_to_volume_info = + time_now - config.last_access_to_volume_info_time; + + volume_validity_time = volume_validity_time * 60; // to seconds + volume_validity_time = volume_validity_time * 1000000000; // to nanoseconds + + if ((config.airplay_volume > config.high_threshold_airplay_volume) && + ((config.last_access_to_volume_info_time == 0) || + (time_since_last_access_to_volume_info > volume_validity_time))) { + + debug(2, + "the current volume %.6f is higher than the high volume threshold %.6f, so the " + "default volume %.6f is suggested.", + config.airplay_volume, config.high_threshold_airplay_volume, + config.default_airplay_volume); + response = config.default_airplay_volume; + } + } + } + } + return response; +} + void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { @@ -2192,9 +2227,11 @@ void *player_thread_func(void *arg) { pthread_setcancelstate(oldState, NULL); - double initial_volume = config.airplay_volume; // default - // 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); + // if not already set, set the volume to the pending_airplay_volume, if any, or otherwise to the + // suggested volume. + + double initial_volume = suggested_volume(conn); + debug(2, "Set initial volume to %.6f.", initial_volume); player_volume(initial_volume, conn); // will contain a cancellation point if asked to wait debug(2, "Play begin"); @@ -2216,6 +2253,9 @@ void *player_thread_func(void *arg) { 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 + config.last_access_to_volume_info_time = + local_time_now; // ensure volume info remains seen as valid + if (inframe) { inbuf = inframe->data; inbuflength = inframe->length; @@ -3462,6 +3502,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c } // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; + conn->own_airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); } diff --git a/player.h b/player.h index 14e352ff..a35b54e6 100644 --- a/player.h +++ b/player.h @@ -224,8 +224,8 @@ typedef struct { pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex; int fix_volume; - double initial_airplay_volume; - int initial_airplay_volume_set; + double own_airplay_volume; + int own_airplay_volume_set; uint32_t timestamp_epoch, last_timestamp, maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2 @@ -448,4 +448,6 @@ int64_t monotonic_timestamp(uint32_t timestamp, // assumes, without checking, that successive timestamps in a series always span an interval of less // than one minute. +double suggested_volume(rtsp_conn_info *conn); // volume suggested for the connection + #endif //_PLAYER_H diff --git a/rtsp.c b/rtsp.c index aa95e2d5..83374deb 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-2022 + * metadata handling copyright (c) Mike Brady 2014-2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -739,10 +739,13 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_unlock(&conns_lock, 3); } +int old_connection_count = -1; + void cleanup_threads(void) { void *retval; int i; + int connection_count = 0; // debug(2, "culling threads."); debug_mutex_lock(&conns_lock, 1000000, 3); for (i = 0; i < nconns; i++) { @@ -754,8 +757,19 @@ void cleanup_threads(void) { free(conns[i]); conns[i] = NULL; } + if (conns[i] != NULL) + connection_count++; } debug_mutex_unlock(&conns_lock, 3); + if (old_connection_count != connection_count) { + if (connection_count == 0) + debug(2, "No active connections."); + else if (connection_count == 1) + debug(2, "One active connection."); + else + debug(2, "%d active connections.", connection_count); + old_connection_count = connection_count; + } } // park a null at the line ending, and return the next line pointer @@ -3572,9 +3586,15 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else { #endif lock_player(); - if (playing_conn == conn) + if (playing_conn == conn) { player_volume(volume, conn); + } + if (conn != NULL) { + conn->own_airplay_volume = volume; + conn->own_airplay_volume_set = 1; + } unlock_player(); + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); #ifdef CONFIG_DBUS_INTERFACE } #endif @@ -4327,12 +4347,13 @@ 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, - config.airplay_volume); + debug(2, "Connection %d: current volume (%.6f) requested", conn->connection_number, + suggested_volume(conn)); + char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { resp->content = p; - resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", config.airplay_volume); + resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", suggested_volume(conn)); } else { debug(1, "Couldn't allocate space for a response."); } diff --git a/shairport.c b/shairport.c index 4b2129cc..dc7753ef 100644 --- a/shairport.c +++ b/shairport.c @@ -465,9 +465,14 @@ int parse_options(int argc, char **argv) { #endif config.audio_backend_silent_lead_in_time_auto = - 1; // start outputting silence as soon as packets start arriving - config.airplay_volume = -24.0; // if no volume is ever set, default to initial default value if - // nothing else comes in first. + 1; // start outputting silence as soon as packets start arriving + config.default_airplay_volume = -24.0; + config.high_threshold_airplay_volume = + -20.0; // if the volume exceeds this, reset to this if idle for the + // limit_to_high_volume_threshold_time_in_minutes time + config.limit_to_high_volume_threshold_time_in_minutes = + 1; // after this time, if the volume is higher, use the high_threshold_airplay_volume volume + // for new play sessions. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; config.active_state_timeout = 10.0; @@ -1440,6 +1445,10 @@ int parse_options(int argc, char **argv) { if (tdebuglev != 0) debuglev = tdebuglev; + // now set the initial volume to the default volume + config.airplay_volume = + config.default_airplay_volume; // if no volume is ever set or requested, default to initial + // default value if nothing else comes in first. // now, do the substitutions in the service name char hostname[100]; gethostname(hostname, 100); @@ -2373,8 +2382,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.resync_threshold); debug(1, "resync recovery time is %f seconds.", config.resync_recovery_time); From 07211e7e0f364377fa8612fa55e92b1e0f5c044c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:02:42 +0000 Subject: [PATCH 29/85] Set the high volume threshold to -16.0 and the timeout period to three hours. Hardwired for the present. --- shairport.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shairport.c b/shairport.c index dc7753ef..dd2edd4d 100644 --- a/shairport.c +++ b/shairport.c @@ -468,10 +468,10 @@ int parse_options(int argc, char **argv) { 1; // start outputting silence as soon as packets start arriving config.default_airplay_volume = -24.0; config.high_threshold_airplay_volume = - -20.0; // if the volume exceeds this, reset to this if idle for the + -16.0; // if the volume exceeds this, reset to the default volume if idle for the // limit_to_high_volume_threshold_time_in_minutes time config.limit_to_high_volume_threshold_time_in_minutes = - 1; // after this time, if the volume is higher, use the high_threshold_airplay_volume volume + 3 * 60; // after this time in minutes, if the volume is higher, use the default_airplay_volume volume // for new play sessions. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; @@ -2276,7 +2276,7 @@ int main(int argc, char **argv) { if (sodium_init() < 0) { debug(1, "Can't initialise libsodium!"); } else { - debug(1, "libsodium initialised."); + debug(2, "libsodium initialised."); } // this code is based on @@ -2301,7 +2301,7 @@ int main(int argc, char **argv) { /* Tell Libgcrypt that initialization has completed. */ gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); - debug(1, "libgcrypt initialised."); + debug(2, "libgcrypt initialised."); #endif From 66ca645002dd21771fba1f2b0adef617425a57e4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:52:11 +0000 Subject: [PATCH 30/85] Check the pulseaudio stream whenever the pa backend is called. Warnings are issued for apparent problems. --- audio_pa.c | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index 3370cfed..eef771db 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -58,6 +58,38 @@ void stream_state_cb(pa_stream *s, void *mainloop); void stream_success_cb(pa_stream *stream, int success, void *userdata); void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata); +int status_error_notifications = 0; +static void check_pa_stream_status(const pa_stream *p, const char *message) { + if (status_error_notifications < 10) { + if (p == NULL) { + warn("%s No pulseaudio stream!", message); + status_error_notifications++; + } else { + status_error_notifications++; // assume an error + switch (pa_stream_get_state(p)) { + case PA_STREAM_UNCONNECTED: + warn("%s Pulseaudio stream unconnected!", message); + break; + case PA_STREAM_CREATING: + warn("%s Pulseaudio stream being created!", message); + break; + case PA_STREAM_READY: + status_error_notifications--; // no error + break; + case PA_STREAM_FAILED: + warn("%s Pulseaudio stream failed!", message); + break; + case PA_STREAM_TERMINATED: + warn("%s Pulseaudio stream unexpectedly terminated!", message); + break; + default: + warn("%s Pulseaudio stream in unexpected state %d!", message, pa_stream_get_state(p)); + break; + } + } + } +} + static void connect_stream() { // debug(1, "connect_stream"); uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here @@ -203,10 +235,12 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * pa_threaded_mainloop_unlock(mainloop); connect_stream(); + check_pa_stream_status(stream, "audio_pa initialisation."); return 0; } static void deinit(void) { + check_pa_stream_status(stream, "audio_pa deinitialisation."); pa_stream_disconnect(stream); pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); @@ -215,7 +249,7 @@ static void deinit(void) { static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - // debug(1, "pa_start"); + check_pa_stream_status(stream, "audio_pa start."); } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, @@ -223,6 +257,7 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint64_t playtime) { // debug(1,"pa_play of %d samples.",samples); // copy the samples into the queue + check_pa_stream_status(stream, "audio_pa play."); size_t bytes_to_transfer = samples * 2 * 2; size_t space_to_end_of_buffer = audio_umb - audio_eoq; if (space_to_end_of_buffer >= bytes_to_transfer) { @@ -250,6 +285,7 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type, } int pa_delay(long *the_delay) { + check_pa_stream_status(stream, "audio_pa delay."); // debug(1,"pa_delay"); long result = 0; int reply = 0; @@ -273,7 +309,7 @@ int pa_delay(long *the_delay) { } void flush(void) { - // debug(1, "pa_flush."); + check_pa_stream_status(stream, "audio_pa flush."); pa_threaded_mainloop_lock(mainloop); if (pa_stream_is_corked(stream) == 0) { // debug(1,"Flush and cork for flush."); @@ -287,7 +323,7 @@ void flush(void) { } static void stop(void) { - // debug(1, "pa_stop."); + check_pa_stream_status(stream, "audio_pa stop."); // Cork the stream so it will stop playing pa_threaded_mainloop_lock(mainloop); if (pa_stream_is_corked(stream) == 0) { @@ -329,6 +365,7 @@ void stream_state_cb(__attribute__((unused)) pa_stream *s, void *mainloop) { void stream_write_cb(pa_stream *stream, size_t requested_bytes, __attribute__((unused)) void *userdata) { + check_pa_stream_status(stream, "audio_pa stream_write_cb."); int bytes_to_transfer = requested_bytes; int bytes_transferred = 0; uint8_t *buffer = NULL; From 72f1554cbbfa8a10d59641c25187b4a9ea77e187 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 12:44:26 +0000 Subject: [PATCH 31/85] Continuing to check the high-volume limiter. Add configuration file settings for default and threshold volume settings and timeout. Include in diagnostics, set tentative defaults. --- player.c | 46 ++++++++++++++++++------------------- rtsp.c | 12 +++++++--- scripts/shairport-sync.conf | 17 +++++++++++++- shairport.c | 44 +++++++++++++++++++++++++++++++++-- 4 files changed, 89 insertions(+), 30 deletions(-) diff --git a/player.c b/player.c index db7b8ae0..84f0a9e4 100644 --- a/player.c +++ b/player.c @@ -1704,33 +1704,31 @@ void statistics_item(const char *heading, const char *format, ...) { double suggested_volume(rtsp_conn_info *conn) { double response = config.airplay_volume; - if (conn) { - if (conn->own_airplay_volume_set != 0) { - response = conn->own_airplay_volume; - } else if (config.airplay_volume > config.high_threshold_airplay_volume) { - int64_t volume_validity_time = config.limit_to_high_volume_threshold_time_in_minutes; - // zero means never check the volume - if (volume_validity_time != 0) { - // If the volume is higher than the high volume threshold - // and enough time has gone past, suggest the default volume. - uint64_t time_now = get_absolute_time_in_ns(); - int64_t time_since_last_access_to_volume_info = - time_now - config.last_access_to_volume_info_time; + if ((conn != NULL) && (conn->own_airplay_volume_set != 0)) { + response = conn->own_airplay_volume; + } else if (config.airplay_volume > config.high_threshold_airplay_volume) { + int64_t volume_validity_time = config.limit_to_high_volume_threshold_time_in_minutes; + // zero means never check the volume + if (volume_validity_time != 0) { + // If the volume is higher than the high volume threshold + // and enough time has gone past, suggest the default volume. + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_last_access_to_volume_info = + time_now - config.last_access_to_volume_info_time; - volume_validity_time = volume_validity_time * 60; // to seconds - volume_validity_time = volume_validity_time * 1000000000; // to nanoseconds + volume_validity_time = volume_validity_time * 60; // to seconds + volume_validity_time = volume_validity_time * 1000000000; // to nanoseconds - if ((config.airplay_volume > config.high_threshold_airplay_volume) && - ((config.last_access_to_volume_info_time == 0) || - (time_since_last_access_to_volume_info > volume_validity_time))) { + if ((config.airplay_volume > config.high_threshold_airplay_volume) && + ((config.last_access_to_volume_info_time == 0) || + (time_since_last_access_to_volume_info > volume_validity_time))) { - debug(2, - "the current volume %.6f is higher than the high volume threshold %.6f, so the " - "default volume %.6f is suggested.", - config.airplay_volume, config.high_threshold_airplay_volume, - config.default_airplay_volume); - response = config.default_airplay_volume; - } + debug(2, + "the current volume %.6f is higher than the high volume threshold %.6f, so the " + "default volume %.6f is suggested.", + config.airplay_volume, config.high_threshold_airplay_volume, + config.default_airplay_volume); + response = config.default_airplay_volume; } } } diff --git a/rtsp.c b/rtsp.c index 83374deb..11f09ed0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -757,19 +757,24 @@ void cleanup_threads(void) { free(conns[i]); conns[i] = NULL; } - if (conns[i] != NULL) + if (conns[i] != NULL) { + debug(1, "Airplay Volume for connection %d is %.6f.", conns[i]->connection_number, + suggested_volume(conns[i])); connection_count++; + } } debug_mutex_unlock(&conns_lock, 3); + if (old_connection_count != connection_count) { - if (connection_count == 0) + if (connection_count == 0) { debug(2, "No active connections."); - else if (connection_count == 1) + } else if (connection_count == 1) debug(2, "One active connection."); else debug(2, "%d active connections.", connection_count); old_connection_count = connection_count; } + debug(1, "Airplay Volume for new connections is %.6f.", suggested_volume(NULL)); } // park a null at the line ending, and return the next line pointer @@ -5477,6 +5482,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { FD_SET(sockfd[i], &fds); ret = select(maxfd + 1, &fds, 0, 0, &tv); + if (ret < 0) { if (errno == EINTR) continue; diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 918ad377..5731fe08 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -43,9 +43,24 @@ general = // "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_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. + +// default_airplay_volume = 24.0; // this is the suggested volume after a reset or after the high_volume_threshold has been exceed and the high_volume_idle_timeout_in_minutes has passed + +// The following settings are for dealing with potentially surprising high ("very loud") volume levels. +// When a new play session starts, it usually requests a suggested volume level from Shairport Sync. This is normally the volume level of the last session. +// This can cause unpleasant surprises if the last session was (a) very loud and (b) a long time ago. +// Thus, the user could be unpleasantly surprised by the volume level of the new session. + +// To deal with this, when the last session volume is "very loud", the following two settings will lower the suggested volume after a period of idleness: + +// high_threshold_airplay_volume = -16.0; // airplay volume greater or equal to this is "very loud" +// high_volume_idle_timeout_in_minutes = 0; // if the current volume is "very loud" and the device is not playing for more than this time, suggest the default volume for new connections instead of the current volume. +// Note 1: This timeout is set to 0 by default to disable this feature. Set it to some positive number, e.g. 180 to activate the feature. +// Note 2: Not all applications use the suggested volume: MacOS Music and Mac OS System Sounds use their own settings. + // 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". +// AirPlay volume goes from 0.0 to -30.0 and -144.0 means "mute". // audio_backend_latency_offset_in_seconds = 0.0; // This is added to the latency requested by the player to delay or advance the output by a fixed amount. // Use it, for example, to compensate for a fixed delay in the audio back end. diff --git a/shairport.c b/shairport.c index dd2edd4d..17ab9717 100644 --- a/shairport.c +++ b/shairport.c @@ -471,8 +471,8 @@ int parse_options(int argc, char **argv) { -16.0; // if the volume exceeds this, reset to the default volume if idle for the // limit_to_high_volume_threshold_time_in_minutes time config.limit_to_high_volume_threshold_time_in_minutes = - 3 * 60; // after this time in minutes, if the volume is higher, use the default_airplay_volume volume - // for new play sessions. + 0; // after this time in minutes, if the volume is higher, use the default_airplay_volume + // volume for new play sessions. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; config.active_state_timeout = 10.0; @@ -845,6 +845,37 @@ int parse_options(int argc, char **argv) { config.volume_max_db_set = 1; } + /* Get the optional default_volume setting. */ + if (config_lookup_float(config.cfg, "general.default_airplay_volume", &dvalue)) { + // debug(1, "Default airplay volume setting of %f on the -30.0 to 0 scale", dvalue); + if ((dvalue >= -30.0) && (dvalue <= 0.0)) { + config.default_airplay_volume = dvalue; + } else { + warn("The default airplay volume setting must be between -30.0 and 0.0."); + } + } + + /* Get the optional high_volume_threshold setting. */ + if (config_lookup_float(config.cfg, "general.high_threshold_airplay_volume", &dvalue)) { + // debug(1, "High threshold airplay volume setting of %f on the -30.0 to 0 scale", dvalue); + if ((dvalue >= -30.0) && (dvalue <= 0.0)) { + config.high_threshold_airplay_volume = dvalue; + } else { + warn("The high threshold airplay volume setting must be between -30.0 and 0.0."); + } + } + + /* Get the optional high volume idle tiomeout setting. */ + if (config_lookup_float(config.cfg, "general.high_volume_idle_timeout_in_minutes", &dvalue)) { + // debug(1, "High high_volume_idle_timeout_in_minutes setting of %f", dvalue); + if (dvalue >= 0.0) { + config.limit_to_high_volume_threshold_time_in_minutes = dvalue; + } else { + warn("The high volume idle timeout in minutes setting must be 0.0 or greater. A setting " + "of 0.0 disables the high volume check."); + } + } + if (config_lookup_string(config.cfg, "general.run_this_when_volume_is_set", &str)) { config.cmd_set_volume = (char *)str; } @@ -2392,6 +2423,15 @@ int main(int argc, char **argv) { debug(1, "busy timeout time is %d.", config.timeout); debug(1, "drift tolerance is %f seconds.", config.tolerance); debug(1, "password is \"%s\".", strnull(config.password)); + debug(1, "default airplay volume is %.6f.", config.default_airplay_volume); + debug(1, "high threshold airplay volume is %.6f.", config.high_threshold_airplay_volume); + if (config.limit_to_high_volume_threshold_time_in_minutes == 0) + debug(1, "check for higher-than-threshold volume for new play session is disabled."); + else + debug(1, + "suggest default airplay volume for new play session instead of higher-than-threshold " + "airplay volume after %d minutes.", + config.limit_to_high_volume_threshold_time_in_minutes); 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 17c39797c6678f5cebcc2c964a149e50130af067 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 13:04:43 +0000 Subject: [PATCH 32/85] Quieten some debug messages. --- rtsp.c | 4 ++-- shairport.c | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rtsp.c b/rtsp.c index 11f09ed0..83ee0261 100644 --- a/rtsp.c +++ b/rtsp.c @@ -758,7 +758,7 @@ void cleanup_threads(void) { conns[i] = NULL; } if (conns[i] != NULL) { - debug(1, "Airplay Volume for connection %d is %.6f.", conns[i]->connection_number, + debug(2, "Airplay Volume for connection %d is %.6f.", conns[i]->connection_number, suggested_volume(conns[i])); connection_count++; } @@ -774,7 +774,7 @@ void cleanup_threads(void) { debug(2, "%d active connections.", connection_count); old_connection_count = connection_count; } - debug(1, "Airplay Volume for new connections is %.6f.", suggested_volume(NULL)); + debug(2, "Airplay Volume for new connections is %.6f.", suggested_volume(NULL)); } // park a null at the line ending, and return the next line pointer diff --git a/shairport.c b/shairport.c index 17ab9717..4829c616 100644 --- a/shairport.c +++ b/shairport.c @@ -2423,14 +2423,14 @@ int main(int argc, char **argv) { debug(1, "busy timeout time is %d.", config.timeout); debug(1, "drift tolerance is %f seconds.", config.tolerance); debug(1, "password is \"%s\".", strnull(config.password)); - debug(1, "default airplay volume is %.6f.", config.default_airplay_volume); - debug(1, "high threshold airplay volume is %.6f.", config.high_threshold_airplay_volume); + debug(1, "default airplay volume is: %.6f.", config.default_airplay_volume); + debug(1, "high threshold airplay volume is: %.6f.", config.high_threshold_airplay_volume); if (config.limit_to_high_volume_threshold_time_in_minutes == 0) debug(1, "check for higher-than-threshold volume for new play session is disabled."); else debug(1, - "suggest default airplay volume for new play session instead of higher-than-threshold " - "airplay volume after %d minutes.", + "suggest default airplay volume for new play sessions instead of higher-than-threshold " + "airplay volume after: %d minutes.", config.limit_to_high_volume_threshold_time_in_minutes); debug(1, "ignore_volume_control is %d.", config.ignore_volume_control); if (config.volume_max_db_set) @@ -2516,7 +2516,7 @@ int main(int argc, char **argv) { // calculate the 12-hex-digit prefix by hashing the service name. uint8_t ap_md5[16]; - debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); + // debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); From 6eb70eee70097482e03c38cf8b8cb1c4c75f696f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 14:32:32 +0000 Subject: [PATCH 33/85] Add modifications for V9 of the nqptp interface. --- nqptp-shm-structures.h | 42 ++++++++++++++++++++++++++---------------- player.c | 3 +++ rtsp.c | 5 ++++- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 3dbe0c90..251d06c1 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -22,26 +22,36 @@ #define NQPTP_INTERFACE_NAME "/nqptp" -#define NQPTP_SHM_STRUCTURES_VERSION 8 +#define NQPTP_SHM_STRUCTURES_VERSION 9 #define NQPTP_CONTROL_PORT 9000 -// 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 control port expects a UDP packet with the first character being a command letter +// and the rest being any arguments, the whole not to exceed 4096 characters. +// The "T" command, must followed by nothing or by +// a space and a space-delimited list of IPv4 or IPv6 numbers. // 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. +// previous list. The first IP number is the clock that NQPTP will listen to. +// The remaining IP address are the addresses of all the timing peers. The timing peers +// are not used in this version of NQPTP. // 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. +// The "B" command is a message that the client -- which generates the clock -- +// is about to start playing. +// NQPTP uses it to determine that the clock is active and will not sleep. +// The "E" command signifies that the client has stopped playing and that +// the clock may shortly sleep. +// The "P" command signifies that SPS has paused play (buffered audio only). +// The clock seems to stay running in this state. + +// When the clock is active, it is assumed that any decreases in the offset +// between the local and remote clocks are due to delays in the network. +// NQPTP smooths the offset by clamping any decreases to a small value. +// In this way, it can follow clock drift but ignore network delays. + +// When the clock is inactive, it can stop running. This causes the offset to decrease. +// NQPTP clock smoothing would treat this as a network delay, causing true sync to be lost. +// To avoid this, when the clock goes from inactive to active, +// NQPTP resets clock smoothing to the new offset. + #include #include diff --git a/player.c b/player.c index 84f0a9e4..9d52c6ec 100644 --- a/player.c +++ b/player.c @@ -3606,6 +3606,9 @@ int player_stop(rtsp_conn_info *conn) { // debuglev = 3; debug(3, "player_stop"); if (conn->player_thread) { +#ifdef CONFIG_AIRPLAY_2 + ptp_send_control_message_string("E"); // signify play is "E"nding +#endif debug(3, "player_thread cancel..."); pthread_cancel(*conn->player_thread); debug(3, "player_thread join..."); diff --git a/rtsp.c b/rtsp.c index 83ee0261..55220b75 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2022,6 +2022,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) { + ptp_send_control_message_string("B"); // signify play is "B"eginning or resuming debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); @@ -2031,7 +2032,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message #endif conn->ap2_play_enabled = 1; } else { - debug(2, "Connection %d: Stop playing.", conn->connection_number); + ptp_send_control_message_string("P"); // signify play is "P"ausing + debug(2, "Connection %d: Pause playing.", conn->connection_number); conn->ap2_play_enabled = 0; activity_monitor_signify_activity(0); reset_anchor_info(conn); @@ -2955,6 +2957,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("B"); // signify play is "B"eginning 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?"); From 4350f5e60b3aa6e76ba751f3e363073805dd82cf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 15:20:15 +0000 Subject: [PATCH 34/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index a3b0fbd5..7455905a 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,14 @@ +Version 4.1-dev-862-g6eb70eee +==== +***Pesky Changes You Can't Ignore*** + +* **Important**. Please update and re-start NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to version 9 to reflect changes in NQPTP's operation. + +**Enhancements** +* A new "high volume check" attempts to prevent the unpleasant surprise of a new session playing at the same very high volume as the previous session if that previous session was some time, e.g. some hours, ago. How it works is that if a timeout period has elapsed since the last play session and the last play session was very loud, a lower volume will be suggested for new play sessions. Most apps accept the suggestion. The timeout, loudness threshold and suggested volume are all settable in the configuration file. Please refer to the sample configuration file for more details. The feature is disabled by default. + +* The PulseAudio backend has been reworked to simplify the code and to remove a number of issues. Hopefully nothing has been broken. + Version 4.1-dev-846-gb5bb2fef ==== * Improvements to the detection of a missing or non-existent output device. Look for `ENODEV` and `ENOENT` errors when opening or playing to an output device. From accc6777da09b4944d4f3fb17c97b0c2c13dd54c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:15:14 +0000 Subject: [PATCH 35/85] Update check_classic_mac_basic.yml Upgrade Brew as well --- .github/workflows/check_classic_mac_basic.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index d41d0fc8..77a49015 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -14,6 +14,7 @@ jobs: - name: Install Dependencies run: | brew update + brew upgrade brew install automake brew install openssl brew install popt From 90ca27f9bea4cc94d1ae78883eaa0f879f5f8763 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:30:40 +0000 Subject: [PATCH 36/85] Update check_classic_mac_basic.yml Don't. ask for brew update/upgrade --- .github/workflows/check_classic_mac_basic.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index 77a49015..6b698714 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -13,8 +13,6 @@ jobs: - uses: actions/checkout@v3 - name: Install Dependencies run: | - brew update - brew upgrade brew install automake brew install openssl brew install popt From 8c607e2d61ea5fde8ed0b48b2471a543840c31e6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:46:30 +0000 Subject: [PATCH 37/85] Update shairport-sync.conf default volume typo in configuration file --- 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 5731fe08..03cb4339 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -44,7 +44,7 @@ general = // "flat" makes the volume change at the same rate at all volumes. // 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. -// default_airplay_volume = 24.0; // this is the suggested volume after a reset or after the high_volume_threshold has been exceed and the high_volume_idle_timeout_in_minutes has passed +// default_airplay_volume = -24.0; // this is the suggested volume after a reset or after the high_volume_threshold has been exceed and the high_volume_idle_timeout_in_minutes has passed // The following settings are for dealing with potentially surprising high ("very loud") volume levels. // When a new play session starts, it usually requests a suggested volume level from Shairport Sync. This is normally the volume level of the last session. From a6c66db2761619456e80611d2ffc6054684f9caf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 20 Jan 2023 10:23:03 +0000 Subject: [PATCH 38/85] 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 7455905a..b5110df5 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -5,7 +5,7 @@ Version 4.1-dev-862-g6eb70eee * **Important**. Please update and re-start NQPTP. The Shared Memory Interface protocol that Shairport Sync and NQPTP use to communicate with one another has been updated to version 9 to reflect changes in NQPTP's operation. **Enhancements** -* A new "high volume check" attempts to prevent the unpleasant surprise of a new session playing at the same very high volume as the previous session if that previous session was some time, e.g. some hours, ago. How it works is that if a timeout period has elapsed since the last play session and the last play session was very loud, a lower volume will be suggested for new play sessions. Most apps accept the suggestion. The timeout, loudness threshold and suggested volume are all settable in the configuration file. Please refer to the sample configuration file for more details. The feature is disabled by default. +* A new "high volume check" attempts to prevent the unpleasant surprise of a new session playing at the same very high volume as the previous session if that previous session was some time, e.g. some hours, ago. How it works is that if a timeout period has elapsed since the last play session and the last play session was very loud, a lower volume will be suggested for new play sessions. Most iOS and iPadOS apps accept the suggestion as the new volume. However, Macs, HomePods and AppleTVs use the suggested volume only when connecting the first time. The timeout, loudness threshold and suggested volume are all settable in the configuration file. Please refer to the sample configuration file for more details. The feature is disabled by default. * The PulseAudio backend has been reworked to simplify the code and to remove a number of issues. Hopefully nothing has been broken. From bea0faf488f18ca51552705519a67fa65ff29af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Wed, 1 Feb 2023 14:10:06 +0100 Subject: [PATCH 39/85] Update Dockerfiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- docker/Dockerfile | 10 +++++----- docker/classic/Dockerfile | 8 ++++---- .../etc/s6-overlay/s6-rc.d/01-startup/dependencies | 1 + .../s6-rc.d/{startup => 01-startup}/script.sh | 0 .../s6-overlay/s6-rc.d/{startup => 01-startup}/type | 0 docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up | 1 + docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run | 8 -------- .../etc/s6-overlay/s6-rc.d/02-dbus/dependencies | 1 + .../etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/finish | 0 .../etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/run | 2 +- .../etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/type | 0 .../etc/s6-overlay/s6-rc.d/03-avahi/dependencies | 1 + .../s6-overlay/s6-rc.d/{02-avahi => 03-avahi}/finish | 0 docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run | 7 +++++++ .../etc/s6-overlay/s6-rc.d/{02-avahi => 03-avahi}/type | 0 .../s6-overlay/s6-rc.d/04-shairport-sync/dependencies | 1 + .../etc/s6-overlay/s6-rc.d/04-shairport-sync}/finish | 0 .../etc/s6-overlay/s6-rc.d/04-shairport-sync/run | 7 +++++++ .../etc/s6-overlay/s6-rc.d/04-shairport-sync}/type | 0 docker/classic/etc/s6-overlay/s6-rc.d/startup/up | 1 - .../s6-rc.d/user/contents.d/{01-dbus => 01-startup} | 0 .../s6-rc.d/user/contents.d/{02-avahi => 02-dbus} | 0 .../s6-rc.d/user/contents.d/{startup => 03-avahi} | 0 .../s6-rc.d/user/contents.d/04-shairport-sync} | 0 docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies | 1 + .../s6-rc.d/{startup => 01-startup}/script.sh | 0 .../s6-overlay/s6-rc.d/{startup => 01-startup}/type | 0 docker/etc/s6-overlay/s6-rc.d/01-startup/up | 1 + docker/etc/s6-overlay/s6-rc.d/02-avahi/run | 8 -------- docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies | 1 + .../s6-overlay/s6-rc.d/{02-avahi => 02-dbus}/finish | 0 docker/etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/run | 2 +- .../etc/s6-overlay/s6-rc.d/{02-avahi => 02-dbus}/type | 0 docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies | 1 + .../s6-overlay/s6-rc.d/{03-nqptp => 03-avahi}/finish | 0 docker/etc/s6-overlay/s6-rc.d/03-avahi/run | 7 +++++++ .../etc/s6-overlay/s6-rc.d/{03-nqptp => 03-avahi}/type | 0 docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies | 1 + docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish | 2 ++ .../etc/s6-overlay/s6-rc.d/{03-nqptp => 04-nqptp}/run | 0 docker/etc/s6-overlay/s6-rc.d/04-nqptp/type | 1 + .../s6-overlay/s6-rc.d/05-shairport-sync/dependencies | 1 + docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish | 2 ++ docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run | 7 +++++++ docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type | 1 + docker/etc/s6-overlay/s6-rc.d/startup/up | 1 - .../s6-rc.d/user/contents.d/{02-avahi => 01-startup} | 0 .../s6-rc.d/user/contents.d/{03-nqptp => 02-dbus} | 0 .../s6-rc.d/user/contents.d/{startup => 03-avahi} | 0 docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp | 0 .../s6-rc.d/user/contents.d/05-shairport-sync | 0 51 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies rename docker/classic/etc/s6-overlay/s6-rc.d/{startup => 01-startup}/script.sh (100%) rename docker/classic/etc/s6-overlay/s6-rc.d/{startup => 01-startup}/type (100%) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies rename docker/classic/etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/finish (100%) rename docker/classic/etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/run (73%) rename docker/classic/etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/type (100%) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies rename docker/classic/etc/s6-overlay/s6-rc.d/{02-avahi => 03-avahi}/finish (100%) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run rename docker/classic/etc/s6-overlay/s6-rc.d/{02-avahi => 03-avahi}/type (100%) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies rename docker/{etc/s6-overlay/s6-rc.d/01-dbus => classic/etc/s6-overlay/s6-rc.d/04-shairport-sync}/finish (100%) create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run rename docker/{etc/s6-overlay/s6-rc.d/01-dbus => classic/etc/s6-overlay/s6-rc.d/04-shairport-sync}/type (100%) delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/startup/up rename docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/{01-dbus => 01-startup} (100%) rename docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/{02-avahi => 02-dbus} (100%) rename docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/{startup => 03-avahi} (100%) rename docker/{etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus => classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync} (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies rename docker/etc/s6-overlay/s6-rc.d/{startup => 01-startup}/script.sh (100%) rename docker/etc/s6-overlay/s6-rc.d/{startup => 01-startup}/type (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/01-startup/up delete mode 100644 docker/etc/s6-overlay/s6-rc.d/02-avahi/run create mode 100644 docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies rename docker/etc/s6-overlay/s6-rc.d/{02-avahi => 02-dbus}/finish (100%) rename docker/etc/s6-overlay/s6-rc.d/{01-dbus => 02-dbus}/run (73%) rename docker/etc/s6-overlay/s6-rc.d/{02-avahi => 02-dbus}/type (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies rename docker/etc/s6-overlay/s6-rc.d/{03-nqptp => 03-avahi}/finish (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-avahi/run rename docker/etc/s6-overlay/s6-rc.d/{03-nqptp => 03-avahi}/type (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies create mode 100644 docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish rename docker/etc/s6-overlay/s6-rc.d/{03-nqptp => 04-nqptp}/run (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/04-nqptp/type create mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies create mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish create mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run create mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type delete mode 100644 docker/etc/s6-overlay/s6-rc.d/startup/up rename docker/etc/s6-overlay/s6-rc.d/user/contents.d/{02-avahi => 01-startup} (100%) rename docker/etc/s6-overlay/s6-rc.d/user/contents.d/{03-nqptp => 02-dbus} (100%) rename docker/etc/s6-overlay/s6-rc.d/user/contents.d/{startup => 03-avahi} (100%) create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp create mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync diff --git a/docker/Dockerfile b/docker/Dockerfile index 5a6bc678..f1ea8d66 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine:3.17 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. @@ -26,7 +26,6 @@ RUN apk -U add \ ffmpeg-dev \ xxd \ libressl-dev \ - openssl-dev \ libplist-dev ##### ALAC ##### @@ -66,7 +65,7 @@ WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-3.1.1.2 +FROM crazymax/alpine-s6:3.17-3.1.1.2 RUN apk -U add \ alsa-lib \ @@ -83,6 +82,7 @@ RUN apk -U add \ ffmpeg \ libsodium \ libgcrypt \ + libressl3.6-libcrypto \ libplist \ mandoc \ man-pages \ @@ -100,7 +100,7 @@ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport- 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 +RUN chmod +x /etc/s6-overlay/s6-rc.d/01-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 @@ -114,4 +114,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init" ] diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 03baac5e..f6cb01c7 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine:3.17 AS builder # Classic (aka AirPlay 1) Build @@ -49,7 +49,7 @@ WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-3.1.1.2 +FROM crazymax/alpine-s6:3.17-3.1.1.2 RUN apk -U add \ alsa-lib \ @@ -78,7 +78,7 @@ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport- COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ COPY ./docker/classic/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d -RUN chmod +x /etc/s6-overlay/s6-rc.d/startup/script.sh +RUN chmod +x /etc/s6-overlay/s6-rc.d/01-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 @@ -92,4 +92,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init" ] diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies new file mode 100644 index 00000000..df967b96 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies @@ -0,0 +1 @@ +base diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/script.sh b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/script.sh similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/startup/script.sh rename to docker/classic/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/type b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/startup/type rename to docker/classic/etc/s6-overlay/s6-rc.d/01-startup/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up new file mode 100644 index 00000000..0521bff8 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run deleted file mode 100644 index 2f6be7b4..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies new file mode 100644 index 00000000..f04b211f --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies @@ -0,0 +1 @@ +01-startup diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/finish b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/finish similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/finish rename to docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/finish diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run similarity index 73% rename from docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run rename to docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run index bf1cb501..2278697a 100644 --- a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -1,4 +1,4 @@ #!/command/with-contenv sh -rm -rf /var/run/dbus.pid +rm -rf /var/run/dbus/dbus.pid echo "Starting dbus" exec dbus-daemon --system --nofork diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/type b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/type rename to docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies new file mode 100644 index 00000000..497f42ec --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies @@ -0,0 +1 @@ +02-dbus diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/finish b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/finish similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/finish rename to docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/finish diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run new file mode 100644 index 00000000..20275d40 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -0,0 +1,7 @@ +#!/command/with-contenv sh +while [ ! -f /var/run/dbus/dbus.pid ]; do + echo "s6-rc: warning: dbus is not running, sleeping for 1 seconds before trying to start avahi" + sleep 1 +done +echo "Starting avahi" +exec avahi-daemon --no-chroot diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/type b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/type rename to docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies new file mode 100644 index 00000000..97763270 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies @@ -0,0 +1 @@ +03-avahi diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/finish b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/01-dbus/finish rename to docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run new file mode 100644 index 00000000..c533db5e --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run @@ -0,0 +1,7 @@ +#!/command/with-contenv sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "s6-rc: warning: avahi is not running, sleeping for 1 seconds before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +exec /usr/local/bin/shairport-sync diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/type b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/01-dbus/type rename to docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/up b/docker/classic/etc/s6-overlay/s6-rc.d/startup/up deleted file mode 100644 index 06df8350..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/startup/up +++ /dev/null @@ -1 +0,0 @@ -/etc/s6-overlay/s6-rc.d/startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/startup b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/startup rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync diff --git a/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies b/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies new file mode 100644 index 00000000..df967b96 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies @@ -0,0 +1 @@ +base diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/script.sh b/docker/etc/s6-overlay/s6-rc.d/01-startup/script.sh similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/startup/script.sh rename to docker/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/type b/docker/etc/s6-overlay/s6-rc.d/01-startup/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/startup/type rename to docker/etc/s6-overlay/s6-rc.d/01-startup/type diff --git a/docker/etc/s6-overlay/s6-rc.d/01-startup/up b/docker/etc/s6-overlay/s6-rc.d/01-startup/up new file mode 100644 index 00000000..0521bff8 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-startup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run deleted file mode 100644 index 2f6be7b4..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies b/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies new file mode 100644 index 00000000..f04b211f --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies @@ -0,0 +1 @@ +01-startup diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/finish b/docker/etc/s6-overlay/s6-rc.d/02-dbus/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/02-avahi/finish rename to docker/etc/s6-overlay/s6-rc.d/02-dbus/finish diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run similarity index 73% rename from docker/etc/s6-overlay/s6-rc.d/01-dbus/run rename to docker/etc/s6-overlay/s6-rc.d/02-dbus/run index bf1cb501..2278697a 100644 --- a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -1,4 +1,4 @@ #!/command/with-contenv sh -rm -rf /var/run/dbus.pid +rm -rf /var/run/dbus/dbus.pid echo "Starting dbus" exec dbus-daemon --system --nofork diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/type b/docker/etc/s6-overlay/s6-rc.d/02-dbus/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/02-avahi/type rename to docker/etc/s6-overlay/s6-rc.d/02-dbus/type diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies b/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies new file mode 100644 index 00000000..497f42ec --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies @@ -0,0 +1 @@ +02-dbus diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish b/docker/etc/s6-overlay/s6-rc.d/03-avahi/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish rename to docker/etc/s6-overlay/s6-rc.d/03-avahi/finish diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run new file mode 100644 index 00000000..20275d40 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -0,0 +1,7 @@ +#!/command/with-contenv sh +while [ ! -f /var/run/dbus/dbus.pid ]; do + echo "s6-rc: warning: dbus is not running, sleeping for 1 seconds before trying to start avahi" + sleep 1 +done +echo "Starting avahi" +exec avahi-daemon --no-chroot diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/type b/docker/etc/s6-overlay/s6-rc.d/03-avahi/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/type rename to docker/etc/s6-overlay/s6-rc.d/03-avahi/type diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies new file mode 100644 index 00000000..97763270 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies @@ -0,0 +1 @@ +03-avahi diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish new file mode 100644 index 00000000..e4a13866 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish @@ -0,0 +1,2 @@ +#!/command/execlineb -S0 +/run/s6/basedir/bin/halt diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/run rename to docker/etc/s6-overlay/s6-rc.d/04-nqptp/run diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/type b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/type @@ -0,0 +1 @@ +longrun diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies new file mode 100644 index 00000000..a8d402ef --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies @@ -0,0 +1 @@ +04-nqptp diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish new file mode 100644 index 00000000..e4a13866 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish @@ -0,0 +1,2 @@ +#!/command/execlineb -S0 +/run/s6/basedir/bin/halt diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run new file mode 100644 index 00000000..c533db5e --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run @@ -0,0 +1,7 @@ +#!/command/with-contenv sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "s6-rc: warning: avahi is not running, sleeping for 1 seconds before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +exec /usr/local/bin/shairport-sync diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type @@ -0,0 +1 @@ +longrun diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/up b/docker/etc/s6-overlay/s6-rc.d/startup/up deleted file mode 100644 index 06df8350..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/startup/up +++ /dev/null @@ -1 +0,0 @@ -/etc/s6-overlay/s6-rc.d/startup/script.sh 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/01-startup similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup 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/02-dbus similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus 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/03-avahi similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp new file mode 100644 index 00000000..e69de29b diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync new file mode 100644 index 00000000..e69de29b From dec597c96e74f11262fb75a4e5d387baeca76a10 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 4 Feb 2023 09:32:55 +0000 Subject: [PATCH 40/85] Update ADDINGTOHOME.md --- ADDINGTOHOME.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ADDINGTOHOME.md b/ADDINGTOHOME.md index 3d3ed5dc..1d35fbf7 100644 --- a/ADDINGTOHOME.md +++ b/ADDINGTOHOME.md @@ -1,5 +1,10 @@ Here is how to add 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 0: Speaker & TV access. +--- + +In the Home app > Home settings "Allow Speaker & TV access", select the option "Anyone On the Same Network". + *** Step 1: Open the "Home" app From 24a941988f759337ea4e051841528e0c8acc33b3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 4 Feb 2023 09:33:24 +0000 Subject: [PATCH 41/85] Update ADDINGTOHOME.md --- ADDINGTOHOME.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ADDINGTOHOME.md b/ADDINGTOHOME.md index 1d35fbf7..df5f4d5a 100644 --- a/ADDINGTOHOME.md +++ b/ADDINGTOHOME.md @@ -1,12 +1,12 @@ Here is how to add 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 0: Speaker & TV access. --- In the Home app > Home settings "Allow Speaker & TV access", select the option "Anyone On the Same Network". -*** - Step 1: Open the "Home" app --- From f79c3e1579bee23799c9345c685cf6ee2be24044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 5 Feb 2023 20:29:49 +0100 Subject: [PATCH 42/85] Allow passing arguments via environment variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- docker/Dockerfile | 2 -- docker/README.md | 7 ++++--- docker/classic/Dockerfile | 2 -- .../classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run | 2 +- docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f1ea8d66..47e9de46 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -113,5 +113,3 @@ 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" ] diff --git a/docker/README.md b/docker/README.md index 295d2170..9953cefc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -31,11 +31,12 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ### Options -Command line options will be passed to Shairport Sync. Here is an example: +To pass command line options to Shairport Sync they need to be set with `-e ARG=`. Here is an example: + ``` $ docker run -d --restart unless-stopped --net host --device /dev/snd \ - mikebrady/shairport-sync:latest \ - -v --statistics -a DenSystem -d hw:0 -c PCM + -e ARG="-v --statistics -a DenSystem -d hw:0 -c PCM" \ + mikebrady/shairport-sync:latest ``` This will send 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. diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index f6cb01c7..962ef28c 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -91,5 +91,3 @@ 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" ] diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run index c533db5e..5642097d 100644 --- a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run +++ b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run @@ -4,4 +4,4 @@ while [ ! -f /var/run/avahi-daemon/pid ]; do sleep 1 done echo "Starting shairport-sync" -exec /usr/local/bin/shairport-sync +exec /usr/local/bin/shairport-sync $ARG diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run index c533db5e..5642097d 100644 --- a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run +++ b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run @@ -4,4 +4,4 @@ while [ ! -f /var/run/avahi-daemon/pid ]; do sleep 1 done echo "Starting shairport-sync" -exec /usr/local/bin/shairport-sync +exec /usr/local/bin/shairport-sync $ARG From c924387aacdfa82597d750f0e4e41afc6ae76038 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Feb 2023 14:51:14 +0000 Subject: [PATCH 43/85] Make sure that the AirPlay 1 hex prefix to the service name matches the AirPlay 2 device ID in AirPlay2 mode. In AirPlaty 1 mode, the prefix is a hash of the service name. --- shairport.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shairport.c b/shairport.c index 4829c616..db54c8c6 100644 --- a/shairport.c +++ b/shairport.c @@ -1411,6 +1411,9 @@ int parse_options(int argc, char **argv) { int i; char hexchar[] = "0123456789abcdef"; for (i = 5; i >= 0; i--) { + // In AirPlay 2 mode, the AP1 name prefix must be + // the same as the AirPlay 2 device id less the colons. + config.ap1_prefix[i] = temporary_airplay_id & 0xFF; apids[i * 3 + 1] = hexchar[temporary_airplay_id & 0xF]; temporary_airplay_id = temporary_airplay_id >> 4; apids[i * 3] = hexchar[temporary_airplay_id & 0xF]; @@ -2513,7 +2516,11 @@ int main(int argc, char **argv) { soxr_time_check_thread_started = 1; #endif - // calculate the 12-hex-digit prefix by hashing the service name. + + // In AirPlay 2 mode, the AP1 prefix is the same as the device ID less the colons + // In AirPlay 1 mode, the AP1 prefix is calculated by hashing the service name. +#ifndef CONFIG_AIRPLAY_2 + uint8_t ap_md5[16]; // debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); @@ -2550,6 +2557,7 @@ int main(int argc, char **argv) { #endif memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix)); +#endif #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary From aa1d578bc2b188dbcbb0d07f1b84dbf2dd683314 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Tue, 7 Feb 2023 15:07:48 +0000 Subject: [PATCH 44/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b5110df5..42bc377e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,10 @@ +Version 4.1-dev-870-gc924387a +==== +**Bug Fix** +* When built for AirPlay 2, ensure the hexadecimal string that prefixes the AirPlay 1 Service Name in the Bonjour text strings matches the AirPlay 2 Device ID. For example, if the Service Name is `Kitchen` and the AirPlay 2 Device ID is `b8:2f:eb:d7:85:df`, the AirPlay 1 Service Name should be `B82FEBD785DF@Kitchen`. (When built for AirPlay 1, the hexadecimal prefix is simply a hash of the Service Name.) + + Thanks to [Casper](https://github.com/casperghst42) for raising the [issue](https://github.com/owntone/owntone-server/issues/1578) and to [ejurgensen](https://github.com/ejurgensen) for identifying the cause of the problem. + Version 4.1-dev-862-g6eb70eee ==== ***Pesky Changes You Can't Ignore*** From 65c6975ea67bdf6f20489708025395d93deb8b46 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Feb 2023 20:14:07 +0000 Subject: [PATCH 45/85] Remove some race conditions between Shairport Sync ('SPS') opening a TCP connection and the client checking that it's open. If SPS is slow for some reason (e.g. slow CPU), the client could find that the connection hasn't been opened and may then terminate the session. The fix is to ensure the TCP connections are open for reading before telling the client about them. --- rtp.c | 8 ++++---- rtsp.c | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rtp.c b/rtp.c index e61d521c..7f9ebe83 100644 --- a/rtp.c +++ b/rtp.c @@ -1575,7 +1575,7 @@ void *rtp_event_receiver(void *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); + // listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code uint8_t packet[4096]; ssize_t nread; @@ -2017,7 +2017,8 @@ 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); + // listen(descriptor->sock_fd, 5); // this is done in the handle_setup_2 code to ensure it's open + // when the client hears about it... ssize_t nread; SOCKADDR remote_addr; memset(&remote_addr, 0, sizeof(remote_addr)); @@ -2059,8 +2060,7 @@ void *buffered_tcp_reader(void *arg) { // 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); + // 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); diff --git a/rtsp.c b/rtsp.c index 55220b75..9bf28a80 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3088,6 +3088,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->event_socket, 128); // ensure socket is open before telling client + debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); @@ -3110,7 +3112,6 @@ 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 { debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", @@ -3142,6 +3143,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->event_socket, 128); // ensure socket is open before telling client + 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) @@ -3330,6 +3333,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->buffered_audio_socket, 128); // ensure it's open before telling the client + debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); @@ -3356,7 +3361,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // 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: From 72e5b5309b14e3021ec83a30c8781e41ae93d9f3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 12 Feb 2023 20:29:20 +0000 Subject: [PATCH 46/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 42bc377e..562603bf 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-872-g65c6975e +==== +**Bug Fix** +* Remove three potential race conditions between Shairport Sync opening a TCP connection and the client checkiing that the connections are open. The problem was that the connections were being opened in threads that were created just before the client was given the connection information. If the threads were delayed (e.g. on a slow or busy processor), the client could use the connection information to check the connections, but find that they were not (yet) open. This could cause the client to terminate the session immediately with a `TEARDOWN`. The fix was to open the connections before creating those threads and before sending the connection information back to the client. In this way, the connections are guaranteed to be open before the client has the information it needs to try to open them, even if the threads ared delayed in starting. This bug would manifest itself by allowing play to proceed but not play anything. + Version 4.1-dev-870-gc924387a ==== **Bug Fix** From 551734b398e63f83b1e2235546f88f360d476666 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:34:55 +0000 Subject: [PATCH 47/85] Treat a keepalive timeout the same as if the client had closed the connection. Total timeout is 35 + 5 * 5 = 60(?) seconds. --- rtsp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rtsp.c b/rtsp.c index 9bf28a80..ed67c581 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1385,6 +1385,12 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes conn->connection_number); continue; } + if (errno == ETIMEDOUT) { + debug(1, "Connection %d: ETIMEDOUT -- keepalive timeout.", conn->connection_number); + reply = rtsp_read_request_response_channel_closed; + // Note: the socket will be closed when the thread exits + goto shutdown; + } if (errno != ECONNRESET) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); From f164f19a3d9052423ba203ecc24ad2cba5b6fdee Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:50:07 +0000 Subject: [PATCH 48/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 562603bf..0a6b4ea7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-874-g551734b3 +==== +**Bug Fix** +* Use TCP keepalive a little more generally -- treat it the same as a client closing the link rather than just an error. + Version 4.1-dev-872-g65c6975e ==== **Bug Fix** From 29ca32256da427696e8a55f54e44ba47107aa2b4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:01:29 +0000 Subject: [PATCH 49/85] Ensure that the PTP clock is restarted on seconds and subsequent plays on the same connection. --- rtsp.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rtsp.c b/rtsp.c index ed67c581..f53bd351 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2028,7 +2028,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) { - ptp_send_control_message_string("B"); // signify play is "B"eginning or resuming + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or resuming) debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); @@ -2963,7 +2963,6 @@ 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("B"); // signify play is "B"eginning 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?"); @@ -3079,6 +3078,7 @@ 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); set_client_as_ptp_clock(conn); + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or continuing) 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); @@ -3193,6 +3193,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { // get stream[0] + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or continuing) plist_t stream0 = plist_array_get_item(streams, 0); plist_t streams_array = plist_new_array(); // to hold the ports and stuff From 6d45657373f396ef407ba8668f114419a4d35a7c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:14:58 +0000 Subject: [PATCH 50/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 0a6b4ea7..3a117700 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-876-g29ca3225 +==== +**Bug Fix** +* Fix a bug reported in Issue [#1633](https://github.com/mikebrady/shairport-sync/issues/1633). The bug was that when a Realtime Audio stream (e.g. playing from Spotify on iOS or using Shairport Sync as the Sound Output on a Mac) was played, it was fine, but when a second stream was started, it could not be heard. The problem was that the PTP clock was not being correctly revalidated for second and subsequent Realtime Audio streams. The fix was to ensure that the PTP clock is revalidated on second and subsequent plays on the same connection. Thanks again to [David Leibovic](https://github.com/dasl-) for finding the problem. + Version 4.1-dev-874-g551734b3 ==== **Bug Fix** From b6e691263e29d83a6f1a26a29c876361dfd0f787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 7 Mar 2023 21:52:07 +0100 Subject: [PATCH 51/85] Do not start SPS as s6 service to avoid passing arguments via -e MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- docker/Dockerfile | 6 ++++++ docker/README.md | 6 +++--- docker/classic/Dockerfile | 6 ++++++ docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run | 2 +- .../etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies | 1 - .../etc/s6-overlay/s6-rc.d/04-shairport-sync/finish | 2 -- .../classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run | 7 ------- .../classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type | 1 - .../s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync | 0 docker/classic/run.sh | 8 ++++++++ docker/etc/s6-overlay/s6-rc.d/03-avahi/run | 2 +- .../etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies | 1 - docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish | 2 -- docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run | 7 ------- docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type | 1 - .../s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync | 0 docker/run.sh | 8 ++++++++ 17 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type delete mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync create mode 100644 docker/classic/run.sh delete mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies delete mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish delete mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run delete mode 100644 docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type delete mode 100644 docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync create mode 100644 docker/run.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 47e9de46..dd21bb8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -113,3 +113,9 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* + +# Add run script that will start SPS +COPY ./docker/run.sh ./run.sh +RUN chmod +x /run.sh + +Entrypoint ["/init","./run.sh"] diff --git a/docker/README.md b/docker/README.md index 9953cefc..560dc89b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -31,12 +31,12 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ### Options -To pass command line options to Shairport Sync they need to be set with `-e ARG=`. Here is an example: +Command line options will be passed to Shairport Sync. Here is an example: ``` $ docker run -d --restart unless-stopped --net host --device /dev/snd \ - -e ARG="-v --statistics -a DenSystem -d hw:0 -c PCM" \ - mikebrady/shairport-sync:latest + mikebrady/shairport-sync:latest \ + -v --statistics -a DenSystem -d hw:0 -c PCM ``` This will send 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. diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 962ef28c..3d072a55 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -91,3 +91,9 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* + +# Add run script that will start SPS +COPY ./docker/run.sh ./run.sh +RUN chmod +x /run.sh + +Entrypoint ["/init","./run.sh"] diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run index 20275d40..e3ea2ff9 100644 --- a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -1,6 +1,6 @@ #!/command/with-contenv sh while [ ! -f /var/run/dbus/dbus.pid ]; do - echo "s6-rc: warning: dbus is not running, sleeping for 1 seconds before trying to start avahi" + echo "s6-rc: warning: dbus is not running, sleeping for 1 second before trying to start avahi" sleep 1 done echo "Starting avahi" diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies deleted file mode 100644 index 97763270..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/dependencies +++ /dev/null @@ -1 +0,0 @@ -03-avahi diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish deleted file mode 100644 index e4a13866..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/finish +++ /dev/null @@ -1,2 +0,0 @@ -#!/command/execlineb -S0 -/run/s6/basedir/bin/halt diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run deleted file mode 100644 index 5642097d..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/run +++ /dev/null @@ -1,7 +0,0 @@ -#!/command/with-contenv sh -while [ ! -f /var/run/avahi-daemon/pid ]; do - echo "s6-rc: warning: avahi is not running, sleeping for 1 seconds before trying to start shairport-sync" - sleep 1 -done -echo "Starting shairport-sync" -exec /usr/local/bin/shairport-sync $ARG diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type b/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type deleted file mode 100644 index 5883cff0..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/04-shairport-sync/type +++ /dev/null @@ -1 +0,0 @@ -longrun diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/04-shairport-sync deleted file mode 100644 index e69de29b..00000000 diff --git a/docker/classic/run.sh b/docker/classic/run.sh new file mode 100644 index 00000000..f901817d --- /dev/null +++ b/docker/classic/run.sh @@ -0,0 +1,8 @@ +#!/bin/sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "Warning: avahi is not running, sleeping for 1 second before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +# pass all commandline options to shairport-sync +/usr/local/bin/shairport-sync "$@" diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run index 20275d40..e3ea2ff9 100644 --- a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -1,6 +1,6 @@ #!/command/with-contenv sh while [ ! -f /var/run/dbus/dbus.pid ]; do - echo "s6-rc: warning: dbus is not running, sleeping for 1 seconds before trying to start avahi" + echo "s6-rc: warning: dbus is not running, sleeping for 1 second before trying to start avahi" sleep 1 done echo "Starting avahi" diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies deleted file mode 100644 index a8d402ef..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/dependencies +++ /dev/null @@ -1 +0,0 @@ -04-nqptp diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish deleted file mode 100644 index e4a13866..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/finish +++ /dev/null @@ -1,2 +0,0 @@ -#!/command/execlineb -S0 -/run/s6/basedir/bin/halt diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run deleted file mode 100644 index 5642097d..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/run +++ /dev/null @@ -1,7 +0,0 @@ -#!/command/with-contenv sh -while [ ! -f /var/run/avahi-daemon/pid ]; do - echo "s6-rc: warning: avahi is not running, sleeping for 1 seconds before trying to start shairport-sync" - sleep 1 -done -echo "Starting shairport-sync" -exec /usr/local/bin/shairport-sync $ARG diff --git a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type b/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type deleted file mode 100644 index 5883cff0..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/05-shairport-sync/type +++ /dev/null @@ -1 +0,0 @@ -longrun diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/05-shairport-sync deleted file mode 100644 index e69de29b..00000000 diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 00000000..f901817d --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,8 @@ +#!/bin/sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "Warning: avahi is not running, sleeping for 1 second before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +# pass all commandline options to shairport-sync +/usr/local/bin/shairport-sync "$@" From c62ae679f3b0b66d044a868ba8ba4e0936007ac0 Mon Sep 17 00:00:00 2001 From: hunhejj Date: Tue, 21 Feb 2023 22:19:27 +0100 Subject: [PATCH 52/85] Migrate to new HA mqtt configuration The old way of configuring mqtt entities is not supported anymore since `v2022.12` --- MQTT.md | 81 ++++++++++++++++++++++++++------------------------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/MQTT.md b/MQTT.md index 95779c76..7fa56b79 100644 --- a/MQTT.md +++ b/MQTT.md @@ -171,54 +171,47 @@ Examples of consuming "parsed" MQTT data in [Home Assistant](https://www.home-as The `active_start` and `active_end` have good potential use as triggers to turn on and off various connect receivers/zones. The messages published are empty and therefor no "payload_on" is set, "payload_off" however is set to prevent accidental triggering. ```yml -binary_sensor: +mqtt: + binary_sensor: + - name: "shairport active start" + state_topic: "shairport/active_start" + payload_on: "" + payload_off: "OFF" + off_delay: 300 - - platform: mqtt - name: "shairport active start" - state_topic: "shairport/active_start" - payload_on: "" - payload_off: "OFF" - off_delay: 300 - - - platform: mqtt - name: "shairport active end" - state_topic: "shairport/active_end" - payload_on: "" - payload_off: "OFF" - off_delay: 300 + - name: "shairport active end" + state_topic: "shairport/active_end" + payload_on: "" + payload_off: "OFF" + off_delay: 300 ``` 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: - - platform: mqtt - name: "shairport album" - state_topic: "shairport/album" - expire_after: 600 - - - platform: mqtt - name: "shairport artist" - state_topic: "shairport/artist" - expire_after: 600 - - - platform: mqtt - name: "shairport title" - state_topic: "shairport/title" - expire_after: 600 - - - platform: mqtt - name: "shairport genre" - state_topic: "shairport/genre" - expire_after: 600 - - - platform: mqtt - name: "shairport volume (dB)" - state_topic: "shairport/volume" - - - platform: mqtt - name: "shairport volume (PCT)" - state_topic: "shairport/volume" - value_template: "{{ value | regex_findall_index(find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1 }}" - unit_of_measurement: 'percent' +mqtt: + sensor: + - name: "shairport album" + state_topic: "shairport/album" + expire_after: 600 + + - name: "shairport artist" + state_topic: "shairport/artist" + expire_after: 600 + + - name: "shairport title" + state_topic: "shairport/title" + expire_after: 600 + + - name: "shairport genre" + state_topic: "shairport/genre" + expire_after: 600 + + - name: "shairport volume (dB)" + state_topic: "shairport/volume" + + - name: "shairport volume (PCT)" + state_topic: "shairport/volume" + value_template: "{{ value | regex_findall_index(find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1 }}" + unit_of_measurement: 'percent' ``` From 16af2d2c4c7af12b1cb4d38ddf086fa150637373 Mon Sep 17 00:00:00 2001 From: hunhejj Date: Tue, 21 Feb 2023 22:24:34 +0100 Subject: [PATCH 53/85] Fix MQTT active start / active end examples The payload for `active start` & `active end` is not empty but `--`. Fix the examples accordingly. --- MQTT.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MQTT.md b/MQTT.md index 95779c76..fbb90a80 100644 --- a/MQTT.md +++ b/MQTT.md @@ -150,7 +150,7 @@ The MQTT service can parse the above raw messages into a subset of human-readabl * `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. +and empty messages (`--`) at the following topics are published. * `play_start` -- fired at the begining of every song * `play_end` -- fired at the end of every song @@ -168,7 +168,7 @@ Users will find examples on how to consume the MQTT data in various home automat ### Home Assistant Variable Templates Examples of consuming "parsed" MQTT data in [Home Assistant](https://www.home-assistant.io/) -The `active_start` and `active_end` have good potential use as triggers to turn on and off various connect receivers/zones. The messages published are empty and therefor no "payload_on" is set, "payload_off" however is set to prevent accidental triggering. +The `active_start` and `active_end` have good potential use as triggers to turn on and off various connect receivers/zones. "payload_off" is set to prevent accidental triggering. ```yml binary_sensor: @@ -176,14 +176,14 @@ binary_sensor: - platform: mqtt name: "shairport active start" state_topic: "shairport/active_start" - payload_on: "" + payload_on: "--" payload_off: "OFF" off_delay: 300 - platform: mqtt name: "shairport active end" state_topic: "shairport/active_end" - payload_on: "" + payload_on: "--" payload_off: "OFF" off_delay: 300 ``` From 55c8fb830546e99abc00a1b0ff94badb58dcdbb8 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 19 Mar 2023 09:50:45 +0000 Subject: [PATCH 54/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 3a117700..7387a328 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-886-ge74300d7 +==== +**Updates and Enhancements** +* Merge [Pull Request #1635](https://github.com/mikebrady/shairport-sync/pull/1635) and [Pull Request #1636](https://github.com/mikebrady/shairport-sync/pull/1636) . The PRs update the MQTT documentation to correspond to updates in [Hoome Assistant](https://www.home-assistant.io/blog/2022/06/01/release-20226/#breaking-changes). Thanks to [hunhejj](https://github.com/hunhejj) for these. +* Merge [Pull Request #1624](https://github.com/mikebrady/shairport-sync/pull/1624). The PR updates the Docker images to use Alpine 3.17 and ensure services such as Avahi and D-Bus start in the correct order. Thanks to [yubiuser](https://github.com/yubiuser) for their work on this. + Version 4.1-dev-876-g29ca3225 ==== **Bug Fix** From a1c9387ca81bedebb986e237403db0cd57ae45dc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 19 Mar 2023 09:51:07 +0000 Subject: [PATCH 55/85] 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 7387a328..38c412bc 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-886-ge74300d7 ==== **Updates and Enhancements** -* Merge [Pull Request #1635](https://github.com/mikebrady/shairport-sync/pull/1635) and [Pull Request #1636](https://github.com/mikebrady/shairport-sync/pull/1636) . The PRs update the MQTT documentation to correspond to updates in [Hoome Assistant](https://www.home-assistant.io/blog/2022/06/01/release-20226/#breaking-changes). Thanks to [hunhejj](https://github.com/hunhejj) for these. +* Merge [Pull Request #1635](https://github.com/mikebrady/shairport-sync/pull/1635) and [Pull Request #1636](https://github.com/mikebrady/shairport-sync/pull/1636) . The PRs update the MQTT documentation to correspond to updates in [Home Assistant](https://www.home-assistant.io/blog/2022/06/01/release-20226/#breaking-changes). Thanks to [hunhejj](https://github.com/hunhejj) for these. * Merge [Pull Request #1624](https://github.com/mikebrady/shairport-sync/pull/1624). The PR updates the Docker images to use Alpine 3.17 and ensure services such as Avahi and D-Bus start in the correct order. Thanks to [yubiuser](https://github.com/yubiuser) for their work on this. Version 4.1-dev-876-g29ca3225 From 24e267de7815520ee6c70eeb24a0db43d077a8c5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 09:12:02 +0000 Subject: [PATCH 56/85] Create PulseAudioAndPipeWire.md --- ADVANCED TOPICS/PulseAudioAndPipeWire.md | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ADVANCED TOPICS/PulseAudioAndPipeWire.md diff --git a/ADVANCED TOPICS/PulseAudioAndPipeWire.md b/ADVANCED TOPICS/PulseAudioAndPipeWire.md new file mode 100644 index 00000000..1d30daaf --- /dev/null +++ b/ADVANCED TOPICS/PulseAudioAndPipeWire.md @@ -0,0 +1,34 @@ +# Introduction +Many Linux systems have [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) or [PipeWire](https://pipewire.org) installed as [sound servers](https://en.wikipedia.org/wiki/Sound_server), typically providing +audio facilities for the system's GUI. Unfortunately, they cause problems for Shairport Sync. As you'll see, these problems can be worked around. + +The following remarks apply to PulseAudio and PipeWire, so, for simplicity, let's refer to PulseAudio only. + +To understand the problems, first consider a Linux system that _does not_ have PulseAudio installed. +1. Sound is managed by the Advanced Linux Sound Architecture ("[ALSA](https://www.alsa-project.org/wiki/Main_Page)") subsystem. +2. The ALSA subsystem is loaded and becomes available at system startup. +3. The ALSA `"default"` output device is mapped to the system's audio output DAC. + +Shairport Sync loads when system startup is complete and provides its service, routing audio to the ALSA default device. + +Now, consider a Linux system with a Graphical User Interface (GUI) that has PulseAudio installed. +1. As before, sound is managed by the ALSA subsystem. +2. As before, The ALSA subsystem is loaded and becomes available at system startup. +3. PulseAudio loads and becomes a client of ALSA. +4. The PulseAudio service becomes available after a user has logged in through the GUI. Importantly, if you don't log in through the GUI, you won't have a PulseAudio service. +5. The ALSA `"default"` device no longer connects to a real output device. Instead audio sent to the `"default"` device is routed into PulseAudio. Importantly, if you have no PulseAudio service, then the ALSA default device either doesn't exist at all or goes nowhere. + +# The Problem +When Shairport Sync is installed as a system service, it starts after the system has booted up. +It runs under a low-priviliged user called `shairport-sync`. +Unfortunately, per (4) above, since it was not logged in through the GUI, it won't have a PulseAudio service. +Therefore, per (5) above, the ALSA `"default"` device either won't exist or -- if it does exist -- won't work. Hence, Shairport Sync will either terminate +or remain silent. + +# The Fixes +There are three ways to address this problem: +1. Stop using Shairport Sync as a system service. Instead, launch it from a terminal window after log-in through the GUI. +Alternatively, create a user service startup script so that it will be launched automatically after the user has logged in. This means that the system can not run without user intervention. +2. Stop using the ALSA `"default"` device. Instead, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, `sps-alsa-explore`) to get a list of output devices, and use the configuration file to set the device. Now this might prevent PulseAudio from working properly. +# The Best Fix +3. The best of all fixes is, if possible, to avoid using a system containing PulseAudio altogether. Linux operating systems typically have a "server", "lite" or "headless" version that has no GUI and no PulseAudio. From d23e62d7eada5b56f99fd9a887172a5f0d2cdbfe Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 09:18:37 +0000 Subject: [PATCH 57/85] Update PulseAudioAndPipeWire.md --- ADVANCED TOPICS/PulseAudioAndPipeWire.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ADVANCED TOPICS/PulseAudioAndPipeWire.md b/ADVANCED TOPICS/PulseAudioAndPipeWire.md index 1d30daaf..b6fb7335 100644 --- a/ADVANCED TOPICS/PulseAudioAndPipeWire.md +++ b/ADVANCED TOPICS/PulseAudioAndPipeWire.md @@ -21,14 +21,13 @@ Now, consider a Linux system with a Graphical User Interface (GUI) that has Puls # The Problem When Shairport Sync is installed as a system service, it starts after the system has booted up. It runs under a low-priviliged user called `shairport-sync`. -Unfortunately, per (4) above, since it was not logged in through the GUI, it won't have a PulseAudio service. -Therefore, per (5) above, the ALSA `"default"` device either won't exist or -- if it does exist -- won't work. Hence, Shairport Sync will either terminate -or remain silent. +Unfortunately, per (4) above, since it was not logged in through the GUI, it won't have a PulseAudio service. If you are using the `"pa"` backend, Shairport Sync may well crash. +Per (5) above, the ALSA `"default"` device either won't exist or -- if it does exist -- won't work. Hence, using the `"alsa"` backend, Shairport Sync will either terminate or remain silent. # The Fixes There are three ways to address this problem: 1. Stop using Shairport Sync as a system service. Instead, launch it from a terminal window after log-in through the GUI. Alternatively, create a user service startup script so that it will be launched automatically after the user has logged in. This means that the system can not run without user intervention. -2. Stop using the ALSA `"default"` device. Instead, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, `sps-alsa-explore`) to get a list of output devices, and use the configuration file to set the device. Now this might prevent PulseAudio from working properly. +2. If you are using the `"alsa"` backend, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, `sps-alsa-explore`) to get a list of output devices, and use the configuration file to set the device. Now this might prevent PulseAudio from working properly. # The Best Fix 3. The best of all fixes is, if possible, to avoid using a system containing PulseAudio altogether. Linux operating systems typically have a "server", "lite" or "headless" version that has no GUI and no PulseAudio. From 7be37f82196f41ebab47a51d249b27aacccc9c54 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 10:46:44 +0000 Subject: [PATCH 58/85] Update PulseAudioAndPipeWire.md --- ADVANCED TOPICS/PulseAudioAndPipeWire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADVANCED TOPICS/PulseAudioAndPipeWire.md b/ADVANCED TOPICS/PulseAudioAndPipeWire.md index b6fb7335..000d6ebf 100644 --- a/ADVANCED TOPICS/PulseAudioAndPipeWire.md +++ b/ADVANCED TOPICS/PulseAudioAndPipeWire.md @@ -28,6 +28,6 @@ Per (5) above, the ALSA `"default"` device either won't exist or -- if it does e There are three ways to address this problem: 1. Stop using Shairport Sync as a system service. Instead, launch it from a terminal window after log-in through the GUI. Alternatively, create a user service startup script so that it will be launched automatically after the user has logged in. This means that the system can not run without user intervention. -2. If you are using the `"alsa"` backend, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, `sps-alsa-explore`) to get a list of output devices, and use the configuration file to set the device. Now this might prevent PulseAudio from working properly. +2. If you are using the `"alsa"` backend, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore)) to get a list of output devices, and use the configuration file to set the device. There is a possibility this might prevent PulseAudio from working properly. # The Best Fix 3. The best of all fixes is, if possible, to avoid using a system containing PulseAudio altogether. Linux operating systems typically have a "server", "lite" or "headless" version that has no GUI and no PulseAudio. From 85e1c08e30f4696262e52e2e3afba40b0c019fac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 10:49:52 +0000 Subject: [PATCH 59/85] Update README.md --- ADVANCED TOPICS/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index 19db0266..014fc13b 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -1,6 +1,7 @@ # Advanced Topics Here you will find links to some advanced features and things you can do with Shairport Sync. * [Finish Setting Up](InitialConfiguration.md). +* [Working with PulseAudio or PipeWire](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/PulseAudioAndPipeWire.md). * [Adjusting Sync](AdjustingSync.md) – advance or delay the timing of the output from Shairport Sync to compensate for amplifier delays. * [Get The Best](GetTheBest.md) from your system. * [Metadata](Metadata.md). From d3f0b6ca2030b9070ab7863a73a9c8bbceebf3d5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 10:54:24 +0000 Subject: [PATCH 60/85] Update BUILD.md --- BUILD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index f6dc3ad3..e73e3bda 100644 --- a/BUILD.md +++ b/BUILD.md @@ -155,7 +155,8 @@ If you have problems, please check the items in Final Notes below, or in the [TR 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: +If your system has a Graphical User Interface (GUI) it probably uses PulseAudio or PipeWire for audio services. If that is the case, please review [Working with PulseAudio or PipeWire](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/PulseAudioAndPipeWire.md). +Otherwise, once you are happy that Shairport Sync runs from the command line, you should enable and start the `shairport-sync` service. This will launch Shairport Sync automatically as a background "daemon" service when the system powers up: ### Linux ``` From 1b53d9d3068fcc39ec8a286f01312e4fc54cb1e4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 24 Mar 2023 10:58:15 +0000 Subject: [PATCH 61/85] Update PulseAudioAndPipeWire.md --- ADVANCED TOPICS/PulseAudioAndPipeWire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADVANCED TOPICS/PulseAudioAndPipeWire.md b/ADVANCED TOPICS/PulseAudioAndPipeWire.md index 000d6ebf..520c9607 100644 --- a/ADVANCED TOPICS/PulseAudioAndPipeWire.md +++ b/ADVANCED TOPICS/PulseAudioAndPipeWire.md @@ -1,4 +1,4 @@ -# Introduction +# Working with PulseAudio or PipeWire Many Linux systems have [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) or [PipeWire](https://pipewire.org) installed as [sound servers](https://en.wikipedia.org/wiki/Sound_server), typically providing audio facilities for the system's GUI. Unfortunately, they cause problems for Shairport Sync. As you'll see, these problems can be worked around. From 0d9baf525de87ba6eecf4a90b5c0314dd972866d Mon Sep 17 00:00:00 2001 From: nipsufn Date: Tue, 28 Mar 2023 19:36:54 +0200 Subject: [PATCH 62/85] feat: add PulseAudio support to images --- docker/Dockerfile | 64 ++++++++++++++++++++------------------- docker/classic/Dockerfile | 44 ++++++++++++++------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index dd21bb8b..42844dce 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,25 +8,26 @@ ARG NQPTP_BRANCH RUN test -n "$NQPTP_BRANCH" RUN apk -U add \ - git \ - build-base \ + alsa-lib-dev \ autoconf \ automake \ - libtool \ - dbus \ - alsa-lib-dev \ - popt-dev \ - soxr-dev \ avahi-dev \ - libconfig-dev \ - libsndfile-dev \ - mosquitto-dev \ - libsodium-dev \ - libgcrypt-dev \ + build-base \ + dbus \ ffmpeg-dev \ - xxd \ + git \ + libconfig-dev \ + libgcrypt-dev \ + libplist-dev \ libressl-dev \ - libplist-dev + libsndfile-dev \ + libsodium-dev \ + libtool \ + mosquitto-dev \ + popt-dev \ + pulseaudio-dev \ + soxr-dev \ + xxd ##### ALAC ##### RUN git clone https://github.com/mikebrady/alac @@ -55,8 +56,8 @@ COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" 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 \ +RUN ../configure --sysconfdir=/etc --with-alsa -=-with-pa --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 $(nproc) @@ -69,25 +70,26 @@ FROM crazymax/alpine-s6:3.17-3.1.1.2 RUN apk -U add \ alsa-lib \ - dbus \ - popt \ - glib \ - soxr \ avahi \ avahi-tools \ - libconfig \ - libsndfile \ - mosquitto \ - libuuid \ + dbus \ ffmpeg \ - libsodium \ - libgcrypt \ - libressl3.6-libcrypto \ - libplist \ - mandoc \ - man-pages \ + glib \ less \ - less-doc + less-doc \ + libconfig \ + libgcrypt \ + libplist \ + libpulse \ + libressl3.6-libcrypto \ + libsndfile \ + libsodium \ + libuuid \ + man-pages \ + mandoc \ + mosquitto \ + popt \ + soxr # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 3d072a55..1514d097 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -8,20 +8,21 @@ ARG SHAIRPORT_SYNC_BRANCH RUN test -n "$SHAIRPORT_SYNC_BRANCH" RUN apk -U add \ - git \ - build-base \ + alsa-lib-dev \ autoconf \ automake \ - libtool \ - dbus \ - alsa-lib-dev \ - popt-dev \ - mbedtls-dev \ - soxr-dev \ avahi-dev \ + build-base \ + dbus \ + git \ libconfig-dev \ + libsndfile-dev \ + libtool \ + mbedtls-dev \ mosquitto-dev \ - libsndfile-dev + popt-dev \ + pulseaudio-dev \ + soxr-dev ##### ALAC ##### RUN git clone https://github.com/mikebrady/alac @@ -39,7 +40,7 @@ COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" WORKDIR /shairport-sync/build RUN autoreconf -i ../ -RUN ../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=mbedtls \ +RUN ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=mbedtls \ --with-metadata --with-dummy --with-pipe --with-dbus-interface \ --with-stdout --with-mpris-interface --with-mqtt-client \ --with-apple-alac --with-convolution @@ -53,20 +54,21 @@ FROM crazymax/alpine-s6:3.17-3.1.1.2 RUN apk -U add \ alsa-lib \ - dbus \ - popt \ - glib \ - mbedtls \ - soxr \ avahi \ avahi-tools \ - libconfig \ - mosquitto \ - libsndfile-dev \ - mandoc \ - man-pages \ + dbus \ + glib \ less \ - less-doc + less-doc \ + libconfig \ + libsndfile-dev \ + libpulse \ + man-pages \ + mandoc \ + mbedtls \ + mosquitto \ + popt \ + soxr \ # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync From 23c8646149a72912227d16a97bfde5bcb4fa9f6f Mon Sep 17 00:00:00 2001 From: nipsufn Date: Tue, 28 Mar 2023 19:51:13 +0200 Subject: [PATCH 63/85] fix: typo --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 42844dce..4ab4bed0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -56,7 +56,7 @@ COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" WORKDIR /shairport-sync/build RUN autoreconf -i ../ -RUN ../configure --sysconfdir=/etc --with-alsa -=-with-pa --with-soxr --with-avahi --with-ssl=openssl \ +RUN ../configure --sysconfdir=/etc --with-alsa --with-pa --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 From 8502f015e6cb0b9be3e527d611fb5954dddbc574 Mon Sep 17 00:00:00 2001 From: nipsufn Date: Tue, 28 Mar 2023 19:59:24 +0200 Subject: [PATCH 64/85] fix: second typo --- docker/classic/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 1514d097..960822f3 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -68,7 +68,7 @@ RUN apk -U add \ mbedtls \ mosquitto \ popt \ - soxr \ + soxr # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync From 4f54775645101a5c3bc7cb1d6f40f27e7e95ef10 Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 11:31:24 +0200 Subject: [PATCH 65/85] test: nqtp fork --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4ab4bed0..dbb8d44a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,7 +40,8 @@ WORKDIR / ##### ALAC END ##### ##### NQPTP ##### -RUN git clone https://github.com/mikebrady/nqptp +#RUN git clone https://github.com/mikebrady/nqptp +RUN git clone https://github.com/nipsufn/nqptp WORKDIR /nqptp RUN git checkout "$NQPTP_BRANCH" RUN autoreconf -i From d98ba46d9b1fdf4823f2c6d41acff38fec7b51be Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 12:23:26 +0200 Subject: [PATCH 66/85] test: nqtp fork p2 --- docker/etc/s6-overlay/s6-rc.d/04-nqptp/run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run index 27a489b8..0d41862f 100644 --- a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run @@ -1,3 +1,3 @@ #!/command/with-contenv sh echo "Starting nqptp" -exec /usr/local/bin/nqptp +exec /usr/local/bin/nqptp -ipv4-only From 70998d981d484e2ae7675a5c4d4ea62d36f569de Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 13:05:06 +0200 Subject: [PATCH 67/85] test: nqtp fork p3 --- docker/etc/s6-overlay/s6-rc.d/04-nqptp/run | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run index 0d41862f..6930a2e1 100644 --- a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run @@ -1,3 +1,7 @@ #!/command/with-contenv sh echo "Starting nqptp" -exec /usr/local/bin/nqptp -ipv4-only +if [ -z ${IPV4ONLY+x} ]; then + exec /usr/local/bin/nqptp +else + exec /usr/local/bin/nqptp -ipv4-only +fi \ No newline at end of file From 0e9b20280508d9c92b9d01556385e12d8a92061e Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 13:53:41 +0200 Subject: [PATCH 68/85] chore: cleanup --- README.md | 2 +- docker/Dockerfile | 1 + docker/README.md | 12 ++++++++++++ docker/docker-compose.yaml | 7 ++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a65b3ec..37992583 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/r/mikebrady/shairport-sync). +* A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). Also see [docker/README.md](docker/README.md). * 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 dbb8d44a..be3cba82 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -103,6 +103,7 @@ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport- 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 +COPY ./docker/etc/pulse /etc/pulse RUN chmod +x /etc/s6-overlay/s6-rc.d/01-startup/script.sh # Create non-root user for running the container -- running as the user 'shairport-sync' also allows diff --git a/docker/README.md b/docker/README.md index 560dc89b..02a57cd3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -40,10 +40,22 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ``` This will send 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. +The image is built with PulseAudio backend support. To use it, refer to [`docker-compose.yaml`](docker-compose.yaml) for required environment variables and mounts. You might need to adjust authentication on your PulseAudio server ([PA documentation](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-native-protocol-unixtcp)) and set default backend to `pa` via either command line option `-o` or `general.output_backend` field in config file. + ## Configuration File 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. +## Environment variables + +Set `IPV4ONLY` to any value to force NQPTP to use IPv4 (might be useful on hosts not running IPv6): + +``` +$ docker run --env IPV4ONLY=yes -d --restart unless-stopped --net host --device /dev/snd \ + mikebrady/shairport-sync:latest \ + -v --statistics -a DenSystem -d hw:0 -c PCM +``` + ## Building ### Build Example (for arm7 devices) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 88831298..c6cb8330 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,10 +5,15 @@ services: image: mikebrady/shairport-sync:latest network_mode: host restart: unless-stopped + # environment: + # PULSE_SERVER: unix:/tmp/pulseaudio.socket # Path for PulseAudio socket + # PULSE_COOKIE: /tmp/pulseaudio.cookie # Path for PulseAudio cookie + # IPV4ONLY: "yes" # Force IPv4 in nqptp devices: - - "/dev/snd" + - "/dev/snd" # ALSA device # volumes: # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. + # - /run/user/1000/pulse/native:/tmp/pulseaudio.socket # PulseAudio socket when using that backend logging: options: max-size: "200k" From 3cdc5b9da8eebb932c4c6242fe9f166aaf203999 Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 13:54:30 +0200 Subject: [PATCH 69/85] chore: cleanup p2 --- docker/etc/pulse/client.conf | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docker/etc/pulse/client.conf diff --git a/docker/etc/pulse/client.conf b/docker/etc/pulse/client.conf new file mode 100644 index 00000000..5fbb34d1 --- /dev/null +++ b/docker/etc/pulse/client.conf @@ -0,0 +1,6 @@ +default-server = unix:/tmp/pulseaudio.socket +# Prevent a server running in the container +autospawn = no +daemon-binary = /bin/true +# Prevent the use of shared memory +enable-shm = false From 585ba4f743cc66e759b58602cd15bb3d27b10da2 Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sat, 1 Apr 2023 14:05:20 +0200 Subject: [PATCH 70/85] chore: cleanup p3 --- docker/Dockerfile | 3 +-- docker/etc/s6-overlay/s6-rc.d/04-nqptp/run | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index be3cba82..3ce13cb0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,8 +40,7 @@ WORKDIR / ##### ALAC END ##### ##### NQPTP ##### -#RUN git clone https://github.com/mikebrady/nqptp -RUN git clone https://github.com/nipsufn/nqptp +RUN git clone https://github.com/mikebrady/nqptp WORKDIR /nqptp RUN git checkout "$NQPTP_BRANCH" RUN autoreconf -i diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run index 6930a2e1..7ac01716 100644 --- a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run @@ -4,4 +4,4 @@ if [ -z ${IPV4ONLY+x} ]; then exec /usr/local/bin/nqptp else exec /usr/local/bin/nqptp -ipv4-only -fi \ No newline at end of file +fi From 847aff15f9b2c88d542b232656382800cdefb0e9 Mon Sep 17 00:00:00 2001 From: nipsufn Date: Sun, 2 Apr 2023 15:22:27 +0200 Subject: [PATCH 71/85] fix: adjust to follow nqptp --- docker/README.md | 10 ---------- docker/docker-compose.yaml | 1 - docker/etc/s6-overlay/s6-rc.d/04-nqptp/run | 6 +----- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/docker/README.md b/docker/README.md index 02a57cd3..6caf1c4d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,16 +46,6 @@ The image is built with PulseAudio backend support. To use it, refer to [`docker 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. -## Environment variables - -Set `IPV4ONLY` to any value to force NQPTP to use IPv4 (might be useful on hosts not running IPv6): - -``` -$ docker run --env IPV4ONLY=yes -d --restart unless-stopped --net host --device /dev/snd \ - mikebrady/shairport-sync:latest \ - -v --statistics -a DenSystem -d hw:0 -c PCM -``` - ## Building ### Build Example (for arm7 devices) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index c6cb8330..563622d6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -8,7 +8,6 @@ services: # environment: # PULSE_SERVER: unix:/tmp/pulseaudio.socket # Path for PulseAudio socket # PULSE_COOKIE: /tmp/pulseaudio.cookie # Path for PulseAudio cookie - # IPV4ONLY: "yes" # Force IPv4 in nqptp devices: - "/dev/snd" # ALSA device # volumes: diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run index 7ac01716..27a489b8 100644 --- a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run @@ -1,7 +1,3 @@ #!/command/with-contenv sh echo "Starting nqptp" -if [ -z ${IPV4ONLY+x} ]; then - exec /usr/local/bin/nqptp -else - exec /usr/local/bin/nqptp -ipv4-only -fi +exec /usr/local/bin/nqptp From 13ac11bfe003e5f23ea6000b7ba9916838404117 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 3 Apr 2023 09:01:51 +0100 Subject: [PATCH 72/85] Fix a bug in get_device_id which allowed it to return with an all-zeros ID. Not it waits for up to 10 seconds for a MAC address. --- common.c | 63 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/common.c b/common.c index d10a2fad..2fb5003a 100644 --- a/common.c +++ b/common.c @@ -1937,48 +1937,61 @@ int32_t mod32Difference(uint32_t a, uint32_t b) { } int get_device_id(uint8_t *id, int int_length) { - int response = 0; + + uint64_t wait_time = 10000000000L; // wait up to this (ns) long to get a MAC address + + int response = -1; 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) { - response = -1; - } else { - t = id; - int found = 0; + uint64_t wait_until = get_absolute_time_in_ns(); + wait_until = wait_until + wait_time; - for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) { + int64_t time_to_wait; + do { + if (getifaddrs(&ifaddr) == 0) { + 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)) { - struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; - 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]; + 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 = 1; + response = 0; + for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { + *t++ = s->sll_addr[i]; + } } } - } #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) { - found = 1; - uint8_t *s = (uint8_t *)LLADDR(sdl); - for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) { - *t++ = *s++; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + if ((sdl) && (sdl->sdl_family == AF_LINK)) { + if (sdl->sdl_type == IFT_ETHER) { + found = 1; + response = 0; + uint8_t *s = (uint8_t *)LLADDR(sdl); + for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) { + *t++ = *s++; + } } } +#endif +#endif } -#endif -#endif + freeifaddrs(ifaddr); } - freeifaddrs(ifaddr); - } + time_to_wait = wait_until - get_absolute_time_in_ns(); + } while ((response != 0) && (time_to_wait > 0)); + if (response != 0) + warn("Can't create a device ID -- no valid MAC address can be found."); return response; } From 039618302a6ee584ad86aced5591bca8f768a0aa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:09:43 +0100 Subject: [PATCH 73/85] Add a short delay before retrying for a MAC address in get_device_id. --- common.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common.c b/common.c index 2fb5003a..3630ea36 100644 --- a/common.c +++ b/common.c @@ -1989,6 +1989,10 @@ int get_device_id(uint8_t *id, int int_length) { } freeifaddrs(ifaddr); } + // wait a little time if we haven't got a response + if (response != 0) { + usleep(100000); + } time_to_wait = wait_until - get_absolute_time_in_ns(); } while ((response != 0) && (time_to_wait > 0)); if (response != 0) From 32fddbfdc775069fb4f2898ff8b7f8d97d63ac53 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:17:32 +0100 Subject: [PATCH 74/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 38c412bc..60c24697 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-896-g03961830 +==== +**Bug Fix** +* In some situations, the Device ID generated by Shairport Sync was all zeros, and so was invalid, causing connectivity problems. The cause of the problem was that `get_device_id` was not interpreting `getaddrinfo` information correctly. Thanks to [Carl Johnson](https://github.com/ridgelift) for [reporting](https://github.com/mikebrady/shairport-sync/issues/1657) the issue, finding the (rather obscure) cause of the problem and for providing [code](https://github.com/mikebrady/shairport-sync/issues/1657#issuecomment-1493423175) to fix it. + Version 4.1-dev-886-ge74300d7 ==== **Updates and Enhancements** From ada0b58d6264c6c4fe3e61de433fafa64a6bd6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 15 Apr 2023 10:07:24 +0200 Subject: [PATCH 75/85] Update github actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/check_ap2_systemd_basic.yml | 2 +- .github/workflows/check_ap2_systemd_full.yml | 2 +- .../check_ap2_systemd_full_build_folder.yml | 2 +- .github/workflows/check_ap2_systemv_full.yml | 2 +- .github/workflows/check_classic_mac_basic.yml | 2 +- .github/workflows/check_classic_systemd_basic.yml | 2 +- .github/workflows/check_classic_systemd_full.yml | 2 +- .github/workflows/docker-build-on-push.yaml | 12 ++++++------ .github/workflows/docker-build-on-tag.yaml | 12 ++++++------ 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/check_ap2_systemd_basic.yml b/.github/workflows/check_ap2_systemd_basic.yml index 3359bace..cb1ecdc8 100644 --- a/.github/workflows/check_ap2_systemd_basic.yml +++ b/.github/workflows/check_ap2_systemd_basic.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends 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 libgcrypt-dev xxd - name: Configure diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 858b2af5..94239fe0 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-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 - name: Configure diff --git a/.github/workflows/check_ap2_systemd_full_build_folder.yml b/.github/workflows/check_ap2_systemd_full_build_folder.yml index f3b743b5..cad5bbe4 100644 --- a/.github/workflows/check_ap2_systemd_full_build_folder.yml +++ b/.github/workflows/check_ap2_systemd_full_build_folder.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-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 - name: Configure diff --git a/.github/workflows/check_ap2_systemv_full.yml b/.github/workflows/check_ap2_systemv_full.yml index d3ff26f8..4e514430 100644 --- a/.github/workflows/check_ap2_systemv_full.yml +++ b/.github/workflows/check_ap2_systemv_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-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 - name: Configure diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index 6b698714..f53de4a2 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -10,7 +10,7 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: | brew install automake diff --git a/.github/workflows/check_classic_systemd_basic.yml b/.github/workflows/check_classic_systemd_basic.yml index 5988e965..82525484 100644 --- a/.github/workflows/check_classic_systemd_basic.yml +++ b/.github/workflows/check_classic_systemd_basic.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev - name: Configure diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index 06744a03..57bb7ac5 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev - name: Configure diff --git a/.github/workflows/docker-build-on-push.yaml b/.github/workflows/docker-build-on-push.yaml index a5f1d9b3..79ac8eac 100644 --- a/.github/workflows/docker-build-on-push.yaml +++ b/.github/workflows/docker-build-on-push.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -40,20 +40,20 @@ jobs: echo "IMAGE_TAG_BASE=development" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2.5.0 - name: Login to Docker Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2.1.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Build and push (classic) - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/classic/Dockerfile @@ -64,7 +64,7 @@ jobs: SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/Dockerfile diff --git a/.github/workflows/docker-build-on-tag.yaml b/.github/workflows/docker-build-on-tag.yaml index 3ce06306..a83882bc 100644 --- a/.github/workflows/docker-build-on-tag.yaml +++ b/.github/workflows/docker-build-on-tag.yaml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -46,20 +46,20 @@ jobs: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v2.5.0 - name: Login to Docker Registry - uses: docker/login-action@v2 + uses: docker/login-action@v2.1.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Build and push (classic) - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/classic/Dockerfile @@ -72,7 +72,7 @@ jobs: SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/Dockerfile From 5391dc3c4bef94d8f00671501b63d82d53904fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 15 Apr 2023 10:08:13 +0200 Subject: [PATCH 76/85] Add Dependabot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..60aac425 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: sunday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development From 93e8c90b47b15491797c2edb6860949efc8b7661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 15 Apr 2023 10:09:33 +0200 Subject: [PATCH 77/85] Update stale action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .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 d938bd71..ff36ea20 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@v4 + - uses: actions/stale@v8.0.0 with: stale-issue-message: 'This issue has been inactive for 45 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 45 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' From 849e2478d2112287d5f8e656cad66cc4782d52da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 18 Apr 2023 22:41:56 +0200 Subject: [PATCH 78/85] Use s6-notifyoncheck to check for services startup readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .../classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check | 2 ++ .../etc/s6-overlay/s6-rc.d/02-dbus/notification-fd | 1 + docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run | 3 +-- .../classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check | 10 ++++++++++ .../etc/s6-overlay/s6-rc.d/03-avahi/notification-fd | 1 + docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run | 6 +----- docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check | 2 ++ docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd | 1 + docker/etc/s6-overlay/s6-rc.d/02-dbus/run | 3 +-- docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check | 10 ++++++++++ docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd | 1 + docker/etc/s6-overlay/s6-rc.d/03-avahi/run | 6 +----- 12 files changed, 32 insertions(+), 14 deletions(-) create mode 100755 docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd create mode 100755 docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check create mode 100644 docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd create mode 100755 docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check create mode 100644 docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd create mode 100755 docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check create mode 100644 docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check new file mode 100755 index 00000000..39eeac11 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check @@ -0,0 +1,2 @@ +#!/bin/sh +exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run index 2278697a..15018b7e 100644 --- a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -1,4 +1,3 @@ #!/command/with-contenv sh -rm -rf /var/run/dbus/dbus.pid echo "Starting dbus" -exec dbus-daemon --system --nofork +exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check new file mode 100755 index 00000000..ccb833b6 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check @@ -0,0 +1,10 @@ +#!/bin/sh +state="$(dbus-send --system --dest=org.freedesktop.Avahi --print-reply / org.freedesktop.Avahi.Server.GetState | grep int32 | awk '{printf $2}')" + +# Avahi will return 'state=2' when 'Server startup complete' +if [ "$state" = 2 ]; then + exit 0 +else + exit 1 +fi + diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run index e3ea2ff9..edb5e74b 100644 --- a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -1,7 +1,3 @@ #!/command/with-contenv sh -while [ ! -f /var/run/dbus/dbus.pid ]; do - echo "s6-rc: warning: dbus is not running, sleeping for 1 second before trying to start avahi" - sleep 1 -done echo "Starting avahi" -exec avahi-daemon --no-chroot +exec s6-notifyoncheck avahi-daemon --no-chroot diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check b/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check new file mode 100755 index 00000000..39eeac11 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check @@ -0,0 +1,2 @@ +#!/bin/sh +exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd b/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/run b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run index 2278697a..15018b7e 100644 --- a/docker/etc/s6-overlay/s6-rc.d/02-dbus/run +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -1,4 +1,3 @@ #!/command/with-contenv sh -rm -rf /var/run/dbus/dbus.pid echo "Starting dbus" -exec dbus-daemon --system --nofork +exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check b/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check new file mode 100755 index 00000000..ccb833b6 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check @@ -0,0 +1,10 @@ +#!/bin/sh +state="$(dbus-send --system --dest=org.freedesktop.Avahi --print-reply / org.freedesktop.Avahi.Server.GetState | grep int32 | awk '{printf $2}')" + +# Avahi will return 'state=2' when 'Server startup complete' +if [ "$state" = 2 ]; then + exit 0 +else + exit 1 +fi + diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd b/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run index e3ea2ff9..edb5e74b 100644 --- a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -1,7 +1,3 @@ #!/command/with-contenv sh -while [ ! -f /var/run/dbus/dbus.pid ]; do - echo "s6-rc: warning: dbus is not running, sleeping for 1 second before trying to start avahi" - sleep 1 -done echo "Starting avahi" -exec avahi-daemon --no-chroot +exec s6-notifyoncheck avahi-daemon --no-chroot From bd848a754816c5c6964910c7ba1747cb94138e7e Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 8 May 2023 00:56:47 +0200 Subject: [PATCH 79/85] Fix compilation with `libplist` 2.3.0. --- configure.ac | 91 ++++++++++++++++++++++++++++++++++++++++------------ rtsp.c | 8 +++-- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/configure.ac b/configure.ac index 55bfe880..47b606ff 100644 --- a/configure.ac +++ b/configure.ac @@ -408,27 +408,78 @@ if test "x$with_airplay_2" = "xyes" ; then AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2]) AC_CHECK_PROG(XXD_CHECK,xxd,yes) AS_IF([test x"$XXD_CHECK" != x"yes"], [AC_MSG_ERROR([xxd can not be found. Please install xxd for building for AirPlay 2.])]) - PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ - PKG_CHECK_MODULES([libplist], [libplist-2.0 >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ - AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!) - ]) + PKG_CHECK_MODULES( + [libplist], [libplist >= 2.3.0], + [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}" AC_DEFINE([HAVE_LIBPLIST_GE_2_3_0], 1, [libplist >= 2.3.0])], + [PKG_CHECK_MODULES( + [libplist], [libplist-2.0 >= 2.3.0], + [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}" AC_DEFINE([HAVE_LIBPLIST_GE_2_3_0], 1, [libplist >= 2.3.0])], + [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)] + ) + 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([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}"],[ - AC_CHECK_LIB([avcodec], [avcodec_find_decoder], [], [ - AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested) - ]) - ]) - AC_CHECK_LIB([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])]) - AC_CHECK_LIB([swresample],[swr_convert], [], [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])]) - AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])]) + 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_CHECK_LIB( + [avcodec], [avcodec_find_decoder], + [], + [AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested)] + )] + ) + PKG_CHECK_MODULES( + [libavformat], [libavformat], + [CFLAGS="${libavformat_CFLAGS} ${CFLAGS}" LIBS="${libavformat_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [avformat], [avformat_new_stream], + [], + [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])] + )] + ) + PKG_CHECK_MODULES( + [libswresample], [libswresample], + [CFLAGS="${libswresample_CFLAGS} ${CFLAGS}" LIBS="${libswresample_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [swresample], [swr_convert], + [], + [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])] + )] + ) + PKG_CHECK_MODULES( + [libuuid], [uuid], + [CFLAGS="${libuuid_CFLAGS} ${CFLAGS}" LIBS="${libuuid_LIBS} ${LIBS}"], + [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"]) diff --git a/rtsp.c b/rtsp.c index f53bd351..02c63cce 100644 --- a/rtsp.c +++ b/rtsp.c @@ -91,6 +91,10 @@ #include "plist/plist.h" #include "plist_xml_strings.h" #include "ptp-utilities.h" + +#ifdef HAVE_LIBPLIST_GE_2_3_0 +#define plist_from_memory(plist_data, length, plist) plist_from_memory((plist_data), (length), (plist), NULL) +#endif #endif #ifdef CONFIG_DBUS_INTERFACE @@ -4247,7 +4251,7 @@ int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char * // 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 @@ -4265,7 +4269,7 @@ 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 From 7f05e61327c1583adee63b25a9a6881f3d627b29 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 8 May 2023 04:12:42 +0200 Subject: [PATCH 80/85] Simplify `libplist` check. --- configure.ac | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/configure.ac b/configure.ac index 47b606ff..41cd613f 100644 --- a/configure.ac +++ b/configure.ac @@ -408,21 +408,15 @@ if test "x$with_airplay_2" = "xyes" ; then AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2]) AC_CHECK_PROG(XXD_CHECK,xxd,yes) AS_IF([test x"$XXD_CHECK" != x"yes"], [AC_MSG_ERROR([xxd can not be found. Please install xxd for building for AirPlay 2.])]) + LIBPLIST_PACKAGE=libplist + PKG_CHECK_EXISTS(libplist-2.0, LIBPLIST_PACKAGE=libplist-2.0) PKG_CHECK_MODULES( - [libplist], [libplist >= 2.3.0], + [libplist], [${LIBPLIST_PACKAGE} >= 2.3.0], [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}" AC_DEFINE([HAVE_LIBPLIST_GE_2_3_0], 1, [libplist >= 2.3.0])], [PKG_CHECK_MODULES( - [libplist], [libplist-2.0 >= 2.3.0], - [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}" AC_DEFINE([HAVE_LIBPLIST_GE_2_3_0], 1, [libplist >= 2.3.0])], - [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!)] - )] - )] + [libplist], [${LIBPLIST_PACKAGE} >= 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( From da58bc01634c90ccc2ab581497d4f79e86296b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 13 May 2023 19:33:58 +0200 Subject: [PATCH 81/85] Fix s6-timeout issues on slower devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- docker/Dockerfile | 3 +++ docker/classic/Dockerfile | 3 +++ docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3ce13cb0..1dc34141 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,6 +68,9 @@ WORKDIR / # Shairport Sync Runtime System FROM crazymax/alpine-s6:3.17-3.1.1.2 +ENV S6_CMD_WAIT_FOR_SERVICES=1 +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + RUN apk -U add \ alsa-lib \ avahi \ diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 960822f3..9f5c0fe6 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -52,6 +52,9 @@ WORKDIR / # Shairport Sync Runtime System FROM crazymax/alpine-s6:3.17-3.1.1.2 +ENV S6_CMD_WAIT_FOR_SERVICES=1 +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + RUN apk -U add \ alsa-lib \ avahi \ diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies index 97763270..f04b211f 100644 --- a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies @@ -1 +1 @@ -03-avahi +01-startup From 0be523d759547418bf06941b399f0361e3ad9325 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 15 May 2023 15:06:23 +0100 Subject: [PATCH 82/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 60c24697..6845c48e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-919-gbd068fb6 +==== +**Enhancements** +* Allow compilation with `libplist` 2.3.0. Thanks to [Markus Reiter](https://github.com/reitermarkus) for the update. +* Updates to GitHub Action Workflows -- many thanks to [yubiuser](https://github.com/yubiuser) for this detailed work. +* Include PulseAudio support in the Shairport Sync builds within the Docker images. Thanks to [Ferdynand Naczynski](https://github.com/nipsufn) and thanks also to [Noel Hibbard](https://github.com/noelhibbard) for championing this idea for a long time. +* Documentation Updates. + Version 4.1-dev-896-g03961830 ==== **Bug Fix** From 004bf2655f88f09e9591bc49302756c81b6e0319 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 May 2023 11:55:09 +0100 Subject: [PATCH 83/85] Update RELEASENOTES-DEVELOPMENT.md Typo fix. --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 6845c48e..57cbf8ec 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -30,7 +30,7 @@ Version 4.1-dev-874-g551734b3 Version 4.1-dev-872-g65c6975e ==== **Bug Fix** -* Remove three potential race conditions between Shairport Sync opening a TCP connection and the client checkiing that the connections are open. The problem was that the connections were being opened in threads that were created just before the client was given the connection information. If the threads were delayed (e.g. on a slow or busy processor), the client could use the connection information to check the connections, but find that they were not (yet) open. This could cause the client to terminate the session immediately with a `TEARDOWN`. The fix was to open the connections before creating those threads and before sending the connection information back to the client. In this way, the connections are guaranteed to be open before the client has the information it needs to try to open them, even if the threads ared delayed in starting. This bug would manifest itself by allowing play to proceed but not play anything. +* Remove three potential race conditions between Shairport Sync opening a TCP connection and the client checking that the connections are open. The problem was that the connections were being opened in threads that were created just before the client was given the connection information. If the threads were delayed (e.g. on a slow or busy processor), the client could use the connection information to check the connections, but find that they were not (yet) open. This could cause the client to terminate the session immediately with a `TEARDOWN`. The fix was to open the connections before creating those threads and before sending the connection information back to the client. In this way, the connections are guaranteed to be open before the client has the information it needs to try to open them, even if the threads ared delayed in starting. This bug would manifest itself by allowing play to proceed but not play anything. Version 4.1-dev-870-gc924387a ==== From ffeeacc4bd68003eeccadda3c0e227cc75657f8d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 May 2023 16:42:52 +0100 Subject: [PATCH 84/85] add Shared Memory Interface Revision number, e.g. "smi9" to the version string for AirPlay2. --- common.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common.c b/common.c index 3630ea36..f60f1ea5 100644 --- a/common.c +++ b/common.c @@ -110,6 +110,10 @@ void set_alsa_out_dev(char *); #endif +#ifdef CONFIG_AIRPLAY_2 +#include "nqptp-shm-structures.h" +#endif + config_t config_file_stuff; int type_of_exit_cleanup; uint64_t ns_time_at_startup, ns_time_at_last_debug_message; @@ -1612,6 +1616,9 @@ char *get_version_string() { strcpy(version_string, PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 strcat(version_string, "-AirPlay2"); + char smiv[1024]; + snprintf(smiv, 1024, "-smi%u", NQPTP_SHM_STRUCTURES_VERSION); + strcat(version_string, smiv); #endif #ifdef CONFIG_APPLE_ALAC strcat(version_string, "-alac"); From 7804b3acc517887bc318efbbf092e590446a7e86 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 20 May 2023 16:55:58 +0100 Subject: [PATCH 85/85] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 57cbf8ec..7a7157ff 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-922-gffeeacc4 +==== +**Enhancement** +* When built for AirPlay 2, include the Shared Memory Interface (SMI) version number in the version string, in the form: `smi*` where `*` is the version number, e.g. `smi9`. This must be same as the SMI version number of NQPTP, which can be listed by entering `$ nqptp -V`. + Version 4.1-dev-919-gbd068fb6 ==== **Enhancements**