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 dev.quoije.umd.mps.MpsDemuxer;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.
|
* 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 AnnexBSplitter videoSplitter = new AnnexBSplitter();
|
||||||
private final AudioFramer audioFramer = new AudioFramer();
|
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 Atrac3pDecoder audioDecoder;
|
||||||
private final ArrayDeque<VideoFrame> videoQueue = new ArrayDeque<>();
|
private final BlockingQueue<VideoFrame> videoQueue = new LinkedBlockingQueue<>(VIDEO_QUEUE_CAPACITY);
|
||||||
private final ArrayDeque<AudioChunk> audioQueue = new ArrayDeque<>();
|
private final BlockingQueue<AudioChunk> audioQueue = new LinkedBlockingQueue<>(AUDIO_QUEUE_CAPACITY);
|
||||||
|
|
||||||
private long pendingVideoPts = -1;
|
private long pendingVideoPts = -1;
|
||||||
private long nextVideoPts = 0;
|
private long nextVideoPts = 0;
|
||||||
@@ -59,71 +64,98 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
private boolean audioPtsSet = false;
|
private boolean audioPtsSet = false;
|
||||||
private long audioFramesEmitted = 0;
|
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 {
|
private PlaybackEngine(UmdIsoReader iso, String streamName, int audioSubstream) throws IOException {
|
||||||
this.demuxer = MpsDemuxer.open(iso, streamName);
|
this.demuxer = MpsDemuxer.open(iso, streamName);
|
||||||
this.audioSubstream = audioSubstream;
|
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 {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The single decode thread: routes every packet and fills both queues. */
|
||||||
* Decodes until the next video frame is available, or end of stream.
|
private void decodeLoop() {
|
||||||
* Returns {@code null} at end of stream.
|
try {
|
||||||
*/
|
while (true) {
|
||||||
public synchronized VideoFrame nextVideoFrame() throws IOException {
|
MpsDemuxer.Packet packet = demuxer.nextPacket();
|
||||||
int budget = 1000;
|
if (packet == null) {
|
||||||
while (videoQueue.isEmpty() && budget-- > 0) {
|
finish();
|
||||||
if (!routeOne()) {
|
break;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Waits for the next audio chunk, or returns {@code null} at end of stream. */
|
||||||
* Decodes until the next audio chunk is available, or end of stream.
|
public AudioChunk nextAudioChunk() throws IOException {
|
||||||
* Returns {@code null} at end of stream.
|
while (true) {
|
||||||
*/
|
AudioChunk chunk;
|
||||||
public synchronized AudioChunk nextAudioChunk() throws IOException {
|
try {
|
||||||
int budget = 1000;
|
chunk = audioQueue.poll(50, TimeUnit.MILLISECONDS);
|
||||||
while (audioQueue.isEmpty() && budget-- > 0) {
|
} catch (InterruptedException e) {
|
||||||
if (!routeOne()) {
|
Thread.currentThread().interrupt();
|
||||||
break;
|
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. */
|
/** True once the stream has been fully decoded and drained. */
|
||||||
public synchronized boolean isEof() {
|
public boolean isEof() {
|
||||||
return eof && videoQueue.isEmpty() && audioQueue.isEmpty();
|
return eof && videoQueue.isEmpty() && audioQueue.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reads and routes one packet. Returns false at end of stream. */
|
private void handleVideo(MpsDemuxer.Packet packet) throws InterruptedException {
|
||||||
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) {
|
|
||||||
if (packet.pts() >= 0) {
|
if (packet.pts() >= 0) {
|
||||||
if (pendingVideoPts < 0) {
|
if (pendingVideoPts < 0) {
|
||||||
nextVideoPts = packet.pts();
|
nextVideoPts = packet.pts();
|
||||||
@@ -137,7 +169,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
videoDecoder.feedAccessUnit(accessUnit);
|
videoDecoder.feedAccessUnit(accessUnit);
|
||||||
VideoDecoder.Frame frame;
|
VideoDecoder.Frame frame;
|
||||||
while ((frame = videoDecoder.receive()) != null) {
|
while ((frame = videoDecoder.receive()) != null) {
|
||||||
videoQueue.add(new VideoFrame(
|
videoQueue.put(new VideoFrame(
|
||||||
assignVideoPts(), frame.width(), frame.height(),
|
assignVideoPts(), frame.width(), frame.height(),
|
||||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||||
@@ -167,7 +199,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
return videoFrameDurationTicks;
|
return videoFrameDurationTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAudio(MpsDemuxer.Packet packet) {
|
private void handleAudio(MpsDemuxer.Packet packet) throws InterruptedException {
|
||||||
if (packet.pts() >= 0) {
|
if (packet.pts() >= 0) {
|
||||||
if (!audioPtsSet) {
|
if (!audioPtsSet) {
|
||||||
audioFirstPts = packet.pts();
|
audioFirstPts = packet.pts();
|
||||||
@@ -191,12 +223,12 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
audioDecoder.feedFrame(frame);
|
audioDecoder.feedFrame(frame);
|
||||||
byte[] pcm;
|
byte[] pcm;
|
||||||
while ((pcm = audioDecoder.receive()) != null) {
|
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();
|
byte[] tail = videoSplitter.drain();
|
||||||
if (tail != null && tail.length > 0) {
|
if (tail != null && tail.length > 0) {
|
||||||
videoDecoder.feedAccessUnit(tail);
|
videoDecoder.feedAccessUnit(tail);
|
||||||
@@ -204,7 +236,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
videoDecoder.flush();
|
videoDecoder.flush();
|
||||||
VideoDecoder.Frame frame;
|
VideoDecoder.Frame frame;
|
||||||
while ((frame = videoDecoder.receive()) != null) {
|
while ((frame = videoDecoder.receive()) != null) {
|
||||||
videoQueue.add(new VideoFrame(
|
videoQueue.put(new VideoFrame(
|
||||||
assignVideoPts(), frame.width(), frame.height(),
|
assignVideoPts(), frame.width(), frame.height(),
|
||||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||||
@@ -214,7 +246,7 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
audioDecoder.flush();
|
audioDecoder.flush();
|
||||||
byte[] pcm;
|
byte[] pcm;
|
||||||
while ((pcm = audioDecoder.receive()) != null) {
|
while ((pcm = audioDecoder.receive()) != null) {
|
||||||
audioQueue.add(new AudioChunk(
|
audioQueue.put(new AudioChunk(
|
||||||
audioFirstPts + audioFramesEmitted * AUDIO_FRAME_TICKS, pcm));
|
audioFirstPts + audioFramesEmitted * AUDIO_FRAME_TICKS, pcm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +254,12 @@ public final class PlaybackEngine implements AutoCloseable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
decodeThread.interrupt();
|
||||||
|
try {
|
||||||
|
decodeThread.join(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
videoDecoder.close();
|
videoDecoder.close();
|
||||||
if (audioDecoder != null) {
|
if (audioDecoder != null) {
|
||||||
audioDecoder.close();
|
audioDecoder.close();
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public final class PlayerApp extends Application {
|
|||||||
private final WritableImage[] videoImages = new WritableImage[2];
|
private final WritableImage[] videoImages = new WritableImage[2];
|
||||||
private final int[][] argbBuffers = new int[2][];
|
private final int[][] argbBuffers = new int[2][];
|
||||||
private int videoBufferIndex = 0;
|
private int videoBufferIndex = 0;
|
||||||
|
private volatile boolean skipRequested = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) {
|
public void start(Stage stage) {
|
||||||
@@ -114,6 +115,10 @@ public final class PlayerApp extends Application {
|
|||||||
togglePause();
|
togglePause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case S, TRACK_NEXT -> {
|
||||||
|
skipRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
default -> { }
|
default -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +207,10 @@ public final class PlayerApp extends Application {
|
|||||||
|
|
||||||
PlaybackEngine.VideoFrame frame;
|
PlaybackEngine.VideoFrame frame;
|
||||||
while ((frame = engine.nextVideoFrame()) != null) {
|
while ((frame = engine.nextVideoFrame()) != null) {
|
||||||
|
if (skipRequested) {
|
||||||
|
skipRequested = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
int index = videoBufferIndex++ & 1;
|
int index = videoBufferIndex++ & 1;
|
||||||
int[] argb = argbBuffers[index];
|
int[] argb = argbBuffers[index];
|
||||||
int pixelCount = frame.width() * frame.height();
|
int pixelCount = frame.width() * frame.height();
|
||||||
|
|||||||
Reference in New Issue
Block a user