Expandable player: static poster during collapse/morph, live player only when expanded — vc=77

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.
This commit is contained in:
Sulkta 2026-06-20 17:02:03 -07:00
parent d7f86ebf14
commit 05381e8d8b
2 changed files with 45 additions and 28 deletions

View file

@ -9,6 +9,13 @@ const val STRAW_SDK_TARGET = 35
// Sulkta fork — Straw // 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: // vc=76 / 0.1.0-CJ — expandable-player smoothness pass:
// * The detail body no longer renders to an offscreen buffer every // * The detail body no longer renders to an offscreen buffer every
// frame during the morph (CompositingStrategy.ModulateAlpha) — that // 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 // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
// strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no // strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path. // NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 76 const val STRAW_VERSION_CODE = 77
const val STRAW_VERSION_NAME = "0.1.0-CJ" const val STRAW_VERSION_NAME = "0.1.0-CK"
const val STRAW_APPLICATION_ID = "com.sulkta.straw" const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -385,7 +385,7 @@ fun ExpandablePlayer(
InlinePlayerSurface( InlinePlayerSurface(
streamUrl = cur.streamUrl, streamUrl = cur.streamUrl,
title = cur.title, title = cur.title,
controlsEnabled = fullyExpanded, expanded = fullyExpanded,
onFullscreen = { onFullscreen(cur.streamUrl, cur.title) }, onFullscreen = { onFullscreen(cur.streamUrl, cur.title) },
) )
} }
@ -413,17 +413,18 @@ private fun BarIconButton(
* The single TextureView-backed PlayerView, plus the resolveplay wiring * The single TextureView-backed PlayerView, plus the resolveplay wiring
* that used to live in VideoDetailScreen's InlinePlayer. Renders a * that used to live in VideoDetailScreen's InlinePlayer. Renders a
* thumbnail + spinner until the shared controller has actually swapped to * thumbnail + spinner until the shared controller has actually swapped to
* this video, then the live surface. [controlsEnabled] gates the Media3 * this video. [expanded] = fully expanded + settled: only then do we mount
* controller overlay (off while collapsed so taps fall through to the * the live PlayerView (+ controls). While collapsed or mid-morph we show a
* expand gesture). Honors the Auto-start-playback setting: when off, a * static poster instead, so the morph never scales a live TextureView frame
* Play overlay waits for a tap before priming the stream. * by frame (the sluggishness). Honors Auto-start-playback: when off, a Play
* overlay waits for a tap before priming the stream.
*/ */
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
private fun InlinePlayerSurface( private fun InlinePlayerSurface(
streamUrl: String, streamUrl: String,
title: String, title: String,
controlsEnabled: Boolean, expanded: Boolean,
onFullscreen: () -> Unit, onFullscreen: () -> Unit,
) { ) {
val controller = LocalStrawController.current val controller = LocalStrawController.current
@ -533,40 +534,49 @@ private fun InlinePlayerSurface(
} }
CircularProgressIndicator(color = Color.White) 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 -> { else -> {
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
// Inflate from XML for a TEXTURE_VIEW surface — a // Inflate from XML for a TEXTURE_VIEW surface — a
// SurfaceView won't follow the graphicsLayer scale, // SurfaceView won't follow the graphicsLayer scale.
// which is exactly the morph here.
(LayoutInflater.from(ctx) (LayoutInflater.from(ctx)
.inflate(R.layout.inline_player_view, null) as PlayerView) .inflate(R.layout.inline_player_view, null) as PlayerView)
.apply { .apply {
player = controller player = controller
useController = controlsEnabled useController = true
keepScreenOn = true keepScreenOn = true
} }
}, },
update = { update = { it.player = controller },
it.player = controller
it.useController = controlsEnabled
},
onRelease = { it.player = null }, onRelease = { it.player = null },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
if (controlsEnabled) { Box(
Box( modifier = Modifier
modifier = Modifier .align(Alignment.TopEnd)
.align(Alignment.TopEnd) .padding(8.dp)
.padding(8.dp) .size(36.dp)
.size(36.dp) .clip(RoundedCornerShape(6.dp))
.clip(RoundedCornerShape(6.dp)) .background(OverlayChromeColor)
.background(OverlayChromeColor) .clickable(onClick = onFullscreen),
.clickable(onClick = onFullscreen), contentAlignment = Alignment.Center,
contentAlignment = Alignment.Center, ) {
) { Text("", color = Color.White)
Text("", color = Color.White)
}
} }
} }
} }