fix(android player): seek broke after seamless track transitions (durationMs stuck at 0)

After skipping through already-buffered tracks the player stays READY, so
onPlaybackStateChanged never fires and syncState() isn't re-called; ctrl.duration
was TIME_UNSET(→0) at the transition, leaving state.durationMs stuck at 0. The
seek bar then computed fraction*0 = 0 and snapped playback to the start every time
(or to a stale value → "only 10s"). Restarting the app re-read the real duration,
which is why it recovered.

Fix: durationMs now self-heals — the 500ms ticker refreshes it too, and
resolveDurationMs() falls back to the current song's known duration while
ctrl.duration is still unset. Also record the Erkunden swipe-between-tabs idea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-16 21:47:58 +02:00
1 parent 803331e807
commit 87ee8cd097
2 files changed
+17 -2

No files matched your search

+1
View File
@@ -453,6 +453,7 @@ Primary causes identified:
- **New-release alerts:** periodic check for new albums/singles by followed artists (sources: iTunes lookup / MusicBrainz / Last.fm), surfaced in the existing in-app notification bell (and optionally push).
- **Concert alerts:** reuse the existing **Ticketmaster** events integration to notify when a followed artist has nearby/upcoming shows.
- Ties into the importer: a "download this new release" action straight from the notification.
- **Android** — **Swipe between Erkunden sub-tabs** — a left/right swipe gesture on the Erkunden tab to switch between "Bibliothek" and "Global" search (instead of only tapping the chips). A fun, natural pager-style gesture (e.g. HorizontalPager).
- **Android** — Playlist drag-and-drop / sort order
- **Android** — Artist page iTunes fallback (for artists not in Navidrome)
- **Android** — Romaji / Japanese sort support
@@ -227,11 +227,25 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
while (isActive) {
delay(500)
val ctrl = controller ?: break
_state.update { it.copy(positionMs = ctrl.currentPosition) }
// Refresh durationMs too — after a SEAMLESS track transition (skipping through
// already-buffered tracks) the player stays READY, so onPlaybackStateChanged never
// fires and syncState() isn't re-called; ctrl.duration was TIME_UNSET(→0) at the
// transition, leaving durationMs stuck at 0. Seek then computes fraction*0 = 0 and
// snaps to the start every time. Re-reading here lets it self-heal within 500ms.
_state.update { it.copy(positionMs = ctrl.currentPosition, durationMs = resolveDurationMs(ctrl)) }
}
}
}
/** Player duration, falling back to the current song's known duration while ctrl.duration is
* still TIME_UNSET (right after a media transition). Prevents a 0-duration seek → snap-to-0. */
private fun resolveDurationMs(ctrl: MediaController): Long {
val d = ctrl.duration
if (d > 0) return d
val songDurSecs = queue.getOrNull(ctrl.currentMediaItemIndex)?.duration?.toLong() ?: 0L
return songDurSecs * 1000L
}
private fun stopTicker() {
tickerJob?.cancel()
tickerJob = null
@@ -271,7 +285,7 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
currentSong = queue.getOrNull(idx),
currentIndex = idx,
positionMs = ctrl.currentPosition,
durationMs = ctrl.duration.coerceAtLeast(0),
durationMs = resolveDurationMs(ctrl),
queue = queue,
repeatMode = ctrl.repeatMode,
shuffled = isManuallyShuffled,