Part A: split loadQueueOnSonos into an initial phase (tracks[0..currentIndex]
only, then SetAV+Seek+Play) plus a background extendQueueOnSonos coroutine
that appends the remainder after activation. Reduces the UPnP activation
block from ~17s (100 tracks serial) to ~200ms (1 track at currentIndex=0).
Background extension cancels cleanly when activeUpnpHolder.active changes.
Part B: add PlayerUiState.isUpnpLoading (target set, active null). Projected
inline in onEvents so it stays consistent with the rest of the snapshot, plus
a separate combine(target, active) collector that updates uiState between
player-event fires. NowPlayingScreen.TransportRow and MiniPlayer.MiniRow
replace the play/pause icon with a CircularProgressIndicator while loading
and disable the button tap to prevent premature commands to the Sonos queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Polling alone cannot distinguish Sonos auto-advancing via SetNextAVTransportURI
from URI changes we made ourselves via syncCurrentItemToRemote. This produced
two races: (1) activation race -- first poll returns stale URI from prior
session, second returns new URI, false-positive fires and double-advances the
cursor; (2) user-skip race -- skip's syncCurrentItemToRemote changes the URI,
next poll sees the change and fires again. Remove the detection block and
previousTrackUri capture from pollOnce entirely. pollLoop is now a pure
state-tracker (position + transport state) plus the one-shot initial pre-queue
gate. GENA event subscriptions to AVTransport LastChange are the correct fix;
deferred to its own slice (see parity-map).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional onRawResponse callback to SoapClient; loggingSoapClient
factory emits the first 6 GetPositionInfo/GetTransportInfo bodies
(3 poll cycles) at WARN so release logs capture them. Wire into
transportFor so every AVTransportClient for a new UPnP session logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move local ExoPlayer pause from OutputPickerController.selectUpnp
into MinstrelForwardingPlayer.onActiveChanged (handler.post { delegate.pause() }).
This guarantees the pause hits ExoPlayer before the holder is live, eliminating
the async race that caused SOAP fault 701 on Sonos when pause() was dispatched
via playerController after holder.active was already set.
Also adds per-poll Timber.w before initialPreQueueDone for diagnostics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OutputPickerController.selectUpnp: pause ExoPlayer BEFORE setting
activeUpnpHolder so ForwardingPlayer.pause() routes to ExoPlayer,
not SOAP; remove now-redundant playerController.pause() from inside
runCatching; bump activation Timber.i -> Timber.w for release logcat
- MinstrelForwardingPlayer: remove Player.Listener onMediaItemTransition
that raced with seekToNext/Prev override's syncCurrentItemToRemote;
seekToNext/Prev now launch sync -> preQueueNext sequentially in one
coroutine; remove early preQueueNext from onActiveChanged (raced with
selectUpnp SOAP); move initial pre-queue to pollLoop, fires once
trackUri lands confirming Sonos accepted SetAV+Play
- Extract pollOnce from pollLoop to stay within detekt LongMethod=60;
natural-advance branch now calls preQueueNext explicitly (no listener)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>