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:
@@ -9,6 +9,9 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.ffmpeg)
|
||||
runtimeOnly("org.bytedeco:ffmpeg:${libs.versions.ffmpeg.get()}:linux-x86_64")
|
||||
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user