vc=83 — fix: swapping videos from the minibar kept playing the old one
All checks were successful
build-apk / build-and-publish (push) Successful in 7m11s
gitleaks / scan (push) Successful in 40s

The inline player's auto-resolve→play LaunchedEffect read the shared
activity-scoped VideoDetailViewModel's `resolved` stream without checking
it belonged to the newly-opened video. Right after a swap (e.g. pick
another video from the browse screen while one sits in the minibar), the
VM still holds the previous video's resolved URLs for one composition
frame until vm.load() nulls them — so setPlayingFrom fired with
streamUrl=NEW but resolved=OLD: NowPlaying.claim won under the new url
(title/details/minibar flipped to NEW) while the controller kept
streaming OLD, and the correct re-fire with NEW's resolved was then
swallowed by the 'already playing this url' short-circuit.

Restore the loadedUrl fence (the same guard VideoDetailBody and every
ViewModel already use) that the vc=75 expandable-player rearchitect
dropped when the inline resolve→play wiring moved out of VideoDetailScreen.
This commit is contained in:
Cobb 2026-06-21 10:26:21 -07:00
parent 2b3eb8bef4
commit ea82ba765a
2 changed files with 34 additions and 2 deletions

View file

@ -447,6 +447,23 @@ private fun InlinePlayerSurface(
LaunchedEffect(controller, resolved, streamUrl, retryVersion, started) {
if (!started) return@LaunchedEffect
val c = controller ?: return@LaunchedEffect
// Only start playback once the VM's resolved stream actually belongs
// to THIS video. The VM is activity-scoped, so the instant the user
// swaps to a new video (e.g. picks another from the browse screen
// while the current one sits in the minibar) `state` still holds the
// PREVIOUS video's resolved for one composition frame — until
// vm.load(streamUrl) nulls it. Without this fence we'd call
// setPlayingFrom(streamUrl=NEW, resolved=OLD): NowPlaying.claim wins
// under the NEW url, so the title/details/minibar all flip to NEW,
// but the controller keeps streaming OLD's media — and the later
// re-fire with NEW's resolved short-circuits on the "already playing
// this url" guard below, so NEW never actually starts. That's the
// "swap from the minibar keeps playing the old video" bug.
// VideoDetailUiState.loadedUrl exists for exactly this fence (it's
// used by VideoDetailBody + every ViewModel); the vc=75 expandable-
// player rearchitect dropped it when the inline-player resolve→play
// wiring moved out of the old VideoDetailScreen into here.
if (state.loadedUrl != streamUrl) return@LaunchedEffect
val r = resolved ?: return@LaunchedEffect
if (NowPlaying.current.value?.streamUrl == streamUrl) return@LaunchedEffect
c.setPlayingFrom(