feat(android): tune frame rate and subtitle rendering

This commit is contained in:
edde746
2026-06-24 01:34:03 +02:00
parent c9dc15b1d8
commit 21c4ab430e
15 changed files with 187 additions and 37 deletions
@@ -3272,6 +3272,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
fps: Float,
videoDurationMs: Long,
extraDelayMs: Long,
videoWidth: Int,
videoHeight: Int,
onComplete: (switched: Boolean) -> Unit
) {
val mgr = frameRateManager
@@ -3279,7 +3281,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
onComplete(false)
return
}
mgr.setVideoFrameRate(fps, videoDurationMs, extraDelayMs, onComplete)
mgr.setVideoFrameRate(fps, videoDurationMs, extraDelayMs, videoWidth, videoHeight, onComplete)
}
fun clearVideoFrameRate() {
@@ -500,22 +500,24 @@ class ExoPlayerPlugin :
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
val extraDelayMs = call.argument<Number>("extraDelayMs")?.toLong() ?: 0L
val videoWidth = call.argument<Number>("videoWidth")?.toInt() ?: 0
val videoHeight = call.argument<Number>("videoHeight")?.toInt() ?: 0
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration, extraDelayMs=$extraDelayMs")
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration, extraDelayMs=$extraDelayMs, video=${videoWidth}x$videoHeight")
val onComplete: (Boolean) -> Unit = { switched -> result.success(switched) }
if (usingMpvFallback) {
val core = mpvCore
if (core == null) {
result.success(false)
} else {
core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
core.setVideoFrameRate(fps, duration, extraDelayMs, videoWidth, videoHeight, onComplete)
}
} else {
val core = playerCore
if (core == null) {
result.success(false)
} else {
core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
core.setVideoFrameRate(fps, duration, extraDelayMs, videoWidth, videoHeight, onComplete)
}
}
}
@@ -886,6 +886,8 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
fps: Float,
videoDurationMs: Long,
extraDelayMs: Long,
videoWidth: Int,
videoHeight: Int,
onComplete: (switched: Boolean) -> Unit
) {
val mgr = frameRateManager
@@ -893,7 +895,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
onComplete(false)
return
}
mgr.setVideoFrameRate(fps, videoDurationMs, extraDelayMs) { switched ->
mgr.setVideoFrameRate(fps, videoDurationMs, extraDelayMs, videoWidth, videoHeight) { switched ->
player?.let {
updateDisplayFpsOverride(it, "frame rate switch, switched=$switched") {
onComplete(switched)
@@ -343,14 +343,16 @@ class MpvPlayerPlugin :
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
val extraDelayMs = call.argument<Number>("extraDelayMs")?.toLong() ?: 0L
val videoWidth = call.argument<Number>("videoWidth")?.toInt() ?: 0
val videoHeight = call.argument<Number>("videoHeight")?.toInt() ?: 0
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration, extraDelayMs=$extraDelayMs")
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration, extraDelayMs=$extraDelayMs, video=${videoWidth}x$videoHeight")
val core = playerCore
if (core == null) {
result.success(false)
return
}
core.setVideoFrameRate(fps, duration, extraDelayMs) { switched ->
core.setVideoFrameRate(fps, duration, extraDelayMs, videoWidth, videoHeight) { switched ->
result.success(switched)
}
}
@@ -37,6 +37,8 @@ class FrameRateManager(
)
private var currentVideoFps: Float = 0f
private var currentVideoWidth: Int = 0
private var currentVideoHeight: Int = 0
private var displayListener: DisplayManager.DisplayListener? = null
private var pendingSettleRunnable: Runnable? = null
private var watchdogRunnable: Runnable? = null
@@ -57,9 +59,13 @@ class FrameRateManager(
fps: Float,
videoDurationMs: Long,
extraDelayMs: Long,
videoWidth: Int = 0,
videoHeight: Int = 0,
onComplete: (switched: Boolean) -> Unit
) {
currentVideoFps = fps
currentVideoWidth = videoWidth
currentVideoHeight = videoHeight
if (fps <= 0f) {
Log.d(TAG, "setVideoFrameRate: Invalid fps ($fps), skipping")
onComplete(false)
@@ -68,7 +74,7 @@ class FrameRateManager(
log(
"request fps=$fps, duration=${videoDurationMs}ms, extraDelayMs=$extraDelayMs, " +
"API=${Build.VERSION.SDK_INT}, currentMode=${currentModeDescription()}"
"video=${videoWidth}x$videoHeight, API=${Build.VERSION.SDK_INT}, currentMode=${currentModeDescription()}"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -208,17 +214,42 @@ class FrameRateManager(
private fun describeSupportedModes(modes: Array<Display.Mode>): String = modes.joinToString(prefix = "[", postfix = "]") { describeMode(it) }
@RequiresApi(Build.VERSION_CODES.M)
private fun findBestModeMatch(fps: Float, currentMode: Display.Mode, supportedModes: Array<Display.Mode>): DisplayModeCandidate? = supportedModes.asSequence()
.filter { mode ->
mode.physicalHeight == currentMode.physicalHeight &&
mode.physicalWidth == currentMode.physicalWidth
}
.mapNotNull { mode -> matchRefreshRate(mode.refreshRate, fps)?.let { DisplayModeCandidate(mode, it) } }
.minWithOrNull(
compareBy<DisplayModeCandidate> { it.match.priority }
.thenBy { it.match.error }
.thenBy { abs(it.mode.refreshRate - currentMode.refreshRate) }
)
private fun findBestModeMatch(
fps: Float,
currentMode: Display.Mode,
supportedModes: Array<Display.Mode>,
videoWidth: Int,
videoHeight: Int
): DisplayModeCandidate? {
// Tier 1 — a matching-refresh mode at the CURRENT resolution: a refresh-only
// switch, the least disruptive (no resolution/HDMI renegotiation).
supportedModes.asSequence()
.filter { it.physicalHeight == currentMode.physicalHeight && it.physicalWidth == currentMode.physicalWidth }
.mapNotNull { mode -> matchRefreshRate(mode.refreshRate, fps)?.let { DisplayModeCandidate(mode, it) } }
.minWithOrNull(
compareBy<DisplayModeCandidate> { it.match.priority }
.thenBy { it.match.error }
.thenBy { abs(it.mode.refreshRate - currentMode.refreshRate) }
)
?.let { return it }
// Tier 2 — no same-resolution match (e.g. a 4K panel with no 4K@24 mode, but a
// 1080p@23.976 mode for 1080p content). Allow a resolution change, but never one
// that downscales the video below its native size (trading detail for cadence).
// Requires known video dimensions; without them keep Tier-1-only behaviour.
if (videoWidth <= 0 || videoHeight <= 0) return null
val currentArea = currentMode.physicalWidth.toLong() * currentMode.physicalHeight
return supportedModes.asSequence()
.filter { it.physicalWidth >= videoWidth && it.physicalHeight >= videoHeight }
.mapNotNull { mode -> matchRefreshRate(mode.refreshRate, fps)?.let { DisplayModeCandidate(mode, it) } }
.minWithOrNull(
// Prefer the resolution closest to the panel's current one (least change,
// keeps panel-native res when a high-res match exists), then refresh match.
compareBy<DisplayModeCandidate> { abs(it.mode.physicalWidth.toLong() * it.mode.physicalHeight - currentArea) }
.thenBy { it.match.priority }
.thenBy { it.match.error }
)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setDisplayMode(fps: Float, extraDelayMs: Long, onComplete: (switched: Boolean) -> Unit) {
@@ -239,9 +270,12 @@ class FrameRateManager(
val currentMode = display.mode
log("supported modes=${describeSupportedModes(supportedModes)}")
val modeMatch = findBestModeMatch(fps, currentMode, supportedModes)
val modeMatch = findBestModeMatch(fps, currentMode, supportedModes, currentVideoWidth, currentVideoHeight)
if (modeMatch == null) {
log("no matching display mode for ${fps}fps at ${currentMode.physicalWidth}x${currentMode.physicalHeight}")
log(
"no matching display mode for ${fps}fps at ${currentMode.physicalWidth}x${currentMode.physicalHeight} " +
"(video=${currentVideoWidth}x$currentVideoHeight)"
)
onComplete(false)
return
}