fix(android): duration falls back to local while Sonos hasn't reported one
android / Build + lint + test (push) Has been cancelled

This commit is contained in:
2026-06-04 00:00:45 -04:00
parent 85926f4ec0
commit 9628ed1749
@@ -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"
}