Add playback engine with A/V timestamping
- AnnexBSplitter incrementally splits the Annex-B stream into access units, holding partial slices across packet feeds. - VideoDecoder exposes feedAccessUnit and the decoded frame rate. - PlaybackEngine demuxes and decodes in lockstep, tagging video frames and audio chunks with 90 kHz PTS (audio from PES headers, video from PES anchors interpolated by decoder frame rate). - CLI 'play' command runs the engine headlessly and reports A/V drift. - Validated against Batman Begins: 95 frames in 4.0s, max drift -78.6 ms (video trails audio at stream start), avg frame duration 3833.6 ticks. - Tests for AnnexBSplitter.
This commit is contained in:
@@ -7,6 +7,7 @@ import dev.quoije.umd.media.Atrac3pDecoder;
|
||||
import dev.quoije.umd.media.VideoDecoder;
|
||||
import dev.quoije.umd.mps.AudioFramer;
|
||||
import dev.quoije.umd.mps.MpsDemuxer;
|
||||
import dev.quoije.umd.player.PlaybackEngine;
|
||||
import dev.quoije.umd.playlist.Playlist;
|
||||
import dev.quoije.umd.sfo.Sfo;
|
||||
|
||||
@@ -19,7 +20,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>\n umdplayer decode <image.iso> <streamName>\n umdplayer audio <image.iso> <streamName> [count]\n umdplayer adecode <image.iso> <streamName> [maxPackets]";
|
||||
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>\n umdplayer audio <image.iso> <streamName> [count]\n umdplayer adecode <image.iso> <streamName> [maxPackets]\n umdplayer play <image.iso> <streamName> [seconds]";
|
||||
|
||||
private Main() {
|
||||
}
|
||||
@@ -38,6 +39,7 @@ public final class Main {
|
||||
case "decode" -> decode(requireArg(args, 1), requireArg(args, 2));
|
||||
case "audio" -> audio(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 8);
|
||||
case "adecode" -> adecode(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 500);
|
||||
case "play" -> play(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Double.parseDouble(args[3]) : 5.0);
|
||||
case "help", "--help", "-h" -> {
|
||||
System.out.println(USAGE);
|
||||
yield 0;
|
||||
@@ -333,6 +335,55 @@ public final class Main {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int play(String imagePath, String streamName, double seconds) throws IOException {
|
||||
long targetPts = (long) (seconds * PlaybackEngine.MPEG_TIMESTAMP);
|
||||
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath));
|
||||
PlaybackEngine engine = PlaybackEngine.open(iso, streamName, 0)) {
|
||||
|
||||
int frames = 0;
|
||||
long firstPts = -1;
|
||||
long lastPts = -1;
|
||||
long audioClock = -1;
|
||||
long maxDrift = 0;
|
||||
|
||||
PlaybackEngine.VideoFrame frame;
|
||||
while ((frame = engine.nextVideoFrame()) != null) {
|
||||
if (firstPts < 0) {
|
||||
firstPts = frame.pts();
|
||||
}
|
||||
lastPts = frame.pts();
|
||||
frames++;
|
||||
|
||||
// Advance the audio clock to at least this frame's timestamp.
|
||||
while (audioClock < frame.pts()) {
|
||||
PlaybackEngine.AudioChunk chunk = engine.nextAudioChunk();
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
audioClock = chunk.pts() + PlaybackEngine.AUDIO_FRAME_TICKS;
|
||||
}
|
||||
|
||||
long drift = frame.pts() - audioClock;
|
||||
if (Math.abs(drift) > Math.abs(maxDrift)) {
|
||||
maxDrift = drift;
|
||||
}
|
||||
|
||||
if (frame.pts() >= targetPts) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double spanSeconds = (lastPts - firstPts) / (double) PlaybackEngine.MPEG_TIMESTAMP;
|
||||
double frameDuration = frames > 1 ? (lastPts - firstPts) / (double) (frames - 1) : 0;
|
||||
System.out.printf("video: %d frames, pts %d..%d (%.2fs), avg frame duration %.2f ticks%n",
|
||||
frames, firstPts, lastPts, spanSeconds, frameDuration);
|
||||
System.out.printf("audio clock: %d (%.2fs), max |video-audio| drift: %d ticks (%.1f ms)%n",
|
||||
audioClock, audioClock / (double) PlaybackEngine.MPEG_TIMESTAMP,
|
||||
maxDrift, maxDrift * 1000.0 / PlaybackEngine.MPEG_TIMESTAMP);
|
||||
}
|
||||
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++) {
|
||||
|
||||
Reference in New Issue
Block a user