fix(vlc): stale video surface

This commit is contained in:
2026-08-14 13:08:29 +02:00
parent 456f903f97
commit 4f6d544591
2 changed files with 15 additions and 29 deletions
@@ -244,6 +244,7 @@ class OmniView(val context: ThemedReactContext) :
} }
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) scheduleVideoRebuild()
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) :
movedSurfaceToRootForPip = false movedSurfaceToRootForPip = false
} }
// A surface that is destroyed and recreated while playback runs (the PIP private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() }
// 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 private fun scheduleVideoRebuild() {
// once the (PIP) window has settled at its final size — firing it mid-resize surfaceView.removeCallbacks(rebuildRunnable)
// leaves the vout stopped — so it is debounced off surfaceChanged. surfaceView.postDelayed(rebuildRunnable, 200)
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?.setVideoView(surfaceView) boundPlayer?.setVideoView(surfaceView)
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) :
height: Int height: Int
) { ) {
boundPlayer?.updateVideoLayout(width, height) boundPlayer?.updateVideoLayout(width, height)
if (pendingVideoRebuild) { scheduleVideoRebuild()
surfaceView.removeCallbacks(rebuildRunnable)
surfaceView.postDelayed(rebuildRunnable, 200)
}
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
@@ -107,6 +107,7 @@ class VlcPlayer(ctx: Context) :
@Volatile @Volatile
private var cachedBufferedPosition: Long = 0L private var cachedBufferedPosition: Long = 0L
private var boundSurfaceView: SurfaceView? = null private var boundSurfaceView: SurfaceView? = null
private var videoOutputStale = false
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
private val availableCommands: Player.Commands = Player.Commands.Builder() private val availableCommands: Player.Commands = Player.Commands.Builder()
@@ -348,6 +349,7 @@ class VlcPlayer(ctx: Context) :
playerError = null playerError = null
player.stop() player.stop()
videoOutputStale = false
mediaItems.getOrNull(targetIndex)?.let { item -> mediaItems.getOrNull(targetIndex)?.let { item ->
val uri = item.localConfiguration?.uri?.toString() val uri = item.localConfiguration?.uri?.toString()
@@ -897,30 +899,22 @@ class VlcPlayer(ctx: Context) :
if (surfaceView == null) return clearVideoSurface() if (surfaceView == null) return clearVideoSurface()
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
boundSurfaceView = surfaceView boundSurfaceView = surfaceView
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view videoOutputStale = (
// size and keeps the video layout correct as the surface resizes (e.g. the player.playerState == IMedia.State.Playing ||
// PIP window shrinking/growing); see updateVideoLayout for size updates. player.playerState == IMedia.State.Paused
)
vlcVout.setVideoView(surfaceView) 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() { fun rebuildVideoOutput() {
if (!vlcVout.areViewsAttached() || !player.isPlaying) return if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return
videoOutputStale = false
player.setVideoTrackEnabled(false) player.setVideoTrackEnabled(false)
player.setVideoTrackEnabled(true) player.setVideoTrackEnabled(true)
player.updateVideoSurfaces() 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) { fun updateVideoLayout(width: Int, height: Int) {
if (!vlcVout.areViewsAttached()) return if (!vlcVout.areViewsAttached()) return
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height) if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)