Files
terebi/Checks/network-playback.mm
T

723 lines
44 KiB
Plaintext

#include "../Sources/Core/player_macos.h"
#import <AppKit/AppKit.h>
#import <MetalKit/MetalKit.h>
#import <dispatch/dispatch.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <signal.h>
#include <sys/resource.h>
#include <unistd.h>
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<bool(PlayerState)> &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<NSDictionary *> *progressRecords(const std::string &path) {
NSString *text = [NSString stringWithContentsOfFile:[NSString stringWithUTF8String:path.c_str()]
encoding:NSUTF8StringEncoding error:nullptr];
if (!text) return @[];
NSArray<NSString *> *lines = [text componentsSeparatedByString:@"\n"];
NSMutableArray<NSDictionary *> *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, decltype(&player_destroy)> 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, decltype(&player_destroy)> 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<Player, decltype(&player_destroy)> 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");
}
}