feat: visualize retained network playback buffer
This commit is contained in:
+146
-23
@@ -14,6 +14,29 @@ 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(std::abs(state.buffered_start - state.position) < .00001,
|
||||
"retained range follows the authoritative playback position, not a loaded prefix");
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -21,6 +44,7 @@ static PlayerState until(Player *player, const std::function<bool(PlayerState)>
|
||||
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);
|
||||
@@ -29,8 +53,9 @@ static PlayerState until(Player *player, const std::function<bool(PlayerState)>
|
||||
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)\n",
|
||||
scenario, state.status, state.position, state.duration, state.seek_pending, state.buffering);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -64,6 +89,7 @@ static NSDictionary *control(const std::string &base, const std::string &path) {
|
||||
struct GateState {
|
||||
bool entered, released;
|
||||
uint64_t waiting, requests, ranges, bytes, total;
|
||||
double audioEnd, videoEnd;
|
||||
};
|
||||
|
||||
static GateState gateState(const std::string &base, const std::string &token) {
|
||||
@@ -71,7 +97,8 @@ static GateState gateState(const std::string &base, const std::string &token) {
|
||||
return {[state[@"entered"] boolValue], [state[@"released"] boolValue],
|
||||
[state[@"waiting"] unsignedLongLongValue], [state[@"requests"] unsignedLongLongValue],
|
||||
[state[@"ranges"] unsignedLongLongValue], [state[@"bytes"] unsignedLongLongValue],
|
||||
[state[@"total"] unsignedLongLongValue]};
|
||||
[state[@"total"] unsignedLongLongValue],
|
||||
[state[@"prefix_ends"][@"audio"] doubleValue], [state[@"prefix_ends"][@"video"] doubleValue]};
|
||||
}
|
||||
|
||||
static void stillBlocked(const std::string &base, const std::string &token) {
|
||||
@@ -106,6 +133,7 @@ static void closed(Player *player) {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -113,6 +141,7 @@ static PlayerState seekCompleted(Player *player, double target, PlayerStatus sta
|
||||
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) {
|
||||
@@ -144,6 +173,7 @@ static void transport(Player *player, MTKView *view, const std::string &base, bo
|
||||
[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) {
|
||||
@@ -166,6 +196,7 @@ static void transport(Player *player, MTKView *view, const std::string &base, bo
|
||||
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");
|
||||
@@ -201,9 +232,10 @@ static void frameSteps(Player *player, MTKView *view, const std::string &base) {
|
||||
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; },
|
||||
"audio continues after independent video EOF");
|
||||
require(beyondVideo.position < 3.9, "video EOF does not end longer audio source");
|
||||
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");
|
||||
@@ -215,8 +247,9 @@ static void frameSteps(Player *player, MTKView *view, const std::string &base) {
|
||||
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; },
|
||||
"video continues after independent audio EOF");
|
||||
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);
|
||||
@@ -250,15 +283,21 @@ static void nativeHLS(Player *player, MTKView *view, const std::string &base) {
|
||||
|
||||
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; }, "live-flag 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");
|
||||
seekCompleted(player, 1.5, PLAYER_PAUSED);
|
||||
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");
|
||||
@@ -280,16 +319,78 @@ static void blockedOpening(Player *player, const std::string &base, const std::s
|
||||
}, "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 bufferCoverage(Player *player, const std::string &base) {
|
||||
require(openNetwork(player, base, "/media/streaming.mp4", "", 30),
|
||||
"open long network fixture for bounded retained coverage");
|
||||
auto initial = until(player, [](auto s) { return s.status == PLAYER_PAUSED && buffered(s); },
|
||||
"paused network source exposes actual retained media");
|
||||
require(initial.buffered_end < initial.duration,
|
||||
"bounded queues do not report an entire downloaded movie as retained");
|
||||
require(player_play(player), "play to advance retained network coverage");
|
||||
until(player, [=](auto s) {
|
||||
return s.status == PLAYER_PLAYING && s.position > initial.buffered_end && buffered(s) &&
|
||||
s.buffered_end > initial.buffered_end;
|
||||
}, "retained horizon advances with real playback and refills");
|
||||
require(player_close(player), "close advancing buffered source");
|
||||
closed(player);
|
||||
|
||||
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: bounded queues/advancing clock/stalled 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 splitAudio = false, bool splitVideo = false) {
|
||||
auto stalled = "/stall/" + token + "/streaming.mp4";
|
||||
require(openNetwork(player, base, splitAudio ? "/media/streaming.mp4" : stalled,
|
||||
splitAudio ? stalled : "", 30), "open bounded streaming prefix");
|
||||
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) { return s.status == PLAYER_PLAYING && s.position > .1; },
|
||||
"stream starts 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.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);
|
||||
@@ -297,28 +398,42 @@ static void blockedPlayback(Player *player, const std::string &base, const std::
|
||||
}
|
||||
|
||||
static void cancellation(Player *player, const std::string &base, const std::string &fixtures) {
|
||||
for (bool splitAudio : {false, true}) {
|
||||
const std::string token = splitAudio ? "split-playing-seek" : "pause-seek";
|
||||
blockedPlayback(player, base, token, splitAudio);
|
||||
if (!splitAudio) {
|
||||
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 ? PLAYER_PLAYING : PLAYER_PAUSED);
|
||||
seekCompleted(player, 28, (splitAudio || splitVideo) ? PLAYER_PLAYING : PLAYER_PAUSED);
|
||||
stillBlocked(base, token);
|
||||
auto afterSeek = gateState(base, token);
|
||||
require(afterSeek.requests > 1 && afterSeek.ranges > 1, "seek replaces stalled read with an actual new byte-range request");
|
||||
require(player_play(player), "resume at seek target with stale read still blocked");
|
||||
until(player, [](auto s) { return s.status == PLAYER_PLAYING && !s.seek_pending && s.position > 28.1; },
|
||||
"resume after stalled-read seek");
|
||||
until(player, [](auto s) {
|
||||
return s.status == PLAYER_PLAYING && !s.seek_pending && s.position > 28.1 && buffered(s);
|
||||
}, "retained coverage follows new 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);
|
||||
auto rewound = until(player, [](auto s) {
|
||||
return s.status == PLAYER_PAUSED && !s.seek_pending && buffered(s);
|
||||
}, "backward seek rebuilds retained coverage at the new target");
|
||||
require(rewound.buffered_end < 28,
|
||||
"backward seek drops the old high-water horizon instead of retaining distant media");
|
||||
stillBlocked(base, token);
|
||||
require(player_close(player), "close resumed formerly stalled source");
|
||||
closed(player);
|
||||
std::puts(splitAudio
|
||||
? "PASS stalled secondary audio reader: split playing seek/both-input acknowledgement/new range/resume without server release"
|
||||
: "PASS stalled demux read: buffering/bounded prefix/pause/paused seek acknowledgement/new range/resume without server release");
|
||||
? "PASS stalled secondary audio: required-track bound/seek invalidation/new-target coverage/backward high-water clearing"
|
||||
: splitVideo
|
||||
? "PASS stalled primary video: required-track bound/seek invalidation/new-target coverage/backward high-water clearing"
|
||||
: "PASS stalled combined input: bounded prefix/pause/seek invalidation/new-target coverage/backward high-water clearing");
|
||||
}
|
||||
|
||||
blockedOpening(player, base, "opening-close");
|
||||
@@ -331,6 +446,7 @@ static void cancellation(Player *player, const std::string &base, const std::str
|
||||
"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);
|
||||
@@ -347,6 +463,8 @@ static void cancellation(Player *player, const std::string &base, const std::str
|
||||
"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);
|
||||
@@ -354,6 +472,10 @@ static void cancellation(Player *player, const std::string &base, const std::str
|
||||
}
|
||||
|
||||
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 &&
|
||||
@@ -393,6 +515,7 @@ int main(int argc, char **argv) {
|
||||
frameSteps(player.get(), view, base);
|
||||
nativeHLS(player.get(), view, base);
|
||||
liveAndReplacement(player.get(), base, fixtures);
|
||||
bufferCoverage(player.get(), base);
|
||||
cancellation(player.get(), base, fixtures);
|
||||
failures(player.get(), base);
|
||||
blockedPlayback(player.get(), base, "destroy-playing");
|
||||
|
||||
Reference in New Issue
Block a user