From ea82ba765aa6dd5fca091a2b3a48d6321c174c1c Mon Sep 17 00:00:00 2001 From: Cobb Date: Sun, 21 Jun 2026 10:26:21 -0700 Subject: [PATCH] =?UTF-8?q?vc=3D83=20=E2=80=94=20fix:=20swapping=20videos?= =?UTF-8?q?=20from=20the=20minibar=20kept=20playing=20the=20old=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 19 +++++++++++++++++-- .../straw/feature/player/ExpandablePlayer.kt | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index e9a69fa95..0cd1236e6 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -9,6 +9,21 @@ const val STRAW_SDK_TARGET = 35 // Sulkta fork — Straw // +// vc=83 / 0.1.0-CQ — fix: swapping videos from the minibar kept playing +// the old one: +// * With a video minimized to the bottom minibar, picking a different +// video updated the title, details and related list but the OLD video +// kept playing. The inline player's resolve→play effect read the +// shared (activity-scoped) view-model's `resolved` stream without +// checking it actually belonged to the newly-opened video. For one +// frame after the swap the view-model still holds the previous video's +// resolved URLs, so playback was claimed under the NEW url while +// streaming the OLD media — and the correct re-fire was then swallowed +// by the "already playing this url" short-circuit, so the new video +// never started. Restored the loadedUrl fence (the same guard +// VideoDetailBody and every ViewModel already use) that the vc=75 +// expandable-player rearchitect had dropped from the inline wiring. +// // vc=82 / 0.1.0-CP — subscription-feed enrichment goes lightweight: // * Filling in a feed row's view count + duration used to run the FULL // stream extraction per item (the same path opening a video does): @@ -149,6 +164,6 @@ const val STRAW_SDK_TARGET = 35 // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 82 -const val STRAW_VERSION_NAME = "0.1.0-CP" +const val STRAW_VERSION_CODE = 83 +const val STRAW_VERSION_NAME = "0.1.0-CQ" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ExpandablePlayer.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ExpandablePlayer.kt index a7552815e..339399dd4 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ExpandablePlayer.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ExpandablePlayer.kt @@ -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(