From bdfb8c4f6c1c75b645ae93f89b296bc35d44ca7a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:05:37 +0100 Subject: [PATCH 1/5] Bring up to date with changes in the development branch, most of which come from the master branch. --- .github/ISSUE_TEMPLATE/Bug Report.yaml | 31 +++++-- .../docker-build-on-formal-release.yaml | 83 +++++++++++++++++++ .github/workflows/stale.yaml | 21 +++++ CAR INSTALL.md | 40 +++++++++ RELEASENOTES.md | 27 +++++- 5 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/docker-build-on-formal-release.yaml create mode 100644 .github/workflows/stale.yaml diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yaml b/.github/ISSUE_TEMPLATE/Bug Report.yaml index 11e2f3a1..8017b5db 100644 --- a/.github/ISSUE_TEMPLATE/Bug Report.yaml +++ b/.github/ISSUE_TEMPLATE/Bug Report.yaml @@ -1,12 +1,12 @@ -name: Bug Report -description: File a bug report -title: "[Bug]: " -labels: ["bug"] +name: Issue Report +description: File an Issue +title: "[Problem]: " +labels: ["new issue"] body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! + Thanks for taking the time to fill out this report! - type: textarea id: what-happened attributes: @@ -20,6 +20,13 @@ body: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell + - type: textarea + id: os + attributes: + label: Operating System? + description: What Operating System are you running on ? (`uname -a`) + validations: + required: true - type: textarea id: version attributes: @@ -27,6 +34,17 @@ body: description: What version of Shairport Sync are you running? (`shairport-sync -V`) validations: required: true + - type: dropdown + id: install + attributes: + label: How did you install Shairport Sync? + options: + - Homebrew for Mac + - Docker + - A package manager (apt, yum, pkg, etc.) + - Built from source + validations: + required: true - type: checkboxes id: checked-current-issues attributes: @@ -34,4 +52,5 @@ body: description: Please check previous issues (including closed ones) for duplicates. options: - label: Confirm - required: true \ No newline at end of file + required: true + diff --git a/.github/workflows/docker-build-on-formal-release.yaml b/.github/workflows/docker-build-on-formal-release.yaml new file mode 100644 index 00000000..fc698373 --- /dev/null +++ b/.github/workflows/docker-build-on-formal-release.yaml @@ -0,0 +1,83 @@ +# Builds & pushes a docker image when a "formal" release tag of the format X, X.Y or X.Y.Z occurs, where X, Y and Z are all numbers. +# It seems this yaml has to exist on the branch the tag refers to. + +# Thanks to Charles Omer for the workfow upon which this is based. + +# The following docker tags are created & pushed: +# trial, trial-classic +# not building 'latest' yet... + +name: Formal (X, X.Y or X.Y.Z) release tags -- docker build and push. + +on: + push: + tags: + - '[0-9]+\.[0-9]+\.[0-9]+' # e.g. 8.9.0 + - '[0-9]+\.[0-9]+' # e.g. 8.9 + - '[0-9]+' # e.g. 8 + - '[0-9]+\.[0-9]+rc[0-9]+' # e.g. 4.1rc0 + +env: + DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7 + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set SHAIRPORT_SYNC_BRANCH env. + run: | + raw=$(git branch -r --contains ${{ github.ref }}) + branch=${raw##*/} + echo "SHAIRPORT_SYNC_BRANCH=${branch}" >> $GITHUB_ENV + + - name: Set tag env + run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Determine NQPTP env (part 1). + if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }} + run: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV + + - name: Determine NQPTP env (part 2). + if: ${{ env.SHAIRPORT_SYNC_BRANCH != 'development' }} + run: echo "NQPTP_BRANCH=main" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Registry + uses: docker/login-action@v1 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/Dockerfile + platforms: ${{ env.DOCKER_PLATFORMS }} + push: true + tags: trial + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} + NQPTP_BRANCH=${{ env.NQPTP_BRANCH }} + + - name: Build and push (Classic) + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./docker/classic/Dockerfile + platforms: ${{ env.DOCKER_PLATFORMS }} + push: true + tags: trial-classic + build-args: | + SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 00000000..51658c01 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + stale-issue-message: 'This issue has been inactive for 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.' + operations-per-run: 100 + # These are the defaults at the time of writing. https://github.com/marketplace/actions/close-stale-issues + # days-before-stale: 45 + # days-before-close: 7 diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 8cc47b04..c2259a5f 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -215,3 +215,43 @@ Install the Raspberry Pi in your car. It should be powered from a source that is When the power source is switched on, typically when you start the car, it will take maybe a minute for the system to boot up. ### Enjoy! + +--- + +## Updating +From time to time, you may wish to update this installation. However, in order to update Shairport Sync, you must reconnect the system to a network that can access the internet. The easiest thing is to temporarily reconnect to the network you used when you created the system. To do that, you have to temporarily undo the "Final Steps" and some of the "Raspberry Pi Specific" steps you used. This will enable you to connect your device back to the network it was created on. You should then be able to update the operating system and libraries in the normal way and then update Shairport Sync. + +So, take the following steps: + +1. If it's a Raspberry Pi and you have enabled the Read-only mode, you must take the device out of Read-only mode: +Run `sudo raspi-config` and then choose `Performance Options` > `Overlay Filesystem` and choose to disable the overlay filesystem and to set the boot partition not to be write-protected. This is so that changes can be written to the file system; you can make the filesystem read-only again later. Save the changes and reboot the system. + +2. If you have disabled the `dhcpcd`, `wpa_supplicant` or `systemd-timesyncd` services as suggested in the "Optimise startup time -- Raspberry Pi Specific" section, you need to temporarily re-enable them: +`# systemctl enable dhcpcd.service` +`# systemctl enable wpa_supplicant.service` +`# systemctl enable systemd-timesyncd.service` +Reboot. + +3. To allow your device to reconnect to the network it was created on, edit `/etc/dhcpcd.conf` and comment out the following line at the start: +`denyinterfaces wlan0` +so that it looks like this: +`# denyinterfaces wlan0` +From this point on, if you reboot the machine, it will connect to the network it was configured on, i.e. the network you used when you set it up for the first time. This is because the name and password of the network it was created on would have been placed in `/etc/wpa_supplicant/wpa_supplicant` when the system was initially configured and will still be there. + +4. Reboot and do Normal Updating + + You can perform updates in the normal way -- see [UPDATING](https://github.com/mikebrady/shairport-sync/blob/master/UPDATING.md). When you are finished, you need to undo the temporary changes you made to the setup, as follows: + +5. If you had temporarily re-enabled services that are normally disabled, then it's time to disable them again: +`# systemctl disable dhcpcd.service` +`# systemctl disable wpa_supplicant.service` +`# systemctl disable systemd-timesyncd.service` + +6. To re-enable the system to create its own network, edit `/etc/dhcpcd.conf` and uncomment the line that you had temporarily commented out at the start of the update. Change: +`# denyinterfaces wlan0` +so that it looks like this: +`denyinterfaces wlan0` + +7. Reboot. The system should start as it would if it was in the car. + +8. If the device is a Raspberry Pi and you wish to make the file system read-only, connect to the system, run `sudo raspi-config` and then choose `Performance Options` > `Overlay Filesystem`. In there, choose to enable the overlay filesystem, and to set the boot partition to be write-protected. Do a final reboot and check that everyting is in order. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfb2c8fa..9fdc0628 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,29 @@ -Please see the [Release Notes for 3.3](https://github.com/mikebrady/shairport-sync/releases/tag/3.3). +Version 4.1-rc0 Release Candidate +==== + +Really Big Update -- AirPlay 2! +---- +Version 4.1 brings support for AirPlay 2 operation. It works with iOS, iPadOS, macOS, HomePod mini and Apple TV sources, but not with Windows. +Limited support is available for HomeKit -- Shairport Sync speakers can be added to the Home app, though not all features are working. +AirPlay 2 operation requires a companion program called NQPTP and requires a somewhat more powerful system. Please see [AIRPLAY2.md](./AIRPLAY.md) for more. + +Note that you can still build Shairport Sync to support "classic" AirPlay (aka "AirPlay 1") as before. + +The various guides for building and updating Shairport Sync have been consolidated into [BUILD.md](./BUILD.md). + +Here is a brief list of the high-level new features and changes (more to be added): +* AirPlay 2 operation. +* Improved `libao` backend for better compatibility with HomeBrew installations. +* Improved MQTT and D-Bus facilities. +* Automatically-generated Docker images (forthcoming). + +There have been many many bug fixes and enhancements to the core operation of Shairport Sync. + +Version 3.3.9 +==== +**Bug Fix** +* Version 3.3.9 fixes a build failure with convolution raised since `automake 1.16.5` and https://git.savannah.gnu.org/cgit/automake.git/commit/?id=f4a3a70f69e1dbccb6578f39ef47835098a04624. +Further details in [Fabrice Fontaine's original PR](https://github.com/mikebrady/shairport-sync/pull/1314) to the `development` branch. Many thanks to [Fabrice Fontaine](https://github.com/ffontaine) for the bug report and for the fix. Version 3.3.8 ==== From e036d64d780224ee5e6519fdfe3cb702e99ec16a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:11:59 +0100 Subject: [PATCH 2/5] Bring up to date with changes in the development branch, most of which come from the master branch. --- TROUBLESHOOTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 73e9e7b9..d6436fdf 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -15,6 +15,15 @@ If you are using the default ALSA backend, don't forget to check two simple thin You can use `alsamixer` for both of theses checks. A muted output has the letter(s) `M` as its value. Select it and type `M` again to unmute. +### Audio is Delayed! +If the audio from your Shairport Sync device is delayed slightly by comparison with audio from other devices, it may be that the output device being fed by Shairport Sync is introducing a delay while it processes the audio. If your output device include any digital processing component, it probably delays the audio while it processing occurs. + +For instance, if your output device is a HDMI-connected device such as a TV or an AV Receiver, it will almost certainly delay audio by anything up to several hundred milliseconds. + +The fix for this is to ask Shairport Sync to provide the audio to the output device _slightly ahead of time_, so that by the time the output device has processed it, the audio emerges at exactly the right time. The setting to look for is in the `general` section of the Shairport Sync configuration file and is called `audio_backend_latency_offset_in_seconds`. By default it is `0.0` seconds. + +For example, if your output device is delaying audio by 100 milliseconds (0.1 seconds), set the `audio_backend_latency_offset_in_seconds` to `-0.1`, so that audio is provided to your output device 0.1 seconds early. Remember to uncomment the line by removing the initial `//` and then restart Shairport Sync (or reboot the device) for the changed setting to take effect. + ### WiFi adapter running in power-saving / low-power mode **Check Throughput** From c269e815e6bbf1ccb7aed0808ea464ff3c2c0064 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 29 Sep 2022 16:39:01 +0100 Subject: [PATCH 3/5] Add name of client when we can get it in AP2 --- rtsp.c | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/rtsp.c b/rtsp.c index d61ffeb1..b03d6f4e 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1622,7 +1622,7 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (conn->player_thread) warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); else { - debug(1, "Connection %d: AP1 ALAC Stream, from %s:%u to self at %s:%u.", + debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); activity_monitor_signify_activity(1); @@ -2830,6 +2830,15 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t messagePlist = plist_from_rtsp_content(req); plist_t setupResponsePlist = plist_new_dict(); resp->respcode = 400; + + // see if we can get a name for the client + char *clientNameString = NULL; + plist_t nameItem = plist_dict_get_item(messagePlist, "name"); + if (nameItem != NULL) { + plist_get_string_val(nameItem, &clientNameString); + } else { + clientNameString = strdup(""); + } // see if the incoming plist contains a "streams" array plist_t streams = plist_dict_get_item(messagePlist, "streams"); @@ -2847,8 +2856,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_get_string_val(timingProtocol, &timingProtocolString); if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { - debug(2, "Connection %d: AP2 PTP connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + debug(1, "Connection %d: AP2 PTP connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; @@ -2862,20 +2871,24 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); #endif } else if (strcmp(timingProtocolString, "NTP") == 0) { - debug(1, "Connection %d: SETUP: NTP setup detected.", conn->connection_number); + debug(1, "Connection %d: SETUP: NTP setup from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ntp_stream; conn->timing_type = ts_ntp; } else if (strcmp(timingProtocolString, "None") == 0) { - debug(2, "Connection %d: SETUP: a \"None\" setup detected.", conn->connection_number); + debug(3, "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); // now check to see if it's got the "isRemoteControlOnly" item and check it's true plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); if (isRemoteControlOnly != NULL) { uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(2, "Connection %d: Remote Control connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); + debug(1, "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = remote_control_stream; } else { debug(1, @@ -3354,6 +3367,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); } plist_free(messagePlist); + if (clientNameString != NULL) + free(clientNameString); debug_log_rtsp_message(2, " SETUP response", resp); } #endif @@ -4407,7 +4422,9 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag #ifdef CONFIG_AIRPLAY_2 conn->airplay_type = ap_1; conn->timing_type = ts_ntp; - debug(1, "Connection %d. AirPlay 1 Audio Stream Detected.", conn->connection_number); + debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); #endif conn->stream.type = ast_unknown; @@ -5460,7 +5477,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, sizeof(conn->self_ip_string)); - debug(1, "Connection %d: New connection from %s:%u to self at %s:%u.", + debug(2, "Connection %d: New connection from %s:%u to self at %s:%u.", conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); conn->connection_start_time = get_absolute_time_in_ns(); From 84c9b8b35a28fd35fd6be08a0064bcef4a960bf4 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:05:40 +0100 Subject: [PATCH 4/5] Close the DAC when the keep_dac_busy flag is changed (by the activity monitor) from true to false. Wait a little longer for the DAC to drop all frames. --- audio_alsa.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index fc53ee65..19b35084 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -1850,7 +1850,8 @@ static int do_close() { // debug(1,"alsa: do_close() -- closing the output device"); if ((derr = snd_pcm_drop(alsa_handle))) debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); - usleep(5000); // this make the function pthread cancellable + usleep(10000); // wait for the hardware to do its trick. BTW, this make the function pthread + // cancellable if ((derr = snd_pcm_hw_free(alsa_handle))) debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr)); debug(2, "alsa: do_close() -- closing alsa handle"); @@ -2061,9 +2062,12 @@ static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) int error_count = 0; int error_detected = 0; int okb = -1; - while (error_detected == - 0) { // if too many play errors occur early on, we will turn off the disable stanby mode + // if too many play errors occur early on, we will turn off the disable standby mode + while (error_detected == 0) { + int keep_dac_busy_has_just_gone_off = 0; if (okb != config.keep_dac_busy) { + if ((okb != 0) && (config.keep_dac_busy == 0)) + keep_dac_busy_has_just_gone_off = 1; debug(2, "keep_dac_busy is now \"%s\"", config.keep_dac_busy == 0 ? "no" : "yes"); okb = config.keep_dac_busy; } @@ -2081,14 +2085,14 @@ static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) if (do_open(1) == 0) // no automatic setup of rate and speed if necessary debug(2, "alsa: alsa_buffer_monitor_thread_code() -- output device opened; " "alsa_backend_state => abm_connected"); - } else if ((alsa_backend_state == abm_connected) && (config.keep_dac_busy == 0)) { + } else if ((alsa_backend_state != abm_disconnected) && (keep_dac_busy_has_just_gone_off != 0)) { stall_monitor_start_time = 0; // frame_index = 0; // measurement_data_is_valid = 0; - debug(2, "alsa: alsa_buffer_monitor_thread_code() -- closing the output " + debug(1, "alsa: alsa_buffer_monitor_thread_code() -- closing the output " "device"); do_close(); - debug(2, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state " + debug(1, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state " "=> abm_disconnected"); } // now, if the backend is not in the abm_disconnected state From 29b01b54939f25bda39188cda55b157a4908d928 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:08:11 +0100 Subject: [PATCH 5/5] Quieten some debug messages, duh. --- audio_alsa.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_alsa.c b/audio_alsa.c index 19b35084..62db843f 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -2089,10 +2089,10 @@ static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) stall_monitor_start_time = 0; // frame_index = 0; // measurement_data_is_valid = 0; - debug(1, "alsa: alsa_buffer_monitor_thread_code() -- closing the output " + debug(2, "alsa: alsa_buffer_monitor_thread_code() -- closing the output " "device"); do_close(); - debug(1, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state " + debug(2, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state " "=> abm_disconnected"); } // now, if the backend is not in the abm_disconnected state