|
|
|
@@ -2,6 +2,8 @@ package com.edde746.plezy.mpv
|
|
|
|
|
|
|
|
|
|
import android.app.Activity
|
|
|
|
|
import android.graphics.Color
|
|
|
|
|
import android.graphics.PixelFormat
|
|
|
|
|
import android.media.ImageReader
|
|
|
|
|
import android.os.Handler
|
|
|
|
|
import android.os.Looper
|
|
|
|
|
import android.util.Log
|
|
|
|
@@ -17,6 +19,8 @@ import com.edde746.plezy.shared.FrameRateManager
|
|
|
|
|
import com.edde746.plezy.shared.PlayerDelegate
|
|
|
|
|
import dev.jdtech.mpv.*
|
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
|
|
|
|
|
|
|
|
|
class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
|
|
|
|
@@ -28,13 +32,18 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
private var surfaceContainer: android.widget.FrameLayout? = null
|
|
|
|
|
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
|
|
|
@Volatile private var disposing: Boolean = false
|
|
|
|
|
private var pendingSurface: Surface? = null
|
|
|
|
|
private var lastSurfaceSize: String? = null
|
|
|
|
|
@Volatile private var pendingSurface: Surface? = null
|
|
|
|
|
@Volatile private var attachedSurface: Surface? = null
|
|
|
|
|
private var placeholderImageReader: ImageReader? = null
|
|
|
|
|
@Volatile private var placeholderSurface: Surface? = null
|
|
|
|
|
@Volatile private var lastAppliedSurfaceSize: String? = null
|
|
|
|
|
@Volatile private var lastKnownSurfaceWidth: Int = 0
|
|
|
|
|
@Volatile private var lastKnownSurfaceHeight: Int = 0
|
|
|
|
|
var delegate: PlayerDelegate? = null
|
|
|
|
|
var isInitialized: Boolean = false
|
|
|
|
|
private set
|
|
|
|
|
|
|
|
|
|
private var player: MpvPlayer? = null
|
|
|
|
|
@Volatile private var player: MpvPlayer? = null
|
|
|
|
|
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
|
|
|
|
|
|
|
|
// Frame rate matching
|
|
|
|
@@ -44,6 +53,16 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
// Audio focus
|
|
|
|
|
private var audioFocusManager: AudioFocusManager? = null
|
|
|
|
|
@Volatile private var cachedPaused: Boolean = true
|
|
|
|
|
@Volatile private var pausedForSurfaceLoss: Boolean = false
|
|
|
|
|
@Volatile private var hasAttachedSurface: Boolean = false
|
|
|
|
|
@Volatile private var attachedToPlaceholder: Boolean = false
|
|
|
|
|
@Volatile private var videoOutputRestoring: Boolean = false
|
|
|
|
|
@Volatile private var deferredResumeRequested: Boolean = false
|
|
|
|
|
@Volatile private var resumeBlockedByPublicPause: Boolean = false
|
|
|
|
|
@Volatile private var videoOutputEpoch: Long = 0L
|
|
|
|
|
private val videoOutputMutex = Mutex()
|
|
|
|
|
private var pendingVideoOutputDisableJob: Job? = null
|
|
|
|
|
private var pendingVideoOutputRefreshJob: Job? = null
|
|
|
|
|
|
|
|
|
|
private var flutterOverlayApplied = false
|
|
|
|
|
|
|
|
|
@@ -63,6 +82,14 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun ensurePlaceholderSurface() {
|
|
|
|
|
if (placeholderSurface?.isValid == true) return
|
|
|
|
|
placeholderImageReader?.close()
|
|
|
|
|
placeholderImageReader = ImageReader.newInstance(1, 1, PixelFormat.RGBA_8888, 2)
|
|
|
|
|
placeholderSurface = placeholderImageReader?.surface
|
|
|
|
|
Log.d(TAG, "Created MPV placeholder surface")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun initialize(onResult: (Boolean) -> Unit) {
|
|
|
|
|
if (isInitialized) {
|
|
|
|
|
Log.d(TAG, "Already initialized")
|
|
|
|
@@ -73,7 +100,21 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
try {
|
|
|
|
|
disposing = false
|
|
|
|
|
cachedPaused = true
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
attachedSurface = null
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
hasAttachedSurface = false
|
|
|
|
|
videoOutputRestoring = false
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
resumeBlockedByPublicPause = false
|
|
|
|
|
videoOutputEpoch = 0L
|
|
|
|
|
pendingVideoOutputDisableJob?.cancel()
|
|
|
|
|
pendingVideoOutputDisableJob = null
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
lastKnownSurfaceWidth = 0
|
|
|
|
|
lastKnownSurfaceHeight = 0
|
|
|
|
|
ensurePlaceholderSurface()
|
|
|
|
|
|
|
|
|
|
// Initialize audio focus handling
|
|
|
|
|
audioFocusManager = AudioFocusManager(
|
|
|
|
@@ -86,10 +127,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onResume = {
|
|
|
|
|
scope.launch {
|
|
|
|
|
try { player?.setProperty("pause", false) }
|
|
|
|
|
catch (e: Exception) { Log.w(TAG, "Failed to resume after focus gain", e) }
|
|
|
|
|
}
|
|
|
|
|
requestAutoResume("audio focus gain")
|
|
|
|
|
},
|
|
|
|
|
isPaused = { cachedPaused }
|
|
|
|
|
)
|
|
|
|
@@ -97,16 +135,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
activity = activity,
|
|
|
|
|
handler = handler,
|
|
|
|
|
onDisplayChanged = {
|
|
|
|
|
scope.launch {
|
|
|
|
|
try {
|
|
|
|
|
if (player?.getFlag("pause") == true) {
|
|
|
|
|
Log.d(TAG, "Display changed, resuming playback")
|
|
|
|
|
player?.setProperty("pause", false)
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to resume after display change", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
requestAutoResume("display change")
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -176,9 +205,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
player = p
|
|
|
|
|
isInitialized = true
|
|
|
|
|
|
|
|
|
|
// Attach pending surface
|
|
|
|
|
pendingSurface?.takeIf { it.isValid }?.let { attachSurfaceInternal(it) }
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
refreshVideoOutput("initialize")
|
|
|
|
|
|
|
|
|
|
// Start collecting events/properties/logs
|
|
|
|
|
collectEvents(p)
|
|
|
|
@@ -263,66 +290,299 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
if (disposing) return
|
|
|
|
|
|
|
|
|
|
val surface = holder.surface
|
|
|
|
|
pendingSurface = surface.takeIf { it.isValid }
|
|
|
|
|
pendingVideoOutputDisableJob?.cancel()
|
|
|
|
|
videoOutputEpoch += 1L
|
|
|
|
|
rememberCurrentSurfaceSize()
|
|
|
|
|
if (player == null) {
|
|
|
|
|
pendingSurface = surface
|
|
|
|
|
Log.d(TAG, "Deferring surface attach until MPV init completes")
|
|
|
|
|
Log.d(TAG, "Deferring video output refresh until MPV init completes")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachSurfaceInternal(surface)
|
|
|
|
|
flutterOverlayApplied = false
|
|
|
|
|
ensureFlutterOverlayOnTop()
|
|
|
|
|
refreshVideoOutput("surfaceCreated")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
|
|
|
Log.d(TAG, "Surface changed: ${width}x${height}")
|
|
|
|
|
applySurfaceSize(width, height)
|
|
|
|
|
rememberSurfaceSize(width, height)
|
|
|
|
|
refreshVideoOutput("surfaceChanged")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
|
|
|
Log.d(TAG, "Surface destroyed")
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
if (player == null || disposing) return
|
|
|
|
|
detachSurfaceInternal()
|
|
|
|
|
detachSurfaceInternal(reason = "surfaceDestroyed")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun attachSurfaceInternal(surface: Surface) {
|
|
|
|
|
val p = player ?: return
|
|
|
|
|
if (disposing || !surface.isValid) return
|
|
|
|
|
try {
|
|
|
|
|
p.attachSurface(surface)
|
|
|
|
|
scope.launch {
|
|
|
|
|
p.setProperty("force-window", "yes")
|
|
|
|
|
p.setProperty("vo", "gpu")
|
|
|
|
|
private fun rememberSurfaceSize(width: Int, height: Int) {
|
|
|
|
|
if (width <= 0 || height <= 0) return
|
|
|
|
|
lastKnownSurfaceWidth = width
|
|
|
|
|
lastKnownSurfaceHeight = height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun rememberCurrentSurfaceSize() {
|
|
|
|
|
val sv = surfaceView ?: return
|
|
|
|
|
rememberSurfaceSize(sv.width, sv.height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun currentCandidateSurface(): Surface? =
|
|
|
|
|
surfaceView?.holder?.surface?.takeIf { it.isValid }
|
|
|
|
|
?: pendingSurface?.takeIf { it.isValid }
|
|
|
|
|
|
|
|
|
|
private fun hasAttachedRealSurface(): Boolean =
|
|
|
|
|
hasAttachedSurface && !attachedToPlaceholder && (attachedSurface?.isValid == true)
|
|
|
|
|
|
|
|
|
|
private fun hasReadyVideoOutput(): Boolean =
|
|
|
|
|
hasAttachedRealSurface() && !videoOutputRestoring
|
|
|
|
|
|
|
|
|
|
private fun isCurrentVideoOutputEpoch(epoch: Long): Boolean =
|
|
|
|
|
!disposing && epoch == videoOutputEpoch
|
|
|
|
|
|
|
|
|
|
private fun isVideoOutputRefreshCurrent(epoch: Long): Boolean {
|
|
|
|
|
if (disposing) return false
|
|
|
|
|
if (epoch != videoOutputEpoch) return false
|
|
|
|
|
return hasAttachedRealSurface()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun refreshVideoOutput(reason: String) {
|
|
|
|
|
if (disposing) return
|
|
|
|
|
|
|
|
|
|
rememberCurrentSurfaceSize()
|
|
|
|
|
val p = player
|
|
|
|
|
val surface = currentCandidateSurface()
|
|
|
|
|
if (p == null) {
|
|
|
|
|
pendingSurface = surface?.takeIf { it.isValid }
|
|
|
|
|
Log.d(TAG, "refreshVideoOutput($reason): player not ready yet")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (surface == null || !surface.isValid) {
|
|
|
|
|
hasAttachedSurface = false
|
|
|
|
|
attachedSurface = null
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
videoOutputRestoring = true
|
|
|
|
|
Log.d(TAG, "refreshVideoOutput($reason): no valid surface available")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val refreshEpoch = videoOutputEpoch
|
|
|
|
|
pendingVideoOutputDisableJob?.cancel()
|
|
|
|
|
videoOutputRestoring = true
|
|
|
|
|
flutterOverlayApplied = false
|
|
|
|
|
ensureFlutterOverlayOnTop()
|
|
|
|
|
Log.d(TAG, "refreshVideoOutput($reason): scheduling async refresh (epoch=$refreshEpoch)")
|
|
|
|
|
pendingVideoOutputRefreshJob = scope.launch(Dispatchers.IO) {
|
|
|
|
|
try {
|
|
|
|
|
videoOutputMutex.withLock {
|
|
|
|
|
if (!isCurrentVideoOutputEpoch(refreshEpoch)) {
|
|
|
|
|
Log.d(TAG, "Skipping stale MPV video output refresh ($reason, epoch=$refreshEpoch)")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
if (!surface.isValid) {
|
|
|
|
|
hasAttachedSurface = false
|
|
|
|
|
attachedSurface = null
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
videoOutputRestoring = true
|
|
|
|
|
Log.d(TAG, "Skipping MPV video output refresh with invalid surface ($reason, epoch=$refreshEpoch)")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val needsAttach = !hasAttachedSurface || attachedSurface !== surface
|
|
|
|
|
val wasAttachedToPlaceholder = attachedToPlaceholder
|
|
|
|
|
val wasPausedForSurfaceLoss = pausedForSurfaceLoss
|
|
|
|
|
if (needsAttach) {
|
|
|
|
|
p.attachSurface(surface)
|
|
|
|
|
attachedSurface = surface
|
|
|
|
|
hasAttachedSurface = true
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
Log.d(TAG, "refreshVideoOutput($reason): attached surface")
|
|
|
|
|
} else {
|
|
|
|
|
Log.d(TAG, "refreshVideoOutput($reason): surface already attached, refreshing surface state")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isVideoOutputRefreshCurrent(refreshEpoch)) {
|
|
|
|
|
Log.d(TAG, "Skipping stale MPV video output refresh after attach ($reason, epoch=$refreshEpoch)")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
applySurfaceSizeInternal(p, force = true)
|
|
|
|
|
if (!isVideoOutputRefreshCurrent(refreshEpoch)) {
|
|
|
|
|
Log.d(TAG, "Skipping stale MPV video output refresh after surface size ($reason, epoch=$refreshEpoch)")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
videoOutputRestoring = false
|
|
|
|
|
applyDeferredResumeIfNeeded(p, reason)
|
|
|
|
|
if (wasPausedForSurfaceLoss) {
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
Log.d(TAG, "Cleared surface-loss pause after $reason")
|
|
|
|
|
}
|
|
|
|
|
if (wasAttachedToPlaceholder) {
|
|
|
|
|
Log.d(TAG, "Restored MPV real surface after placeholder ($reason)")
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "Video output ready after $reason")
|
|
|
|
|
}
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
Log.d(TAG, "Canceled pending MPV video output refresh ($reason, epoch=$refreshEpoch)")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to finalize MPV video output refresh ($reason)", e)
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to attach MPV surface", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun applySurfaceSize(width: Int, height: Int) {
|
|
|
|
|
val p = player ?: return
|
|
|
|
|
if (disposing || width <= 0 || height <= 0) return
|
|
|
|
|
val size = "${width}x${height}"
|
|
|
|
|
if (size == lastSurfaceSize) return
|
|
|
|
|
lastSurfaceSize = size
|
|
|
|
|
rememberSurfaceSize(width, height)
|
|
|
|
|
if (!hasReadyVideoOutput()) return
|
|
|
|
|
scope.launch {
|
|
|
|
|
try { p.setProperty("android-surface-size", size) }
|
|
|
|
|
try { applySurfaceSizeInternal(p) }
|
|
|
|
|
catch (e: Exception) { Log.w(TAG, "Failed to apply surface size to MPV", e) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun detachSurfaceInternal() {
|
|
|
|
|
lastSurfaceSize = null
|
|
|
|
|
val p = player ?: return
|
|
|
|
|
try {
|
|
|
|
|
scope.launch {
|
|
|
|
|
p.setProperty("vo", "null")
|
|
|
|
|
p.setProperty("force-window", "no")
|
|
|
|
|
private suspend fun applySurfaceSizeInternal(p: MpvPlayer, force: Boolean = false) {
|
|
|
|
|
if (disposing) return
|
|
|
|
|
val width = lastKnownSurfaceWidth
|
|
|
|
|
val height = lastKnownSurfaceHeight
|
|
|
|
|
if (width <= 0 || height <= 0) return
|
|
|
|
|
|
|
|
|
|
val size = "${width}x${height}"
|
|
|
|
|
if (!force && size == lastAppliedSurfaceSize) return
|
|
|
|
|
p.setProperty("android-surface-size", size)
|
|
|
|
|
lastAppliedSurfaceSize = size
|
|
|
|
|
Log.d(TAG, "Applied MPV surface size $size${if (force) " (forced)" else ""}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun schedulePlaceholderSurfaceAttach(
|
|
|
|
|
p: MpvPlayer,
|
|
|
|
|
reason: String,
|
|
|
|
|
epoch: Long
|
|
|
|
|
) {
|
|
|
|
|
pendingVideoOutputDisableJob?.cancel()
|
|
|
|
|
pendingVideoOutputDisableJob = scope.launch(Dispatchers.IO) {
|
|
|
|
|
try {
|
|
|
|
|
videoOutputMutex.withLock {
|
|
|
|
|
if (!isCurrentVideoOutputEpoch(epoch)) {
|
|
|
|
|
Log.d(TAG, "Skipping stale MPV placeholder attach ($reason, epoch=$epoch)")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
val wasPaused = try {
|
|
|
|
|
p.getFlag("pause") == true
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
cachedPaused
|
|
|
|
|
}
|
|
|
|
|
if (!wasPaused) {
|
|
|
|
|
try {
|
|
|
|
|
p.setProperty("pause", true)
|
|
|
|
|
cachedPaused = true
|
|
|
|
|
pausedForSurfaceLoss = true
|
|
|
|
|
Log.d(TAG, "Paused MPV for surface loss ($reason, epoch=$epoch)")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
Log.w(TAG, "Failed to pause MPV before placeholder attach ($reason)", e)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
}
|
|
|
|
|
val surface = placeholderSurface?.takeIf { it.isValid } ?: run {
|
|
|
|
|
Log.w(TAG, "No valid MPV placeholder surface available for $reason")
|
|
|
|
|
return@withLock
|
|
|
|
|
}
|
|
|
|
|
p.attachSurface(surface)
|
|
|
|
|
attachedSurface = surface
|
|
|
|
|
hasAttachedSurface = true
|
|
|
|
|
attachedToPlaceholder = true
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
Log.d(TAG, "Attached MPV placeholder surface ($reason, epoch=$epoch)")
|
|
|
|
|
}
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
Log.d(TAG, "Canceled pending MPV placeholder attach ($reason, epoch=$epoch)")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to attach MPV placeholder surface ($reason)", e)
|
|
|
|
|
}
|
|
|
|
|
p.detachSurface()
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to detach MPV surface", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun detachSurfaceInternal(reason: String) {
|
|
|
|
|
val hadAttachedSurface = hasAttachedSurface || attachedSurface != null
|
|
|
|
|
hasAttachedSurface = false
|
|
|
|
|
attachedSurface = null
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
videoOutputRestoring = true
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
val detachEpoch = videoOutputEpoch + 1L
|
|
|
|
|
videoOutputEpoch = detachEpoch
|
|
|
|
|
|
|
|
|
|
val p = player ?: return
|
|
|
|
|
if (!hadAttachedSurface) {
|
|
|
|
|
Log.d(TAG, "detachSurfaceInternal($reason): no attached surface to clear")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
schedulePlaceholderSurfaceAttach(
|
|
|
|
|
p = p,
|
|
|
|
|
reason = reason,
|
|
|
|
|
epoch = detachEpoch
|
|
|
|
|
)
|
|
|
|
|
Log.d(TAG, "Cleared MPV surface attachment ($reason, epoch=$detachEpoch)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun normalizePauseValue(value: String): Boolean? = when (value.lowercase()) {
|
|
|
|
|
"yes", "true", "1" -> true
|
|
|
|
|
"no", "false", "0" -> false
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun requestAutoResume(reason: String) {
|
|
|
|
|
val p = player ?: return
|
|
|
|
|
if (disposing) return
|
|
|
|
|
|
|
|
|
|
if (resumeBlockedByPublicPause) {
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
Log.d(TAG, "Skipping auto-resume after $reason because playback is explicitly paused")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasReadyVideoOutput()) {
|
|
|
|
|
deferredResumeRequested = true
|
|
|
|
|
Log.d(TAG, "Deferring auto-resume after $reason until video output is ready")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scope.launch {
|
|
|
|
|
try {
|
|
|
|
|
if (p.getFlag("pause") == true) {
|
|
|
|
|
Log.d(TAG, "Auto-resuming playback after $reason")
|
|
|
|
|
p.setProperty("pause", false)
|
|
|
|
|
} else {
|
|
|
|
|
Log.d(TAG, "Skipping auto-resume after $reason because playback is already running")
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to resume after $reason", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun applyDeferredResumeIfNeeded(p: MpvPlayer, reason: String) {
|
|
|
|
|
if (!deferredResumeRequested) return
|
|
|
|
|
|
|
|
|
|
if (resumeBlockedByPublicPause) {
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
Log.d(TAG, "Dropping deferred auto-resume after $reason because playback is explicitly paused")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
if (p.getFlag("pause") == true) {
|
|
|
|
|
Log.d(TAG, "Applying deferred auto-resume after $reason")
|
|
|
|
|
p.setProperty("pause", false)
|
|
|
|
|
} else {
|
|
|
|
|
Log.d(TAG, "Skipping deferred auto-resume after $reason because playback is already running")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -330,6 +590,26 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
|
|
|
|
|
fun setProperty(name: String, value: String) {
|
|
|
|
|
if (!isInitialized || disposing) return
|
|
|
|
|
if (name == "pause") {
|
|
|
|
|
val paused = normalizePauseValue(value)
|
|
|
|
|
if (paused == true) {
|
|
|
|
|
cachedPaused = true
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
resumeBlockedByPublicPause = true
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
Log.d(TAG, "Public pause state updated: paused=true")
|
|
|
|
|
} else if (paused == false) {
|
|
|
|
|
resumeBlockedByPublicPause = false
|
|
|
|
|
if (!hasReadyVideoOutput()) {
|
|
|
|
|
deferredResumeRequested = true
|
|
|
|
|
Log.d(TAG, "Deferring public resume until video output is ready")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cachedPaused = false
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
Log.d(TAG, "Public pause state updated: paused=false")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
scope.launch {
|
|
|
|
|
try { player?.setProperty(name, value) }
|
|
|
|
|
catch (e: Exception) { Log.w(TAG, "setProperty($name) failed", e) }
|
|
|
|
@@ -370,6 +650,21 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
activity.runOnUiThread {
|
|
|
|
|
if (disposing) return@runOnUiThread
|
|
|
|
|
surfaceContainer?.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
|
|
|
|
if (visible) {
|
|
|
|
|
flutterOverlayApplied = false
|
|
|
|
|
ensureFlutterOverlayOnTop()
|
|
|
|
|
rememberCurrentSurfaceSize()
|
|
|
|
|
val surface = currentCandidateSurface()
|
|
|
|
|
if (surface != null) {
|
|
|
|
|
pendingSurface = surface
|
|
|
|
|
refreshVideoOutput("setVisible")
|
|
|
|
|
} else {
|
|
|
|
|
val sv = surfaceView
|
|
|
|
|
if (sv != null) {
|
|
|
|
|
applySurfaceSize(sv.width, sv.height)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "setVisible($visible)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -378,6 +673,38 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
// MPV handles aspect ratio internally via its own surface management
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun updateFrame() {
|
|
|
|
|
if (disposing) return
|
|
|
|
|
activity.runOnUiThread {
|
|
|
|
|
if (disposing) return@runOnUiThread
|
|
|
|
|
flutterOverlayApplied = false
|
|
|
|
|
ensureFlutterOverlayOnTop()
|
|
|
|
|
rememberCurrentSurfaceSize()
|
|
|
|
|
val p = player
|
|
|
|
|
if (p == null) {
|
|
|
|
|
Log.d(TAG, "updateFrame(): skipping Android MPV surface refresh because player is not ready")
|
|
|
|
|
return@runOnUiThread
|
|
|
|
|
}
|
|
|
|
|
if (!hasReadyVideoOutput()) {
|
|
|
|
|
val surface = currentCandidateSurface()
|
|
|
|
|
if (surface != null) {
|
|
|
|
|
pendingSurface = surface
|
|
|
|
|
refreshVideoOutput("updateFrame")
|
|
|
|
|
} else {
|
|
|
|
|
Log.d(TAG, "updateFrame(): skipping Android MPV surface refresh because no surface is attached")
|
|
|
|
|
}
|
|
|
|
|
return@runOnUiThread
|
|
|
|
|
}
|
|
|
|
|
scope.launch {
|
|
|
|
|
try {
|
|
|
|
|
applySurfaceSizeInternal(p, force = true)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to update Android MPV surface frame", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Frame Rate Matching
|
|
|
|
|
|
|
|
|
|
fun setVideoFrameRate(fps: Float, videoDurationMs: Long) {
|
|
|
|
@@ -409,13 +736,27 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
|
|
|
|
|
// Cancel all coroutines
|
|
|
|
|
scope.cancel()
|
|
|
|
|
pendingVideoOutputDisableJob?.cancel()
|
|
|
|
|
pendingVideoOutputDisableJob = null
|
|
|
|
|
pendingVideoOutputRefreshJob?.cancel()
|
|
|
|
|
pendingVideoOutputRefreshJob = null
|
|
|
|
|
|
|
|
|
|
// Detach surface from MPV BEFORE removing views to prevent GPU mutex contention
|
|
|
|
|
val p = player
|
|
|
|
|
if (p != null) {
|
|
|
|
|
try {
|
|
|
|
|
runBlocking(Dispatchers.IO) { p.setProperty("vo", "null") }
|
|
|
|
|
runBlocking(Dispatchers.IO) {
|
|
|
|
|
p.setProperty("force-window", "no")
|
|
|
|
|
p.setProperty("vo", "null")
|
|
|
|
|
}
|
|
|
|
|
p.detachSurface()
|
|
|
|
|
hasAttachedSurface = false
|
|
|
|
|
attachedSurface = null
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
videoOutputRestoring = false
|
|
|
|
|
lastAppliedSurfaceSize = null
|
|
|
|
|
videoOutputEpoch += 1L
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.w(TAG, "Failed to detach surface during dispose", e)
|
|
|
|
|
}
|
|
|
|
@@ -436,6 +777,17 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|
|
|
|
overlayLayoutListener = null
|
|
|
|
|
|
|
|
|
|
pendingSurface = null
|
|
|
|
|
placeholderSurface?.release()
|
|
|
|
|
placeholderSurface = null
|
|
|
|
|
placeholderImageReader?.close()
|
|
|
|
|
placeholderImageReader = null
|
|
|
|
|
pausedForSurfaceLoss = false
|
|
|
|
|
attachedToPlaceholder = false
|
|
|
|
|
videoOutputRestoring = false
|
|
|
|
|
deferredResumeRequested = false
|
|
|
|
|
resumeBlockedByPublicPause = false
|
|
|
|
|
videoOutputEpoch = 0L
|
|
|
|
|
pendingVideoOutputDisableJob = null
|
|
|
|
|
isInitialized = false
|
|
|
|
|
|
|
|
|
|
// Close player on background thread, then remove views
|
|
|
|
|