Repository navigation
Conversation
After Spotify single-active-device switching, a room's sendspin client can desync — the reference client logs "Audio underflow detected; requesting re-anchor" and re-anchors its own timeline, but when that client-local re-anchor does not converge the room plays nothing while ALSA stays RUNNING. Today the only recovery is restarting the sendspin@room_<x> client process. The protocol carries no inbound re-anchor request, so the sole server-visible signal is a `state: 'error'` player-state report (identical repeats are deduped in handleClientState, so we act on the first transition, debounced). If the client is still in `error` after a short debounce while the output believes it is playing, force a fresh server anchor (startStream preserveAnchor:false) — what a client restart does, without the restart — guarded by a per-output cooldown. WIP / unverified: needs a live capture to confirm (a) the reference client reports `state: 'error'` during this underflow storm and (b) a server-side fresh anchor converges it rather than needing a full client restart. Opening for direction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Field update + upstream context that bears directly on open question 1. The spec confirms the premise. The Sendspin underrun handshake is Heads-up so two failure modes don't get conflated. Chasing "room goes silent |
|
Live capture answers open question 1 — and it refutes this PR's premise. Recommend not merging as-is. A watchdog capture during a real underflow storm (reference client, aiosendspin 6.0.5) shows, for the
So the server is blind to this underrun: the client does not emit the spec's Recommendation: hold / don't merge this server-side (Also worth noting from the same capture: the storm was a very short TTS alert underflowing at stream start |
Symptom
After Spotify single-active-device switching (playing zone A, then moving the
Spotify account to zone B and back), the room's sendspin client desyncs and plays
nothing — while ALSA reports the substream
RUNNINGand frames keep flowing.The reference client logs, in its own journal:
It re-anchors its own timeline. Usually fine — but sometimes that client-local
re-anchor never converges, and the room stays silent until the
sendspin@room_<x>client process is restarted. Restarting the process iscurrently the only known recovery.
Why the server can barely see it
Walking the hook surface (
sendspinHookRegistry.ts→buildCombined), theprotocol exposes no inbound re-anchor request or underflow event. The only
signal that could reach the server is a
state: 'error'player-state reportvia
onPlayerState→handleClientState. AndhandleClientState's dedup guard(
signature === lastClientStateSignature → return) drops identical repeats — sothe server can't even count an error storm; at most it sees the first
errortransition. Today that transition is only logged (
warn), never acted on.What this change does
On the first
errortransition while the output believes it is playing, start ashort debounce (4 s). If the client hasn't recovered to
synchronizedby then,force a fresh server anchor —
startStream({ preserveAnchor: false }), which iswhat a client restart accomplishes without killing the process — guarded by a
per-output cooldown (30 s) so a persistently-erroring client isn't thrashed. If
the client recovers within the debounce, the timer is cancelled.
Minimal and self-contained: two fields + one guarded helper in
sendspinOutput.ts.Open questions (need a live capture during a real desync)
state: 'error'during thisunderflow storm, or does it keep saying
synchronizedand only log there-anchor client-side? If the latter, the server is blind here and the fix must
live in the client/protocol (surface an underflow/re-anchor event) rather than
in lox.
restart genuinely required? The proven recovery so far is a process restart,
which is heavier than
startStream.Happy to adjust the trigger (state value, debounce, cooldown) or move it entirely
once you confirm the client's reporting behaviour.
Interim workaround (in the field, working now)
Until this is settled I run an external journald watchdog on the Pi that counts
requesting re-anchorwarnings persendspin@room_*unit and restarts a clientwhose re-anchor is stuck — the process restart being the currently-proven fix.
Reference: https://github.com/tobsch/snappy/tree/master/timelinewatch