From 3ded804b94ce1367bd4238ec27e455597cc02415 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Tue, 15 Sep 2026 23:47:41 -0400 Subject: [PATCH] Make PlaybackEngine thread-safe to fix FFmpeg segfault Video and audio threads both pull from the same engine, racing on the MPS demuxer file pointer and feeding corrupted H.264 data to FFmpeg (which segfaults). Synchronize the pull methods and wait for the audio thread before closing the engine. --- .../main/java/dev/quoije/umd/player/PlaybackEngine.java | 6 +++--- ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/dev/quoije/umd/player/PlaybackEngine.java b/core/src/main/java/dev/quoije/umd/player/PlaybackEngine.java index 35618c2..a0abcca 100644 --- a/core/src/main/java/dev/quoije/umd/player/PlaybackEngine.java +++ b/core/src/main/java/dev/quoije/umd/player/PlaybackEngine.java @@ -75,7 +75,7 @@ public final class PlaybackEngine implements AutoCloseable { * Decodes until the next video frame is available, or end of stream. * Returns {@code null} at end of stream. */ - public VideoFrame nextVideoFrame() throws IOException { + public synchronized VideoFrame nextVideoFrame() throws IOException { int budget = 1000; while (videoQueue.isEmpty() && budget-- > 0) { if (!routeOne()) { @@ -89,7 +89,7 @@ public final class PlaybackEngine implements AutoCloseable { * Decodes until the next audio chunk is available, or end of stream. * Returns {@code null} at end of stream. */ - public AudioChunk nextAudioChunk() throws IOException { + public synchronized AudioChunk nextAudioChunk() throws IOException { int budget = 1000; while (audioQueue.isEmpty() && budget-- > 0) { if (!routeOne()) { @@ -100,7 +100,7 @@ public final class PlaybackEngine implements AutoCloseable { } /** True once the stream has been fully decoded. */ - public boolean isEof() { + public synchronized boolean isEof() { return eof && videoQueue.isEmpty() && audioQueue.isEmpty(); } diff --git a/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java b/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java index 90f8055..fd57c1d 100644 --- a/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java +++ b/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java @@ -195,7 +195,7 @@ public final class PlayerApp extends Application { try (UmdIsoReader iso = new UmdIsoReader(Path.of(isoPath)); PlaybackEngine engine = PlaybackEngine.open(iso, streamName, 0)) { - startAudio(engine); + Thread audio = startAudio(engine); awaitClock(); long basePts = clockStartPts; long baseNanos = clockStartNanos; @@ -219,6 +219,10 @@ public final class PlayerApp extends Application { sleepUntil(targetNanos); } + // Wait for the audio thread to finish draining before the + // engine (and its decoders) is closed by this try block. + audio.join(2000); + MoviePlayer mp = moviePlayer; if (mp != null) { mp.onPlayListEnd(playListNumber); @@ -232,7 +236,7 @@ public final class PlayerApp extends Application { } /** Plays decoded PCM through the system audio line, acting as the master A/V clock. */ - private void startAudio(PlaybackEngine engine) { + private Thread startAudio(PlaybackEngine engine) { Thread audio = new Thread(() -> { SourceDataLine line = null; try { @@ -264,6 +268,7 @@ public final class PlayerApp extends Application { }, "audio-player"); audio.setDaemon(true); audio.start(); + return audio; } private static SourceDataLine openLine() throws Exception {