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.
This commit is contained in:
Jeena 2026-10-15 13:33:12 +00:00
parent b78e7187c0
commit 9ce367b601
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 var playingVideo by rememberSaveable { mutableStateOf<String?>(null) } // shortUUID
// Non-null while the playing video lives on ANOTHER instance (vidiverse results). // Non-null while the playing video lives on ANOTHER instance (vidiverse results).
var playingVideoHost by rememberSaveable { mutableStateOf<String?>(null) } 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 restoreToken by rememberSaveable { mutableStateOf(0) } // increments when playback ends
var drawerOpen by rememberSaveable { mutableStateOf(false) } var drawerOpen by rememberSaveable { mutableStateOf(false) }
val context = androidx.compose.ui.platform.LocalContext.current 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 localHost = AppGraph.tokenStore.baseUrl?.substringAfter("://")?.substringBefore('/')
val host = video.channel.host val host = video.channel.host
playingVideoHost = if (host != null && host != localHost) host else null 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 playingVideo = video.shortUUID
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -273,13 +277,15 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
PlayerScreen( PlayerScreen(
shortUuid = playingVideo!!, shortUuid = playingVideo!!,
sourceHost = playingVideoHost, sourceHost = playingVideoHost,
posterUrl = playingVideoThumb,
onBack = { playingVideo = null }, onBack = { playingVideo = null },
onOpenChannel = { handle, displayName -> onOpenChannel = { handle, displayName ->
playingVideo = null playingVideo = null
channelDetails = handle channelDetails = handle
channelName = displayName 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, onBack: () -> Unit,
/** Set when the video lives on another instance (vidiverse search results). */ /** Set when the video lives on another instance (vidiverse search results). */
sourceHost: String? = null, 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. // From the info sidebar's local PeerTube video links: switches the player over.
onOpenVideo: (String) -> Unit = {}, onOpenVideo: (String) -> Unit = {},
// From the paused/controls overlay's channel icon: (handle, displayName), // From the paused/controls overlay's channel icon: (handle, displayName),
@ -227,15 +230,27 @@ fun PlayerScreen(
} }
when (val s = state) { when (val s = state) {
is PlayerUiState.Loading -> PlayerLoading() is PlayerUiState.Loading -> PlayerLoading(posterUrl)
is PlayerUiState.Error -> PlayerError(s.message, viewModel, onBack) is PlayerUiState.Error -> PlayerError(s.message, viewModel, onBack)
is PlayerUiState.Ready -> PlayerSurface(s, viewModel, shortUuid, onBack, onOpenChannel, onOpenVideo) is PlayerUiState.Ready -> PlayerSurface(s, viewModel, shortUuid, onBack, onOpenChannel, onOpenVideo)
} }
} }
@Composable @Composable
private fun PlayerLoading() { private fun PlayerLoading(posterUrl: String? = null) {
Box(Modifier.fillMaxSize().background(Color.Black), contentAlignment = Alignment.Center) { 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() CircularProgressIndicator()
} }
} }