Merge branch 'develop' into remove-optimized-server

This commit is contained in:
Gauvain
2026-01-14 20:08:05 +01:00
committed by GitHub
5 changed files with 657 additions and 60 deletions

View File

@@ -50,6 +50,23 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
private var _isLoading: Boolean = false
private var _playbackSpeed: Double = 1.0
private var isReadyToSeek: Boolean = false
// Progress update throttling - CRITICAL for performance!
// DO NOT REMOVE THIS THROTTLE - it is essential for battery life and CPU efficiency.
//
// Without throttling, time-pos fires every video frame (24+ times/sec at 24fps).
// Each update crosses the React Native JS bridge, which is expensive on mobile.
// Even if the JS side does nothing, 24+ bridge calls/sec wastes CPU and battery.
//
// Throttling to 1 update/sec during normal playback is sufficient for:
// - Progress bar updates (users can't perceive 1-second granularity)
// - Playback position tracking
// - Any JS-side logic that needs current position
//
// During seeking, we bypass the throttle for responsive scrubbing.
// This optimization reduced CPU usage by ~50% for downloaded file playback.
private var lastProgressUpdateTime: Long = 0
private var _isSeeking: Boolean = false
// Video dimensions
private var _videoWidth: Int = 0
@@ -548,7 +565,13 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
}
"time-pos" -> {
cachedPosition = value
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
// Always update immediately when seeking, otherwise throttle to once per second
val now = System.currentTimeMillis()
val shouldUpdate = _isSeeking || (now - lastProgressUpdateTime >= 1000)
if (shouldUpdate) {
lastProgressUpdateTime = now
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
}
}
}
}
@@ -580,7 +603,8 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
}
}
MPVLib.MPV_EVENT_SEEK -> {
// Seek started - show loading indicator
// Seek started - show loading indicator and enable immediate progress updates
_isSeeking = true
if (!_isLoading) {
_isLoading = true
mainHandler.post { delegate?.onLoadingChanged(true) }
@@ -588,6 +612,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
}
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> {
// Video playback has started/restarted (including after seek)
_isSeeking = false
if (_isLoading) {
_isLoading = false
mainHandler.post { delegate?.onLoadingChanged(false) }

View File

@@ -33,23 +33,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
companion object {
private const val TAG = "MpvPlayerView"
/**
* Detect if running on an Android emulator.
* MPV player has EGL/OpenGL compatibility issues on emulators.
*/
private fun isEmulator(): Boolean {
return (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk" == Build.PRODUCT
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu"))
}
}
// Event dispatchers
@@ -104,21 +87,14 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
}
}
// Start the renderer (skip on emulators to avoid EGL crashes)
if (isEmulator()) {
Log.w(TAG, "Running on emulator - MPV player disabled due to EGL/OpenGL compatibility issues")
// Don't start renderer on emulator, will show error when trying to play
} else {
try {
renderer?.start()
} catch (e: Exception) {
Log.e(TAG, "Failed to start renderer: ${e.message}")
onError(mapOf("error" to "Failed to start renderer: ${e.message}"))
}
// Start the renderer
try {
renderer?.start()
} catch (e: Exception) {
Log.e(TAG, "Failed to start renderer: ${e.message}")
onError(mapOf("error" to "Failed to start renderer: ${e.message}"))
}
}
private var isOnEmulator: Boolean = isEmulator()
// MARK: - SurfaceHolder.Callback
@@ -149,13 +125,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
// MARK: - Video Loading
fun loadVideo(config: VideoLoadConfig) {
// Block video loading on emulators
if (isOnEmulator) {
Log.w(TAG, "Cannot load video on emulator - MPV player not supported")
onError(mapOf("error" to "MPV player is not supported on emulators. Please test on a real device."))
return
}
// Skip reload if same URL is already playing
if (currentUrl == config.url) {
return

View File

@@ -48,6 +48,23 @@ final class MPVLayerRenderer {
private var _playbackSpeed: Double = 1.0
private var _isLoading: Bool = false
private var _isReadyToSeek: Bool = false
private var _isSeeking: Bool = false
// Progress update throttling - CRITICAL for performance!
// DO NOT REMOVE THIS THROTTLE - it is essential for battery life and CPU efficiency.
//
// Without throttling, time-pos fires every video frame (24+ times/sec at 24fps).
// Each update crosses the React Native JS bridge, which is expensive on mobile.
// Even if the JS side does nothing, 24+ bridge calls/sec wastes CPU and battery.
//
// Throttling to 1 update/sec during normal playback is sufficient for:
// - Progress bar updates (users can't perceive 1-second granularity)
// - Playback position tracking
// - Any JS-side logic that needs current position
//
// During seeking, we bypass the throttle for responsive scrubbing.
// This optimization reduced CPU usage by ~50% for downloaded file playback.
private var lastProgressUpdateTime: CFAbsoluteTime = 0
// Thread-safe accessors
private var cachedDuration: Double {
@@ -74,6 +91,10 @@ final class MPVLayerRenderer {
get { stateQueue.sync { _isReadyToSeek } }
set { stateQueue.async(flags: .barrier) { self._isReadyToSeek = newValue } }
}
private var isSeeking: Bool {
get { stateQueue.sync { _isSeeking } }
set { stateQueue.async(flags: .barrier) { self._isSeeking = newValue } }
}
var isPausedState: Bool {
return isPaused
@@ -408,7 +429,8 @@ final class MPVLayerRenderer {
}
case MPV_EVENT_SEEK:
// Seek started - show loading indicator
// Seek started - show loading indicator and enable immediate progress updates
isSeeking = true
if !isLoading {
isLoading = true
DispatchQueue.main.async { [weak self] in
@@ -419,6 +441,7 @@ final class MPVLayerRenderer {
case MPV_EVENT_PLAYBACK_RESTART:
// Video playback has started/restarted (including after seek)
isSeeking = false
if isLoading {
isLoading = false
DispatchQueue.main.async { [weak self] in
@@ -469,9 +492,15 @@ final class MPVLayerRenderer {
let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value)
if status >= 0 {
cachedPosition = value
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
// Always update immediately when seeking, otherwise throttle to once per second
let now = CFAbsoluteTimeGetCurrent()
let shouldUpdate = isSeeking || (now - lastProgressUpdateTime >= 1.0)
if shouldUpdate {
lastProgressUpdateTime = now
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
}
}
}
case "pause":