feat(android,player): the read-ahead on the seek bar

pstr_mpv observes demuxer-cache-time and hands it over with the rest of
the state, and the seek bar shades from the start to where the demuxer
has read, lighter than the played part. A jump that lands in the shaded
run costs no fetch, as on the desktop.
This commit is contained in:
nvrl committed 2026-10-03 05:17:03 +02:00
1 parent 0bc7cba428
commit cfd654b267
4 files changed
+29 -5

No files matched your search

@@ -75,6 +75,8 @@ data class MpvPlaybackState(
val cachePercent: Double = 0.0,
/** A seek has been issued but playback has not resumed at the new position. */
val seeking: Boolean = false,
/** How far the demuxer has read ahead, in seconds into the file. */
val cachedUntil: Double = 0.0,
) {
/** Whether the picture is stopped for a reason the viewer did not choose. */
val stalled: Boolean get() = (buffering || seeking) && !paused && !ended
@@ -313,6 +315,7 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv
buffering = values[BUFFERING] != 0.0,
cachePercent = values[CACHE_PERCENT],
seeking = values[SEEKING] != 0.0,
cachedUntil = values[CACHED_UNTIL],
)
mutableState.value = next
// The listeners drive the media session and the player UI,
@@ -431,7 +434,7 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv
companion object {
/** Fields `nativeState` fills, in the order `pstr_mpv.cpp` writes them. */
private const val FIELDS = 13
private const val FIELDS = 14
private const val GENERATION = 6
private const val WIDTH = 7
private const val HEIGHT = 8
@@ -439,6 +442,7 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv
private const val BUFFERING = 10
private const val CACHE_PERCENT = 11
private const val SEEKING = 12
private const val CACHED_UNTIL = 13
init {
System.loadLibrary("pstr_android")
@@ -640,7 +640,12 @@ private fun PlayerControls(
scrubbing = null
},
valueRange = 0f..duration,
track = { AccentTrack(it) },
track = {
AccentTrack(
it,
buffered = if (state.duration > 0) (state.cachedUntil / state.duration).toFloat() else 0f,
)
},
)
ChapterMarks(chapters, state.duration, Modifier.matchParentSize())
}
@@ -168,11 +168,21 @@ internal fun AccentProgress(
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AccentTrack(state: SliderState, modifier: Modifier = Modifier) {
internal fun AccentTrack(state: SliderState, modifier: Modifier = Modifier, buffered: Float = 0f) {
val brush = accentBrush()
Canvas(modifier.fillMaxWidth().height(TRACK_HEIGHT.dp)) {
val radius = CornerRadius(size.height / 2)
drawRoundRect(color = Color.White.copy(alpha = 0.30f), cornerRadius = radius)
// What is already read ahead, lighter than the rest and under the
// played part: a jump that lands in it costs no fetch.
val ahead = size.width * buffered.coerceIn(0f, 1f)
if (ahead >= size.height) {
drawRoundRect(
color = Color.White.copy(alpha = 0.55f),
size = Size(ahead, size.height),
cornerRadius = radius,
)
}
val done = size.width * state.coercedValueAsFraction
if (done >= size.height) {
drawRoundRect(brush = brush, size = Size(done, size.height), cornerRadius = radius)