Skip to content

fix(soloist): re-adopt a zone that becomes active again with no stream (end-of-track freeze) - #353

Closed
tobsch wants to merge 1 commit into
sonn-audio:devfrom
tobsch:fix/soloist-reactivation-freeze
Closed

tobsch wants to merge 1 commit into
sonn-audio:devfrom
tobsch:fix/soloist-reactivation-freeze

Conversation

@tobsch

@tobsch tobsch commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #352 — root cause found and fix verified against the deterministic repro (patched into the running
container's dist, reproduced with vs. without).

Root cause (confirmed by instrumenting onEvent)

Logging every onEvent call through a switch-back-at-the-boundary showed the exact sequence for the zone:

type=device_changed  isActive=false  owner=connect              -> deactivate: owner→queue, currentUri→null,
                                                                    finishTrack (stream destroyed), stopPlayback
… (inactive: position_sync / playback_state status=playing, all isActive=false → inactive branch, no-op) …
type=device_changed  isActive=true   owner=queue  status=undefined  uri=undefined   <- became active again
… then NO further ws events — only Soloist stdout `playing … [3:42 / 3:42]` heartbeats …

The reactivation arrives as a device_changed with no status. The only path that re-opens the pipe is
the status === 'playing' adopt, so device_changed slips past it, and owner === 'queue' makes onEvent
return. Mid-track this is harmless — the playing/track_changed events that follow re-adopt the zone. But
when the switch-back lands in the last seconds, Soloist hits the track end before any such event and then
emits none, so the zone is left owner=queue, stream=null, stuck at end-of-track while Soloist streams into
a pipe nobody reads.

Fix

Re-adopt on device_changed when the device is active again but has no stream:

if (event.type === 'device_changed' && !runner.stream) {
  void this.adoptConnectPlayback(zoneId, event);
  return;
}

(placed right after the !runner.ws.isActive block, so isActive is already guaranteed true there.)

Verification

Repro: two Soloist zones on one account; transfer B → seek dur-5s → transfer A.

  • Without fix: zone A pins at progress == duration, is_playing == true, never advances.
  • With fix (live in dist): FIX reactivation re-adopt → the spotify app took this zone over →
    playback advances (220/222s "Dass es hier so schön ist" → 0/130s "Zugabe" → continues). No freeze.

Control (switch not near the boundary) was unaffected either way.

…th no stream

Switching a Connect device back to a zone in the last seconds of a track left the
zone frozen at end-of-track — playback never advanced. The handoff that moved the
account away tears the zone's stream down (finishTrack), and coming back is
announced as a `device_changed`, which carries no `status`. onEvent only re-adopts
on a `status === 'playing'` event, so the reactivation itself never triggers it.
Mid-track the following `playing`/`track_changed` events re-adopt the zone; but when
the switch-back lands at the track boundary Soloist reaches the end before any of
them arrive and then sends none, leaving the zone owner=`queue`, no stream, stuck at
end-of-track while Soloist plays on into a pipe nobody reads.

Re-adopt on `device_changed` when the device is active again but has no stream, so
the pipe is reopened, playback is consumed, and the track advances. Fixes sonn-audio#352.

Verified against a deterministic repro (transfer to zone B -> seek dur-5s -> transfer
back to zone A): without the fix the zone pins at progress==duration; with it, it
adopts ("took this zone over") and advances to the next track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants