fix: avoid unbounded HTTP throttling and make buffer readout opt-in
This commit is contained in:
@@ -90,6 +90,7 @@ 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) {
|
||||
@@ -98,7 +99,8 @@ static GateState gateState(const std::string &base, const std::string &token) {
|
||||
[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[@"prefix_ends"][@"audio"] doubleValue], [state[@"prefix_ends"][@"video"] doubleValue],
|
||||
[state[@"connections"] unsignedLongLongValue]};
|
||||
}
|
||||
|
||||
static void stillBlocked(const std::string &base, const std::string &token) {
|
||||
@@ -369,6 +371,43 @@ static void cachedWindow(Player *player, const std::string &base) {
|
||||
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";
|
||||
@@ -436,12 +475,12 @@ static void cancellation(Player *player, const std::string &base, const std::str
|
||||
}
|
||||
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 && buffered(s);
|
||||
}, "retained coverage follows new stalled-read seek target");
|
||||
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); },
|
||||
@@ -534,6 +573,8 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
|
||||
+24
-11
@@ -24,6 +24,7 @@ class _Gate:
|
||||
self.requests = 0
|
||||
self.ranges = 0
|
||||
self.bytes = 0
|
||||
self.connections = set()
|
||||
|
||||
|
||||
class StreamingServer:
|
||||
@@ -60,10 +61,14 @@ class StreamingServer:
|
||||
pass # Media URLs/headers never appear in logs.
|
||||
|
||||
def do_GET(self):
|
||||
owner._get(self)
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
owner._get(self)
|
||||
super().handle()
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
||||
pass # The engine intentionally cancels active media connections.
|
||||
# Cancellation can close a body read or an idle keep-alive connection.
|
||||
self.close_connection = True
|
||||
|
||||
self.http = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
self.http.daemon_threads = True
|
||||
@@ -163,19 +168,20 @@ class StreamingServer:
|
||||
gate = self.gates.get(parts[2])
|
||||
if gate is None:
|
||||
body = {"entered": False, "released": False, "waiting": 0, "requests": 0,
|
||||
"ranges": 0, "bytes": 0, "total": 0, "prefix_ends": {}}
|
||||
"ranges": 0, "bytes": 0, "total": 0, "connections": 0, "prefix_ends": {}}
|
||||
else:
|
||||
if parts[1] == "release":
|
||||
gate.release.set()
|
||||
body = {"entered": gate.entered.is_set(), "released": gate.release.is_set(),
|
||||
"waiting": gate.waiting, "requests": gate.requests, "ranges": gate.ranges,
|
||||
"bytes": gate.bytes, "total": gate.total, "prefix_ends": gate.prefix_ends}
|
||||
"bytes": gate.bytes, "total": gate.total, "connections": len(gate.connections),
|
||||
"prefix_ends": gate.prefix_ends}
|
||||
self._reply(request, 200, json.dumps(body).encode())
|
||||
return
|
||||
|
||||
mode = parts[0]
|
||||
token = None
|
||||
if len(parts) == 3 and mode in ("stall", "stall-open"):
|
||||
if len(parts) == 3 and mode in ("stall", "stall-open", "chunked-media"):
|
||||
token, name = parts[1:]
|
||||
if not re.fullmatch(r"[A-Za-z0-9_-]+", token):
|
||||
self._reply(request, 400, b"Invalid gate token", "text/plain")
|
||||
@@ -223,21 +229,27 @@ class StreamingServer:
|
||||
query = parse_qs(parsed.query)
|
||||
try:
|
||||
gate_at = (self._timestamp_offset(name, float(query["after_seconds"][0]))
|
||||
if mode == "stall" and "after_seconds" in query else
|
||||
int(query.get("after", ["131072"])[0]) if mode == "stall" else start)
|
||||
if mode in ("stall", "chunked-media") and "after_seconds" in query else
|
||||
int(query.get("after", ["131072"])[0]) if mode in ("stall", "chunked-media") else start)
|
||||
except (ValueError, OverflowError):
|
||||
self._reply(request, 400, b"Invalid gate offset", "text/plain")
|
||||
return
|
||||
with self.lock:
|
||||
if token not in self.gates:
|
||||
prefix = self._prefix_ends(name, gate_at) if mode == "stall" else {}
|
||||
prefix = self._prefix_ends(name, gate_at) if mode in ("stall", "chunked-media") else {}
|
||||
self.gates[token] = _Gate(size, prefix)
|
||||
gate = self.gates[token]
|
||||
gate.requests += 1
|
||||
gate.connections.add(request.client_address)
|
||||
gate.ranges += int(range_header is not None)
|
||||
# Only the original prefix connection is gated: a seek can create a new
|
||||
# range request while the old read is still blocked and never released.
|
||||
should_gate = not gate.claimed and start <= gate_at <= end
|
||||
# Simulate a CDN that throttles oversized/open-ended ranges, while
|
||||
# finite requests of at most 10 MiB run at full available speed.
|
||||
oversized = mode == "chunked-media" and (
|
||||
range_header is None or not range_header.split("-", 1)[1]
|
||||
or int(range_header.split("-", 1)[1]) - start + 1 > 10 * 1024 * 1024)
|
||||
should_gate = (mode != "chunked-media" or oversized) and not gate.claimed and start <= gate_at <= end
|
||||
if should_gate:
|
||||
gate.claimed = True
|
||||
with self.lock:
|
||||
@@ -249,10 +261,11 @@ class StreamingServer:
|
||||
request.send_header("Content-Length", str(end - start + 1))
|
||||
if range_header is not None:
|
||||
request.send_header("Content-Range", f"bytes {start}-{end}/{size}")
|
||||
request.send_header("Connection", "close")
|
||||
persistent = mode == "chunked-media" and parse_qs(parsed.query).get("keep_alive") == ["1"]
|
||||
request.send_header("Connection", "keep-alive" if persistent else "close")
|
||||
request.end_headers()
|
||||
request.wfile.flush()
|
||||
request.close_connection = True
|
||||
request.close_connection = not persistent
|
||||
cursor = start
|
||||
with path.open("rb") as media:
|
||||
media.seek(start)
|
||||
|
||||
@@ -22,8 +22,20 @@ struct SystemPlaybackChecks {
|
||||
let suite = "terebi.system-playback-check.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let initialModel = PlayerModel(defaults: defaults)
|
||||
require(!initialModel.showsBufferInfo, "buffer readout defaults off before playback")
|
||||
initialModel.setShowsBufferInfo(true)
|
||||
require(initialModel.showsBufferInfo && initialModel.controlsVisible,
|
||||
"buffer readout can be enabled before playback and reveals controls")
|
||||
initialModel.shutdown()
|
||||
let restoredModel = PlayerModel(defaults: defaults)
|
||||
require(restoredModel.showsBufferInfo, "enabled buffer readout survives model recreation")
|
||||
restoredModel.setShowsBufferInfo(false)
|
||||
require(!restoredModel.showsBufferInfo, "buffer readout can be explicitly disabled")
|
||||
restoredModel.shutdown()
|
||||
let model = PlayerModel(defaults: defaults)
|
||||
defer { model.shutdown() }
|
||||
require(!model.showsBufferInfo, "explicitly disabled buffer readout survives model recreation")
|
||||
model.setVolume(0)
|
||||
let directory = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
||||
let center = MPNowPlayingInfoCenter.default()
|
||||
@@ -73,6 +85,11 @@ struct SystemPlaybackChecks {
|
||||
retainedSnapshot.buffered_end = min(retainedSnapshot.duration, retainedSnapshot.position + 1)
|
||||
model.applyState(retainedSnapshot)
|
||||
require(model.bufferedRange != nil, "loaded selection exposes retained media")
|
||||
let retainedRange = model.bufferedRange
|
||||
model.setShowsBufferInfo(true)
|
||||
require(model.bufferedRange == retainedRange, "enabling buffer readout preserves retained media")
|
||||
model.setShowsBufferInfo(false)
|
||||
require(model.bufferedRange == retainedRange, "disabling buffer readout preserves retained media")
|
||||
model.closeFile()
|
||||
require(model.bufferedRange == nil, "accepted close immediately hides a stale retained-media snapshot")
|
||||
require(center.nowPlayingInfo == nil && center.playbackState == .stopped,
|
||||
|
||||
Reference in New Issue
Block a user