/
/
/
# What does this implement/fix? When an AirPlay speaker plays through a Sendspin sync group (mixed with Sendspin speakers), starting playback was in sync but a **seek or next-track** left the AirPlay speaker audibly late — up to ~90ms behind the rest of the group — for the remainder of the stream. Native AirPlay playback was unaffected. A seek or track change flushes the AirPlay binary in place and re-anchors it instead of reconnecting. The flush command travels on its own pipe while audio travels on stdin, so audio already handed to stdin was still in flight when the binary ran its drain. Those bytes arrived just after it, and the new anchor mapped them as the first sample of the new track — shifting all of the new content late by their duration. Nothing detected it: the bridge only counts the bytes it queues after the anchor, so no realignment could see the offset. `write()` only waits while the transport is paused, which happens above the high-water mark, so it can return with up to 64 KiB queued — a third of a second of PCM. The ~90ms observed in the logs is the steady state under continuous writing, not the worst case. Verified against debug logs from a reporter and a local reproduction: on a warm seek the binary reports audio already pending on its stdin *before* the start is commanded, whereas on a cold start it reports it *after*. The amount matches how much was queued when the previous track stopped being fed. **Related issue (if applicable):** - n/a — reported on Discord with debug logs ## Types of changes - [x] Bugfix (non-breaking change which fixes an issue) — `bugfix` - [ ] New feature (non-breaking change which adds functionality) — `new-feature` - [ ] Enhancement to an existing feature — `enhancement` - [ ] New music/player/metadata/plugin provider — `new-provider` - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — `breaking-change` - [ ] Refactor (no behaviour change) — `refactor` - [ ] Documentation only — `documentation` - [ ] Maintenance / chore — `maintenance` - [ ] CI / workflow change — `ci` - [ ] Dependencies bump — `dependencies` ## Changes - Empty our own stdin buffer before commanding a flush, so the binary's drain really removes every pre-flush byte — as `flush()` already documented its caller must ensure. This covers the native path too, which had the same exposure. - Add `AsyncProcess.stdin_quiesced()`: it sees queued bytes through to the pipe and then holds the write lock for the block, so no write can land behind the binary's drain while the flush is in flight. `write()` only waits while the transport is paused, which is what let bytes slip through. - Fall back to a cold restart when queued audio cannot be cleared, rather than anchoring onto stale audio. This costs nothing in practice: the residue is at most 64 KiB and the binary reads continuously, so reaching the 2s timeout means its reader has stalled — in which case the flush ack would have timed out and forced the same cold restart anyway. - Warn when the binary still reports pending audio as a bridge anchor is commanded, so this can never go silent again. ## Checklist - [x] The code change is tested and works locally. - [x] `pre-commit run --all-files` passes. - [x] `pytest` passes, and tests have been added/updated under `tests/` where applicable. - [ ] For changes to shared models, the companion PR in `music-assistant/models` is linked. - [ ] For changes affecting the UI, the companion PR in `music-assistant/frontend` is linked. - [x] I have read and complied with the project's [AI Policy](https://github.com/music-assistant/.github/blob/main/AI_POLICY.md) for any AI-assisted contributions. - [ ] I have [raised a PR against the documentation repository](https://github.com/music-assistant/music-assistant.io/blob/main/CONTRIBUTING.md) targeting the main or beta branch as appropriate.