From 4b7361608319960e71153ed426f14599f0f1bbb9 Mon Sep 17 00:00:00 2001 From: Cobb Date: Sat, 20 Jun 2026 17:02:03 -0700 Subject: [PATCH] =?UTF-8?q?Expandable=20player:=20static=20poster=20during?= =?UTF-8?q?=20collapse/morph,=20live=20player=20only=20when=20expanded=20?= =?UTF-8?q?=E2=80=94=20vc=3D77?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remaining sluggishness was scaling a live-playing TextureView through the morph's graphicsLayer every frame. Now the minibar + the whole collapse/expand morph render the video's static poster (AsyncImage); the live PlayerView only mounts once settled fully expanded. Audio is unaffected — it's owned by the foreground service, never stops. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 11 +++- .../straw/feature/player/ExpandablePlayer.kt | 62 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 7fac2149a..c2580fe12 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -9,6 +9,13 @@ const val STRAW_SDK_TARGET = 35 // Sulkta fork — Straw // +// vc=77 / 0.1.0-CK — morph perf: static poster during collapse/morph: +// * The minibar + the whole collapse/expand morph now render the video's +// static poster, not the live TextureView. Scaling a live-playing +// TextureView through the morph's graphicsLayer every frame was the +// remaining sluggishness; the live PlayerView only mounts once settled +// fully expanded. Audio is unaffected (it's in the foreground service). +// // vc=76 / 0.1.0-CJ — expandable-player smoothness pass: // * The detail body no longer renders to an offscreen buffer every // frame during the morph (CompositingStrategy.ModulateAlpha) — that @@ -82,6 +89,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 = 76 -const val STRAW_VERSION_NAME = "0.1.0-CJ" +const val STRAW_VERSION_CODE = 77 +const val STRAW_VERSION_NAME = "0.1.0-CK" 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 c3b008421..f8e0b1535 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 @@ -385,7 +385,7 @@ fun ExpandablePlayer( InlinePlayerSurface( streamUrl = cur.streamUrl, title = cur.title, - controlsEnabled = fullyExpanded, + expanded = fullyExpanded, onFullscreen = { onFullscreen(cur.streamUrl, cur.title) }, ) } @@ -413,17 +413,18 @@ private fun BarIconButton( * The single TextureView-backed PlayerView, plus the resolve→play wiring * that used to live in VideoDetailScreen's InlinePlayer. Renders a * thumbnail + spinner until the shared controller has actually swapped to - * this video, then the live surface. [controlsEnabled] gates the Media3 - * controller overlay (off while collapsed so taps fall through to the - * expand gesture). Honors the Auto-start-playback setting: when off, a - * Play overlay waits for a tap before priming the stream. + * this video. [expanded] = fully expanded + settled: only then do we mount + * the live PlayerView (+ controls). While collapsed or mid-morph we show a + * static poster instead, so the morph never scales a live TextureView frame + * by frame (the sluggishness). Honors Auto-start-playback: when off, a Play + * overlay waits for a tap before priming the stream. */ @OptIn(UnstableApi::class) @Composable private fun InlinePlayerSurface( streamUrl: String, title: String, - controlsEnabled: Boolean, + expanded: Boolean, onFullscreen: () -> Unit, ) { val controller = LocalStrawController.current @@ -533,40 +534,49 @@ private fun InlinePlayerSurface( } CircularProgressIndicator(color = Color.White) } + !expanded -> { + // Collapsed or mid-morph: show the static poster, NOT the live + // TextureView. Scaling a live-playing TextureView through the + // morph's graphicsLayer every frame was the remaining cost — + // only mount the real player once we've settled fully expanded. + // Audio keeps playing via the service the whole time. + val poster = nowPlaying?.thumbnail ?: thumbnail + if (!poster.isNullOrBlank()) { + AsyncImage( + model = poster, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + ) + } + } else -> { AndroidView( factory = { ctx -> // Inflate from XML for a TEXTURE_VIEW surface — a - // SurfaceView won't follow the graphicsLayer scale, - // which is exactly the morph here. + // SurfaceView won't follow the graphicsLayer scale. (LayoutInflater.from(ctx) .inflate(R.layout.inline_player_view, null) as PlayerView) .apply { player = controller - useController = controlsEnabled + useController = true keepScreenOn = true } }, - update = { - it.player = controller - it.useController = controlsEnabled - }, + update = { it.player = controller }, onRelease = { it.player = null }, modifier = Modifier.fillMaxSize(), ) - if (controlsEnabled) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(8.dp) - .size(36.dp) - .clip(RoundedCornerShape(6.dp)) - .background(OverlayChromeColor) - .clickable(onClick = onFullscreen), - contentAlignment = Alignment.Center, - ) { - Text("⛶", color = Color.White) - } + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .size(36.dp) + .clip(RoundedCornerShape(6.dp)) + .background(OverlayChromeColor) + .clickable(onClick = onFullscreen), + contentAlignment = Alignment.Center, + ) { + Text("⛶", color = Color.White) } } }