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
-1
View File
@@ -9,7 +9,6 @@ java {
}
application {
mainModule = "dev.quoije.umd.cli"
mainClass = "dev.quoije.umd.cli.Main"
}
+50 -1
View File
@@ -3,10 +3,12 @@ package dev.quoije.umd.cli;
import dev.quoije.umd.iso.UmdIsoFile;
import dev.quoije.umd.iso.UmdIsoReader;
import dev.quoije.umd.media.VideoDecoder;
import dev.quoije.umd.mps.MpsDemuxer;
import dev.quoije.umd.playlist.Playlist;
import dev.quoije.umd.sfo.Sfo;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
@@ -15,7 +17,7 @@ import java.util.Map;
/** Command-line front-end for the UMD player library. */
public final class Main {
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>";
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>";
private Main() {
}
@@ -31,6 +33,7 @@ public final class Main {
exitCode = switch (args[0]) {
case "dump" -> dump(requireArg(args, 1));
case "mps" -> mps(requireArg(args, 1), requireArg(args, 2));
case "decode" -> decode(requireArg(args, 1), requireArg(args, 2));
case "help", "--help", "-h" -> {
System.out.println(USAGE);
yield 0;
@@ -186,6 +189,52 @@ public final class Main {
return 0;
}
private static int decode(String imagePath, String streamName) throws IOException {
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath));
MpsDemuxer demuxer = MpsDemuxer.open(iso, streamName)) {
ByteArrayOutputStream video = new ByteArrayOutputStream();
int packets = 0;
int videoPackets = 0;
while (packets < 300) {
MpsDemuxer.Packet pkt = demuxer.nextPacket();
if (pkt == null) {
break;
}
packets++;
if (!pkt.isVideo()) {
continue;
}
video.write(pkt.payload(), 0, pkt.payload().length);
videoPackets++;
}
byte[] annexB = video.toByteArray();
try (VideoDecoder decoder = new VideoDecoder()) {
decoder.feedAnnexB(annexB);
decoder.flush();
int frames = 0;
VideoDecoder.Frame frame;
while ((frame = decoder.receive()) != null) {
frames++;
if (frames <= 8) {
long checksum = 0;
for (byte b : frame.yPlane()) {
checksum = checksum * 31 + (b & 0xFF);
}
System.out.printf("frame %d: %dx%d pixfmt=%d checksum=%d%n",
frames, frame.width(), frame.height(), frame.pixelFormat(), checksum);
}
}
System.out.printf("Decoded %d frames from %d video packets (%d total packets)%n",
frames, videoPackets, packets);
}
}
return 0;
}
private static String hexPrefix(byte[] data, int max) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.min(data.length, max); i++) {
-4
View File
@@ -1,4 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
module dev.quoije.umd.cli {
requires dev.quoije.umd.core;
}