feat: step to adjacent decoded video frames
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user