mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-03 19:11:37 +00:00
desktop: fix video playback hang caused by stuck preview snapshot (#6983)
* desktop: fix video playback hang caused by stuck preview snapshot
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.
* plans: add 2026-05-15-fix-video-preview-snapshot-hang.md
* desktop: capture preview via callback surface, keep helper pool
Follows up on the previous commit (4a964c66). The actual hang was in
libvlc's synchronous snapshots().get() on a reused helper, not in the
pooling itself. Replace the polling loop with a CallbackVideoSurface
(the existing SkiaBitmapVideoSurface) wrapped in withTimeoutOrNull —
the wait is bounded, so a non-decoding helper can't block previewThread.
Restore the helper-player pool that the previous commit dropped.
* plans: update 2026-05-15-fix-video-preview-snapshot-hang.md for final fix
This commit is contained in:
+8
-6
@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
|
||||
import uk.co.caprica.vlcj.media.VideoOrientation
|
||||
import uk.co.caprica.vlcj.player.base.*
|
||||
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||
@@ -214,7 +215,7 @@ actual class VideoPlayer actual constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
|
||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(previewThread.asCoroutineDispatcher()) {
|
||||
val mediaComponent = getOrCreateHelperPlayer()
|
||||
val player = mediaComponent.mediaPlayer()
|
||||
if (uri == null || !uri.toFile().exists()) {
|
||||
@@ -222,12 +223,12 @@ actual class VideoPlayer actual constructor(
|
||||
|
||||
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||
}
|
||||
val surface = SkiaBitmapVideoSurface()
|
||||
player.videoSurface().set(surface)
|
||||
player.media().startPaused(uri.toFile().absolutePath)
|
||||
val start = System.currentTimeMillis()
|
||||
var snap: BufferedImage? = null
|
||||
while (snap == null && start + 1500 > System.currentTimeMillis()) {
|
||||
snap = player.snapshots()?.get()
|
||||
delay(50)
|
||||
val snap = withTimeoutOrNull(1500L) {
|
||||
while (surface.bitmap.value == null) delay(50)
|
||||
surface.bitmap.value!!.toAwtImage()
|
||||
}
|
||||
val orientation = player.media().info().videoTracks().firstOrNull()?.orientation()
|
||||
if (orientation == null) {
|
||||
@@ -255,6 +256,7 @@ actual class VideoPlayer actual constructor(
|
||||
}
|
||||
|
||||
val playerThread = Executors.newSingleThreadExecutor()
|
||||
private val previewThread = Executors.newSingleThreadExecutor()
|
||||
private val playersPool: ArrayList<Component> = ArrayList()
|
||||
private val helperPlayersPool: ArrayList<CallbackMediaPlayerComponent> = ArrayList()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user