Merge branch 'feat/frame-rate-matching'
This commit is contained in:
@@ -1,16 +1,26 @@
|
||||
package com.edde746.plezy.mpv
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PixelFormat
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.TextureView
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
interface MpvPlayerDelegate {
|
||||
fun onPropertyChange(name: String, value: Any?)
|
||||
@@ -24,6 +34,7 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MpvPlayerCore"
|
||||
private const val SHORT_VIDEO_LENGTH_MS = 300000L // 5 minutes
|
||||
}
|
||||
|
||||
private var surfaceView: SurfaceView? = null
|
||||
@@ -34,6 +45,11 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
var isInitialized: Boolean = false
|
||||
private set
|
||||
|
||||
// Frame rate matching
|
||||
private var currentVideoFps: Float = 0f
|
||||
private var displayListener: DisplayManager.DisplayListener? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private fun ensureFlutterOverlayOnTop() {
|
||||
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
contentView.post {
|
||||
@@ -300,11 +316,203 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
}
|
||||
}
|
||||
|
||||
// Frame Rate Matching
|
||||
|
||||
private fun getDisplayManager(): DisplayManager {
|
||||
return activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video frame rate for display refresh rate matching.
|
||||
* Based on VLC Android's FrameRateManager implementation.
|
||||
*/
|
||||
fun setVideoFrameRate(fps: Float, videoDurationMs: Long) {
|
||||
currentVideoFps = fps
|
||||
if (fps <= 0f) {
|
||||
Log.d(TAG, "setVideoFrameRate: Invalid fps ($fps), skipping")
|
||||
return
|
||||
}
|
||||
|
||||
val surface = surfaceView?.holder?.surface
|
||||
if (surface == null) {
|
||||
Log.d(TAG, "setVideoFrameRate: Surface not available")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=${videoDurationMs}ms, API=${Build.VERSION.SDK_INT}")
|
||||
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> setFrameRateS(fps, surface, videoDurationMs)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> setFrameRateR(fps, surface)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> setFrameRateM(fps)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear frame rate setting and cleanup display listener.
|
||||
*/
|
||||
fun clearVideoFrameRate() {
|
||||
Log.d(TAG, "clearVideoFrameRate")
|
||||
currentVideoFps = 0f
|
||||
displayListener?.let {
|
||||
getDisplayManager().unregisterDisplayListener(it)
|
||||
displayListener = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and register display listener for mode switch completion.
|
||||
* Resumes playback after display mode change (needed for HDMI/projectors).
|
||||
*/
|
||||
private fun registerDisplayListener() {
|
||||
displayListener?.let {
|
||||
getDisplayManager().unregisterDisplayListener(it)
|
||||
}
|
||||
|
||||
displayListener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(displayId: Int) = Unit
|
||||
override fun onDisplayRemoved(displayId: Int) = Unit
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
// Mode switch may pause playback (HDMI), wait and resume
|
||||
handler.postDelayed({
|
||||
try {
|
||||
val isPaused = MPVLib.getPropertyBoolean("pause")
|
||||
if (isPaused) {
|
||||
Log.d(TAG, "Display changed, resuming playback")
|
||||
MPVLib.setPropertyBoolean("pause", false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to resume playback after display change", e)
|
||||
}
|
||||
}, 2000L) // Wait 2 seconds for mode switch to complete
|
||||
getDisplayManager().unregisterDisplayListener(this)
|
||||
displayListener = null
|
||||
}
|
||||
}
|
||||
getDisplayManager().registerDisplayListener(displayListener, handler)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun setFrameRateR(fps: Float, surface: Surface) {
|
||||
Log.d(TAG, "setFrameRateR: Setting frame rate to $fps")
|
||||
surface.setFrameRate(fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
|
||||
registerDisplayListener()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun setFrameRateS(fps: Float, surface: Surface, videoDurationMs: Long) {
|
||||
Log.d(TAG, "setFrameRateS: fps=$fps, duration=${videoDurationMs}ms")
|
||||
|
||||
// For short videos (<5min), only switch if seamless
|
||||
if (videoDurationMs < SHORT_VIDEO_LENGTH_MS) {
|
||||
Log.d(TAG, "Short video, using seamless-only switching")
|
||||
surface.setFrameRate(
|
||||
fps,
|
||||
Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
|
||||
Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// For longer videos, check if switch will be seamless
|
||||
var seamless = false
|
||||
activity.display?.mode?.alternativeRefreshRates?.let { refreshRates ->
|
||||
for (rate in refreshRates) {
|
||||
// Check if rates match or are integer multiples
|
||||
if (fps.toString().startsWith(rate.toString()) ||
|
||||
rate.toString().startsWith(fps.toString()) ||
|
||||
rate % fps == 0f) {
|
||||
seamless = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seamless) {
|
||||
Log.d(TAG, "Seamless switch available, using CHANGE_FRAME_RATE_ALWAYS")
|
||||
surface.setFrameRate(
|
||||
fps,
|
||||
Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
|
||||
Surface.CHANGE_FRAME_RATE_ALWAYS
|
||||
)
|
||||
registerDisplayListener()
|
||||
} else {
|
||||
// Non-seamless: only switch if user enabled it at OS level
|
||||
val userPreference = getDisplayManager().matchContentFrameRateUserPreference
|
||||
if (userPreference == DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS) {
|
||||
Log.d(TAG, "User preference allows non-seamless switch")
|
||||
surface.setFrameRate(
|
||||
fps,
|
||||
Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
|
||||
Surface.CHANGE_FRAME_RATE_ALWAYS
|
||||
)
|
||||
registerDisplayListener()
|
||||
} else {
|
||||
Log.d(TAG, "Non-seamless switch not allowed by user preference, using seamless-only")
|
||||
surface.setFrameRate(
|
||||
fps,
|
||||
Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
|
||||
Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setFrameRateM(fps: Float) {
|
||||
Log.d(TAG, "setFrameRateM: fps=$fps")
|
||||
val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val display = wm.defaultDisplay ?: return
|
||||
|
||||
display.supportedModes?.let { supportedModes ->
|
||||
val currentMode = display.mode
|
||||
var modeToUse = currentMode
|
||||
|
||||
for (mode in supportedModes) {
|
||||
// Skip modes with different resolution
|
||||
if (mode.physicalHeight != currentMode.physicalHeight ||
|
||||
mode.physicalWidth != currentMode.physicalWidth) {
|
||||
continue
|
||||
}
|
||||
|
||||
Log.d(TAG, "Supported mode: ${mode.modeId} - ${mode.refreshRate}Hz")
|
||||
|
||||
// Check for exact match
|
||||
if (BigDecimal(fps.toString()).setScale(1, RoundingMode.FLOOR) ==
|
||||
BigDecimal(mode.refreshRate.toString()).setScale(1, RoundingMode.FLOOR)) {
|
||||
modeToUse = mode
|
||||
Log.d(TAG, "Found exact match: ${mode.refreshRate}Hz")
|
||||
break
|
||||
}
|
||||
// Check for integer multiple (e.g., 48Hz for 24fps)
|
||||
else if (mode.refreshRate % fps == 0f) {
|
||||
modeToUse = mode
|
||||
Log.d(TAG, "Found integer multiple: ${mode.refreshRate}Hz")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (modeToUse != currentMode) {
|
||||
Log.d(TAG, "Switching to mode ${modeToUse.modeId} (${modeToUse.refreshRate}Hz)")
|
||||
activity.window?.attributes?.let { attrs ->
|
||||
attrs.preferredDisplayModeId = modeToUse.modeId
|
||||
activity.window?.attributes = attrs
|
||||
}
|
||||
registerDisplayListener()
|
||||
} else {
|
||||
Log.d(TAG, "No better mode found, staying at ${currentMode.refreshRate}Hz")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
||||
fun dispose() {
|
||||
Log.d(TAG, "Disposing")
|
||||
|
||||
// Clean up frame rate listener
|
||||
clearVideoFrameRate()
|
||||
|
||||
MPVLib.removeObserver(this)
|
||||
MPVLib.removeLogObserver(this)
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
"observeProperty" -> handleObserveProperty(call, result)
|
||||
"command" -> handleCommand(call, result)
|
||||
"setVisible" -> handleSetVisible(call, result)
|
||||
"setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
|
||||
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
|
||||
"isInitialized" -> result.success(playerCore?.isInitialized ?: false)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
@@ -202,6 +204,21 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
|
||||
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
|
||||
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
|
||||
|
||||
Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration")
|
||||
playerCore?.setVideoFrameRate(fps, duration)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleClearVideoFrameRate(result: MethodChannel.Result) {
|
||||
Log.d(TAG, "clearVideoFrameRate")
|
||||
playerCore?.clearVideoFrameRate()
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// MpvPlayerDelegate
|
||||
|
||||
override fun onPropertyChange(name: String, value: Any?) {
|
||||
|
||||
Reference in New Issue
Block a user