Files
terebi/Checks/playback.mm
T

218 lines
13 KiB
Plaintext

#include "../Sources/Core/player_macos.h"
#import <AppKit/AppKit.h>
#import <MetalKit/MetalKit.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
static void require(bool condition, const char *message) {
if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
}
// Runtime smoke: the actual audio device and monotonic playback clock are exercised.
// No calendar dates, sleeps standing in for command completion, or mocked outputs.
static PlayerState until(Player *player, const std::function<bool(PlayerState)> &ready, bool expectError = false) {
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
while (std::chrono::steady_clock::now() < deadline) {
auto state = player_get_state(player);
if (state.status == PLAYER_ERROR && !expectError) {
std::fprintf(stderr, "Playback error: %s\n", state.error);
std::exit(1);
}
if (ready(state)) return state;
CFRunLoopRunInMode(kCFRunLoopDefaultMode, .005, false);
}
auto state = player_get_state(player);
std::fprintf(stderr, "Timeout: status=%d position=%.4f duration=%.4f\n",
state.status, state.position, state.duration);
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;
// Attach the real renderer to an offscreen native window, not a browser.
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(-2000, -2000, 320, 180)
styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO];
window.contentView = view;
[window orderBack:nil];
for (const char *fixture : {"av.mp4", "video.mp4", "audio.wav", "anamorphic.mp4"}) {
std::string path = std::string(argv[1]) + "/" + fixture;
require(player_open(player.get(), path.c_str()), "open accepted");
auto opened = until(player.get(), [](auto s) { return s.status == PLAYER_PAUSED; });
require(opened.duration > 3.9 && opened.duration < 4.2, "discovered fixture duration");
require(opened.has_video == (std::string(fixture) != "audio.wav"), "video track selection");
require(opened.has_audio == (std::string(fixture) == "av.mp4" || std::string(fixture) == "audio.wav"),
"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; });
[view draw];
require(player_pause(player.get()), "pause accepted");
auto paused = until(player.get(), [](auto s) { return s.status == PLAYER_PAUSED; });
// Observe the device through several run-loop turns while paused.
for (int i = 0; i < 20; ++i)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, .005, false);
require(std::abs(player_get_state(player.get()).position - paused.position) < .002,
"pause freezes authoritative position");
uint64_t revision = paused.revision;
require(player_seek(player.get(), 1.5), "paused seek accepted");
auto pending = player_get_state(player.get());
require(pending.seek_pending || std::abs(pending.position - 1.5) < .03,
"accepted seek reports pending until the target is authoritative");
auto sought = until(player.get(), [revision](auto s) {
return !s.seek_pending && s.revision != revision && s.status == PLAYER_PAUSED && std::abs(s.position - 1.5) < .03;
});
[view draw];
require(sought.status == PLAYER_PAUSED, "seek preserves paused state");
require(player_seek(player.get(), .8) && player_seek(player.get(), 1.5), "queued seeks accepted");
pending = player_get_state(player.get());
require(pending.seek_pending || std::abs(pending.position - 1.5) < .03,
"an earlier queued seek cannot acknowledge the final seek");
until(player.get(), [](auto s) {
return !s.seek_pending && s.status == PLAYER_PAUSED && std::abs(s.position - 1.5) < .03;
});
require(player_play(player.get()), "resume accepted");
until(player.get(), [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.7; });
require(player_seek(player.get(), 3.2), "playing seek accepted");
until(player.get(), [](auto s) { return !s.seek_pending && s.status == PLAYER_PLAYING && s.position >= 3.2; });
auto ended = until(player.get(), [](auto s) { return s.status == PLAYER_ENDED; });
require(ended.position >= 3.8, "EOF drains media before completion");
require(player_play(player.get()), "replay accepted");
until(player.get(), [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1 && s.position < 1; });
require(player_close(player.get()), "close accepted while playing");
auto closed = until(player.get(), [](auto s) { return s.status == PLAYER_EMPTY; });
require(closed.position == 0 && !closed.has_audio && !closed.has_video && !closed.seek_pending,
"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");
require(player_open(player.get(), "/nonexistent/personal-player-smoke.mp4"), "error open accepted");
auto failed = until(player.get(), [](auto s) { return s.status == PLAYER_ERROR; }, true);
require(failed.error[0], "failed open reports an error");
require(player_open(player.get(), (std::string(argv[1]) + "/av.mp4").c_str()), "recover after error");
until(player.get(), [](auto s) { return s.status == PLAYER_PAUSED; });
require(player_play(player.get()), "play before destruction");
until(player.get(), [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1; });
player.reset();
[view draw]; // Native view may outlive the player without a dangling engine pointer.
[window orderOut:nil];
std::puts("PASS HDR rejection/errors/recovery/destroy while playing/native view lifetime");
}
}