Compare commits

...

4 Commits

Author SHA1 Message Date
renovate[bot]
3bb61ae94b chore(deps): Update dependency react-native-worklets to v0.7.2 2026-01-30 09:40:35 +00:00
Fredrik Burmester
358e00d8b7 fix(player): resolve tvOS freeze on player exit by reordering mpv options
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
2026-01-19 08:41:52 +01:00
lance chant
c7077bbcfe fix: subrip mpv (#1375)
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-01-18 19:35:14 +01:00
Alex
c0f25a2b8b Add caching progress in seek slider bar (#1376) 2026-01-18 19:34:39 +01:00
9 changed files with 98 additions and 17 deletions

View File

@@ -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();

View File

@@ -88,7 +88,7 @@
"react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"react-native-worklets": "0.7.2",
"sonner-native": "0.21.2",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
@@ -258,7 +258,7 @@
"@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="],
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
"@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="],
@@ -1708,7 +1708,7 @@
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
"react-native-worklets": ["react-native-worklets@0.5.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w=="],
"react-native-worklets": ["react-native-worklets@0.7.2", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
@@ -2214,6 +2214,8 @@
"@react-native/babel-preset/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
"@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
"@react-native/babel-preset/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@react-native/codegen/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
@@ -2436,7 +2438,9 @@
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
"react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"react-native-worklets/@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
"react-native-worklets/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],

Binary file not shown.

View File

@@ -1,10 +1,13 @@
package expo.modules.mpvplayer
import android.content.Context
import android.content.res.AssetManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import java.io.File
import java.io.FileOutputStream
/**
* MPV renderer that wraps libmpv for video playback.
@@ -26,7 +29,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 +49,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
@@ -101,6 +105,52 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
MPVLib.create(context)
MPVLib.addObserver(this)
/**
* Create mpv config directory and copy font files to ensure SubRip subtitles load properly on Android.
*
* Technical Background:
* ====================
* On Android, mpv requires access to a font file to render text-based subtitles, particularly SubRip (.srt)
* format subtitles. Without an available font in the config directory, mpv will fail to display subtitles
* even when subtitle tracks are properly detected and loaded.
*
* Why This Is Necessary:
* =====================
* 1. Android's font system is isolated from native libraries like mpv. While Android has system fonts,
* mpv cannot access them directly due to sandboxing and library isolation.
*
* 2. SubRip subtitles require a font to render text overlay on video. When no font is available in the
* configured directory, mpv either:
* - Fails silently (subtitles don't appear)
* - Falls back to a default font that may not support the required character set
* - Crashes or produces rendering errors
*
* 3. By placing a font file (font.ttf) in mpv's config directory and setting that directory via
* MPVLib.setOptionString("config-dir", ...), we ensure mpv has a known, accessible font source.
*
* Reference:
* =========
* This workaround is documented in the mpv-android project:
* https://github.com/mpv-android/mpv-android/issues/96
*
* The issue discusses that without a font in the config directory, SubRip subtitles fail to load
* properly on Android, and the solution is to copy a font file to a known location that mpv can access.
*/
// Create mpv config directory and copy font files
val mpvDir = File(context.getExternalFilesDir(null) ?: context.filesDir, "mpv")
//Log.i(TAG, "mpv config dir: $mpvDir")
if (!mpvDir.exists()) mpvDir.mkdirs()
// This needs to be named `subfont.ttf` else it won't work
arrayOf("subfont.ttf").forEach { fileName ->
val file = File(mpvDir, fileName)
if (file.exists()) return@forEach
context.assets
.open(fileName, AssetManager.ACCESS_STREAMING)
.copyTo(FileOutputStream(file))
}
MPVLib.setOptionString("config", "yes")
MPVLib.setOptionString("config-dir", mpvDir.path)
// Configure mpv options before initialization (based on Findroid)
MPVLib.setOptionString("vo", "gpu")
MPVLib.setOptionString("gpu-context", "android")
@@ -124,7 +174,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
MPVLib.setOptionString("hr-seek-framedrop", "yes")
// Subtitle settings
MPVLib.setOptionString("sub-scale-with-window", "yes")
MPVLib.setOptionString("sub-scale-with-window", "no")
MPVLib.setOptionString("sub-use-margins", "no")
MPVLib.setOptionString("subs-match-os-language", "yes")
MPVLib.setOptionString("subs-fallback", "yes")
@@ -283,6 +333,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 +612,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 +621,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
}
}
}

View File

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

View File

@@ -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 } }
@@ -164,6 +169,7 @@ final class MPVLayerRenderer {
// Enable composite OSD mode - renders subtitles directly onto video frames using GPU
// This is better for PiP as subtitles are baked into the video
// NOTE: Must be set BEFORE the #if targetEnvironment check or tvOS will freeze on player exit
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes"))
// Hardware decoding with VideoToolbox
@@ -340,7 +346,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 +491,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 +506,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)

View File

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

View File

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

View File

@@ -108,7 +108,7 @@
"react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"react-native-worklets": "0.7.2",
"sonner-native": "0.21.2",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",