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
This commit is contained in:
Damontecres 2026-05-24 10:11:59 -04:00 committed by GitHub
parent 903c45dfb8
commit 8177700e23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 87 additions and 2 deletions

View file

@ -38,7 +38,9 @@ fun LoadingPage(
focusEnabled: Boolean = true,
) {
val focusRequester = remember { FocusRequester() }
if (focusEnabled) {
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
}
Box(
contentAlignment = Alignment.Center,
modifier =

View file

@ -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

View file

@ -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<PlaybackState> {
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),
}

View file

@ -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) }
}
}
}