#include "../Sources/Core/player_macos.h" #import #import #include #include #include #include #include #include #include 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 &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_open(player, (directory + "/av.mp4").c_str()), "repeated stepping open"); until(player, [](auto s) { return s.status == PLAYER_PAUSED; }); step(player, -1, 0); revision = player_get_state(player).revision; for (int i = 0; i < 64; ++i) require(player_step_frame(player, 1), "held forward steps accepted without queue overflow"); frameCompleted(player, revision, 64.0 / 24); revision = player_get_state(player).revision; for (int i = 0; i < 64; ++i) require(player_step_frame(player, -1), "held backward steps accepted without queue overflow"); frameCompleted(player, revision, 0); revision = player_get_state(player).revision; for (int i = 0; i < 40; ++i) require(player_step_frame(player, 1), "queue forward step run"); require(player_seek(player, 1.5), "seek separates repeated step runs"); for (int i = 0; i < 32; ++i) require(player_step_frame(player, -1), "queue reverse step run"); for (int i = 0; i < 24; ++i) require(player_step_frame(player, 1), "queue direction reversal"); frameCompleted(player, revision, 28.0 / 24); // A new media timeline supersedes the entire pending repeated run. for (int i = 0; i < 4096; ++i) require(player_step_frame(player, 1), "queue long repeated run"); require(player_open(player, (directory + "/audio.wav").c_str()), "open supersedes repeated steps"); auto replacement = until(player, [](auto s) { return s.status == PLAYER_PAUSED && !s.has_video && !s.seek_pending; }); require(replacement.position == 0 && replacement.has_audio, "stale steps do not affect replacement media"); 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/repeat bursts/seek ordering/pause/resume/boundaries/timeline replacement"); } int main(int argc, char **argv) { @autoreleasepool { require(argc == 2, "fixture directory argument required"); [NSApplication sharedApplication]; std::unique_ptr 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"); } }