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)