Decode on a single background thread; add intro skip key
PlaybackEngine now runs one daemon decode thread that routes packets into bounded queues, while the video/audio presenter threads only consume and pace. This removes the demuxer contention that caused stutter, bursts, and the speed-up/catch-up behavior. Press S (or media next) during playback to skip the current playlist and advance the menu's intro chain quickly.
This commit is contained in:
@@ -9,7 +9,9 @@ import dev.quoije.umd.mps.AudioFramer;
|
||||
import dev.quoije.umd.mps.MpsDemuxer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Decodes an MPS stream into timestamped video frames and PCM audio.
|
||||
@@ -47,9 +49,12 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
private final AnnexBSplitter videoSplitter = new AnnexBSplitter();
|
||||
private final AudioFramer audioFramer = new AudioFramer();
|
||||
|
||||
private static final int VIDEO_QUEUE_CAPACITY = 16;
|
||||
private static final int AUDIO_QUEUE_CAPACITY = 64;
|
||||
|
||||
private Atrac3pDecoder audioDecoder;
|
||||
private final ArrayDeque<VideoFrame> videoQueue = new ArrayDeque<>();
|
||||
private final ArrayDeque<AudioChunk> audioQueue = new ArrayDeque<>();
|
||||
private final BlockingQueue<VideoFrame> videoQueue = new LinkedBlockingQueue<>(VIDEO_QUEUE_CAPACITY);
|
||||
private final BlockingQueue<AudioChunk> audioQueue = new LinkedBlockingQueue<>(AUDIO_QUEUE_CAPACITY);
|
||||
|
||||
private long pendingVideoPts = -1;
|
||||
private long nextVideoPts = 0;
|
||||
@@ -59,71 +64,98 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
private boolean audioPtsSet = false;
|
||||
private long audioFramesEmitted = 0;
|
||||
|
||||
private boolean eof = false;
|
||||
private volatile boolean eof = false;
|
||||
private volatile IOException failure = null;
|
||||
private final Thread decodeThread;
|
||||
|
||||
private PlaybackEngine(UmdIsoReader iso, String streamName, int audioSubstream) throws IOException {
|
||||
this.demuxer = MpsDemuxer.open(iso, streamName);
|
||||
this.audioSubstream = audioSubstream;
|
||||
this.decodeThread = new Thread(this::decodeLoop, "umd-decode-" + streamName);
|
||||
this.decodeThread.setDaemon(true);
|
||||
}
|
||||
|
||||
/** Opens the given playlist stream for playback, using the given audio substream (0 or 1). */
|
||||
/** Opens the given playlist stream and starts decoding in the background. */
|
||||
public static PlaybackEngine open(UmdIsoReader iso, String streamName, int audioSubstream) throws IOException {
|
||||
return new PlaybackEngine(iso, streamName, audioSubstream);
|
||||
PlaybackEngine engine = new PlaybackEngine(iso, streamName, audioSubstream);
|
||||
engine.decodeThread.start();
|
||||
return engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes until the next video frame is available, or end of stream.
|
||||
* Returns {@code null} at end of stream.
|
||||
*/
|
||||
public synchronized VideoFrame nextVideoFrame() throws IOException {
|
||||
int budget = 1000;
|
||||
while (videoQueue.isEmpty() && budget-- > 0) {
|
||||
if (!routeOne()) {
|
||||
break;
|
||||
/** The single decode thread: routes every packet and fills both queues. */
|
||||
private void decodeLoop() {
|
||||
try {
|
||||
while (true) {
|
||||
MpsDemuxer.Packet packet = demuxer.nextPacket();
|
||||
if (packet == null) {
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
if (packet.isVideo()) {
|
||||
handleVideo(packet);
|
||||
} else if (packet.isAudio() && packet.channel() == audioSubstream) {
|
||||
handleAudio(packet);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Closed; stop decoding.
|
||||
} catch (IOException e) {
|
||||
failure = e;
|
||||
} finally {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Waits for the next video frame, or returns {@code null} at end of stream. */
|
||||
public VideoFrame nextVideoFrame() throws IOException {
|
||||
while (true) {
|
||||
VideoFrame frame;
|
||||
try {
|
||||
frame = videoQueue.poll(50, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
if (frame != null) {
|
||||
return frame;
|
||||
}
|
||||
if (eof && videoQueue.isEmpty()) {
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return videoQueue.poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes until the next audio chunk is available, or end of stream.
|
||||
* Returns {@code null} at end of stream.
|
||||
*/
|
||||
public synchronized AudioChunk nextAudioChunk() throws IOException {
|
||||
int budget = 1000;
|
||||
while (audioQueue.isEmpty() && budget-- > 0) {
|
||||
if (!routeOne()) {
|
||||
break;
|
||||
/** Waits for the next audio chunk, or returns {@code null} at end of stream. */
|
||||
public AudioChunk nextAudioChunk() throws IOException {
|
||||
while (true) {
|
||||
AudioChunk chunk;
|
||||
try {
|
||||
chunk = audioQueue.poll(50, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
if (chunk != null) {
|
||||
return chunk;
|
||||
}
|
||||
if (eof && audioQueue.isEmpty()) {
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return audioQueue.poll();
|
||||
}
|
||||
|
||||
/** True once the stream has been fully decoded. */
|
||||
public synchronized boolean isEof() {
|
||||
/** True once the stream has been fully decoded and drained. */
|
||||
public boolean isEof() {
|
||||
return eof && videoQueue.isEmpty() && audioQueue.isEmpty();
|
||||
}
|
||||
|
||||
/** Reads and routes one packet. Returns false at end of stream. */
|
||||
private boolean routeOne() throws IOException {
|
||||
if (eof) {
|
||||
return false;
|
||||
}
|
||||
MpsDemuxer.Packet packet = demuxer.nextPacket();
|
||||
if (packet == null) {
|
||||
finish();
|
||||
eof = true;
|
||||
return false;
|
||||
}
|
||||
if (packet.isVideo()) {
|
||||
handleVideo(packet);
|
||||
} else if (packet.isAudio() && packet.channel() == audioSubstream) {
|
||||
handleAudio(packet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleVideo(MpsDemuxer.Packet packet) {
|
||||
private void handleVideo(MpsDemuxer.Packet packet) throws InterruptedException {
|
||||
if (packet.pts() >= 0) {
|
||||
if (pendingVideoPts < 0) {
|
||||
nextVideoPts = packet.pts();
|
||||
@@ -137,7 +169,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
videoDecoder.feedAccessUnit(accessUnit);
|
||||
VideoDecoder.Frame frame;
|
||||
while ((frame = videoDecoder.receive()) != null) {
|
||||
videoQueue.add(new VideoFrame(
|
||||
videoQueue.put(new VideoFrame(
|
||||
assignVideoPts(), frame.width(), frame.height(),
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||
@@ -167,7 +199,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
return videoFrameDurationTicks;
|
||||
}
|
||||
|
||||
private void handleAudio(MpsDemuxer.Packet packet) {
|
||||
private void handleAudio(MpsDemuxer.Packet packet) throws InterruptedException {
|
||||
if (packet.pts() >= 0) {
|
||||
if (!audioPtsSet) {
|
||||
audioFirstPts = packet.pts();
|
||||
@@ -191,12 +223,12 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
audioDecoder.feedFrame(frame);
|
||||
byte[] pcm;
|
||||
while ((pcm = audioDecoder.receive()) != null) {
|
||||
audioQueue.add(new AudioChunk(pts, pcm));
|
||||
audioQueue.put(new AudioChunk(pts, pcm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
private void finish() throws InterruptedException {
|
||||
byte[] tail = videoSplitter.drain();
|
||||
if (tail != null && tail.length > 0) {
|
||||
videoDecoder.feedAccessUnit(tail);
|
||||
@@ -204,7 +236,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
videoDecoder.flush();
|
||||
VideoDecoder.Frame frame;
|
||||
while ((frame = videoDecoder.receive()) != null) {
|
||||
videoQueue.add(new VideoFrame(
|
||||
videoQueue.put(new VideoFrame(
|
||||
assignVideoPts(), frame.width(), frame.height(),
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||
@@ -214,7 +246,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
audioDecoder.flush();
|
||||
byte[] pcm;
|
||||
while ((pcm = audioDecoder.receive()) != null) {
|
||||
audioQueue.add(new AudioChunk(
|
||||
audioQueue.put(new AudioChunk(
|
||||
audioFirstPts + audioFramesEmitted * AUDIO_FRAME_TICKS, pcm));
|
||||
}
|
||||
}
|
||||
@@ -222,6 +254,12 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
decodeThread.interrupt();
|
||||
try {
|
||||
decodeThread.join(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
videoDecoder.close();
|
||||
if (audioDecoder != null) {
|
||||
audioDecoder.close();
|
||||
|
||||
@@ -64,6 +64,7 @@ public final class PlayerApp extends Application {
|
||||
private final WritableImage[] videoImages = new WritableImage[2];
|
||||
private final int[][] argbBuffers = new int[2][];
|
||||
private int videoBufferIndex = 0;
|
||||
private volatile boolean skipRequested = false;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
@@ -114,6 +115,10 @@ public final class PlayerApp extends Application {
|
||||
togglePause();
|
||||
return;
|
||||
}
|
||||
case S, TRACK_NEXT -> {
|
||||
skipRequested = true;
|
||||
return;
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
|
||||
@@ -202,6 +207,10 @@ public final class PlayerApp extends Application {
|
||||
|
||||
PlaybackEngine.VideoFrame frame;
|
||||
while ((frame = engine.nextVideoFrame()) != null) {
|
||||
if (skipRequested) {
|
||||
skipRequested = false;
|
||||
break;
|
||||
}
|
||||
int index = videoBufferIndex++ & 1;
|
||||
int[] argb = argbBuffers[index];
|
||||
int pixelCount = frame.width() * frame.height();
|
||||
|
||||
Reference in New Issue
Block a user