feat: add native local-file playback

This commit is contained in:
2026-10-10 01:07:49 +02:00
commit 8a89d88254
11 changed files with 1722 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
#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);
}
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");
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"}) {
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) != "video.mp4"), "audio track selection");
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 sought = until(player.get(), [revision](auto s) {
return 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_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.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, "close clears media state");
std::printf("PASS %s: open/play/pause/seek/end/replay/close\n", fixture);
}
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");
}
}