feat: step to adjacent decoded video frames

This commit is contained in:
2026-10-10 17:49:04 +02:00
parent 310be5aa61
commit a99b0c1168
6 changed files with 311 additions and 29 deletions
+101
View File
@@ -31,12 +31,109 @@ static PlayerState until(Player *player, const std::function<bool(PlayerState)>
std::exit(1);
}
static PlayerState frameCompleted(Player *player, uint64_t revision, double pts) {
auto state = until(player, [revision](auto s) {
return !s.seek_pending && s.revision != revision && s.status == PLAYER_PAUSED;
});
if (std::abs(state.position - pts) >= .0001) {
std::fprintf(stderr, "Frame PTS: expected %.4f, got %.4f\n", pts, state.position);
require(false, "step selects exact adjacent presentation timestamp");
}
return state;
}
static PlayerState step(Player *player, int direction, double pts) {
auto revision = player_get_state(player).revision;
require(player_step_frame(player, direction), "frame step accepted");
return frameCompleted(player, revision, pts);
}
static void frameSteps(Player *player, MTKView *view, const std::string &directory) {
// Independent literals from the fixture's integer-millisecond setpts input.
constexpr double pts[] = {0, .1, .2, .3, .4, .65, .9, 1.15, 1.4, 1.5, 1.6, 1.7};
require(player_open(player, (directory + "/frame-step.mp4").c_str()), "VFR open accepted");
until(player, [](auto s) { return !s.seek_pending && s.status == PLAYER_PAUSED; });
require(!player_step_frame(player, 0) && !player_step_frame(player, 2) &&
!player_step_frame(player, -2), "only adjacent frame directions accepted");
step(player, -1, 0); // First boundary also ensures the initial preview is ready.
for (unsigned i = 1; i < sizeof(pts) / sizeof(pts[0]); ++i) {
step(player, 1, pts[i]);
[view draw];
}
step(player, 1, 1.7);
for (int i = 10; i >= 0; --i) step(player, -1, pts[i]);
step(player, -1, 0);
auto revision = player_get_state(player).revision;
require(player_step_frame(player, 1) && player_step_frame(player, 1) &&
player_step_frame(player, 1) && player_step_frame(player, 1),
"repeated queued steps accepted");
frameCompleted(player, revision, .4);
revision = player_get_state(player).revision;
require(player_step_frame(player, 1) && player_step_frame(player, 1) &&
player_step_frame(player, -1), "mixed queued steps accepted");
frameCompleted(player, revision, .65);
revision = player_get_state(player).revision;
require(player_seek(player, .72), "non-keyframe seek accepted");
until(player, [revision](auto s) {
return !s.seek_pending && s.revision != revision && s.status == PLAYER_PAUSED;
});
// Seek previews .9, not the .72 authoritative seek clock and not keyframe zero.
step(player, 1, 1.15);
step(player, -1, .9);
step(player, -1, .65);
revision = player_get_state(player).revision;
require(player_seek(player, .72) && player_step_frame(player, 1) &&
player_step_frame(player, -1), "queued seek and steps accepted");
frameCompleted(player, revision, .9);
step(player, -1, .65);
require(player_play(player), "play before frame step accepted");
until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .67 && s.position < .8; });
auto frozen = step(player, 1, .9);
[view draw];
for (int i = 0; i < 20; ++i)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, .005, false);
auto paused = player_get_state(player);
require(paused.status == PLAYER_PAUSED && !paused.seek_pending &&
std::abs(paused.position - frozen.position) < .0001,
"stepping during playback pauses and freezes audio/video timeline");
require(player_play(player), "resume stepped timeline accepted");
until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.19 && s.position < 1.32; });
require(player_pause(player), "pause resumed timeline accepted");
until(player, [](auto s) { return s.status == PLAYER_PAUSED; });
// Resumed presentation has advanced from .9 to 1.15; no pre-step frame remains.
step(player, -1, .9);
require(player_play(player), "play stepped media through EOF");
until(player, [](auto s) { return s.status == PLAYER_ENDED; });
step(player, 1, 1.7);
step(player, -1, 1.6);
step(player, 1, 1.7);
step(player, 1, 1.7);
require(player_open(player, (directory + "/video.mp4").c_str()), "video-only stepping open");
until(player, [](auto s) { return s.status == PLAYER_PAUSED; });
revision = player_get_state(player).revision;
require(player_seek(player, 3.9), "seek near video-only last frame");
until(player, [revision](auto s) { return !s.seek_pending && s.revision != revision; });
step(player, 1, 95.0 / 24);
require(player_play(player), "resume video-only last frame");
auto ended = until(player, [](auto s) { return s.status == PLAYER_ENDED; });
require(std::abs(ended.position - 4) < .0001, "last stepped frame retains its presentation duration");
step(player, -1, 94.0 / 24);
require(player_close(player), "close stepped media accepted");
until(player, [](auto s) { return s.status == PLAYER_EMPTY; });
std::puts("PASS exact VFR/B-frame steps/queued steps/seek reference/pause/resume/boundaries");
}
int main(int argc, char **argv) {
@autoreleasepool {
require(argc == 2, "fixture directory argument required");
[NSApplication sharedApplication];
std::unique_ptr<Player, decltype(&player_destroy)> player(player_create(), player_destroy);
require(bool(player), "player creation");
require(!player_step_frame(nullptr, 1) && !player_step_frame(player.get(), 1),
"null and empty players reject frame steps");
void *native = player_macos_make_video_view(player.get());
require(native != nullptr, "Metal view and shaders initialize");
MTKView *view = (__bridge_transfer MTKView *)native;
@@ -56,6 +153,9 @@ int main(int argc, char **argv) {
"audio track selection");
double aspect = !opened.has_video ? 0 : std::string(fixture) == "anamorphic.mp4" ? 8.0 / 3 : 16.0 / 9;
require(std::abs(opened.video_aspect_ratio - aspect) < .0001, "display aspect includes non-square pixels");
if (!opened.has_video)
require(!player_step_frame(player.get(), 1) && !player_step_frame(player.get(), -1),
"audio-only media rejects frame steps");
require(!player_seek(player.get(), NAN), "non-finite seek rejected");
require(player_play(player.get()), "play accepted");
until(player.get(), [](auto s) { return s.status == PLAYER_PLAYING && s.position > .35; });
@@ -98,6 +198,7 @@ int main(int argc, char **argv) {
"close clears media state and pending seeks");
std::printf("PASS %s: open/play/pause/seek/end/replay/close\n", fixture);
}
frameSteps(player.get(), view, argv[1]);
require(player_open(player.get(), (std::string(argv[1]) + "/hdr.mkv").c_str()), "HDR open accepted");
auto hdr = until(player.get(), [](auto s) { return s.status == PLAYER_ERROR; }, true);
require(std::string(hdr.error).find("HDR") != std::string::npos, "HDR reports unsupported output");