From 976af60185b6cbe9e5d987332ec1754e91b818ce Mon Sep 17 00:00:00 2001 From: Alex <111128610+Alexk2309@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:34:39 +1100 Subject: [PATCH] Add caching progress in seek slider bar (#1376) --- app/(auth)/player/direct-player.tsx | 8 +++++- .../modules/mpvplayer/MPVLayerRenderer.kt | 11 ++++++-- .../expo/modules/mpvplayer/MpvPlayerView.kt | 5 ++-- modules/mpv-player/ios/MPVLayerRenderer.swift | 28 +++++++++++++------ modules/mpv-player/ios/MpvPlayerView.swift | 3 +- modules/mpv-player/src/MpvPlayer.types.ts | 2 ++ 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index efa854e0..b5dcac73 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -449,7 +449,7 @@ export default function page() { async (data: { nativeEvent: MpvOnProgressEventPayload }) => { if (isSeeking.get() || isPlaybackStopped) return; - const { position } = data.nativeEvent; + const { position, cacheSeconds } = data.nativeEvent; // MPV reports position in seconds, convert to ms const currentTime = position * 1000; @@ -459,6 +459,12 @@ export default function page() { progress.set(currentTime); + // Update cache progress (current position + buffered seconds ahead) + if (cacheSeconds !== undefined && cacheSeconds > 0) { + const cacheEnd = currentTime + cacheSeconds * 1000; + cacheProgress.set(cacheEnd); + } + // Update URL immediately after seeking, or every 30 seconds during normal playback const now = Date.now(); const shouldUpdateUrl = wasJustSeeking.get(); diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index 7a6a92f8..83260909 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -26,7 +26,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { } interface Delegate { - fun onPositionChanged(position: Double, duration: Double) + fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) fun onPauseChanged(isPaused: Boolean) fun onLoadingChanged(isLoading: Boolean) fun onReadyToSeek() @@ -46,6 +46,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { // Cached state private var cachedPosition: Double = 0.0 private var cachedDuration: Double = 0.0 + private var cachedCacheSeconds: Double = 0.0 private var _isPaused: Boolean = true private var _isLoading: Boolean = false private var _playbackSpeed: Double = 1.0 @@ -283,6 +284,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.observeProperty("pause", MPV_FORMAT_FLAG) MPVLib.observeProperty("track-list/count", MPV_FORMAT_INT64) MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG) + MPVLib.observeProperty("demuxer-cache-duration", MPV_FORMAT_DOUBLE) // Video dimensions for PiP aspect ratio MPVLib.observeProperty("video-params/w", MPV_FORMAT_INT64) MPVLib.observeProperty("video-params/h", MPV_FORMAT_INT64) @@ -561,7 +563,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { when (property) { "duration" -> { cachedDuration = value - mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) } + mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) } } "time-pos" -> { cachedPosition = value @@ -570,9 +572,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { val shouldUpdate = _isSeeking || (now - lastProgressUpdateTime >= 1000) if (shouldUpdate) { lastProgressUpdateTime = now - mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) } + mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) } } } + "demuxer-cache-duration" -> { + cachedCacheSeconds = value + } } } diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt index ac0b1276..5b8e2dd3 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt @@ -307,7 +307,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context // MARK: - MPVLayerRenderer.Delegate - override fun onPositionChanged(position: Double, duration: Double) { + override fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) { cachedPosition = position cachedDuration = duration @@ -319,7 +319,8 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context onProgress(mapOf( "position" to position, "duration" to duration, - "progress" to if (duration > 0) position / duration else 0.0 + "progress" to if (duration > 0) position / duration else 0.0, + "cacheSeconds" to cacheSeconds )) } diff --git a/modules/mpv-player/ios/MPVLayerRenderer.swift b/modules/mpv-player/ios/MPVLayerRenderer.swift index af346763..deb58b5e 100644 --- a/modules/mpv-player/ios/MPVLayerRenderer.swift +++ b/modules/mpv-player/ios/MPVLayerRenderer.swift @@ -5,7 +5,7 @@ import CoreVideo import AVFoundation protocol MPVLayerRendererDelegate: AnyObject { - func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double) + func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double) func renderer(_ renderer: MPVLayerRenderer, didChangePause isPaused: Bool) func renderer(_ renderer: MPVLayerRenderer, didChangeLoading isLoading: Bool) func renderer(_ renderer: MPVLayerRenderer, didBecomeReadyToSeek: Bool) @@ -44,6 +44,7 @@ final class MPVLayerRenderer { // Thread-safe state for playback private var _cachedDuration: Double = 0 private var _cachedPosition: Double = 0 + private var _cachedCacheSeconds: Double = 0 private var _isPaused: Bool = true private var _playbackSpeed: Double = 1.0 private var _isLoading: Bool = false @@ -75,6 +76,10 @@ final class MPVLayerRenderer { get { stateQueue.sync { _cachedPosition } } set { stateQueue.async(flags: .barrier) { self._cachedPosition = newValue } } } + private var cachedCacheSeconds: Double { + get { stateQueue.sync { _cachedCacheSeconds } } + set { stateQueue.async(flags: .barrier) { self._cachedCacheSeconds = newValue } } + } private var isPaused: Bool { get { stateQueue.sync { _isPaused } } set { stateQueue.async(flags: .barrier) { self._isPaused = newValue } } @@ -162,16 +167,16 @@ final class MPVLayerRenderer { // Use AVFoundation video output - required for PiP support checkError(mpv_set_option_string(handle, "vo", "avfoundation")) - // Enable composite OSD mode - renders subtitles directly onto video frames using GPU - // This is better for PiP as subtitles are baked into the video - checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes")) - // Hardware decoding with VideoToolbox // On simulator, use software decoding since VideoToolbox is not available // On device, use VideoToolbox with software fallback enabled #if targetEnvironment(simulator) checkError(mpv_set_option_string(handle, "hwdec", "no")) #else + // Only enable composite OSD mode on real device (OSD is not supported in simulator). + // This renders subtitles directly onto video frames using the GPU, which is better for PiP since subtitles are baked into the video. + checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes")) + checkError(mpv_set_option_string(handle, "hwdec", "videotoolbox")) #endif checkError(mpv_set_option_string(handle, "hwdec-codecs", "all")) @@ -340,7 +345,8 @@ final class MPVLayerRenderer { ("time-pos", MPV_FORMAT_DOUBLE), ("pause", MPV_FORMAT_FLAG), ("track-list/count", MPV_FORMAT_INT64), - ("paused-for-cache", MPV_FORMAT_FLAG) + ("paused-for-cache", MPV_FORMAT_FLAG), + ("demuxer-cache-duration", MPV_FORMAT_DOUBLE) ] for (name, format) in properties { mpv_observe_property(handle, 0, name, format) @@ -484,7 +490,7 @@ final class MPVLayerRenderer { cachedDuration = value DispatchQueue.main.async { [weak self] in guard let self else { return } - self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds) } } case "time-pos": @@ -499,10 +505,16 @@ final class MPVLayerRenderer { lastProgressUpdateTime = now DispatchQueue.main.async { [weak self] in guard let self else { return } - self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds) } } } + case "demuxer-cache-duration": + var value = Double(0) + let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value) + if status >= 0 { + cachedCacheSeconds = value + } case "pause": var flag: Int32 = 0 let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_FLAG, value: &flag) diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index 608448b8..89502a9a 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -298,7 +298,7 @@ class MpvPlayerView: ExpoView { // MARK: - MPVLayerRendererDelegate extension MpvPlayerView: MPVLayerRendererDelegate { - func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double) { + func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double) { cachedPosition = position cachedDuration = duration @@ -313,6 +313,7 @@ extension MpvPlayerView: MPVLayerRendererDelegate { "position": position, "duration": duration, "progress": duration > 0 ? position / duration : 0, + "cacheSeconds": cacheSeconds, ]) } } diff --git a/modules/mpv-player/src/MpvPlayer.types.ts b/modules/mpv-player/src/MpvPlayer.types.ts index dc25007b..23f86093 100644 --- a/modules/mpv-player/src/MpvPlayer.types.ts +++ b/modules/mpv-player/src/MpvPlayer.types.ts @@ -15,6 +15,8 @@ export type OnProgressEventPayload = { position: number; duration: number; progress: number; + /** Seconds of video buffered ahead of current position */ + cacheSeconds: number; }; export type OnErrorEventPayload = {