From 9628ed17495ecd44c7f386d0c68650465383c11c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 00:00:45 -0400 Subject: [PATCH] fix(android): duration falls back to local while Sonos hasn't reported one --- .../minstrel/player/PlayerController.kt | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index b8602717..4275da1c 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -433,7 +433,11 @@ class PlayerController @Inject constructor( // no real audio loaded -- player.duration / isPlaying / // currentPosition all reflect that. Read from remoteState // instead so an onEvents fire (e.g. activity resume) doesn't - // clobber the UI with zeros. + // clobber the UI with zeros. Duration falls back to the + // wrapped player's value when Sonos hasn't reported one yet + // (pre-first-poll window, or Sonos still buffering) -- the + // wrapped ExoPlayer was prepared with the same MediaItem so + // it knows the real duration before any SOAP poll lands. val upnpActive = activeUpnpHolder.active.value != null uiStateInternal.value = PlayerUiState( @@ -448,11 +452,11 @@ class PlayerController @Inject constructor( } else { player.currentPosition }.coerceAtLeast(0), - durationMs = if (upnpActive) { - remoteState.durationMs - } else { - player.duration - }.coerceAtLeast(0), + durationMs = effectiveDuration( + upnpActive, + remoteState.durationMs, + player.duration, + ), bufferedPositionMs = player.bufferedPosition.coerceAtLeast(0), playbackError = player.playerError?.message, currentSource = source, @@ -497,11 +501,13 @@ class PlayerController @Inject constructor( if (upnpActive) remoteState.isPlaying else controller.isPlaying val effectivePosition = if (upnpActive) remoteState.positionMs else controller.currentPosition - val effectiveDuration = - if (upnpActive) remoteState.durationMs else controller.duration val current = uiStateInternal.value val newPos = effectivePosition.coerceAtLeast(0) - val newDur = effectiveDuration.coerceAtLeast(0) + val newDur = effectiveDuration( + upnpActive, + remoteState.durationMs, + controller.duration, + ) val newBuf = controller.bufferedPosition.coerceAtLeast(0) val changed = current.isPlaying != effectiveIsPlaying || current.positionMs != newPos || @@ -574,6 +580,18 @@ class PlayerController @Inject constructor( private fun sourceExtras(source: String): Bundle = Bundle().apply { putString(MINSTREL_SOURCE_KEY, source) } + /** + * Duration to surface to the UI. When UPnP is active the remote's + * reported duration wins -- but if Sonos hasn't reported one yet + * (pre-first-poll window after activation, or transient 0 during a + * track-change buffer), fall back to the wrapped ExoPlayer's value + * for that MediaItem so the scrubber doesn't blink to zero. + */ + private fun effectiveDuration(upnpActive: Boolean, remoteMs: Long, localMs: Long): Long { + val pick = if (upnpActive && remoteMs > 0) remoteMs else localMs + return pick.coerceAtLeast(0) + } + companion object { const val MINSTREL_SOURCE_KEY: String = "minstrel_source" }