dev → main: Android UPnP/Sonos transport parity + server stream URL extension #79

Merged
bvandeusen merged 69 commits from dev into main 2026-06-04 08:15:15 -04:00
Showing only changes of commit c245b1ef0b - Show all commits
@@ -411,6 +411,11 @@ class PlayerController @Inject constructor(
// Reset the per-item evaluation guard so the new
// item's STATE_READY transition gets a fresh check.
lastEvaluatedItemIndex = -1
// The track flipped -- re-anchor the position interpolator
// so the next polling tick treats the new track's
// remoteState.positionMs as fresh rather than carrying the
// old anchor + elapsed forward.
lastSeenRemotePositionMs = -1L
}
override fun onPlaybackStateChanged(playbackState: Int) {
@@ -493,6 +498,14 @@ class PlayerController @Inject constructor(
while (isActive) {
delay(POSITION_POLL_INTERVAL_MS)
val upnpActive = activeUpnpHolder.active.value != null
// Track-alignment gate: if Sonos's reported Track is past the
// wrapped player's currentMediaItemIndex, a forward cursor sync
// is in flight (pollOnce wrote new remoteState, handler.post'd
// delegate.seekTo; the seek hasn't run yet). Skipping the tick
// here lets onMediaItemTransition rebuild the full uiState
// atomically rather than patching position+duration while
// currentTrack/queueIndex still point at the old track.
if (upnpActive && isForwardCursorSyncPending(controller)) continue
val effectiveIsPlaying =
if (upnpActive) remoteState.isPlaying else controller.isPlaying
val effectivePosition = if (upnpActive) {
@@ -533,6 +546,12 @@ class PlayerController @Inject constructor(
@Volatile private var positionAnchorMs: Long = 0L
@Volatile private var positionAnchorAtRealtimeMs: Long = 0L
private fun isForwardCursorSyncPending(controller: MediaController): Boolean {
val sonosTrack = remoteState.trackNumber
if (sonosTrack <= 0) return false
return (sonosTrack - 1) > controller.currentMediaItemIndex
}
private fun interpolatedRemotePosition(isPlaying: Boolean): Long {
val raw = remoteState.positionMs
val now = SystemClock.elapsedRealtime()