Add MPS demuxer and FFmpeg H.264 video decode

- MpsDemuxer parses MPEG-PS packs (0x1BA) into video (0x1E0-0x1EF) and
  audio (0x1BD) PES packets with PTS/DTS at the 90kHz clock.
- VideoDecoder decodes Annex-B H.264 via JavaCPP FFmpeg 8.1.2, splitting
  the stream into access units (one slice NAL per packet) so each send
  yields a frame.
- CLI 'decode' command dumps frame metadata (720x480 YUVJ420P).
- Validated against Batman Begins: 72 frames decoded.
This commit is contained in:
2026-09-15 21:59:26 -04:00
parent f40fa604a3
commit 4821267d0e
7 changed files with 211 additions and 14 deletions
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.media;
import org.bytedeco.ffmpeg.avcodec.AVCodec;
import org.bytedeco.ffmpeg.avcodec.AVCodecContext;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avutil.AVFrame;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.PointerPointer;
import java.util.ArrayDeque;
import java.util.Arrays;
/** H.264 (Annex B) video decoder backed by FFmpeg. */
public final class VideoDecoder implements AutoCloseable {
private final AVCodecContext context;
private final AVPacket packet;
private final AVFrame frame;
private final ArrayDeque<Frame> frames = new ArrayDeque<>();
public VideoDecoder() {
AVCodec codec = avcodec.avcodec_find_decoder(avcodec.AV_CODEC_ID_H264);
if (codec == null || codec.isNull()) {
throw new IllegalStateException("H.264 decoder not available");
}
context = avcodec.avcodec_alloc_context3(codec);
if (context == null || context.isNull()) {
throw new IllegalStateException("Could not allocate codec context");
}
int ret = avcodec.avcodec_open2(context, codec, (PointerPointer) null);
if (ret < 0) {
throw new IllegalStateException("avcodec_open2 failed: " + ret);
}
packet = avcodec.av_packet_alloc();
frame = avutil.av_frame_alloc();
}
/**
* Feeds Annex-B H.264 data, splitting it into access units: each emitted
* packet ends at the end of a slice NAL (type 1 or 5), so the decoder
* produces one frame per packet.
*/
public void feedAnnexB(byte[] annexB) {
int accessUnitStart = 0;
int i = 0;
while (i < annexB.length) {
int startCodeLength = startCodeLengthAt(annexB, i);
if (startCodeLength > 0) {
int nalu = i + startCodeLength;
if (nalu < annexB.length) {
int type = annexB[nalu] & 0x1F;
if (type == 1 || type == 5) {
int end = findNextStartCode(annexB, nalu);
feedOne(Arrays.copyOfRange(annexB, accessUnitStart, end));
accessUnitStart = end;
i = end;
continue;
}
}
i = nalu;
} else {
i++;
}
}
if (accessUnitStart < annexB.length) {
feedOne(Arrays.copyOfRange(annexB, accessUnitStart, annexB.length));
}
}
private static int startCodeLengthAt(byte[] b, int i) {
if (i + 2 < b.length && b[i] == 0 && b[i + 1] == 0 && b[i + 2] == 1) {
return 3;
}
if (i + 3 < b.length && b[i] == 0 && b[i + 1] == 0 && b[i + 2] == 0 && b[i + 3] == 1) {
return 4;
}
return -1;
}
private static int findNextStartCode(byte[] b, int from) {
int j = from;
while (j < b.length) {
if (startCodeLengthAt(b, j) > 0) {
return j;
}
j++;
}
return b.length;
}
private void feedOne(byte[] data) {
if (avcodec.av_new_packet(packet, data.length) < 0) {
throw new IllegalStateException("av_new_packet failed");
}
packet.data().put(data);
int ret = avcodec.avcodec_send_packet(context, packet);
avcodec.av_packet_unref(packet);
if (ret < 0) {
throw new IllegalStateException("avcodec_send_packet failed: " + ret);
}
drain();
}
/** Flushes the decoder and signals end of stream. */
public void flush() {
avcodec.avcodec_send_packet(context, null);
drain();
}
private void drain() {
Frame f;
while ((f = receiveInternal()) != null) {
frames.add(f);
}
}
private Frame receiveInternal() {
int ret = avcodec.avcodec_receive_frame(context, frame);
if (ret < 0) {
return null; // EAGAIN or EOF
}
int width = frame.width();
int height = frame.height();
BytePointer y = frame.data(0);
int stride = frame.linesize(0);
byte[] yPlane = new byte[height * stride];
y.position(0).get(yPlane);
return new Frame(width, height, frame.format(), yPlane, stride);
}
/** A decoded video frame. */
public record Frame(int width, int height, int pixelFormat, byte[] yPlane, int yStride) {
}
/** Returns the next decoded frame, or {@code null} if none is buffered. */
public Frame receive() {
return frames.poll();
}
@Override
public void close() {
avutil.av_frame_free(frame);
avcodec.av_packet_free(packet);
avcodec.avcodec_free_context(context);
}
}
-7
View File
@@ -1,7 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
module dev.quoije.umd.core {
exports dev.quoije.umd.iso;
exports dev.quoije.umd.sfo;
exports dev.quoije.umd.playlist;
exports dev.quoije.umd.mps;
}