fix(pip): fix black screen

This commit is contained in:
2026-07-26 22:36:05 +02:00
parent 074b821e06
commit 9dae350c73
3 changed files with 74 additions and 11 deletions
@@ -8,7 +8,6 @@ import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.SurfaceHolder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
@@ -208,16 +207,29 @@ class OmniPlayer(
.build() .build()
} }
fun setSurface(holder: SurfaceHolder?) { fun setVideoView(surfaceView: android.view.SurfaceView?) {
runOnMainThread { runOnMainThread {
if (holder == null) { if (surfaceView == null) {
localPlayer.clearVideoSurface() localPlayer.clearVideoSurface()
} else { } else {
localPlayer.setVideoSurfaceHolder(holder) localPlayer.setVideoSurfaceView(surfaceView)
} }
} }
} }
// rebuild video pipeline after pip, noop for exoplayer that recover on it's own.
fun rebuildVideoOutput() {
runOnMainThread {
(localPlayer as? VlcPlayer)?.rebuildVideoOutput()
}
}
fun updateVideoLayout(width: Int, height: Int) {
runOnMainThread {
(localPlayer as? VlcPlayer)?.updateVideoLayout(width, height)
}
}
override val hasPrev: Boolean get() = player.hasPreviousMediaItem() override val hasPrev: Boolean get() = player.hasPreviousMediaItem()
override val hasNext: Boolean get() = player.hasNextMediaItem() override val hasNext: Boolean get() = player.hasNextMediaItem()
override val status by mainThreadProperty { override val status by mainThreadProperty {
@@ -140,12 +140,12 @@ class OmniView(val context: ThemedReactContext) :
} }
boundPlayer?.localPlayer?.removeListener(this) boundPlayer?.localPlayer?.removeListener(this)
boundPlayer?.setSurface(null) boundPlayer?.setVideoView(null)
boundPlayer = omniPlayer boundPlayer = omniPlayer
omniPlayer.localPlayer.addListener(this) omniPlayer.localPlayer.addListener(this)
if (surfaceReady) { if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder) omniPlayer.setVideoView(surfaceView)
} }
if (autoplay == true && !omniPlayer.isPlaying) { if (autoplay == true && !omniPlayer.isPlaying) {
@@ -166,7 +166,7 @@ class OmniView(val context: ThemedReactContext) :
val omniPlayer = player as? OmniPlayer ?: return val omniPlayer = player as? OmniPlayer ?: return
boundPlayer?.localPlayer?.removeListener(this) boundPlayer?.localPlayer?.removeListener(this)
omniPlayer.setSurface(null) omniPlayer.setVideoView(null)
boundPlayer = null boundPlayer = null
} }
@@ -322,9 +322,22 @@ class OmniView(val context: ThemedReactContext) :
movedSurfaceToRootForPip = false movedSurfaceToRootForPip = false
} }
// A surface that is destroyed and recreated while playback runs (the PIP
// reparent) makes VLC drop its video pipeline; it won't rebuild it for the
// new surface on its own, so we must ask it to. The rebuild only succeeds
// once the (PIP) window has settled at its final size — firing it mid-resize
// leaves the vout stopped — so it is debounced off surfaceChanged.
private var pendingVideoRebuild = false
private val rebuildRunnable = Runnable {
if (!pendingVideoRebuild) return@Runnable
pendingVideoRebuild = false
boundPlayer?.rebuildVideoOutput()
}
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true surfaceReady = true
boundPlayer?.setSurface(holder) boundPlayer?.setVideoView(surfaceView)
if (boundPlayer?.isPlaying == true) pendingVideoRebuild = true
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -333,10 +346,17 @@ class OmniView(val context: ThemedReactContext) :
format: Int, format: Int,
width: Int, width: Int,
height: Int height: Int
) { } ) {
boundPlayer?.updateVideoLayout(width, height)
if (pendingVideoRebuild) {
surfaceView.removeCallbacks(rebuildRunnable)
surfaceView.postDelayed(rebuildRunnable, 200)
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
surfaceReady = false surfaceReady = false
boundPlayer?.setSurface(null) surfaceView.removeCallbacks(rebuildRunnable)
boundPlayer?.setVideoView(null)
} }
} }
@@ -101,6 +101,7 @@ class VlcPlayer(ctx: Context) :
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
private var userInitiatedTransition: Boolean = false private var userInitiatedTransition: Boolean = false
private var cachedBufferedPosition: Long = 0L private var cachedBufferedPosition: Long = 0L
private var boundSurfaceView: SurfaceView? = null
private val availableCommands: Player.Commands = Player.Commands.Builder() private val availableCommands: Player.Commands = Player.Commands.Builder()
.add(COMMAND_PLAY_PAUSE) .add(COMMAND_PLAY_PAUSE)
@@ -795,6 +796,7 @@ class VlcPlayer(ctx: Context) :
} }
override fun clearVideoSurface() { override fun clearVideoSurface() {
boundSurfaceView = null
vlcVout.detachViews() vlcVout.detachViews()
} }
@@ -825,10 +827,39 @@ class VlcPlayer(ctx: Context) :
} }
override fun setVideoSurfaceView(surfaceView: SurfaceView?) { override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
vlcVout.setVideoView(surfaceView ?: return clearVideoSurface()) if (surfaceView == null) return clearVideoSurface()
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
boundSurfaceView = surfaceView
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view
// size and keeps the video layout correct as the surface resizes (e.g. the
// PIP window shrinking/growing); see updateVideoLayout for size updates.
vlcVout.setVideoView(surfaceView)
vlcVout.attachViews() vlcVout.attachViews()
} }
/**
* VLC tears down its video decoder + output whenever the output Surface is
* destroyed (e.g. the SurfaceView being reparented for PIP) and does not
* rebuild them when a new Surface arrives — the picture stays black. Toggling
* the video track forces VLC to spin up a fresh decoder/vout against the
* currently-attached Surface. Safe no-op when nothing is attached/playing.
*/
fun rebuildVideoOutput() {
if (!vlcVout.areViewsAttached() || !player.isPlaying) return
player.setVideoTrackEnabled(false)
player.setVideoTrackEnabled(true)
player.updateVideoSurfaces()
}
/** Recompute the video layout for the current surface size. Must run whenever
* the surface resizes (VLC latches a stale geometry otherwise -> the picture
* renders at the wrong size, anchored in a corner). */
fun updateVideoLayout(width: Int, height: Int) {
if (!vlcVout.areViewsAttached()) return
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
player.updateVideoSurfaces()
}
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
clearVideoSurface() clearVideoSurface()
} }