From 4f6d544591d16b887e58f8ca97db2b9b42199f25 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 12 Aug 2026 13:51:12 +0200 Subject: [PATCH] fix(vlc): stale video surface --- .../src/main/java/dev/zoriya/omni/OmniView.kt | 22 ++++++------------- .../main/java/dev/zoriya/omni/VlcPlayer.kt | 22 +++++++------------ 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/android/src/main/java/dev/zoriya/omni/OmniView.kt b/android/src/main/java/dev/zoriya/omni/OmniView.kt index 6f30609..b3c4295 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniView.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniView.kt @@ -244,6 +244,7 @@ class OmniView(val context: ThemedReactContext) : } override fun onIsPlayingChanged(isPlaying: Boolean) { + if (isPlaying) scheduleVideoRebuild() updatePictureInPictureParams() } @@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) : 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() + private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() } + + private fun scheduleVideoRebuild() { + surfaceView.removeCallbacks(rebuildRunnable) + surfaceView.postDelayed(rebuildRunnable, 200) } override fun surfaceCreated(holder: SurfaceHolder) { surfaceReady = true boundPlayer?.setVideoView(surfaceView) - if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true updatePictureInPictureParams() } @@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) : height: Int ) { boundPlayer?.updateVideoLayout(width, height) - if (pendingVideoRebuild) { - surfaceView.removeCallbacks(rebuildRunnable) - surfaceView.postDelayed(rebuildRunnable, 200) - } + scheduleVideoRebuild() } override fun surfaceDestroyed(holder: SurfaceHolder) { diff --git a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt index 3a63d51..ecd69b0 100644 --- a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt @@ -107,6 +107,7 @@ class VlcPlayer(ctx: Context) : @Volatile private var cachedBufferedPosition: Long = 0L private var boundSurfaceView: SurfaceView? = null + private var videoOutputStale = false private var lastVideoSize: VideoSize = VideoSize.UNKNOWN private val availableCommands: Player.Commands = Player.Commands.Builder() @@ -348,6 +349,7 @@ class VlcPlayer(ctx: Context) : playerError = null player.stop() + videoOutputStale = false mediaItems.getOrNull(targetIndex)?.let { item -> val uri = item.localConfiguration?.uri?.toString() @@ -897,30 +899,22 @@ class VlcPlayer(ctx: Context) : 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. + videoOutputStale = ( + player.playerState == IMedia.State.Playing || + player.playerState == IMedia.State.Paused + ) vlcVout.setVideoView(surfaceView) 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 + if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return + videoOutputStale = false 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)