feat: play resolved Miruro HLS with external VTT and stable episode progress

This commit is contained in:
2026-10-12 19:17:22 +02:00
parent 1f12660936
commit 470d8feec2
23 changed files with 1528 additions and 38 deletions
+101 -6
View File
@@ -11,7 +11,29 @@ import tempfile
import time
import uuid
from streaming import StreamingServer
from urllib.parse import urlsplit
from streaming import StreamingServer, resolved_fixtures
class StartupStreamingServer(StreamingServer):
def __init__(self, fixtures):
super().__init__(fixtures)
self.subtitle_requests = [0, 0]
def _get(self, request):
parsed = urlsplit(request.path)
if parsed.path in ("/startup-0.vtt", "/startup-1.vtt"):
index = int(parsed.path[len("/startup-")])
if parsed.query != self.resolved_token or request.headers.get("X-Subtitle") != f"track-{index}":
self._reply(request, 403, b"Missing subtitle credentials")
return
with self.lock:
self.subtitle_requests[index] += 1
body = f"WEBVTT\n\n00:00:00.000 --> 00:01:00.000\nIndependent subtitle {index}\n".encode()
self._reply(request, 200, body, "text/vtt")
return
super()._get(request)
def check_cli_instances(root, app, fixtures):
@@ -87,13 +109,15 @@ def check_cli_instances(root, app, fixtures):
except FileNotFoundError:
return []
records = [json.loads(line) for line in lines if line]
fields = {"version", "event", "media_url", "position_seconds", "duration_seconds", "completion_eligible"}
miruro = isinstance(allowed, list)
identity_field = "media_identity" if miruro else "media_url"
fields = {"version", "event", identity_field, "position_seconds", "duration_seconds", "completion_eligible"}
for record in records:
duration = record.get("duration_seconds")
position = record.get("position_seconds")
valid = (set(record) == fields and record["version"] == 1
valid = (set(record) == fields and record["version"] == (2 if miruro else 1)
and record["event"] in ("progress", "ended", "closed")
and record["media_url"] in allowed
and record[identity_field] in allowed
and type(position) in (int, float) and math.isfinite(position) and position >= 0
and (duration is None or (type(duration) in (int, float)
and math.isfinite(duration) and duration > 0))
@@ -134,14 +158,81 @@ def check_cli_instances(root, app, fixtures):
while time.monotonic() < deadline:
survivors = [json.loads(path.read_text()) for path in root.glob("state-*.json")
if path.stem != f"state-{pid}"]
if survivors and all(pid not in state["pids"] for state in survivors):
exited = False
if count == 1:
try:
os.kill(pid, 0)
except ProcessLookupError:
exited = True
if exited or (survivors and all(pid not in state["pids"] for state in survivors)):
(root / f"state-{pid}.json").unlink()
(root / f"keys-{pid}.json").unlink()
return wait_states(count - 1, lambda _: True, "normal quit unregisters only the targeted GUI")
time.sleep(0.02)
raise RuntimeError(f"normal quit did not unregister GUI PID {pid}")
with StreamingServer(fixtures) as server:
def check_resolved_instances(server, count):
originals = wait_states(count, lambda _: True, "existing players are ready before resolved launches")
states = originals
resolved_pids = []
resolved_reports = []
identities = []
for index in range(2):
source = root / f"resolved source {index}.json"
report = root / f"resolved progress {index}.jsonl"
identity = {"provider": "miruro", "media_id": "startup_opaque", "episode": index + 1}
source.write_text(json.dumps({
"version": 1,
"url": server.base_url + "/resolved/encrypted/master.m3u8?" + server.resolved_token,
"headers": {"X-Terebi-Fixture": "combined", "Authorization": "Bearer " + server.resolved_secret},
"identity": identity, "title": f"Independent resolved episode {index + 1}",
"subtitles": [{"url": server.base_url + f"/startup-{index}.vtt?" + server.resolved_token,
"format": "vtt", "language": "en", "label": f"Track {index}",
"default": True, "headers": {"X-Subtitle": f"track-{index}"}}]
}))
source.chmod(0o600)
previous = set(states)
invoke(["--new-instance", "--source-file", str(source), "--progress-file", str(report)])
states = wait_states(count + index + 1, lambda snapshots: any(
pid not in previous and state["media"] == f"Independent resolved episode {index + 1}"
and state["status"] == 3 and state["hasVideo"] and state["hasAudio"]
and state["canSeek"] and not state["busy"] and not state["seekPending"]
and state["subtitleLabels"] == [f"Track {index}"] and state["selectedSubtitle"] == 0
and state["subtitleText"] == f"Independent subtitle {index}" and not state["subtitleError"]
for pid, state in snapshots.items()), "actual source-file CLI reaches registered GUI HLS and VTT playback")
pid = next(pid for pid in states if pid not in previous)
states = input_request(pid, count + index + 1, lambda state: state["status"] == 2,
"resolved GUI pauses independently after decoded startup", action="pause")
wait_report(report, [identity], lambda rows: any(
row["event"] == "progress" and row["completion_eligible"] for row in rows),
"resolved GUI owns v2 stable-identity reporting after CLI exit")
resolved_pids.append(pid)
resolved_reports.append(report)
identities.append(identity)
for pid, original in originals.items():
if states[pid]["media"] != original["media"] or settings(states[pid]) != settings(original):
raise RuntimeError("resolved launches disturbed an existing player")
with server.lock:
stats = server.resolved_stats.get("encrypted", {})
if any(server.subtitle_requests[index] == 0 for index in range(2)) or any(
stats.get(kind, {}).get("requests", 0) == 0 or stats[kind]["denied"]
for kind in ("master", "variant", "key", "segment")):
raise RuntimeError("actual CLI playback lost complete HLS URLs, credentials, or independent VTT headers")
for index in reversed(range(2)):
pid = resolved_pids[index]
quit_instance(pid, count + index + 1)
wait_report(resolved_reports[index], [identities[index]],
lambda rows: bool(rows) and rows[-1]["event"] == "closed",
"resolved GUI quit drains only its stable episode identity")
report_bytes = resolved_reports[index].read_bytes()
if not report_bytes.endswith(b"\n") or any(secret in report_bytes for secret in (
server.resolved_token.encode(), server.resolved_secret.encode(), server.base_url.encode())):
raise RuntimeError("resolved report is incomplete or exposes transport secrets")
wait_states(count, lambda _: True, "original players survive independent resolved GUI quits")
print("PASS actual resolved CLI handoff: two isolated GUI episodes, authenticated encrypted HLS, independent default VTT, v2 progress and quit")
resolved_fixtures(fixtures)
with StartupStreamingServer(fixtures) as server:
if extractor and runtime:
choices = []
for index, url in enumerate([canonical, *reported_urls[:3]]):
@@ -174,6 +265,9 @@ def check_cli_instances(root, app, fixtures):
runtime_settings = {"directory": str(root), "path": str(root) + os.pathsep + os.environ.get("PATH", "")}
(app / "Contents/Resources/cli-check.json").write_text(json.dumps(runtime_settings))
subprocess.run(["codesign", "--force", "--sign", "-", str(app)], check=True)
if os.environ.get("TEREBI_CHECK_RESOLVED_ONLY") == "1":
check_resolved_instances(server, 0)
return
movie = fixtures / "cache.mp4"
audio = root / "audio with spaces.wav"
shutil.copy2(fixtures / "audio.wav", audio)
@@ -283,6 +377,7 @@ def check_cli_instances(root, app, fixtures):
else:
print("SKIP independent YouTube playback: yt-dlp and a supported JS runtime are required")
check_resolved_instances(server, count)
existing_report = root / "existing progress.jsonl"
sentinel = b"existing destination must remain byte-for-byte unchanged\n"
existing_report.write_bytes(sentinel)
+4
View File
@@ -216,6 +216,10 @@ private enum CLIInstanceCheck {
"hasAudio": model.state.has_audio,
"hasVideo": model.state.has_video,
"error": model.errorText ?? "",
"subtitleLabels": model.subtitleTracks.map(\.label),
"selectedSubtitle": model.selectedSubtitle ?? -1,
"subtitleText": model.subtitleText,
"subtitleError": model.subtitleError ?? "",
"showBufferInfo": model.showsBufferInfo,
"automaticQuality": model.automaticYouTubeQuality,
"snapToEdges": model.compactWindow.snappingEnabled,
+204
View File
@@ -0,0 +1,204 @@
import Foundation
import Darwin
@main
struct ExternalSubtitleChecks {
struct Failure: Error, CustomStringConvertible { let description: String }
static func require(_ condition: @autoclosure () throws -> Bool, _ message: String) throws {
guard try condition() else { throw Failure(description: "FAIL: \(message)") }
}
static func reject(_ operation: () throws -> Void) throws {
do { try operation() }
catch is Failure { throw Failure(description: "FAIL: rejection assertion") }
catch { return }
throw Failure(description: "FAIL: malformed input accepted")
}
static let fixture = """
WEBVTT example
NOTE ignored comment
not a cue
STYLE
::cue { color: white; }
REGION
id:bottom
width:80%
cue-one
00:01.000 --> 00:04.000 line:90% position:50%,center align:center size:80% region:bottom
<v Speaker><b>Hello &amp; goodbye</b></v>
second <c.yellow>line</c> &lt;3 &#33;
cue-two
00:02.000 --> 00:03.000 vertical:rl
Overlap <00:02.500><i>text</i>
00:04.000 --> 00:05.000
Next
"""
static func main() async throws {
let document = try WebVTTDocument.parse("\u{FEFF}" + fixture.replacingOccurrences(of: "\n", with: "\r\n"))
let first = "Hello & goodbye\nsecond line <3 !"
try require(document.cues.count == 3, "BOM, CRLF, identifiers and metadata blocks")
try require(document.text(at: 0.999).isEmpty, "before first cue")
try require(document.text(at: 1) == first, "inclusive start and plain-text markup/entities")
try require(document.text(at: 2.5) == first + "\nOverlap text", "simultaneous overlapping cues")
try require(document.text(at: 3) == first, "exclusive overlap end")
try require(document.text(at: 4) == "Next", "adjacent exclusive end/inclusive start")
try require(document.text(at: 10).isEmpty, "after final cue")
try require(document.text(at: 2) == first + "\nOverlap text", "backward seek after final cue")
try require(document.text(at: .nan).isEmpty && document.text(at: -1).isEmpty, "invalid acknowledged positions")
let nested = try WebVTTDocument.parse("WEBVTT\n\n00:00.000 --> 01:00.000\nlong\n\n00:02.000 --> 00:03.000\nshort\n")
try require(nested.text(at: 4) == "long", "earlier long cue survives later cue ending")
try require(try WebVTTDocument.parse("WEBVTT\n\n01:02:03.004 --> 01:02:04.000\nhour\n").text(at: 3723.004) == "hour", "hour timestamps")
let zeroDuration = try WebVTTDocument.parse("WEBVTT\n\n00:01.000 --> 00:01.000\nnever visible\n\n00:02.000 --> 00:03.000\nvalid later cue\n")
try require(zeroDuration.text(at: 0.999).isEmpty && zeroDuration.text(at: 1).isEmpty && zeroDuration.text(at: 1.001).isEmpty, "zero-duration cue is never active")
try require(zeroDuration.text(at: 2) == "valid later cue", "zero-duration cue does not invalidate later subtitles")
try require(zeroDuration.text(at: 4).isEmpty && zeroDuration.text(at: 1).isEmpty, "zero-duration cue stays inactive after backward seek")
for malformed in [
"not VTT", "WEBVTT\n00:01.000 --> 00:02.000\nmissing separator",
"WEBVTT\n\n00:61.000 --> 00:62.000\nbad time",
"WEBVTT\n\n00:02.000 --> 00:01.000\nreversed interval",
"WEBVTT\n\n00:01.000 --> 00:02.000 size:101%\nbad setting",
"WEBVTT\n\n00:01.000 --> 00:02.000\n<b unfinished",
"WEBVTT\n\norphan identifier", "WEBVTT\n\n00:01.000 --> 00:02.000\ncue\n\nSTYLE\nlate style"
] { try reject { _ = try WebVTTDocument.parse(malformed) } }
try reject { _ = try WebVTTDocument.parse(String(repeating: "x", count: WebVTTDocument.byteLimit + 1)) }
let headers = "Authorization: Bearer fixture-secret\r\nReferer: https://provider.example/\r\nX-Track: english\r\n"
let request = try WebVTTDocument.request(url: "https://provider.example/track.jpeg", headers: headers)
try require(request.value(forHTTPHeaderField: "Authorization") == "Bearer fixture-secret", "resource authorization header")
try require(request.value(forHTTPHeaderField: "X-Track") == "english", "resource-specific custom header")
let whitespace = try WebVTTDocument.request(url: "https://provider.example/track", headers: "X-Exact: padded \t \r\n")
try require(whitespace.value(forHTTPHeaderField: "X-Exact") == " padded \t ", "preserve header value whitespace after generated separator")
for malformed in ["X: a\nInjected: value\r\n", "X: a\rInjected: value\r\n", "X: a\r\nX: b\r\n", "X: a", ": a\r\n"] {
try reject { _ = try WebVTTDocument.request(url: "https://provider.example/track", headers: malformed) }
}
for url in ["file:///tmp/track", "https://user:secret@provider.example/track", "https://provider.example/track#fragment"] {
try reject { _ = try WebVTTDocument.request(url: url, headers: "") }
}
let source = URL(string: "https://provider.example/track.jpeg")!
try require(WebVTTDocument.permitsRedirect(from: source, to: URL(string: "https://PROVIDER.example:443/other")!), "same origin includes default port")
for target in ["https://cdn.example/track", "http://provider.example/track", "https://provider.example:8443/track", "https://user:secret@provider.example/track"] {
try require(!WebVTTDocument.permitsRedirect(from: source, to: URL(string: target)!), "cross-origin or credentialed redirect rejection")
}
let server = try SubtitleFixtureServer(body: fixture)
defer { server.stop() }
let loaded = try await WebVTTDocument.load(url: server.url("/same.jpeg"), headers: headers)
try require(loaded.cues == document.cues, "load JPEG-named, image/jpeg VTT via same-origin redirect")
let requests = server.requests
try require(requests.count == 2 && requests.allSatisfy { $0.contains("Bearer fixture-secret") && $0.contains("english") }, "resource headers preserved only on same origin")
do {
_ = try await WebVTTDocument.load(url: server.url("/cross.jpeg"), headers: headers)
throw Failure(description: "FAIL: cross-origin redirect accepted")
} catch is Failure {
throw Failure(description: "FAIL: cross-origin redirect accepted")
} catch {
let message = error.localizedDescription
try require(!message.contains("fixture-secret") && !message.contains("127.0.0.1"), "sanitized visible transport error")
}
try require(server.requests.count == 3, "cross-origin redirect never sends credential-bearing request")
do {
_ = try await WebVTTDocument.load(url: server.url("/oversized.jpeg"), headers: "")
throw Failure(description: "FAIL: oversized response accepted")
} catch is Failure { throw Failure(description: "FAIL: oversized response accepted") }
catch { }
print("External subtitle checks passed")
}
}
/// A loopback-only HTTP fixture; no external services, dates, credentials or frameworks.
private final class SubtitleFixtureServer: @unchecked Sendable {
private let descriptor: Int32
private let port: UInt16
private let body: String
private let lock = NSLock()
private var recorded: [String] = []
private let ended = DispatchSemaphore(value: 0)
var requests: [String] {
lock.lock()
defer { lock.unlock() }
return recorded
}
init(body: String) throws {
self.body = body
let descriptor = socket(AF_INET, SOCK_STREAM, 0)
self.descriptor = descriptor
guard descriptor >= 0 else { throw ExternalSubtitleChecks.Failure(description: "FAIL: fixture socket") }
var address = sockaddr_in()
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
address.sin_family = sa_family_t(AF_INET)
address.sin_addr.s_addr = inet_addr("127.0.0.1")
let bound = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { bind(descriptor, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) }
}
var size = socklen_t(MemoryLayout<sockaddr_in>.size)
let named = withUnsafeMutablePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { getsockname(descriptor, $0, &size) }
}
guard bound == 0, named == 0, listen(descriptor, 4) == 0 else {
close(descriptor)
throw ExternalSubtitleChecks.Failure(description: "FAIL: fixture listener")
}
port = UInt16(bigEndian: address.sin_port)
DispatchQueue.global().async { self.serve() }
}
func url(_ path: String) -> String { "http://127.0.0.1:\(port)\(path)" }
func stop() {
shutdown(descriptor, SHUT_RDWR)
close(descriptor)
ended.wait()
}
private func serve() {
defer { ended.signal() }
while true {
let client = accept(descriptor, nil, nil)
guard client >= 0 else { return }
var noSignal: Int32 = 1
_ = setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &noSignal, socklen_t(MemoryLayout<Int32>.size))
var buffer = [UInt8](repeating: 0, count: 4096)
var data = Data()
while data.count < 65536 {
let count = recv(client, &buffer, buffer.count, 0)
if count <= 0 { break }
data.append(contentsOf: buffer.prefix(count))
if String(decoding: data, as: UTF8.self).contains("\r\n\r\n") { break }
}
let request = String(decoding: data, as: UTF8.self)
lock.lock()
recorded.append(request)
lock.unlock()
let path = request.split(separator: " ").dropFirst().first ?? ""
let response: String
switch path {
case "/same.jpeg": response = "HTTP/1.1 302 Found\r\nLocation: /track.jpeg\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
case "/cross.jpeg": response = "HTTP/1.1 302 Found\r\nLocation: http://localhost:\(port)/track.jpeg\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
case "/oversized.jpeg": response = "HTTP/1.1 200 OK\r\nContent-Length: \(WebVTTDocument.byteLimit + 1)\r\nConnection: close\r\n\r\n"
default: response = "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: \(body.utf8.count)\r\nConnection: close\r\n\r\n\(body)"
}
let bytes = Array(response.utf8)
bytes.withUnsafeBytes { raw in
var offset = 0
while offset < raw.count {
let count = send(client, raw.baseAddress!.advanced(by: offset), raw.count - offset, 0)
if count <= 0 { break }
offset += count
}
}
close(client)
}
}
}
+96 -1
View File
@@ -1,6 +1,9 @@
#include "../Sources/Core/player_macos.h"
#import <AppKit/AppKit.h>
#import <MetalKit/MetalKit.h>
extern "C" {
#include <libavutil/log.h>
}
#import <dispatch/dispatch.h>
#include <chrono>
#include <cmath>
@@ -135,7 +138,9 @@ static bool openNetwork(Player *player, const std::string &base, const std::stri
}
static void closed(Player *player) {
auto state = until(player, [](auto s) { return s.status == PLAYER_EMPTY; }, "close network source");
// A close submitted from PLAYER_ERROR may still publish the previous error
// until the worker acknowledges it; require the complete clean state below.
auto state = until(player, [](auto s) { return s.status == PLAYER_EMPTY; }, "close network source", true);
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");
@@ -287,6 +292,94 @@ static void nativeHLS(Player *player, MTKView *view, const std::string &base) {
std::puts("PASS native HLS: selected headers/recorded seekability/paused seek/audio-video resume");
}
static bool openResolved(Player *player, const std::string &base, const std::string &scenario,
bool credentials = true) {
const auto url = base + "/resolved/" + scenario + "/master.m3u8?token=part%2Fone+two%26tail";
const std::string headers = credentials ?
"X-Terebi-Fixture: combined\r\nAuthorization: Bearer resolved-header-secret\r\n" :
"X-Terebi-Fixture: combined\r\n";
PlayerNetworkSource source{url.c_str(), nullptr, headers.c_str(), nullptr,
4, false, "miruro:fixture_opaque-ID:1", 0};
return player_open_network(player, &source);
}
static void resolvedHLS(Player *player, MTKView *view, const std::string &base) {
for (const std::string scenario : {"encrypted", "fmp4", "image-encrypted", "image-fmp4"}) {
require(openResolved(player, base, scenario), "resolved HLS asynchronous open accepted");
require(player_play(player), "resolved HLS play accepted");
auto state = until(player, [](auto s) {
return s.status == PLAYER_PLAYING && !s.buffering && s.position > .2;
}, "resolved HLS audio/video decode");
require(state.has_audio && state.has_video && state.can_seek,
"encrypted/fMP4 HLS provides decoded audio, video and seekability");
require(player_pause(player), "pause resolved HLS");
until(player, [](auto s) { return s.status == PLAYER_PAUSED; }, "resolved HLS pause");
seekCompleted(player, 2.5, PLAYER_PAUSED);
auto revision = player_get_state(player).revision;
require(player_step_frame(player, 1), "step an actually decoded HLS video frame after seek");
state = until(player, [revision](auto s) {
return s.status == PLAYER_PAUSED && !s.seek_pending && s.revision != revision && s.position > 2.5;
}, "resolved HLS decoded frame acknowledgement");
require(state.position < 2.6, "HLS seek selects the adjacent 24fps decoded frame");
[view draw];
require(player_play(player), "resume resolved HLS");
until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 2.65; },
"resolved HLS audio clock resumes after frame step");
seekCompleted(player, 1.25, PLAYER_PLAYING);
until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > 1.4; },
"resolved HLS playing backward seek");
require(player_close(player), "close resolved HLS");
closed(player);
NSDictionary *source = control(base, "/control/resolved-stats")[@"sources"]
[[NSString stringWithUTF8String:scenario.c_str()]];
for (NSString *kind in @[@"master", @"variant", @"segment",
scenario.ends_with("encrypted") ? @"key" : @"init"]) {
require([source[kind][@"requests"] unsignedLongLongValue] > 0 &&
[source[kind][@"denied"] unsignedLongLongValue] == 0,
"required headers and unmodified query token reach every HLS resource class");
}
}
for (const std::string scenario : {"redirect-master", "redirect-variant", "redirect-segment",
"redirect-key", "redirect-init", "redirect-same-master",
"external-variant", "external-segment", "external-key", "external-init",
"missing"}) {
require(openResolved(player, base, scenario, scenario != "missing"), "unsafe HLS request accepted asynchronously");
auto state = until(player, [](auto s) { return s.status == PLAYER_ERROR; },
"unsafe HLS transport rejected", true);
require(state.error[0] && std::string(state.error).find("http") == std::string::npos &&
std::string(state.error).find("resolved-header-secret") == std::string::npos,
"application retains meaningful transport error without credentials or URLs");
if (scenario.starts_with("redirect-")) {
const auto kind = scenario == "redirect-same-master" ? "master" : scenario.substr(9);
NSDictionary *source = control(base, "/control/resolved-stats")[@"sources"]
[[NSString stringWithUTF8String:scenario.c_str()]];
require([source[[NSString stringWithUTF8String:kind.c_str()]][@"redirects"] unsignedLongLongValue] > 0,
"redirect isolation actually exercises each HLS redirect resource");
}
if (scenario == "missing") {
NSDictionary *source = control(base, "/control/resolved-stats")[@"sources"][@"missing"];
require([source[@"master"][@"denied"] unsignedLongLongValue] > 0,
"fixture explicitly fails a request missing a required credential");
}
require(player_close(player), "close rejected resolved source");
closed(player);
}
NSDictionary *sink = control(base, "/control/resolved-stats")[@"sink"];
require([sink[@"requests"] unsignedLongLongValue] == 0 &&
[sink[@"credentials"] unsignedLongLongValue] == 0,
"neither redirects nor explicit foreign HLS URLs contact the unrelated origin");
require(openNetwork(player, base, "/youtube-redirect/av.mp4", "", 4, false, true,
"https://www.youtube.com/watch?v=AAAAAAAAAAA"),
"YouTube selected source retains ordinary HTTP redirect behavior");
require(player_play(player), "redirected YouTube fixture play");
until(player, [](auto s) { return s.status == PLAYER_PLAYING && s.position > .2 && s.has_video && s.has_audio; },
"YouTube transport unchanged by resolved source restrictions");
require(player_close(player), "close redirected YouTube fixture");
closed(player);
std::puts("PASS resolved HLS: AES-128/fMP4 audio+video decode/paused+playing seek/frame step/all resource credentials/token preservation/redirect and foreign-origin isolation/redacted errors/YouTube redirects unchanged");
}
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); },
@@ -798,6 +891,7 @@ int main(int argc, char **argv) {
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");
av_log_set_level(AV_LOG_TRACE); // Diagnostics must be safe even at maximal FFmpeg verbosity.
[NSApplication sharedApplication];
reporting(base, fixtures);
std::unique_ptr<Player, decltype(&player_destroy)> player(player_create(), player_destroy);
@@ -819,6 +913,7 @@ int main(int argc, char **argv) {
transport(player.get(), view, base, true);
frameSteps(player.get(), view, base);
nativeHLS(player.get(), view, base);
resolvedHLS(player.get(), view, base);
liveAndReplacement(player.get(), base, fixtures);
cachedWindow(player.get(), base);
bufferCoverage(player.get(), base);
+59
View File
@@ -1,4 +1,5 @@
import Foundation
import Darwin
@main
struct PlayerCommandChecks {
@@ -72,6 +73,64 @@ struct PlayerCommandChecks {
require(try opened(["--", "--progress-file"]) == reportFlagFile,
"escaped reporting option remains a filename")
let sourceFile = directory.appendingPathComponent("source with spaces.json")
let sourceURL = "https://cdn.example/video/master.m3u8?token=secret%2Bvalue"
var source: [String: Any] = [
"version": 1, "url": sourceURL, "headers": ["Authorization": "Bearer secret "],
"identity": ["provider": "miruro", "media_id": "opaque_A-1", "episode": 12],
"subtitles": [["url": "https://cdn.example/sub.vtt?token=secret", "format": "vtt",
"language": "en", "label": "English", "default": true, "headers": ["X-Token": "secret"]]]
]
func saveSource() throws {
try JSONSerialization.data(withJSONObject: source).write(to: sourceFile)
}
try saveSource()
let resolved = try ResolvedSource.load(from: sourceFile)
require(resolved.url == sourceURL && resolved.headers == "Authorization: Bearer secret \r\n",
"resolved URLs and header values remain byte-for-byte intact")
require(resolved.identity == "miruro:opaque_A-1:12" && resolved.title == "Miruro episode 12",
"stable identity excludes CDN tokens and omitted title has a useful fallback")
require(resolved.subtitles.count == 1 && resolved.subtitles[0].isDefault &&
resolved.subtitles[0].headers == "X-Token: secret\r\n", "subtitle metadata retains independent headers")
guard case .launchSource(let delivered, let independent, let bypass, let output) =
try PlayerCommand.parse(["--new-instance", "--no-config", "--source-file", sourceFile.lastPathComponent,
"--progress-file", report.lastPathComponent], directory: directory) else {
fatalError("FAIL: expected resolved source launch")
}
require(delivered == sourceFile && independent && bypass && output == report,
"CLI forwards only an absolute source-file path with existing launch options")
for arguments in [["--source-file", sourceFile.path], ["--new-instance", "--source-file"],
["--new-instance", "--source-file", sourceFile.path, movie.path]] {
do { _ = try PlayerCommand.parse(arguments, directory: directory); fatalError("FAIL: invalid source launch accepted") }
catch {}
}
let original = source
let invalidChanges: [[String: Any]] = [
["version": 2], ["url": "https://user:secret@cdn.example/video"],
["url": "file:///secret"], ["url": "https://cdn.example/video\nsecret"],
["headers": ["X-Token": "secret\r\nInjected: yes"]], ["headers": ["X-Token": "secret\t"]],
["headers": ["Bad Name": "secret"]], ["headers": ["X-Token": "one", "x-token": "two"]],
["identity": ["provider": "miruro", "media_id": "bad:id", "episode": 1]],
["identity": ["provider": "miruro", "media_id": "opaque", "episode": 0]],
["identity": ["provider": "miruro", "media_id": "opaque", "episode": 1.5]],
["subtitles": Array(repeating: (original["subtitles"] as! [[String: Any]])[0], count: 2)]
]
for change in invalidChanges {
source = original.merging(change) { _, replacement in replacement }
try saveSource()
do { _ = try ResolvedSource.load(from: sourceFile); fatalError("FAIL: invalid source accepted") }
catch { require(!error.localizedDescription.contains("secret"), "validation never reveals credentials") }
}
try Data(repeating: 32, count: 1_048_577).write(to: sourceFile)
do { _ = try ResolvedSource.load(from: sourceFile); fatalError("FAIL: oversized source accepted") }
catch {}
let fifo = directory.appendingPathComponent("source.fifo")
require(mkfifo(fifo.path, 0o600) == 0, "create unread FIFO source boundary")
do {
_ = try PlayerCommand.parse(["--new-instance", "--source-file", fifo.path], directory: directory)
fatalError("FAIL: FIFO source accepted")
} catch {}
for arguments in [["--url"], ["--url", canonical.absoluteString, movie.path], ["--"],
[movie.path, movie.path], ["--unknown"], ["missing.mp4"], [directory.path],
["file://remote-host/video.mp4"], ["https://example.com/video.mp4"],
+41
View File
@@ -248,6 +248,47 @@ int main(int argc, char **argv) {
"https://www.youtube.com/watch?v=AAAAAAAAAA%", "https://www.youtube.com/watch?v=AAAAAAAAAA\n"})
require(!ProgressReporter::validMediaURL(invalid), "noncanonical URLs are rejected");
require(ProgressReporter::validMediaIdentity("miruro:opaque_A-1:12") &&
ProgressReporter::validMediaIdentity("miruro:a:2147483647"), "strict Miruro identities accepted");
for (const char *invalid : {"miruro::1", "miruro:a:0", "miruro:a:01", "miruro:a:-1",
"miruro:a:1.5", "miruro:a:2147483648", "miruro:a:1?token=secret",
"miruro:a:b:1", "miruro:a:1\n", "miruro:bad/id:1", "miruro:é:1"})
require(!ProgressReporter::validMediaIdentity(invalid), "ambiguous or token-bearing identities rejected");
const auto miruroPath = root + "/miruro.jsonl";
{
ProgressReporter reporter(miruroPath, [] { return 0.; });
reporter.begin("miruro:opaque_A-1:12");
reporter.observe(sample(PLAYER_PLAYING, 95));
reporter.begin("miruro:opaque_A-1:13");
reporter.observe(sample(PLAYER_ENDED, 100));
reporter.finish();
require(reporter.error().empty(), "Miruro progress drains successfully");
}
NSString *miruroText = [NSString stringWithContentsOfFile:nativePath(miruroPath)
encoding:NSUTF8StringEncoding error:nullptr];
NSArray<NSString *> *miruroLines = [miruroText componentsSeparatedByString:@"\n"];
require(miruroLines.count == 5, "Miruro replacement emits progress, close, ended, close");
NSSet *miruroKeys = [NSSet setWithArray:@[@"version", @"event", @"media_identity",
@"position_seconds", @"duration_seconds", @"completion_eligible"]];
for (NSUInteger i = 0; i < 4; ++i) {
NSDictionary *row = [NSJSONSerialization JSONObjectWithData:[miruroLines[i]
dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nullptr];
require([row isKindOfClass:[NSDictionary class]] &&
[[NSSet setWithArray:row.allKeys] isEqual:miruroKeys] &&
[row[@"version"] intValue] == 2, "Miruro schema is v2 without media_url");
NSDictionary *identity = row[@"media_identity"];
require([identity isKindOfClass:[NSDictionary class]] &&
[[NSSet setWithArray:identity.allKeys] isEqual:
[NSSet setWithArray:@[@"provider", @"media_id", @"episode"]]] &&
[identity[@"provider"] isEqual:@"miruro"] &&
[identity[@"media_id"] isEqual:@"opaque_A-1"] &&
[identity[@"episode"] intValue] == (i < 2 ? 12 : 13),
"structured stable episode identity contains no CDN token");
require([row[@"event"] isEqual:(i == 0 ? @"progress" : i == 2 ? @"ended" : @"closed")] &&
[row[@"position_seconds"] doubleValue] == (i < 2 ? 95 : 100) &&
[row[@"completion_eligible"] boolValue], "Miruro retains terminal snapshot semantics");
}
const auto clockFailurePath = root + "/clock-failure.jsonl";
{
ProgressReporter reporter(clockFailurePath, []() -> double { throw std::runtime_error("clock failure"); });
+79
View File
@@ -0,0 +1,79 @@
"""Local-only resolved HLS/VTT + native model smoke; no live requests."""
import http.server
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import threading
from urllib.parse import urlsplit
binary = str(Path(sys.argv[1]).resolve())
fixtures = Path(sys.argv[2]).resolve()
seen = []
errors = []
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
path = urlsplit(self.path).path
seen.append(path)
expected = "subtitle-private" if path == "/english.jpeg" else "stream-private"
if self.headers.get("X-Resource") != expected:
errors.append("wrong resource headers: " + path)
self.send_error(403)
return
if path == "/master.m3u8":
if urlsplit(self.path).query != "token=fixture":
errors.append("master token lost")
data = b'#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=500000\nvariant.m3u8?token=variant\n'
elif path == "/variant.m3u8":
if urlsplit(self.path).query != "token=variant":
errors.append("variant token lost")
data = (fixtures / "resolved-vod.m3u8").read_bytes()
elif path == "/english.jpeg":
data = b'WEBVTT\n\n00:00.000 --> 00:02.000\nFirst English cue\n\n00:02.000 --> 00:04.000\nSecond English cue\n'
elif path.startswith("/resolved-vod-") and path.endswith(".ts"):
data = (fixtures / path[1:]).read_bytes()
else:
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Length", str(len(data)))
self.send_header("Content-Type", "text/vtt" if path.endswith(".jpeg") else "application/octet-stream")
self.end_headers()
self.wfile.write(data)
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", str(fixtures / "av.mp4"),
"-c:v", "libx264", "-g", "24", "-keyint_min", "24", "-sc_threshold", "0", "-c:a", "aac",
"-hls_time", "1", "-hls_playlist_type", "vod", "-hls_segment_filename", str(fixtures / "resolved-vod-%d.ts"),
str(fixtures / "resolved-vod.m3u8")], check=True)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
base = f"http://127.0.0.1:{server.server_port}"
with tempfile.TemporaryDirectory(prefix="terebi-resolved-") as temp:
source = Path(temp) / "source.json"
progress = Path(temp) / "progress.jsonl"
source.write_text(json.dumps({"version": 1, "url": base + "/master.m3u8?token=fixture",
"headers": {"X-Resource": "stream-private"}, "identity": {"provider": "miruro", "media_id": "fixture-ID", "episode": 1},
"title": "Local resolved fixture", "subtitles": [{"url": base + "/english.jpeg", "format": "vtt", "language": "en",
"label": "English", "default": True, "headers": {"X-Resource": "subtitle-private"}}]}))
source.chmod(0o600)
subprocess.run([binary, str(source), str(progress)], check=True, timeout=100)
records = [json.loads(line) for line in progress.read_text().splitlines()]
assert records and all(record["version"] == 2 and record["media_identity"] ==
{"provider": "miruro", "media_id": "fixture-ID", "episode": 1} for record in records), "stable episode progress identity"
assert any(record["completion_eligible"] for record in records), "acknowledged video progress eligible"
assert "stream-private" not in progress.read_text() and "token=" not in progress.read_text(), "progress has no transport secrets"
assert not errors, errors
assert {"/master.m3u8", "/variant.m3u8", "/english.jpeg"}.issubset(seen), "master, nested playlist and external VTT fetched"
assert any(path.endswith(".ts") for path in seen), "media segment fetched"
print("PASS: model HLS/VTT playback, independent resource headers, tokens, stable progress")
finally:
server.shutdown()
server.server_close()
thread.join()
+66
View File
@@ -0,0 +1,66 @@
import AppKit
@main
@MainActor
struct ResolvedPlaybackCheck {
static func require(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() { fatalError("FAIL: " + message) }
}
static func until(_ message: String, model: PlayerModel, _ predicate: () -> Bool) {
let deadline = ContinuousClock.now.advanced(by: .seconds(45))
while !predicate(), ContinuousClock.now < deadline {
if model.state.status == PLAYER_ERROR || model.commandError != nil {
fatalError("FAIL: " + (model.errorText ?? "Playback failed"))
}
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.01))
}
require(predicate(), message)
}
static func main() async {
do { try await run() }
catch { fatalError("FAIL: " + error.localizedDescription) }
}
static func run() async throws {
_ = NSApplication.shared
NSApp.setActivationPolicy(.prohibited)
let file = URL(fileURLWithPath: CommandLine.arguments[1])
let source = try ResolvedSource.load(from: file)
let suite = "terebi.resolved-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults, progressFile: CommandLine.arguments.count > 2 ? CommandLine.arguments[2] : nil)
defer { model.shutdown() }
model.openResolvedSource(file)
until("video/audio decode and advancing playback", model: model) {
model.state.status == PLAYER_PLAYING && model.state.has_video && model.state.has_audio && model.state.position > 0.2
}
print("PASS: resolved HLS decodes video/audio and advances")
fflush(stdout)
require(model.setPlayback(playing: false), "pause accepted")
until("pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
require(model.state.can_seek && model.state.duration > 0, "seekable measured duration")
guard let index = source.subtitles.firstIndex(where: \.isDefault) else {
fatalError("FAIL: smoke requires a default subtitle")
}
let track = source.subtitles[index]
let document = try await WebVTTDocument.load(url: track.url, headers: track.headers)
let playableCues = document.cues.filter { !$0.text.isEmpty && $0.start + 0.1 < model.state.duration }
require(playableCues.count >= 2, "smoke requires two visible subtitle cues")
let target = playableCues[1].start + min(0.1, (playableCues[1].end - playableCues[1].start) / 2)
require(!document.text(at: target).isEmpty, "forward seek targets a visible English cue")
require(model.seekRelative(target - model.state.position), "seek accepted")
until("seek acknowledged and subtitle synchronized", model: model) {
!model.state.seek_pending && abs(model.state.position - target) < 0.2 &&
model.subtitleText == document.text(at: model.state.position) && model.subtitleError == nil
}
require(model.selectedSubtitle == index && model.subtitleTracks[index].label == track.label, "default selection and label retained")
let backward = playableCues[0].start + min(0.1, (playableCues[0].end - playableCues[0].start) / 2)
require(model.seekRelative(backward - model.state.position), "backward seek accepted")
until("backward seek subtitle synchronized", model: model) {
!model.state.seek_pending && abs(model.state.position - backward) < 0.2 &&
model.subtitleText == document.text(at: model.state.position)
}
print("PASS: default VTT selected, forward/backward seek and acknowledged cue timing")
}
}
+137 -1
View File
@@ -7,6 +7,7 @@ import argparse
import json
import re
import subprocess
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
@@ -44,6 +45,8 @@ class StreamingServer:
"network-vod.m3u8": "combined",
**{f"network-vod-{index}.ts": "combined" for index in range(4)},
}
resolved_token = "token=part%2Fone+two%26tail"
resolved_secret = "resolved-header-secret"
def __init__(self, fixtures: str | Path):
self.fixtures = Path(fixtures).resolve()
@@ -51,6 +54,8 @@ class StreamingServer:
self.gates: dict[str, _Gate] = {}
self.stats: dict[str, dict[str, int]] = {}
self.prefixes: dict[tuple[str, int], dict[str, float]] = {}
self.resolved_stats: dict[str, dict[str, dict[str, int]]] = {}
self.sink_stats = {"requests": 0, "credentials": 0}
self.stopping = threading.Event()
owner = self
@@ -70,6 +75,19 @@ class StreamingServer:
# Cancellation can close a body read or an idle keep-alive connection.
self.close_connection = True
class SinkHandler(Handler):
def do_GET(self):
with owner.lock:
owner.sink_stats["requests"] += 1
owner.sink_stats["credentials"] += int(
self.headers.get("Authorization") is not None or
self.headers.get("X-Terebi-Fixture") is not None)
owner._reply(self, 403, b"Unrelated origin")
self.sink = ThreadingHTTPServer(("127.0.0.1", 0), SinkHandler)
self.sink.daemon_threads = True
self.sink_thread = threading.Thread(target=self.sink.serve_forever, daemon=True)
self.sink_url = f"http://127.0.0.1:{self.sink.server_port}"
self.http = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
self.http.daemon_threads = True
self.thread = threading.Thread(target=self.http.serve_forever, daemon=True)
@@ -77,6 +95,7 @@ class StreamingServer:
def __enter__(self):
self.thread.start()
self.sink_thread.start()
return self
def __exit__(self, *_args):
@@ -87,6 +106,9 @@ class StreamingServer:
self.http.shutdown()
self.http.server_close()
self.thread.join()
self.sink.shutdown()
self.sink.server_close()
self.sink_thread.join()
@staticmethod
def _reply(request, status: int, body: bytes, content_type="application/json"):
@@ -155,9 +177,92 @@ class StreamingServer:
if "pos" in packet and "pts_time" in packet
and float(packet["pts_time"]) - origin >= seconds)
def _resolved(self, request, parsed, parts):
scenario = parts[1]
relative = "/".join(parts[2:])
kind = ("master" if relative == "master.m3u8" else
"variant" if relative == "nested/media.m3u8" else
"key" if relative == "keys/key.bin" else
"init" if relative in ("init.mp4", "init.jpeg") else "segment")
with self.lock:
stats = self.resolved_stats.setdefault(scenario, {}).setdefault(
kind, {"requests": 0, "denied": 0, "redirects": 0})
stats["requests"] += 1
denied = (parsed.query != self.resolved_token or
request.headers.get("X-Terebi-Fixture") != "combined" or
request.headers.get("Authorization") != "Bearer " + self.resolved_secret)
stats["denied"] += int(denied)
if denied:
self._reply(request, 403, b"Required source credentials or complete token missing")
return
if scenario == "redirect-" + kind or scenario == "redirect-same-master" and kind == "master":
with self.lock:
stats["redirects"] += 1
request.send_response(302)
request.send_header("Location", self.base_url + "/media/network-vod.m3u8"
if scenario == "redirect-same-master" else self.sink_url + "/leak")
request.send_header("Content-Length", "0")
request.send_header("Connection", "close")
request.end_headers()
request.close_connection = True
return
fragmented = scenario in ("fmp4", "image-fmp4", "redirect-init", "external-init")
image_named = scenario.startswith("image-")
directory = self.fixtures / "resolved-hls" / ("fmp4" if fragmented else "encrypted")
def uri(path, resource_kind):
if image_named and resource_kind in ("segment", "init"):
path = path.rsplit(".", 1)[0] + (".jpeg" if fragmented else ".jpg")
address = path + "?" + self.resolved_token
if scenario == "external-" + resource_kind:
return self.sink_url + "/leak/" + address
return address
if kind == "master":
body = ("#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=500000\n" +
uri("nested/media.m3u8", "variant") + "\n").encode()
elif kind == "variant":
playlist = (directory / "media.m3u8").read_text()
playlist = re.sub(r'URI="[^"]+"', lambda match: 'URI="' + uri(
"../init.mp4" if fragmented else "../keys/key.bin",
"init" if fragmented else "key") + '"', playlist)
playlist = re.sub(r"(?m)^(seg-[^\n]+)$",
lambda match: uri("../segments/" + match[1], "segment"), playlist)
body = playlist.encode()
else:
name = relative.removeprefix("segments/").removeprefix("keys/")
# Resource routing is deliberately finite; never serve arbitrary paths.
if not re.fullmatch(r"key\.bin|init\.(?:mp4|jpeg)|seg-\d+\.(?:ts|m4s|jpg|jpeg)", name):
self._reply(request, 404, b"Unknown HLS resource")
return
if image_named:
name = (name.rsplit(".", 1)[0] + ".mp4" if kind == "init" else
name.rsplit(".", 1)[0] + (".m4s" if fragmented else ".ts")
if kind == "segment" else name)
body = (directory / name).read_bytes()
self._reply(request, 200, body, "application/vnd.apple.mpegurl"
if kind in ("master", "variant") else
"image/jpeg" if image_named and kind in ("segment", "init") else "application/octet-stream")
def _get(self, request):
parsed = urlsplit(request.path)
parts = unquote(parsed.path).strip("/").split("/")
if parts == ["control", "resolved-stats"]:
with self.lock:
body = json.dumps({"sources": self.resolved_stats, "sink": self.sink_stats}).encode()
self._reply(request, 200, body)
return
if len(parts) >= 3 and parts[0] == "resolved":
self._resolved(request, parsed, parts)
return
if parts == ["youtube-redirect", "av.mp4"]:
request.send_response(302)
request.send_header("Location", self.base_url + "/media/av.mp4")
request.send_header("Content-Length", "0")
request.send_header("Connection", "close")
request.end_headers()
request.close_connection = True
return
if parts == ["control", "stats"]:
with self.lock:
body = json.dumps(self.stats).encode()
@@ -300,6 +405,27 @@ class StreamingServer:
gate.bytes += len(chunk)
def resolved_fixtures(fixtures: Path):
"""Generate deterministic AES-128 TS and fMP4 fixtures without external services."""
for fragmented in (False, True):
directory = fixtures.resolve() / "resolved-hls" / ("fmp4" if fragmented else "encrypted")
directory.mkdir(parents=True, exist_ok=True)
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", str(fixtures.resolve() / "av.mp4"),
"-c:v", "libx264", "-g", "24", "-keyint_min", "24", "-sc_threshold", "0",
"-c:a", "aac", "-hls_time", "1", "-hls_playlist_type", "vod"]
if fragmented:
command += ["-hls_segment_type", "fmp4", "-hls_fmp4_init_filename", "init.mp4"]
else:
(directory / "key.bin").write_bytes(bytes(range(16)))
(directory / "keyinfo").write_text("key.bin\n" + str(directory / "key.bin") +
"\n000102030405060708090a0b0c0d0e0f\n")
command += ["-hls_key_info_file", str(directory / "keyinfo")]
command += ["-hls_segment_filename", str(directory / ("seg-%d.m4s" if fragmented else "seg-%d.ts")),
str(directory / "media.m3u8")]
subprocess.run(command, check=True, timeout=30)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("binary", type=Path)
@@ -310,12 +436,22 @@ def main():
missing = [name for name in required if not (args.fixtures / name).is_file()]
if missing:
parser.error("Missing streaming fixtures: " + ", ".join(missing))
resolved_fixtures(args.fixtures)
with StreamingServer(args.fixtures) as server:
try:
result = subprocess.run([str(args.binary.resolve()), server.base_url,
str(args.fixtures.resolve())], timeout=180, check=False)
str(args.fixtures.resolve())], timeout=180, check=False,
capture_output=True)
except subprocess.TimeoutExpired:
raise SystemExit("FAIL: native network check exceeded its infrastructure deadline")
private = (StreamingServer.resolved_token.encode(), StreamingServer.resolved_secret.encode(),
b"Bearer ", server.base_url.encode(), server.sink_url.encode())
if any(secret in result.stdout or secret in result.stderr for secret in private):
raise SystemExit("FAIL: native diagnostics exposed source URL or credentials")
print(result.stdout.decode(), end="")
print(result.stderr.decode(), end="", file=sys.stderr)
if server.sink_stats["requests"] or server.sink_stats["credentials"]:
raise SystemExit("FAIL: resolved playback contacted an unrelated origin")
raise SystemExit(result.returncode)