41fe76b90c
android / Build + lint + test (push) Successful in 4m23s
The local ExoPlayer cursor and the Sonos renderer were two competing
sources of truth for "what's playing" during a cast. The delegate cursor
lagged (forward-only, index-based, size-capped sync, skipped during every
load/re-cast window), and TWO writers of PlayerUiState.queueIndex fought:
PlayerController.onEvents (reading the lagging cursor) stomped the
Sonos-derived index the position tick published, so the in-app player
flickered to the pre-cast track and the notification metadata went stale.
Step 1 — MinstrelForwardingPlayer.syncLocalCursorToRemote (replaces
maybeSyncLocalCursor): align the paused delegate cursor to the track the
renderer is actually playing, matched by track-id parsed from the Sonos
TrackURI (/api/tracks/{id}/stream) against delegate MediaItem.mediaId
(== TrackRef.id). Both directions; survives queue-reload index wobble;
nearest-occurrence tiebreak for duplicate tracks; falls back to the Sonos
Track index; suppressed during load and while a user transport is pending
Sonos's ack. The cursor is now the single authoritative "current track"
that both the in-app UI (onEvents) and the notification (getCurrentMediaItem)
read.
Step 2 — PlayerController: the position tick now patches only
position/duration/play-pause/buffer; onEvents is the sole writer of
queueIndex/currentTrack. Removed desiredQueueIndex, the forward-only
trackChanged path, and publishTickIfChanged. One writer, no stomp.
Part of milestone #171 (unify local + UPnP behind one cursor). Fixes the
flicker + stale-notification symptoms; supersedes #1211/#608/#612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>