From 8177700e230f0dffdb3bad3d8fc2bbca9f54e55c Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sun, 24 May 2026 10:11:59 -0400 Subject: [PATCH] Show loading indicator when video is buffering (#1441) ## Description Shows a loading indicator and slightly dim the screen when playback is buffering. Showing the indicator is delayed in case the buffering doesn't take very long. Dev note: the changes to `MpvPlayer` need to be ported to wholphin-extensions as well. ### Related issues Closes #1411 ### Testing Emulator ## Screenshots [buffering.webm](https://github.com/user-attachments/assets/dd848696-d6de-4f68-94e4-901bb5db1185) ## AI or LLM usage None --- .../ui/components/CircularProgress.kt | 4 +- .../wholphin/ui/playback/PlaybackPage.kt | 33 ++++++++++++++- .../wholphin/ui/playback/PlaybackState.kt | 42 +++++++++++++++++++ .../wholphin/util/mpv/MpvPlayer.kt | 10 +++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt index b62c24c7..4e3b13ab 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt @@ -38,7 +38,9 @@ fun LoadingPage( focusEnabled: Boolean = true, ) { val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + if (focusEnabled) { + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + } Box( contentAlignment = Alignment.Center, modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 5e748396..f8610e71 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -1,14 +1,17 @@ package com.github.damontecres.wholphin.ui.playback import android.view.Gravity -import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.annotation.Dimension import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box @@ -72,6 +75,7 @@ import com.github.damontecres.wholphin.preferences.AssPlaybackMode import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.skipBackOnResume +import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -216,6 +220,18 @@ fun PlaybackPageContent( } val presentationState = rememberPresentationState(player, false) + val playbackState by rememberPlaybackState(player) + var showBuffering by remember { mutableStateOf(false) } + LaunchedEffect(playbackState) { + if (playbackState == PlaybackState.BUFFERING) { + // Delay before showing the loading indicator + // So if buffering is quick, the UI won't flash + delay(250) + showBuffering = true + } else { + showBuffering = false + } + } val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp) val focusRequester = remember { FocusRequester() } @@ -349,6 +365,21 @@ fun PlaybackPageContent( ) { LoadingPage(focusEnabled = false) } + } else { + AnimatedVisibility( + visible = showBuffering, + enter = fadeIn(tween(easing = LinearEasing)), + exit = fadeOut(tween(easing = LinearEasing)), + modifier = Modifier.matchParentSize(), + ) { + LoadingPage( + focusEnabled = false, + modifier = + Modifier + .fillMaxSize() + .background(AppColors.TransparentBlack25), + ) + } } // If D-pad skipping, show the amount skipped in an animation diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt new file mode 100644 index 00000000..8bd980be --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt @@ -0,0 +1,42 @@ +package com.github.damontecres.wholphin.ui.playback + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.media3.common.Player +import androidx.media3.common.listenTo + +/** + * Remembers the [Player]'s state as it changes. Useful for changing UI if the player is buffering. + * + * @see Player.State + * @see PlaybackState + */ +@Composable +fun rememberPlaybackState(player: Player): State { + val state = remember(player) { mutableStateOf(getPlaybackState(player.playbackState)) } + LaunchedEffect(player) { + player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED) { + state.value = getPlaybackState(player.playbackState) + } + } + return state +} + +private fun getPlaybackState( + @Player.State value: Int, +): PlaybackState = PlaybackState.entries.first { it.value == value } + +/** + * Represents [Player.State] integers as an Enum + */ +enum class PlaybackState( + @param:Player.State val value: Int, +) { + IDLE(Player.STATE_IDLE), + BUFFERING(Player.STATE_BUFFERING), + READY(Player.STATE_READY), + ENDED(Player.STATE_ENDED), +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt index 77613be3..f0792aa3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt @@ -620,6 +620,16 @@ class MpvPlayer( } notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) } } + + MPVProperty.PAUSED_FOR_CACHE -> { + Timber.v("paused-for-cache %s", value) + playbackState.update { + it.copy( + state = if (value) STATE_BUFFERING else it.state, + ) + } + notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(playbackState.load().state) } + } } }