diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 01dccd6..bdd3b38 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -8,7 +8,6 @@ import android.app.PendingIntent import android.content.Intent import android.os.Build import android.os.Bundle -import android.view.SurfaceHolder import androidx.core.app.NotificationCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -208,16 +207,29 @@ class OmniPlayer( .build() } - fun setSurface(holder: SurfaceHolder?) { + fun setVideoView(surfaceView: android.view.SurfaceView?) { runOnMainThread { - if (holder == null) { + if (surfaceView == null) { localPlayer.clearVideoSurface() } 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 hasNext: Boolean get() = player.hasNextMediaItem() override val status by mainThreadProperty { diff --git a/android/src/main/java/dev/zoriya/omni/OmniView.kt b/android/src/main/java/dev/zoriya/omni/OmniView.kt index 99906a0..efcb448 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniView.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniView.kt @@ -140,12 +140,12 @@ class OmniView(val context: ThemedReactContext) : } boundPlayer?.localPlayer?.removeListener(this) - boundPlayer?.setSurface(null) + boundPlayer?.setVideoView(null) boundPlayer = omniPlayer omniPlayer.localPlayer.addListener(this) if (surfaceReady) { - omniPlayer.setSurface(surfaceView.holder) + omniPlayer.setVideoView(surfaceView) } if (autoplay == true && !omniPlayer.isPlaying) { @@ -166,7 +166,7 @@ class OmniView(val context: ThemedReactContext) : val omniPlayer = player as? OmniPlayer ?: return boundPlayer?.localPlayer?.removeListener(this) - omniPlayer.setSurface(null) + omniPlayer.setVideoView(null) boundPlayer = null } @@ -322,9 +322,22 @@ 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() + } + override fun surfaceCreated(holder: SurfaceHolder) { surfaceReady = true - boundPlayer?.setSurface(holder) + boundPlayer?.setVideoView(surfaceView) + if (boundPlayer?.isPlaying == true) pendingVideoRebuild = true updatePictureInPictureParams() } @@ -333,10 +346,17 @@ class OmniView(val context: ThemedReactContext) : format: Int, width: Int, height: Int - ) { } + ) { + boundPlayer?.updateVideoLayout(width, height) + if (pendingVideoRebuild) { + surfaceView.removeCallbacks(rebuildRunnable) + surfaceView.postDelayed(rebuildRunnable, 200) + } + } override fun surfaceDestroyed(holder: SurfaceHolder) { surfaceReady = false - boundPlayer?.setSurface(null) + surfaceView.removeCallbacks(rebuildRunnable) + boundPlayer?.setVideoView(null) } } diff --git a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt index 19aad4a..66a3dd6 100644 --- a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt @@ -101,6 +101,7 @@ class VlcPlayer(ctx: Context) : private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY private var userInitiatedTransition: Boolean = false private var cachedBufferedPosition: Long = 0L + private var boundSurfaceView: SurfaceView? = null private val availableCommands: Player.Commands = Player.Commands.Builder() .add(COMMAND_PLAY_PAUSE) @@ -795,6 +796,7 @@ class VlcPlayer(ctx: Context) : } override fun clearVideoSurface() { + boundSurfaceView = null vlcVout.detachViews() } @@ -825,10 +827,39 @@ class VlcPlayer(ctx: Context) : } 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() } + /** + * 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?) { clearVideoSurface() }