From fcb3be8e96c55d34211a9972aca12b991aeaf54c Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 15 Oct 2026 13:33:12 +0000 Subject: [PATCH] player: show the card thumbnail as a poster while the video loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../jeena/jtubetv/ui/screens/HomeScreen.kt | 8 +++++++- .../jeena/jtubetv/ui/screens/PlayerScreen.kt | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt index 019e2ff..5f8726b 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt @@ -91,6 +91,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi var playingVideo by rememberSaveable { mutableStateOf(null) } // shortUUID // Non-null while the playing video lives on ANOTHER instance (vidiverse results). var playingVideoHost by rememberSaveable { mutableStateOf(null) } + // Thumbnail of the opened card — shown as the player's poster while it loads. + var playingVideoThumb by rememberSaveable { mutableStateOf(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 }, ) } } diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt index 3ba847b..95bcf0a 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt @@ -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() } }