Add MPS/MPEG-PS demuxer (H.264 video + ATRAC3+ audio packets)
- Parse PS packets, PES headers and PTS/DTS timestamps - 'mps' CLI command samples packets from a stream - Validated on Batman Begins 00001.MPS (Annex-B H.264 + 2-channel ATRAC3+)
This commit is contained in:
@@ -3,6 +3,7 @@ package dev.quoije.umd.cli;
|
||||
|
||||
import dev.quoije.umd.iso.UmdIsoFile;
|
||||
import dev.quoije.umd.iso.UmdIsoReader;
|
||||
import dev.quoije.umd.mps.MpsDemuxer;
|
||||
import dev.quoije.umd.playlist.Playlist;
|
||||
import dev.quoije.umd.sfo.Sfo;
|
||||
|
||||
@@ -14,7 +15,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>";
|
||||
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>";
|
||||
|
||||
private Main() {
|
||||
}
|
||||
@@ -29,6 +30,7 @@ public final class Main {
|
||||
try {
|
||||
exitCode = switch (args[0]) {
|
||||
case "dump" -> dump(requireArg(args, 1));
|
||||
case "mps" -> mps(requireArg(args, 1), requireArg(args, 2));
|
||||
case "help", "--help", "-h" -> {
|
||||
System.out.println(USAGE);
|
||||
yield 0;
|
||||
@@ -151,6 +153,47 @@ public final class Main {
|
||||
return String.format("%02d:%02d:%02d.%02d", hours, minutes, secs, hundredth);
|
||||
}
|
||||
|
||||
private static int mps(String imagePath, String streamName) throws IOException {
|
||||
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath))) {
|
||||
try (MpsDemuxer demuxer = MpsDemuxer.open(iso, streamName)) {
|
||||
int count = 0;
|
||||
int videoPackets = 0;
|
||||
int audioPackets = 0;
|
||||
while (count < 40) {
|
||||
MpsDemuxer.Packet packet = demuxer.nextPacket();
|
||||
if (packet == null) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
String type = packet.isVideo()
|
||||
? "video"
|
||||
: packet.isAudio() ? "audio(ch=" + packet.channel() + ")" : "other";
|
||||
System.out.printf("%d: stream=0x%02X %s pts=%d len=%d%n",
|
||||
count, packet.streamId(), type, packet.pts(), packet.payload().length);
|
||||
if (packet.isVideo()) {
|
||||
videoPackets++;
|
||||
if (videoPackets == 1) {
|
||||
System.out.println(" first video payload: " + hexPrefix(packet.payload(), 16));
|
||||
}
|
||||
} else if (packet.isAudio()) {
|
||||
audioPackets++;
|
||||
}
|
||||
}
|
||||
System.out.printf("Sampled %d packets (%d video, %d audio)%n",
|
||||
count, videoPackets, audioPackets);
|
||||
}
|
||||
}
|
||||
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++) {
|
||||
sb.append(String.format("%02X ", data[i] & 0xFF));
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private static byte[] readFile(UmdIsoReader iso, String path) throws IOException {
|
||||
if (!iso.hasFile(path)) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.mps;
|
||||
|
||||
import dev.quoije.umd.iso.UmdIsoFile;
|
||||
import dev.quoije.umd.iso.UmdIsoReader;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Demuxer for the MPEG program stream inside a UMD {@code .MPS} file.
|
||||
*
|
||||
* <p>The stream starts with a PSMF header (magic {@code "PSMF"}) followed by
|
||||
* MPEG program stream packets: video (stream id {@code 0x1E0}-{@code 0x1EF},
|
||||
* H.264) and audio (stream id {@code 0x1BD}, ATRAC3plus).</p>
|
||||
*/
|
||||
public final class MpsDemuxer implements AutoCloseable {
|
||||
|
||||
public static final int PACK_START_CODE = 0x1BA;
|
||||
public static final int SYSTEM_HEADER_START_CODE = 0x1BB;
|
||||
public static final int PRIVATE_STREAM_1 = 0x1BD;
|
||||
public static final int PADDING_STREAM = 0x1BE;
|
||||
public static final int PRIVATE_STREAM_2 = 0x1BF;
|
||||
|
||||
public static final long UNKNOWN_TIMESTAMP = -1L;
|
||||
|
||||
private final UmdIsoFile file;
|
||||
|
||||
private MpsDemuxer(UmdIsoFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/** A single PES packet extracted from the stream. */
|
||||
public record Packet(int streamId, long pts, long dts, int channel, byte[] payload) {
|
||||
public boolean isVideo() {
|
||||
return streamId >= 0x1E0 && streamId <= 0x1EF;
|
||||
}
|
||||
|
||||
public boolean isAudio() {
|
||||
return streamId == PRIVATE_STREAM_1;
|
||||
}
|
||||
}
|
||||
|
||||
public static MpsDemuxer open(UmdIsoReader iso, String streamName) throws IOException {
|
||||
UmdIsoFile file = iso.getFile("UMD_VIDEO/STREAM/" + streamName + ".MPS");
|
||||
return new MpsDemuxer(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next media packet, or {@code null} at end of stream.
|
||||
* Non-media packets (pack headers, padding, private stream 2) are skipped.
|
||||
*/
|
||||
public Packet nextPacket() throws IOException {
|
||||
while (true) {
|
||||
int startCode = read32();
|
||||
if (startCode == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (startCode) {
|
||||
case PACK_START_CODE -> skip(10);
|
||||
case SYSTEM_HEADER_START_CODE -> skip(14);
|
||||
case PADDING_STREAM, PRIVATE_STREAM_2 -> {
|
||||
int len = read16();
|
||||
if (len < 0) {
|
||||
return null;
|
||||
}
|
||||
skip(len);
|
||||
}
|
||||
case PRIVATE_STREAM_1 -> {
|
||||
int codeLength = read16();
|
||||
if (codeLength < 0) {
|
||||
return null;
|
||||
}
|
||||
PesHeader pes = readPesHeader(startCode);
|
||||
int payloadLength = codeLength - pes.headerLength();
|
||||
byte[] payload = readPayload(payloadLength);
|
||||
return new Packet(startCode, pes.pts(), pes.dts(), pes.channel(), payload);
|
||||
}
|
||||
default -> {
|
||||
if (startCode >= 0x1E0 && startCode <= 0x1EF) {
|
||||
int codeLength = read16();
|
||||
if (codeLength < 0) {
|
||||
return null;
|
||||
}
|
||||
PesHeader pes = readPesHeader(startCode);
|
||||
int payloadLength = codeLength - pes.headerLength();
|
||||
byte[] payload = readPayload(payloadLength);
|
||||
return new Packet(startCode, pes.pts(), pes.dts(), -1, payload);
|
||||
}
|
||||
throw new IOException(String.format("Unknown MPEG-PS start code 0x%08X", startCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readPayload(int length) throws IOException {
|
||||
if (length <= 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
byte[] data = new byte[length];
|
||||
file.readFully(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private record PesHeader(long pts, long dts, int channel, int headerLength) {
|
||||
}
|
||||
|
||||
private PesHeader readPesHeader(int startCode) throws IOException {
|
||||
int pesLength = 0;
|
||||
int c = read8();
|
||||
pesLength++;
|
||||
while (c == 0xFF) {
|
||||
c = read8();
|
||||
pesLength++;
|
||||
}
|
||||
|
||||
if ((c & 0xC0) == 0x40) {
|
||||
skip(1);
|
||||
c = read8();
|
||||
pesLength += 2;
|
||||
}
|
||||
|
||||
long pts = UNKNOWN_TIMESTAMP;
|
||||
long dts = UNKNOWN_TIMESTAMP;
|
||||
if ((c & 0xE0) == 0x20) {
|
||||
long first = readPts(c);
|
||||
pesLength += 4;
|
||||
dts = first;
|
||||
pts = first;
|
||||
if ((c & 0x10) != 0) {
|
||||
pts = readPts();
|
||||
pesLength += 5;
|
||||
}
|
||||
} else if ((c & 0xC0) == 0x80) {
|
||||
int flags = read8();
|
||||
int headerLength = read8();
|
||||
pesLength += 2 + headerLength;
|
||||
if ((flags & 0x80) != 0) {
|
||||
pts = readPts();
|
||||
dts = pts;
|
||||
headerLength -= 5;
|
||||
if ((flags & 0x40) != 0) {
|
||||
dts = readPts();
|
||||
headerLength -= 5;
|
||||
}
|
||||
}
|
||||
if ((flags & 0x3F) != 0 && headerLength == 0) {
|
||||
flags &= 0xC0;
|
||||
}
|
||||
if ((flags & 0x01) != 0) {
|
||||
int pesExt = read8();
|
||||
headerLength--;
|
||||
int skipLen = (pesExt >> 4) & 0x0B;
|
||||
skipLen += skipLen & 0x09;
|
||||
if ((pesExt & 0x40) != 0 || skipLen > headerLength) {
|
||||
pesExt = 0;
|
||||
skipLen = 0;
|
||||
}
|
||||
skip(skipLen);
|
||||
headerLength -= skipLen;
|
||||
if ((pesExt & 0x01) != 0) {
|
||||
int ext2Length = read8();
|
||||
headerLength--;
|
||||
if ((ext2Length & 0x7F) != 0) {
|
||||
int idExt = read8();
|
||||
headerLength--;
|
||||
if ((idExt & 0x80) == 0) {
|
||||
startCode = ((startCode & 0xFF) << 8) | idExt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
skip(headerLength);
|
||||
}
|
||||
|
||||
int channel = -1;
|
||||
if (startCode == PRIVATE_STREAM_1) {
|
||||
channel = read8();
|
||||
pesLength++;
|
||||
if (channel >= 0x80 && channel <= 0xCF) {
|
||||
skip(3);
|
||||
pesLength += 3;
|
||||
if (channel >= 0xB0 && channel <= 0xBF) {
|
||||
skip(1);
|
||||
pesLength++;
|
||||
}
|
||||
} else {
|
||||
skip(3);
|
||||
pesLength += 3;
|
||||
}
|
||||
}
|
||||
|
||||
return new PesHeader(pts, dts, channel, pesLength);
|
||||
}
|
||||
|
||||
private long readPts(int c) throws IOException {
|
||||
return (((long) (c & 0x0E)) << 29)
|
||||
| ((long) (read16() >> 1) << 15)
|
||||
| (read16() >> 1);
|
||||
}
|
||||
|
||||
private long readPts() throws IOException {
|
||||
return readPts(read8());
|
||||
}
|
||||
|
||||
private int read8() throws IOException {
|
||||
try {
|
||||
return file.read();
|
||||
} catch (EOFException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private int read16() throws IOException {
|
||||
int a = read8();
|
||||
if (a < 0) {
|
||||
return -1;
|
||||
}
|
||||
int b = read8();
|
||||
if (b < 0) {
|
||||
return -1;
|
||||
}
|
||||
return (a << 8) | b;
|
||||
}
|
||||
|
||||
private int read32() throws IOException {
|
||||
int a = read16();
|
||||
if (a < 0) {
|
||||
return -1;
|
||||
}
|
||||
int b = read16();
|
||||
if (b < 0) {
|
||||
return -1;
|
||||
}
|
||||
return (a << 16) | (b & 0xFFFF);
|
||||
}
|
||||
|
||||
private void skip(int n) throws IOException {
|
||||
if (n > 0) {
|
||||
file.skip(n);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ 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