From 4a964c66158c72c18738ef9ec85828c43fb6533e Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 15 May 2026 16:31:22 +0000 Subject: [PATCH] desktop: fix video playback hang caused by stuck preview snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: clicking play on a video did nothing when an earlier video's preview generation was stuck — every subsequent VideoPlayer.play() was queued behind it on the shared playerThread. Cause: helper player reuse across previews exhausted the libavcodec h264 frame-buffer pool with --avcodec-hw=none (PR #6924), and the synchronous libvlc snapshots().get() call then hung waiting for a frame that was never decoded. Fix: drop the helper-player pool (release each helper after use) and run preview generation on a dedicated previewThread so a stuck preview can no longer block playback. --- .../simplex/common/platform/VideoPlayer.desktop.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt index 90c80d3b2a..3f74f62920 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -214,8 +214,8 @@ actual class VideoPlayer actual constructor( } } - suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) { - val mediaComponent = getOrCreateHelperPlayer() + suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(previewThread.asCoroutineDispatcher()) { + val mediaComponent = createHelperPlayer() val player = mediaComponent.mediaPlayer() if (uri == null || !uri.toFile().exists()) { if (withAlertOnException) showVideoDecodingException() @@ -232,7 +232,7 @@ actual class VideoPlayer actual constructor( val orientation = player.media().info().videoTracks().firstOrNull()?.orientation() if (orientation == null) { player.stop() - putHelperPlayer(mediaComponent) + player.release() if (withAlertOnException) showVideoDecodingException() return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) @@ -250,13 +250,13 @@ actual class VideoPlayer actual constructor( }?.toComposeImageBitmap() val duration = player.duration.toLong() player.stop() - putHelperPlayer(mediaComponent) + player.release() return@withContext VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration) } val playerThread = Executors.newSingleThreadExecutor() + private val previewThread = Executors.newSingleThreadExecutor() private val playersPool: ArrayList = ArrayList() - private val helperPlayersPool: ArrayList = ArrayList() private fun getOrCreatePlayer(): Component = playersPool.removeFirstOrNull() ?: createNew() @@ -280,7 +280,6 @@ actual class VideoPlayer actual constructor( private fun putPlayer(player: Component) = playersPool.add(player) - private fun getOrCreateHelperPlayer(): CallbackMediaPlayerComponent = helperPlayersPool.removeFirstOrNull() ?: CallbackMediaPlayerComponent(MediaPlayerSpecs.callbackMediaPlayerSpec().apply { withFactory(vlcPreviewFactory) }) - private fun putHelperPlayer(player: CallbackMediaPlayerComponent) = helperPlayersPool.add(player) + private fun createHelperPlayer(): CallbackMediaPlayerComponent = CallbackMediaPlayerComponent(MediaPlayerSpecs.callbackMediaPlayerSpec().apply { withFactory(vlcPreviewFactory) }) } }