player: show the card thumbnail as a poster while the video loads

Opening a video flashed plain black during the metadata-loading phase (before
buffering, where the thumbnail already shows) — most visible on the TV where
that phase is longer. Pass the opened card's thumbnail into the player and paint
it full-screen (dimmed, under the spinner) during loading, handing off to the
buffering thumbnail once the stream is ready, so the player opens onto the
video's own poster instead of black.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jeena 2026-10-15 13:33:12 +00:00
parent 46a325eed3
commit fcb3be8e96
2 changed files with 24 additions and 3 deletions

View file

@ -91,6 +91,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
var playingVideo by rememberSaveable { mutableStateOf<String?>(null) } // shortUUID
// Non-null while the playing video lives on ANOTHER instance (vidiverse results).
var playingVideoHost by rememberSaveable { mutableStateOf<String?>(null) }
// Thumbnail of the opened card — shown as the player's poster while it loads.
var playingVideoThumb by rememberSaveable { mutableStateOf<String?>(null) }
var restoreToken by rememberSaveable { mutableStateOf(0) } // increments when playback ends
var drawerOpen by rememberSaveable { mutableStateOf(false) }
val context = androidx.compose.ui.platform.LocalContext.current
@ -175,6 +177,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
val localHost = AppGraph.tokenStore.baseUrl?.substringAfter("://")?.substringBefore('/')
val host = video.channel.host
playingVideoHost = if (host != null && host != localHost) host else null
playingVideoThumb = video.thumbnailUrl
?: video.thumbnailPath?.takeIf { it.isNotBlank() }?.let { AppGraph.repository.absoluteUrl(it) }
playingVideo = video.shortUUID
}
Box(modifier = Modifier.fillMaxSize()) {
@ -273,13 +277,15 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
PlayerScreen(
shortUuid = playingVideo!!,
sourceHost = playingVideoHost,
posterUrl = playingVideoThumb,
onBack = { playingVideo = null },
onOpenChannel = { handle, displayName ->
playingVideo = null
channelDetails = handle
channelName = displayName
},
onOpenVideo = { playingVideo = it; playingVideoHost = null },
// A link-opened video has no card thumbnail to pose with.
onOpenVideo = { playingVideo = it; playingVideoHost = null; playingVideoThumb = null },
)
}
}

View file

@ -207,6 +207,9 @@ fun PlayerScreen(
onBack: () -> Unit,
/** Set when the video lives on another instance (vidiverse search results). */
sourceHost: String? = null,
/** Thumbnail of the opened card, shown full-screen while the metadata loads so
* the player doesn't flash black before buffering starts (user report). */
posterUrl: String? = null,
// From the info sidebar's local PeerTube video links: switches the player over.
onOpenVideo: (String) -> Unit = {},
// From the paused/controls overlay's channel icon: (handle, displayName),
@ -227,15 +230,27 @@ fun PlayerScreen(
}
when (val s = state) {
is PlayerUiState.Loading -> PlayerLoading()
is PlayerUiState.Loading -> PlayerLoading(posterUrl)
is PlayerUiState.Error -> PlayerError(s.message, viewModel, onBack)
is PlayerUiState.Ready -> PlayerSurface(s, viewModel, shortUuid, onBack, onOpenChannel, onOpenVideo)
}
}
@Composable
private fun PlayerLoading() {
private fun PlayerLoading(posterUrl: String? = null) {
Box(Modifier.fillMaxSize().background(Color.Black), contentAlignment = Alignment.Center) {
// Show the card's thumbnail (dimmed) while the metadata resolves, so the
// player opens onto the video's own poster instead of plain black; it
// hands off seamlessly to the buffering thumbnail once the stream is ready.
if (posterUrl != null) {
coil.compose.AsyncImage(
model = posterUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
Box(Modifier.fillMaxSize().background(Color(0xB3000000)))
}
CircularProgressIndicator()
}
}