mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-30 01:22:56 +01:00
Compare commits
2 Commits
fix/subtit
...
fix/pip-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2139673a7f | ||
|
|
32ead6d046 |
@@ -2,14 +2,12 @@ package expo.modules.mpvplayer
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.TextureView
|
import android.view.SurfaceHolder
|
||||||
import android.view.View
|
import android.view.SurfaceView
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import expo.modules.kotlin.AppContext
|
import expo.modules.kotlin.AppContext
|
||||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||||
@@ -30,15 +28,26 @@ data class VideoLoadConfig(
|
|||||||
val cacheEnabled: String? = null,
|
val cacheEnabled: String? = null,
|
||||||
val cacheSeconds: Int? = null,
|
val cacheSeconds: Int? = null,
|
||||||
val demuxerMaxBytes: Int? = null,
|
val demuxerMaxBytes: Int? = null,
|
||||||
val demuxerMaxBackBytes: Int? = null
|
val demuxerMaxBackBytes: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MpvPlayerView - ExpoView that hosts the MPV player.
|
* MpvPlayerView - ExpoView that hosts the MPV player.
|
||||||
* Uses TextureView for reliable Picture-in-Picture support.
|
*
|
||||||
|
* Uses SurfaceView (not TextureView) so the surface routes directly to
|
||||||
|
* SurfaceFlinger (the OS compositor) rather than compositing into the
|
||||||
|
* app's window surface. This matches mpv-android's architecture and
|
||||||
|
* gives mpv a standalone surface.
|
||||||
|
*
|
||||||
|
* PiP black-screen mitigation: SurfaceView's surface is destroyed and
|
||||||
|
* recreated on PiP entry/exit, and the new surface's initial dimensions
|
||||||
|
* can be stale until the next layout pass. We push dimension updates to
|
||||||
|
* mpv via both SurfaceHolder.Callback.surfaceChanged AND an
|
||||||
|
* OnLayoutChangeListener, so the PiP transition (which fires layout
|
||||||
|
* passes on the view itself) reaches mpv promptly.
|
||||||
*/
|
*/
|
||||||
class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext),
|
class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext),
|
||||||
MPVLayerRenderer.Delegate, TextureView.SurfaceTextureListener {
|
MPVLayerRenderer.Delegate, SurfaceHolder.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MpvPlayerView"
|
private const val TAG = "MpvPlayerView"
|
||||||
@@ -52,7 +61,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
val onTracksReady by EventDispatcher()
|
val onTracksReady by EventDispatcher()
|
||||||
val onPictureInPictureChange by EventDispatcher()
|
val onPictureInPictureChange by EventDispatcher()
|
||||||
|
|
||||||
private var textureView: TextureView
|
private var surfaceView: SurfaceView
|
||||||
private var renderer: MPVLayerRenderer? = null
|
private var renderer: MPVLayerRenderer? = null
|
||||||
private var pipController: PiPController? = null
|
private var pipController: PiPController? = null
|
||||||
|
|
||||||
@@ -63,31 +72,45 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
private var surfaceReady: Boolean = false
|
private var surfaceReady: Boolean = false
|
||||||
private var pendingConfig: VideoLoadConfig? = null
|
private var pendingConfig: VideoLoadConfig? = null
|
||||||
private var rendererStarted: Boolean = false
|
private var rendererStarted: Boolean = false
|
||||||
private var pendingSurface: Surface? = null
|
|
||||||
private var activeSurface: Surface? = null
|
private var activeSurface: Surface? = null
|
||||||
private var surfaceTexture: SurfaceTexture? = null
|
|
||||||
|
|
||||||
// PiP state tracking
|
// PiP state tracking
|
||||||
private var isWaitingForPiPTransition: Boolean = false
|
|
||||||
private var isPiPSurfaceForced: Boolean = false
|
|
||||||
private val pipHandler = Handler(Looper.getMainLooper())
|
private val pipHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setBackgroundColor(Color.BLACK)
|
setBackgroundColor(Color.BLACK)
|
||||||
|
|
||||||
// Create TextureView for video rendering (composites into app window for PiP support)
|
// SurfaceView for video rendering. Routes the surface directly to
|
||||||
textureView = TextureView(context).apply {
|
// SurfaceFlinger (the OS compositor), giving mpv a standalone
|
||||||
|
// surface. TextureView composites into the app's window surface
|
||||||
|
// which is less efficient and breaks PiP transitions.
|
||||||
|
surfaceView = SurfaceView(context).apply {
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
surfaceTextureListener = this@MpvPlayerView
|
|
||||||
}
|
}
|
||||||
addView(textureView)
|
surfaceView.holder.addCallback(this@MpvPlayerView)
|
||||||
|
addView(surfaceView)
|
||||||
|
|
||||||
|
// Push dimension updates to mpv on every view bounds change. This
|
||||||
|
// is the primary PiP black-screen fix: entering PiP fires a layout
|
||||||
|
// pass on the SurfaceView itself, and we proactively tell mpv the
|
||||||
|
// new size so it resizes its EGL swapchain before rendering.
|
||||||
|
surfaceView.addOnLayoutChangeListener { _, left, top, right, bottom,
|
||||||
|
oldLeft, oldTop, oldRight, oldBottom ->
|
||||||
|
val w = right - left
|
||||||
|
val h = bottom - top
|
||||||
|
val oldW = oldRight - oldLeft
|
||||||
|
val oldH = oldBottom - oldTop
|
||||||
|
if (w > 0 && h > 0 && (w != oldW || h != oldH)) {
|
||||||
|
renderer?.updateSurfaceSize(w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize PiP controller with Expo's AppContext for proper activity access
|
// Initialize PiP controller with Expo's AppContext for proper activity access
|
||||||
pipController = PiPController(context, appContext)
|
pipController = PiPController(context, appContext)
|
||||||
pipController?.setPlayerView(textureView)
|
pipController?.setPlayerView(surfaceView)
|
||||||
pipController?.delegate = object : PiPController.Delegate {
|
pipController?.delegate = object : PiPController.Delegate {
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
play()
|
play()
|
||||||
@@ -103,17 +126,17 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
|
|
||||||
override fun onPictureInPictureModeChanged(isInPiP: Boolean) {
|
override fun onPictureInPictureModeChanged(isInPiP: Boolean) {
|
||||||
if (isInPiP) {
|
if (isInPiP) {
|
||||||
if (!isWaitingForPiPTransition) {
|
// Post size syncs after the PiP layout settles. Two passes
|
||||||
isWaitingForPiPTransition = true
|
// catch both the immediate surface re-attach and the
|
||||||
|
// post-animation layout pass. Replaces the old TextureView
|
||||||
|
// measure/layout polling hack (forcePiPBufferSize).
|
||||||
pipHandler.removeCallbacksAndMessages(null)
|
pipHandler.removeCallbacksAndMessages(null)
|
||||||
for (delay in longArrayOf(500, 1000, 1500, 2000)) {
|
pipHandler.postDelayed({ syncSurfaceSizeToView() }, 100)
|
||||||
pipHandler.postDelayed({ forcePiPBufferSize() }, delay)
|
pipHandler.postDelayed({ syncSurfaceSizeToView() }, 500)
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
isWaitingForPiPTransition = false
|
// Restore from PiP: surface resized back to fullscreen.
|
||||||
pipHandler.removeCallbacksAndMessages(null)
|
pipHandler.removeCallbacksAndMessages(null)
|
||||||
restoreFromPiP()
|
pipHandler.postDelayed({ syncSurfaceSizeToView() }, 100)
|
||||||
}
|
}
|
||||||
onPictureInPictureChange(mapOf("isActive" to isInPiP))
|
onPictureInPictureChange(mapOf("isActive" to isInPiP))
|
||||||
}
|
}
|
||||||
@@ -126,7 +149,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the renderer with the given VO driver.
|
* Start the renderer with the given VO driver.
|
||||||
* Called lazily on first loadVideo so the voDriver setting is available.
|
* Called lazily on first loadVideo so user settings are available.
|
||||||
*/
|
*/
|
||||||
private fun ensureRendererStarted(voDriver: String?) {
|
private fun ensureRendererStarted(voDriver: String?) {
|
||||||
if (rendererStarted) return
|
if (rendererStarted) return
|
||||||
@@ -135,10 +158,14 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
renderer?.start(voDriver ?: "gpu-next")
|
renderer?.start(voDriver ?: "gpu-next")
|
||||||
rendererStarted = true
|
rendererStarted = true
|
||||||
|
|
||||||
pendingSurface?.let { surface ->
|
// If the surface is already alive (surfaceCreated fired before
|
||||||
|
// loadVideo), attach it now. With SurfaceView the surface is
|
||||||
|
// owned by the holder, so we read it from there directly rather
|
||||||
|
// than stashing it on the side.
|
||||||
|
surfaceView.holder.surface?.takeIf { it.isValid }?.let { surface ->
|
||||||
activeSurface = surface
|
activeSurface = surface
|
||||||
renderer?.attachSurface(surface)
|
renderer?.attachSurface(surface)
|
||||||
pendingSurface = null
|
syncSurfaceSizeToView()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to start renderer: ${e.message}")
|
Log.e(TAG, "Failed to start renderer: ${e.message}")
|
||||||
@@ -146,23 +173,20 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TextureView.SurfaceTextureListener
|
// MARK: - SurfaceHolder.Callback
|
||||||
|
|
||||||
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
this.surfaceTexture = surfaceTexture
|
val surface = holder.surface
|
||||||
val surface = Surface(surfaceTexture)
|
|
||||||
surfaceTexture.setDefaultBufferSize(width, height)
|
|
||||||
surfaceReady = true
|
surfaceReady = true
|
||||||
|
|
||||||
if (rendererStarted) {
|
if (rendererStarted) {
|
||||||
// Release the previous wrapper Surface before losing the only
|
// The previous Surface reference is holder-owned; do NOT release
|
||||||
// reference to it. cleanup() only runs on detach, so without this
|
// it (SurfaceView manages its lifecycle). Just track the new one.
|
||||||
// repeated PiP/background/resize cycles leak native surface objects.
|
|
||||||
activeSurface?.release()
|
|
||||||
activeSurface = surface
|
activeSurface = surface
|
||||||
renderer?.attachSurface(surface)
|
renderer?.attachSurface(surface)
|
||||||
} else {
|
// Push the actual view dimensions immediately so mpv doesn't
|
||||||
pendingSurface = surface
|
// render against stale full-screen geometry during PiP transitions.
|
||||||
|
syncSurfaceSizeToView()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a pending load, execute it now
|
// If we have a pending load, execute it now
|
||||||
@@ -173,20 +197,36 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||||
surfaceTexture.setDefaultBufferSize(width, height)
|
if (width > 0 && height > 0) {
|
||||||
renderer?.updateSurfaceSize(width, height)
|
renderer?.updateSurfaceSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
|
|
||||||
this.surfaceTexture = null
|
|
||||||
surfaceReady = false
|
|
||||||
renderer?.detachSurface()
|
|
||||||
return false // mpv manages the SurfaceTexture
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
// Called every frame — no action needed, mpv drives rendering directly
|
surfaceReady = false
|
||||||
|
renderer?.detachSurface()
|
||||||
|
// Do NOT issue mpv "stop" here. Playback continues against the
|
||||||
|
// demuxer; when surfaceCreated fires again (PiP entry/exit, app
|
||||||
|
// background/foreground), we re-attach and frames resume. This
|
||||||
|
// matches the keep-open=always setting in MPVLayerRenderer.
|
||||||
|
//
|
||||||
|
// Do NOT release activeSurface — SurfaceView owns it via the holder.
|
||||||
|
activeSurface = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the actual SurfaceView width/height and push them to mpv.
|
||||||
|
* The PiP transition can fire surfaceCreated before the view's layout
|
||||||
|
* has settled to PiP dimensions, so we re-sync after layout passes.
|
||||||
|
*/
|
||||||
|
private fun syncSurfaceSizeToView() {
|
||||||
|
if (!surfaceReady) return
|
||||||
|
val w = surfaceView.width
|
||||||
|
val h = surfaceView.height
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
renderer?.updateSurfaceSize(w, h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Video Loading
|
// MARK: - Video Loading
|
||||||
@@ -275,7 +315,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
|
|
||||||
// Reset view-level state so a subsequent loadVideo() on the SAME view
|
// Reset view-level state so a subsequent loadVideo() on the SAME view
|
||||||
// instance re-creates the mpv handle and re-attaches the still-live
|
// instance re-creates the mpv handle and re-attaches the still-live
|
||||||
// TextureView surface. Without this, rendererStarted stays true and
|
// SurfaceView surface. Without this, rendererStarted stays true and
|
||||||
// ensureRendererStarted() early-returns, so renderer.start() is never
|
// ensureRendererStarted() early-returns, so renderer.start() is never
|
||||||
// called again — but stop() already nulled the renderer's mpv handle.
|
// called again — but stop() already nulled the renderer's mpv handle.
|
||||||
// The next loadVideo() then runs loadVideoInternal() -> renderer.load()
|
// The next loadVideo() then runs loadVideoInternal() -> renderer.load()
|
||||||
@@ -286,13 +326,12 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
// which call destroy() immediately before router.replace() to the
|
// which call destroy() immediately before router.replace() to the
|
||||||
// same route — Expo Router reuses the same MpvPlayerView instance,
|
// same route — Expo Router reuses the same MpvPlayerView instance,
|
||||||
// so the next source load happens on this view without a remount.
|
// so the next source load happens on this view without a remount.
|
||||||
|
//
|
||||||
|
// SurfaceView note: the surface is owned by the holder and survives
|
||||||
|
// across destroy()/loadVideo() on the same view instance. The next
|
||||||
|
// ensureRendererStarted() reads it from surfaceView.holder.surface.
|
||||||
rendererStarted = false
|
rendererStarted = false
|
||||||
currentUrl = null
|
currentUrl = null
|
||||||
// Move the active surface back to pending so ensureRendererStarted()
|
|
||||||
// re-attaches it to the freshly created mpv instance on next load.
|
|
||||||
// The Surface itself is still valid — onSurfaceTextureDestroyed has
|
|
||||||
// not fired because the TextureView is not being unmounted.
|
|
||||||
activeSurface?.let { pendingSurface = it }
|
|
||||||
activeSurface = null
|
activeSurface = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,59 +366,10 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
// MARK: - Picture in Picture
|
// MARK: - Picture in Picture
|
||||||
|
|
||||||
fun startPictureInPicture() {
|
fun startPictureInPicture() {
|
||||||
isWaitingForPiPTransition = true
|
|
||||||
pipController?.startPictureInPicture()
|
pipController?.startPictureInPicture()
|
||||||
|
|
||||||
// Resize buffer to match PiP window after animation settles
|
|
||||||
pipHandler.removeCallbacksAndMessages(null)
|
|
||||||
for (delay in longArrayOf(500, 1000, 1500, 2000)) {
|
|
||||||
pipHandler.postDelayed({ forcePiPBufferSize() }, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize the SurfaceTexture buffer AND TextureView layout to match the PiP
|
|
||||||
* visible rect so mpv renders at the PiP window's actual dimensions.
|
|
||||||
*/
|
|
||||||
private fun forcePiPBufferSize() {
|
|
||||||
if (!isWaitingForPiPTransition || !surfaceReady) return
|
|
||||||
|
|
||||||
val rect = Rect()
|
|
||||||
textureView.getGlobalVisibleRect(rect)
|
|
||||||
val visW = rect.width()
|
|
||||||
val visH = rect.height()
|
|
||||||
val vw = textureView.width
|
|
||||||
val vh = textureView.height
|
|
||||||
|
|
||||||
if (visW <= 0 || visH <= 0 || (vw == visW && vh == visH)) return
|
|
||||||
|
|
||||||
surfaceTexture?.setDefaultBufferSize(visW, visH)
|
|
||||||
renderer?.updateSurfaceSize(visW, visH)
|
|
||||||
|
|
||||||
// Force TextureView layout to match PiP visible area.
|
|
||||||
// layoutParams alone doesn't work during PiP because the parent
|
|
||||||
// never re-lays out its children.
|
|
||||||
textureView.measure(
|
|
||||||
View.MeasureSpec.makeMeasureSpec(visW, View.MeasureSpec.EXACTLY),
|
|
||||||
View.MeasureSpec.makeMeasureSpec(visH, View.MeasureSpec.EXACTLY)
|
|
||||||
)
|
|
||||||
textureView.layout(0, 0, visW, visH)
|
|
||||||
isPiPSurfaceForced = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreFromPiP() {
|
|
||||||
if (!isPiPSurfaceForced) return
|
|
||||||
isPiPSurfaceForced = false
|
|
||||||
|
|
||||||
val lp = textureView.layoutParams
|
|
||||||
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
textureView.layoutParams = lp
|
|
||||||
textureView.requestLayout()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPictureInPicture() {
|
fun stopPictureInPicture() {
|
||||||
isWaitingForPiPTransition = false
|
|
||||||
pipHandler.removeCallbacksAndMessages(null)
|
pipHandler.removeCallbacksAndMessages(null)
|
||||||
pipController?.stopPictureInPicture()
|
pipController?.stopPictureInPicture()
|
||||||
}
|
}
|
||||||
@@ -547,20 +537,12 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
* off the JS path.
|
* off the JS path.
|
||||||
*/
|
*/
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
isWaitingForPiPTransition = false
|
|
||||||
pipHandler.removeCallbacksAndMessages(null)
|
pipHandler.removeCallbacksAndMessages(null)
|
||||||
pipController?.stopPictureInPicture()
|
pipController?.stopPictureInPicture()
|
||||||
renderer?.stop()
|
renderer?.stop()
|
||||||
renderer?.delegate = null
|
renderer?.delegate = null
|
||||||
|
|
||||||
// Release the Surface that wraps the SurfaceTexture. These Surface
|
// SurfaceView owns the Surface via its holder — do NOT release it.
|
||||||
// objects are created in onSurfaceTextureAvailable and were never
|
|
||||||
// released; each playback session previously leaked one. The
|
|
||||||
// SurfaceTexture itself is owned by TextureView and released by it
|
|
||||||
// via onSurfaceTextureDestroyed, so we leave it alone.
|
|
||||||
pendingSurface?.release()
|
|
||||||
pendingSurface = null
|
|
||||||
activeSurface?.release()
|
|
||||||
activeSurface = null
|
activeSurface = null
|
||||||
surfaceReady = false
|
surfaceReady = false
|
||||||
currentUrl = null
|
currentUrl = null
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
|||||||
private var currentPosition: Double = 0.0
|
private var currentPosition: Double = 0.0
|
||||||
private var currentDuration: Double = 0.0
|
private var currentDuration: Double = 0.0
|
||||||
private var playbackRate: Double = 1.0
|
private var playbackRate: Double = 1.0
|
||||||
|
// Independently tracks whether the system should auto-enter PiP on home
|
||||||
|
// press. Decoupled from playbackRate so that disabling auto-enter
|
||||||
|
// (e.g. when the player unmounts) doesn't corrupt the play/pause icon
|
||||||
|
// state that buildPiPActions() derives from playbackRate.
|
||||||
|
private var autoEnterEnabled: Boolean = false
|
||||||
|
|
||||||
private var videoWidth: Int = 0
|
private var videoWidth: Int = 0
|
||||||
private var videoHeight: Int = 0
|
private var videoHeight: Int = 0
|
||||||
@@ -106,15 +111,37 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stopPictureInPicture() {
|
fun stopPictureInPicture() {
|
||||||
|
// Disable auto-enter eligibility without touching playbackRate.
|
||||||
|
// playbackRate drives the play/pause icon in buildPiPActions();
|
||||||
|
// mutating it here would cause a stale icon if PiP is re-entered
|
||||||
|
// before the next playback state callback corrects it.
|
||||||
|
autoEnterEnabled = false
|
||||||
isInPiPMode = false
|
isInPiPMode = false
|
||||||
pipEntryNotified = false
|
pipEntryNotified = false
|
||||||
unregisterLifecycleCallbacks()
|
unregisterLifecycleCallbacks()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val activity = getActivity()
|
val activity = getActivity() ?: return
|
||||||
if (activity?.isInPictureInPictureMode == true) {
|
|
||||||
activity.moveTaskToBack(false)
|
// Push minimal params with just auto-enter disabled. Do NOT call
|
||||||
|
// buildPiPParams() — it calls ensurePiPReceiverRegistered() and
|
||||||
|
// setActions(), which would re-register the broadcast receiver
|
||||||
|
// (just unregistered above) and attach play/pause/skip actions to
|
||||||
|
// params being torn down. That leaves a live receiver + stale
|
||||||
|
// actions after the player has unmounted.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
try {
|
||||||
|
activity.setPictureInPictureParams(
|
||||||
|
PictureInPictureParams.Builder()
|
||||||
|
.setAutoEnterEnabled(false)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to clear PiP auto-enter params: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (activity.isInPictureInPictureMode) {
|
||||||
|
activity.moveTaskToBack(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCurrentlyInPiP(): Boolean = isInPiPMode
|
fun isCurrentlyInPiP(): Boolean = isInPiPMode
|
||||||
@@ -126,6 +153,7 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
|||||||
|
|
||||||
fun setPlaybackRate(rate: Double) {
|
fun setPlaybackRate(rate: Double) {
|
||||||
playbackRate = rate
|
playbackRate = rate
|
||||||
|
autoEnterEnabled = rate > 0
|
||||||
|
|
||||||
if (rate > 0) {
|
if (rate > 0) {
|
||||||
registerLifecycleCallbacks()
|
registerLifecycleCallbacks()
|
||||||
@@ -208,7 +236,7 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
|||||||
builder.setActions(buildPiPActions())
|
builder.setActions(buildPiPActions())
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
builder.setAutoEnterEnabled(forEntering || playbackRate > 0)
|
builder.setAutoEnterEnabled(forEntering || autoEnterEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
|||||||
Reference in New Issue
Block a user