#include "../Sources/Core/player_macos.h" #import #import #import #include #include #include #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); } } static bool buffered(PlayerState state) { return state.buffered_end > state.buffered_start; } static void noBuffer(PlayerState state, const char *message) { require(state.buffered_start == 0 && state.buffered_end == 0, message); } static void validBuffer(PlayerState state) { require(std::isfinite(state.buffered_start) && std::isfinite(state.buffered_end), "consumer receives finite buffered timestamps"); if (state.status == PLAYER_EMPTY || state.status == PLAYER_OPENING || state.status == PLAYER_ERROR || state.status == PLAYER_ENDED || state.seek_pending) { noBuffer(state, "unavailable/opening/error/ended/pending timeline has no retained range"); } if (state.buffered_start == 0 && state.buffered_end == 0) return; require(std::isfinite(state.duration) && state.duration > 0 && state.buffered_start >= 0 && buffered(state) && state.buffered_end <= state.duration, "retained range is nonempty and bounded by finite media duration"); require(state.buffered_start <= state.position && state.position <= state.buffered_end, "the retained interval covers the current playhead"); } // Only these deadlines use elapsed time: a stuck command/read must fail the check, // not hang CI. Playback assertions use fixture presentation timestamps and events. static PlayerState until(Player *player, const std::function &ready, const char *scenario, bool expectError = false) { auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); while (std::chrono::steady_clock::now() < deadline) { auto state = player_get_state(player); validBuffer(state); if (state.status == PLAYER_ERROR && !expectError) { std::fprintf(stderr, "FAIL %s: %s\n", scenario, state.error); std::exit(1); } if (ready(state)) return state; CFRunLoopRunInMode(kCFRunLoopDefaultMode, .005, false); } auto state = player_get_state(player); std::fprintf(stderr, "FAIL %s: command deadline (status=%d position=%.4f duration=%.4f pending=%d buffering=%d retained=%.4f..%.4f)\n", scenario, state.status, state.position, state.duration, state.seek_pending, state.buffering, state.buffered_start, state.buffered_end); std::exit(1); } static NSDictionary *control(const std::string &base, const std::string &path) { NSString *address = [NSString stringWithUTF8String:(base + path).c_str()]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:address]]; request.timeoutInterval = 5; request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; dispatch_semaphore_t done = dispatch_semaphore_create(0); __block NSData *body = nil; __block NSHTTPURLResponse *response = nil; __block NSError *failure = nil; NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *result, NSError *error) { body = data; response = (NSHTTPURLResponse *)result; failure = error; dispatch_semaphore_signal(done); }]; [task resume]; if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, 6 * NSEC_PER_SEC)) != 0) { [task cancel]; require(false, "loopback control request deadline"); } require(failure == nil && response.statusCode == 200 && body != nil, "loopback control endpoint responds"); id json = [NSJSONSerialization JSONObjectWithData:body options:0 error:nullptr]; require([json isKindOfClass:[NSDictionary class]], "loopback control response is a dictionary"); return (NSDictionary *)json; } struct GateState { bool entered, released; uint64_t waiting, requests, ranges, bytes, total; double audioEnd, videoEnd; uint64_t connections; }; static GateState gateState(const std::string &base, const std::string &token) { NSDictionary *state = control(base, "/control/status/" + token); return {[state[@"entered"] boolValue], [state[@"released"] boolValue], [state[@"waiting"] unsignedLongLongValue], [state[@"requests"] unsignedLongLongValue], [state[@"ranges"] unsignedLongLongValue], [state[@"bytes"] unsignedLongLongValue], [state[@"total"] unsignedLongLongValue], [state[@"prefix_ends"][@"audio"] doubleValue], [state[@"prefix_ends"][@"video"] doubleValue], [state[@"connections"] unsignedLongLongValue]}; } static void stillBlocked(const std::string &base, const std::string &token) { auto gate = gateState(base, token); require(gate.entered && !gate.released && gate.waiting > 0, "command completed while original server read remained blocked"); } static std::string headersFor(const std::string &path) { const char *kind = path.find("audio.wav") != std::string::npos || path.find("frame-step-audio.m4a") != std::string::npos ? "audio" : path.find("video.mp4") != std::string::npos ? "video" : "combined"; return std::string("X-Terebi-Fixture: ") + kind + "\r\n"; } static bool openNetwork(Player *player, const std::string &base, const std::string &videoPath, const std::string &audioPath = {}, double duration = 4, bool live = false, bool includeHeaders = true, const std::string &mediaURL = {}, double initialPosition = 0) { // Locals intentionally die immediately after the call: the asynchronous engine // must own both selected URLs and both selected HTTP-header blocks. auto video = base + videoPath; auto audio = audioPath.empty() ? std::string{} : base + audioPath; auto videoHeaders = includeHeaders ? headersFor(videoPath) : std::string{}; auto audioHeaders = includeHeaders ? headersFor(audioPath) : std::string{}; PlayerNetworkSource source{video.c_str(), audio.empty() ? nullptr : audio.c_str(), videoHeaders.c_str(), audio.empty() ? nullptr : audioHeaders.c_str(), duration, live, mediaURL.empty() ? nullptr : mediaURL.c_str(), initialPosition}; return player_open_network(player, &source); } static void closed(Player *player) { auto state = until(player, [](auto s) { return s.status == PLAYER_EMPTY; }, "close network source"); require(state.position == 0 && state.duration == 0 && !state.has_audio && !state.has_video && !state.can_seek && !state.buffering && !state.seek_pending && !state.error[0], "close clears all network/media/seek/error state"); noBuffer(state, "close clears retained network coverage"); require(std::abs(player_get_volume(player) - .37f) < .00001f, "volume survives network close"); } static PlayerState seekCompleted(Player *player, double target, PlayerStatus status) { auto revision = player_get_state(player).revision; require(player_seek(player, target), "network seek accepted"); auto pending = player_get_state(player); validBuffer(pending); require(pending.seek_pending || std::abs(pending.position - target) < .03, "network seek remains pending until target is authoritative"); return until(player, [=](auto s) { return !s.seek_pending && s.revision != revision && s.status == status && std::abs(s.position - target) < (status == PLAYER_PLAYING ? .2 : .03); }, "network seek acknowledgement"); } static void transport(Player *player, MTKView *view, const std::string &base, bool split) { require(openNetwork(player, base, split ? "/media/video.mp4" : "/media/av.mp4", split ? "/media/audio.wav" : ""), "network open accepted"); require(player_play(player), "play queues during asynchronous network open"); auto playing = until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .15; }, "queued network autoplay"); require(playing.has_audio && playing.has_video && playing.can_seek && !playing.seek_pending, "combined and split inputs select both tracks and expose seekability"); require(playing.duration >= 3.9 && playing.duration <= 4.2, "network media duration preserves source timing"); require(std::abs(playing.video_aspect_ratio - 16.0 / 9) < .0001, "network video keeps display aspect"); require(std::abs(player_get_volume(player) - .37f) < .00001f, "volume survives network open"); [view draw]; require(player_pause(player), "pause streaming playback"); auto paused = until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "network pause"); // An acknowledged external request is an event barrier, not a sleep pretending // the audio callback or command queue must have completed by now. control(base, "/control/stats"); require(std::abs(player_get_state(player).position - paused.position) < .0001, "paused streaming audio clock does not advance"); seekCompleted(player, 1.5, PLAYER_PAUSED); [view draw]; require(player_seek(player, .8) && player_seek(player, 1.7), "queued seeks accepted on both inputs"); auto pending = player_get_state(player); validBuffer(pending); require(pending.seek_pending || std::abs(pending.position - 1.7) < .03, "earlier seek cannot acknowledge final split-input seek"); until(player, [](auto s) { return !s.seek_pending && s.status == PLAYER_PAUSED && std::abs(s.position - 1.7) < .03; }, "queued final network seek"); require(player_play(player), "resume both network inputs"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.8; }, "resume after split seek"); seekCompleted(player, 3.2, PLAYER_PLAYING); auto ended = until(player, [](auto s) { return s.status == PLAYER_ENDED; }, "drain streaming media"); require(ended.position >= 3.9 && ended.position < 4.2, "network EOF drains decoded audio and video"); require(player_play(player), "network replay accepted"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1 && s.position < 1; }, "network replay restarts both streams"); require(player_close(player), "close playing network source"); closed(player); std::puts(split ? "PASS split stream-index-0 inputs: headers/autoplay/duration/pause/paused+playing seek/acknowledgement/EOF/replay" : "PASS combined network: headers/autoplay/duration/pause/paused+playing seek/acknowledgement/EOF/replay"); } static PlayerState step(Player *player, int direction, double pts) { auto revision = player_get_state(player).revision; require(player_step_frame(player, direction), "split-source adjacent frame step accepted"); validBuffer(player_get_state(player)); auto state = until(player, [revision](auto s) { return !s.seek_pending && s.revision != revision && s.status == PLAYER_PAUSED; }, "split frame-step acknowledgement"); if (std::abs(state.position - pts) >= .0001) { std::fprintf(stderr, "FAIL split frame PTS: expected %.4f, got %.4f\n", pts, state.position); std::exit(1); } return state; } static void frameSteps(Player *player, MTKView *view, const std::string &base) { constexpr double pts[] = {0, .1, .2, .3, .4, .65, .9, 1.15, 1.4, 1.5, 1.6, 1.7}; require(openNetwork(player, base, "/media/frame-step-video.mp4", "/media/audio.wav"), "video-only VFR plus independent audio open"); until(player, [](auto s) { return s.status == PLAYER_PAUSED && s.has_audio && s.has_video; }, "split VFR open"); require(!player_step_frame(player, 0) && !player_step_frame(player, 2), "invalid step directions rejected"); step(player, -1, 0); 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); seekCompleted(player, .72, PLAYER_PAUSED); step(player, 1, 1.15); // The .72 seek previews .9, never the preceding keyframe. step(player, -1, .9); step(player, -1, .65); require(player_play(player), "resume frame-stepped split audio/video"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.3; }, "audio rebuild resumes authoritative stepped timestamp"); require(player_pause(player), "pause resumed split VFR timeline"); until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "pause split VFR after resume"); seekCompleted(player, 1.5, PLAYER_PAUSED); require(player_play(player), "play past independent video EOF"); auto beyondVideo = until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 2.2 && buffered(s); }, "audio retains playable coverage after independent video EOF"); require(beyondVideo.position < 3.9, "video EOF does not end or cap longer audio coverage"); auto ended = until(player, [](auto s) { return s.status == PLAYER_ENDED; }, "longer audio input drains"); require(ended.position >= 3.9 && ended.position < 4.2, "split EOF follows last audio sample"); require(player_close(player), "close split stepped source"); closed(player); std::puts("PASS split VFR/B-frames: exact bidirectional steps/boundaries/non-keyframe seek/audio resume/independent video EOF"); require(openNetwork(player, base, "/media/video.mp4", "/media/frame-step-audio.m4a"), "long video plus short independent audio open"); require(player_play(player), "queued play with shorter audio"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1; }, "asymmetric split autoplay"); seekCompleted(player, 1.5, PLAYER_PLAYING); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 2.3 && buffered(s); }, "video retains playable coverage after independent audio EOF"); require(player_pause(player), "pause in independent audio tail"); until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "pause longer video after audio EOF"); seekCompleted(player, 3.2, PLAYER_PAUSED); require(player_play(player), "resume beyond shorter separate audio endpoint"); ended = until(player, [](auto s) { return s.status == PLAYER_ENDED; }, "longer video input drains"); require(std::abs(ended.position - 4) < .03, "audio EOF does not truncate video presentation duration"); require(player_close(player), "close asymmetric split source"); closed(player); std::puts("PASS independent audio EOF: video presentation continues and final duration is retained"); } static void nativeHLS(Player *player, MTKView *view, const std::string &base) { require(openNetwork(player, base, "/media/network-vod.m3u8"), "recorded native HLS open"); require(player_play(player), "recorded HLS autoplay"); auto playing = until(player, [](auto s) { return s.status == PLAYER_PLAYING && !s.buffering && s.position > .2; }, "recorded HLS playback"); require(playing.has_audio && playing.has_video && playing.can_seek, "recorded HLS owns its I/O but supports timestamp seeking"); require(player_pause(player), "pause native HLS"); until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "native HLS pause"); seekCompleted(player, 2.5, PLAYER_PAUSED); require(player_play(player), "resume native HLS seek"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 2.6; }, "native HLS seek resumes actual audio/video clock"); [view draw]; require(player_close(player), "close native HLS"); closed(player); std::puts("PASS native HLS: selected headers/recorded seekability/paused seek/audio-video resume"); } static void liveAndReplacement(Player *player, const std::string &base, const std::string &fixtures) { require(openNetwork(player, base, "/media/av.mp4", "", 4, true), "live-flag network open"); auto live = until(player, [](auto s) { return s.status == PLAYER_PAUSED && buffered(s); }, "live-flag source retains media before local replacement"); require(!live.can_seek && !player_seek(player, 1) && !player_step_frame(player, 1) && !player_step_frame(player, -1), "live source rejects seeks and both frame-step directions"); require(player_open(player, (fixtures + "/video.mp4").c_str()), "ordinary local source replaces network live source"); auto local = until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "local replacement"); require(local.can_seek && !local.buffering && local.has_video && !local.has_audio && !local.seek_pending, "local replacement restores seekability and clears network flags"); noBuffer(local, "local replacement does not inherit network buffered coverage"); require(std::abs(player_get_volume(player) - .37f) < .00001f, "local replacement keeps volume"); noBuffer(seekCompleted(player, 1.5, PLAYER_PAUSED), "ordinary local seek never reports network coverage"); require(player_play(player), "play ordinary local replacement"); local = until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.6; }, "local playback after replacing a buffered network source"); noBuffer(local, "ordinary local playback never reports network coverage"); require(player_close(player), "close local replacement"); closed(player); require(openNetwork(player, base, "/no-range/streaming.mp4", "", 30), "non-range HTTP open"); auto noRange = until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "non-seekable HTTP open"); require(!noRange.can_seek && !player_seek(player, 1) && !player_step_frame(player, 1), "HTTP input without byte ranges rejects seek and step even without live metadata"); require(player_play(player), "non-seekable network source still plays"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1; }, "non-seekable playback"); require(player_close(player), "close non-seekable HTTP source"); closed(player); std::puts("PASS live/non-range seek rejection/local replacement/volume/state reset"); } static void blockedOpening(Player *player, const std::string &base, const std::string &token) { require(openNetwork(player, base, "/stall-open/" + token + "/av.mp4"), "open deliberately stalled HTTP response"); until(player, [&](auto s) { auto gate = gateState(base, token); return s.status == PLAYER_OPENING && gate.entered && gate.waiting > 0 && !gate.released; }, "opening response-body read underway"); } static double prefixEnd(GateState gate, bool splitAudio, bool splitVideo) { return splitAudio ? gate.audioEnd : splitVideo ? gate.videoEnd : std::fmin(gate.audioEnd, gate.videoEnd); } static void prefixBound(PlayerState state, GateState gate, bool splitAudio = false, bool splitVideo = false) { const double end = prefixEnd(gate, splitAudio, splitVideo); require(end > 0 && end < 30, "gated thirty-second fixture withholds known presentation timestamps"); require(!buffered(state) || state.buffered_end <= end + .002, "retained coverage cannot bridge withheld audio/video packets or reordered B-frames"); } static void cachedWindow(Player *player, const std::string &base) { const std::string token = "cached-window"; require(openNetwork(player, base, "/stall/" + token + "/cache.mp4?after_seconds=100", "", 120), "open a two-minute source with a controlled future network stall"); auto initial = until(player, [](auto s) { return s.status == PLAYER_PAUSED && buffered(s) && s.buffered_end >= 59; }, "paused playback prefetches a minute without waiting to start"); require(initial.buffered_end < 65, "read-ahead is time-bounded rather than a full download"); seekCompleted(player, 50, PLAYER_PAUSED); auto retained = until(player, [&](auto s) { return s.status == PLAYER_PAUSED && buffered(s) && s.buffered_end > 95 && gateState(base, token).entered; }, "cache follows a new playhead until the future network gate"); require(retained.position - retained.buffered_start >= 29, "a reusable thirty-second rewind window survives forward playback"); require(retained.buffered_start > 15, "old media is evicted instead of caching the whole movie"); const auto before = gateState(base, token); auto rewind = seekCompleted(player, 20.5, PLAYER_PAUSED); require(buffered(rewind), "cached rewind preserves the retained interval"); auto after = gateState(base, token); require(after.requests == before.requests && after.ranges == before.ranges, "rewinding within history makes no new HTTP request"); stillBlocked(base, token); seekCompleted(player, 45, PLAYER_PAUSED); after = gateState(base, token); require(after.requests == before.requests && after.ranges == before.ranges, "forward seeking inside retained media makes no new HTTP request"); stillBlocked(base, token); seekCompleted(player, 5, PLAYER_PAUSED); until(player, [&](auto s) { return s.status == PLAYER_PAUSED && buffered(s) && s.buffered_end > 6 && gateState(base, token).requests > before.requests; }, "seeking before retained history refills missing media over HTTP"); require(player_close(player), "close cache while the original future response remains blocked"); closed(player); stillBlocked(base, token); std::puts("PASS sliding cache: minute read-ahead/thirty-second history/keyframe replay/cached bidirectional seeks/miss refill/cancel"); } static void chunkedFetch(Player *player, const std::string &base, bool split) { const std::string videoToken = split ? "chunked-video" : "chunked-combined"; const std::string audioToken = "chunked-audio"; auto path = [](const std::string &token) { return "/chunked-media/" + token + "/cache.mp4?after_seconds=2"; }; require(openNetwork(player, base, path(videoToken) + (split ? "" : "&keep_alive=1"), split ? path(audioToken) : "", 120), "open recorded media whose CDN throttles oversized requests"); require(player_play(player), "start playback while the minute reserve fills"); until(player, [&](auto s) { const auto video = gateState(base, videoToken); const bool throttled = video.entered || (split && gateState(base, audioToken).entered); require(!(s.position > .5 && s.buffering && throttled), "oversized HTTP request starves playback before a minute can buffer"); return s.status == PLAYER_PLAYING && buffered(s) && s.buffered_end - s.position >= 59; }, "finite HTTP requests build a minute of playable reserve without CDN throttling"); require(gateState(base, videoToken).requests >= 2, "minute read-ahead continues across the first bounded HTTP response"); if (split) require(gateState(base, audioToken).requests >= 2, "secondary audio also continues bounded HTTP requests"); else require(gateState(base, videoToken).connections == 1, "minute reserve reuses one HTTP connection across bounded responses"); require(player_pause(player), "pause before an out-of-cache seek"); until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "pause chunked source"); seekCompleted(player, 119.5, PLAYER_PAUSED); require(player_play(player), "resume the final partial HTTP range"); until(player, [](auto s) { return s.status == PLAYER_ENDED; }, "bounded final response reaches true media EOF"); require(!gateState(base, videoToken).entered && (!split || !gateState(base, audioToken).entered), "initial fetch, continuation and seek never use CDN-throttled ranges"); require(player_close(player), "close chunked source"); closed(player); std::puts(split ? "PASS bounded split fetch: minute reserve/secondary audio/range continuation/seek/EOF" : "PASS bounded combined fetch: minute reserve/range continuation/seek/EOF"); } static void bufferCoverage(Player *player, const std::string &base) { const std::string token = "buffer-release"; require(openNetwork(player, base, "/stall/" + token + "/streaming.mp4", "", 30), "open event-gated retained coverage"); auto paused = until(player, [&](auto s) { auto gate = gateState(base, token); if (gate.entered) prefixBound(s, gate); return s.status == PLAYER_PAUSED && buffered(s) && gate.entered && gate.waiting > 0; }, "paused retained horizon is bounded by supplied fixture timestamps"); const auto gate = gateState(base, token); prefixBound(paused, gate); stillBlocked(base, token); require(player_play(player), "play gated retained prefix"); until(player, [&](auto s) { prefixBound(s, gate); return s.status == PLAYER_PLAYING && s.position > .1 && buffered(s); }, "gated prefix has real playable-ahead coverage"); until(player, [&](auto s) { prefixBound(s, gate); return s.status == PLAYER_PLAYING && s.buffering; }, "gated coverage cannot advance into unavailable packets"); const double suppliedEnd = prefixEnd(gate, false, false); stillBlocked(base, token); control(base, "/control/release/" + token); until(player, [=](auto s) { return s.status == PLAYER_PLAYING && !s.buffering && buffered(s) && s.buffered_end > suppliedEnd; }, "released server gate advances retained coverage beyond withheld timestamps"); require(player_close(player), "close released buffered source"); closed(player); std::puts("PASS retained coverage: supplied timestamp bounds/B-frame dependencies/gate-release refill"); } static void blockedPlayback(Player *player, const std::string &base, const std::string &token, bool splitAudio = false, bool splitVideo = false) { auto stalled = "/stall/" + token + "/streaming.mp4"; require(openNetwork(player, base, splitAudio ? "/media/streaming.mp4" : stalled, splitAudio ? stalled : splitVideo ? "/media/streaming.mp4" : "", 30), "open bounded streaming prefix"); require(player_play(player), "queue play before gated source opens"); until(player, [&](auto s) { auto gate = gateState(base, token); if (gate.entered) prefixBound(s, gate, splitAudio, splitVideo); return s.status == PLAYER_PLAYING && s.position > .1 && buffered(s) && gate.entered; }, "stream retains playable media before entire file arrives"); until(player, [&](auto s) { auto gate = gateState(base, token); if (gate.entered) prefixBound(s, gate, splitAudio, splitVideo); return s.status == PLAYER_PLAYING && s.buffering && gate.entered && gate.waiting > 0 && !gate.released; }, "real demux read blocks at server gate"); auto gate = gateState(base, token); require(gate.bytes < gate.total && gate.total > 131072, "engine streams playable prefix without whole-file download"); } static void cancellation(Player *player, const std::string &base, const std::string &fixtures) { for (int input : {0, 1, 2}) { const bool splitAudio = input == 1, splitVideo = input == 2; const std::string token = splitAudio ? "split-playing-seek" : splitVideo ? "split-video-seek" : "pause-seek"; blockedPlayback(player, base, token, splitAudio, splitVideo); if (!splitAudio && !splitVideo) { require(player_pause(player), "pause while actual network read is blocked"); auto paused = until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "pause does not wait for network read"); stillBlocked(base, token); require(std::abs(player_get_state(player).position - paused.position) < .0001, "buffered pause freezes native clock"); } seekCompleted(player, 28, (splitAudio || splitVideo) ? PLAYER_PLAYING : PLAYER_PAUSED); stillBlocked(base, token); require(player_play(player), "resume at seek target with stale read still blocked"); until(player, [&](auto s) { auto afterSeek = gateState(base, token); return s.status == PLAYER_PLAYING && !s.seek_pending && s.position > 28.1 && buffered(s) && afterSeek.requests > 1 && afterSeek.ranges > 1; }, "new byte-range refill reaches stalled-read seek target"); stillBlocked(base, token); require(player_pause(player), "pause high-timestamp coverage before backward seek"); until(player, [](auto s) { return s.status == PLAYER_PAUSED && buffered(s); }, "retain high-timestamp media before invalidating it"); seekCompleted(player, .5, PLAYER_PAUSED); until(player, [](auto s) { return s.status == PLAYER_PAUSED && !s.seek_pending && buffered(s); }, "backward seek rebuilds retained coverage at the new target"); stillBlocked(base, token); require(player_close(player), "close resumed formerly stalled source"); closed(player); std::puts(splitAudio ? "PASS stalled secondary audio: required-track bound/seek acknowledgement/new-target coverage/backward refill" : splitVideo ? "PASS stalled primary video: required-track bound/seek acknowledgement/new-target coverage/backward refill" : "PASS stalled combined input: bounded prefix/pause/seek acknowledgement/new-target coverage/backward refill"); } blockedOpening(player, base, "opening-close"); require(player_close(player), "close cancels opening read"); closed(player); stillBlocked(base, "opening-close"); blockedOpening(player, base, "opening-replace"); require(player_open(player, (fixtures + "/audio.wav").c_str()), "local open replaces blocked network open"); auto local = until(player, [](auto s) { return s.status == PLAYER_PAUSED && s.has_audio && !s.has_video; }, "local replacement cancels blocked opening read"); require(local.can_seek && !local.buffering && !local.seek_pending && local.position == 0, "stale opening read cannot publish over replacement timeline"); noBuffer(local, "local audio replacement clears retained network media"); stillBlocked(base, "opening-replace"); require(player_close(player), "close audio-only replacement"); closed(player); blockedPlayback(player, base, "playing-close"); require(player_close(player), "close cancels blocked playing read"); closed(player); stillBlocked(base, "playing-close"); blockedPlayback(player, base, "playing-replace"); require(openNetwork(player, base, "/media/video.mp4", "/media/audio.wav"), "new network open replaces blocked playing read"); auto replacement = until(player, [](auto s) { return s.status == PLAYER_PAUSED && s.has_audio && s.has_video && !s.buffering; }, "network replacement cancels blocked playing read"); require(replacement.position == 0 && !replacement.buffering && !replacement.seek_pending && replacement.can_seek, "replacement ignores stale stream data and buffering state"); require(!buffered(replacement) || replacement.buffered_end <= 4.2, "new network source cannot retain the previous thirty-second horizon"); stillBlocked(base, "playing-replace"); require(player_close(player), "close split network replacement"); closed(player); std::puts("PASS stalled opening+playing reads: close/local+network replacement cancel without server release"); } static void failures(Player *player, const std::string &base) { require(openNetwork(player, base, "/media/streaming.mp4", "", 30), "open retained network media before failure replacement"); until(player, [](auto s) { return s.status == PLAYER_PAUSED && buffered(s); }, "failure replacement starts with retained coverage to invalidate"); require(openNetwork(player, base, "/media/av.mp4", "", 4, false, false), "header-denied request accepted asynchronously"); auto denied = until(player, [](auto s) { return s.status == PLAYER_ERROR; }, "required header denial", true); require(denied.error[0] && !denied.has_audio && !denied.has_video && !denied.can_seek && !denied.buffering && !denied.seek_pending, "failed network open clears media and reports useful error"); NSDictionary *stats = control(base, "/control/stats"); require([stats[@"av.mp4"][@"denied"] unsignedLongLongValue] > 0, "fixture server actually rejected request without selected-format header"); require(openNetwork(player, base, "/failure/av.mp4"), "controlled failed network open accepted"); auto failed = until(player, [](auto s) { return s.status == PLAYER_ERROR; }, "HTTP failure", true); require(failed.error[0] && !failed.buffering && !failed.can_seek, "HTTP failure has explicit error and cleared flags"); require(openNetwork(player, base, "/media/video.mp4", "/media/audio.wav"), "recover from real HTTP failures"); require(player_play(player), "recovered split source autoplay"); until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .1; }, "failed network recovery"); require(player_close(player), "close recovered network source"); closed(player); std::puts("PASS actual HTTP header denial/503 failure/useful errors/cleared state/split-source recovery"); } static NSArray *progressRecords(const std::string &path) { NSString *text = [NSString stringWithContentsOfFile:[NSString stringWithUTF8String:path.c_str()] encoding:NSUTF8StringEncoding error:nullptr]; if (!text) return @[]; NSArray *lines = [text componentsSeparatedByString:@"\n"]; NSMutableArray *records = [NSMutableArray array]; for (NSUInteger index = 0; index + 1 < lines.count; ++index) { id record = [NSJSONSerialization JSONObjectWithData:[lines[index] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nullptr]; require([record isKindOfClass:[NSDictionary class]], "native reporting produces complete JSON records"); [records addObject:record]; } return records; } static void reporting(const std::string &base, const std::string &fixtures) { char temporary[] = "/tmp/terebi-native-progress-XXXXXX"; require(mkdtemp(temporary) != nullptr, "isolated native report directory"); const std::string path = std::string(temporary) + "/native.jsonl"; const std::string first = "https://www.youtube.com/watch?v=AAAAAAAAAAA"; const std::string second = "https://www.youtube.com/watch?v=BBBBBBBBBBB"; std::unique_ptr player(player_create(), player_destroy); char error[512]{}; NSString *nativePath = [NSString stringWithUTF8String:path.c_str()]; require([@"original" writeToFile:nativePath atomically:NO encoding:NSUTF8StringEncoding error:nullptr], "create existing destination sentinel"); require(!player_set_progress_file(player.get(), path.c_str(), error) && error[0] && player_get_state(player.get()).status == PLAYER_EMPTY, "report initialization error leaves playback state usable"); require([[NSString stringWithContentsOfFile:nativePath encoding:NSUTF8StringEncoding error:nullptr] isEqualToString:@"original"], "native initialization cannot overwrite an existing destination"); require(unlink(path.c_str()) == 0, "remove only the test-owned sentinel"); require(player_set_progress_file(player.get(), path.c_str(), error), "initialize native reporting before playback"); require(player_set_volume(player.get(), 0), "mute native reporting fixture"); require(openNetwork(player.get(), base, "/media/cache.mp4", "", 120, false, true, first, 50), "open canonical source with initial resume intent"); until(player.get(), [&](auto state) { auto rows = progressRecords(path); return state.status == PLAYER_PAUSED && !state.seek_pending && rows.count > 0; }, "backend acknowledges initial restoration before report"); auto rows = progressRecords(path); require([rows[0][@"media_url"] isEqualToString:[NSString stringWithUTF8String:first.c_str()]] && std::abs([rows[0][@"position_seconds"] doubleValue] - 50) < .03 && [rows[0][@"completion_eligible"] boolValue], "initial report is restored native position, not pre-resume zero"); require(openNetwork(player.get(), base, "/failure/av.mp4", "", 4, false, true, second), "failed requested identity accepted asynchronously"); until(player.get(), [](auto state) { return state.status == PLAYER_ERROR; }, "failed reported open", true); require(openNetwork(player.get(), base, "/media/audio.wav", "", 4, false, true, first), "audio-only input with canonical identity opens"); until(player.get(), [&](auto state) { auto current = progressRecords(path); return state.status == PLAYER_PAUSED && state.has_audio && !state.has_video && [current.lastObject[@"duration_seconds"] doubleValue] == 4 && [current.lastObject[@"event"] isEqualToString:@"progress"]; }, "audio-only native report"); rows = progressRecords(path); require(![rows.lastObject[@"completion_eligible"] boolValue], "actual audio-only input cannot mark watched"); require(openNetwork(player.get(), base, "/media/cache.mp4", "", 120, true, true, first), "live source with known duration opens"); until(player.get(), [](auto state) { return state.status == PLAYER_PAUSED && state.has_video; }, "reported live input ready"); require(player_close(player.get()), "close actual reported live input"); until(player.get(), [](auto state) { return state.status == PLAYER_EMPTY; }, "reported live close"); require(openNetwork(player.get(), base, "/stall-open/report-cancelled/streaming.mp4", "", 30, false, true, second, 29), "gated initial resume opens"); until(player.get(), [&](auto state) { return state.status == PLAYER_OPENING && gateState(base, "report-cancelled").entered; }, "reported initial open remains unacknowledged"); require(player_close(player.get()), "cancel reported open before ready"); until(player.get(), [](auto state) { return state.status == PLAYER_EMPTY; }, "cancelled report open close"); require(player_shutdown(player.get(), error), "normal shutdown drains native reporter"); rows = progressRecords(path); bool sawLive = false; for (NSDictionary *record in rows) { require([record[@"media_url"] isEqualToString:[NSString stringWithUTF8String:first.c_str()]], "failed/cancelled opens never report their requested identity"); if ([record[@"duration_seconds"] doubleValue] == 120 && [record[@"position_seconds"] doubleValue] == 0) { sawLive = true; require(![record[@"completion_eligible"] boolValue], "live known-duration records stay ineligible"); } } require(sawLive && [rows.lastObject[@"event"] isEqualToString:@"closed"], "native close/quit retain terminal facts for acknowledged media"); player.reset(); const auto failedPath = std::string(temporary) + "/write-failure.jsonl"; player.reset(player_create()); require(player_set_progress_file(player.get(), failedPath.c_str(), error), "initialize failure-test reporter"); require(player_set_volume(player.get(), 0), "mute failure-test playback"); struct rlimit previous{}, limited{}; require(getrlimit(RLIMIT_FSIZE, &previous) == 0, "read original file-size limit"); limited = previous; limited.rlim_cur = 1; auto previousSignal = signal(SIGXFSZ, SIG_IGN); require(previousSignal != SIG_ERR && setrlimit(RLIMIT_FSIZE, &limited) == 0, "force actual reporting write failure"); require(openNetwork(player.get(), base, "/media/cache.mp4", "", 120, false, true, first), "playback opens with failing output"); until(player.get(), [&](auto state) { return state.status == PLAYER_PAUSED && player_get_progress_error(player.get(), error); }, "write error surfaces independently from decoder status"); require(setrlimit(RLIMIT_FSIZE, &previous) == 0 && signal(SIGXFSZ, previousSignal) != SIG_ERR, "restore resource limit and signal handler"); require(error[0] && player_play(player.get()), "reporting failure does not prevent playback"); until(player.get(), [](auto state) { return state.status == PLAYER_PLAYING && state.position > .1; }, "actual playback continues after reporting failure"); seekCompleted(player.get(), 5, PLAYER_PLAYING); require(player_open(player.get(), (fixtures + "/audio.wav").c_str()), "failed reporter permits media replacement"); until(player.get(), [](auto state) { return state.status == PLAYER_PAUSED && state.has_audio; }, "replacement remains functional with disabled reporter"); require(!player_shutdown(player.get(), error) && error[0], "final reporting failure remains observable"); player.reset(); [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithUTF8String:temporary] error:nullptr]; std::puts("PASS native reporting: acknowledged initial resume/audio-only/live eligibility/failed+cancelled opens/final drain/write-failure playback+seek+replacement continuity"); } int main(int argc, char **argv) { @autoreleasepool { require(argc == 3, "loopback base URL and local fixture directory required"); std::string base = argv[1], fixtures = argv[2]; require(base.rfind("http://127.0.0.1:", 0) == 0, "network fixture server is loopback-only"); [NSApplication sharedApplication]; reporting(base, fixtures); std::unique_ptr player(player_create(), player_destroy); require(bool(player), "player creation"); require(player_set_volume(player.get(), .37f), "set persistent network playback gain"); void *native = player_macos_make_video_view(player.get()); require(native != nullptr, "actual Metal renderer initializes"); MTKView *view = (__bridge_transfer MTKView *)native; NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(-2000, -2000, 320, 180) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; window.contentView = view; [window orderBack:nil]; chunkedFetch(player.get(), base, false); chunkedFetch(player.get(), base, true); transport(player.get(), view, base, false); transport(player.get(), view, base, true); frameSteps(player.get(), view, base); nativeHLS(player.get(), view, base); liveAndReplacement(player.get(), base, fixtures); cachedWindow(player.get(), base); bufferCoverage(player.get(), base); cancellation(player.get(), base, fixtures); failures(player.get(), base); blockedPlayback(player.get(), base, "destroy-playing"); player.reset(); // Python's process deadline catches a join waiting for server unblock. stillBlocked(base, "destroy-playing"); [view draw]; // The native view is allowed to outlive its cancelled engine. [window orderOut:nil]; std::unique_ptr opening(player_create(), player_destroy); require(bool(opening), "second player creation for opening cancellation"); blockedOpening(opening.get(), base, "destroy-opening"); opening.reset(); stillBlocked(base, "destroy-opening"); std::puts("PASS stalled opening+playing destruction: joins without server release/native view lifetime"); } }