Skip to content

fix: speaker recovers from the wrong song, stalls and duplicate tabs - #74

Merged
madeofpendletonwool merged 1 commit into
mainfrom
fix/speaker-resync
Oct 15, 2026
Merged

madeofpendletonwool merged 1 commit into
mainfrom
fix/speaker-resync

Conversation

@madeofpendletonwool

Copy link
Copy Markdown
Owner

What happened

On 2026-10-15 in The Work Boys, with a friend's iPhone (Chrome, iOS 26) playing to a Bluetooth speaker:

  1. Feel It Still played over Helena Beat, and kept going until a pause then play from the big screen. Navidrome's logs show a second player starting up about a minute into Helena Beat (12:43:43 and again at 12:44:06 CDT). It loaded Helena Beat and preloaded Feel It Still, on top of the iPhone's normal requests.
  2. Roxanne (Spotify, transcoded for iOS) went silent after a skip onto its preloaded stream, but still showed as playing. It's still marked as playing in the DB. Prod logged no automatic playback notices that day, so the server never timed it out.

The exact device actions couldn't be reconstructed, and the server didn't log playback events. So this PR fixes the client-side gaps that lead to those symptoms, and adds logging.

Changes

  • Wrong song: offSong() notices when a device is on another song than the room at the same revision, and apply loads the room's song. Before, only a new revision corrected it, which is why pause then play fixed it. The gapless handoff (the next song in the last 10s of the room's) is left alone.
  • Stall watchdog: if the position hasn't moved for 10s while the room plays, the song reloads where it was. After two in a row, the speaker reports error. Reloading back to the stall point doesn't count as progress.
  • One player per browser: tabs share the device ID, so the newest to play tells the others to go quiet over a BroadcastChannel. If the other tab played as a different device, it releases instead.
  • begin() clears curItem/preItem, since it puts silence in both elements. Starting while already active could play silence and skip songs.
  • Server logs (Info): speaker claimed and released, commands, next song with its reason, and reports. Progress reports are logged only when stale.
  • Docs: docs/player-mode.md.

Testing

  • go test ./internal/playback ./internal/httpapi pass
  • Web: 309 tests pass (new offSong cases), oxlint and tsc clean
  • Not yet checked in a browser or on a device. Worth a run with an iPhone speaker locked through several songs, including a skip onto a Spotify song.

🤖 Generated with Claude Code

Two incidents in The Work Boys on 2026-10-15, with an iPhone as the speaker:
the next song played over the current one until a pause and play, and a
song skipped to played silently while showing as playing.

- A device on another song than the room at the same revision now loads
  the room's song. Before, only a new revision corrected it, which is why
  pause then play fixed it. The gapless handoff (the next song in the last
  10s of the room's) is left alone.
- A stream that says it's playing but hasn't moved for 10s is reloaded
  where it was; after two in a row the speaker reports an error.
- Tabs of one browser share a device ID, so the newest to play tells the
  others to go quiet over a BroadcastChannel.
- begin() clears the loaded songs, since it puts silence in both elements:
  starting while already playing could play silence and skip songs.
- The server logs speaker claims and releases, commands, song changes and
  reports (stale ones marked), so the next incident can be read from logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@madeofpendletonwool
madeofpendletonwool merged commit 1a6a54e into main Oct 15, 2026
5 checks passed
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.

1 participant