fix: reconnect interrupted video streams

Enable bounded FFmpeg reconnection for recorded range-seekable HTTP inputs, preserving compressed cache, byte offset and playback intent. A five-second socket timeout leaves recovery room inside the existing fifteen-second I/O deadline. Keep live/non-range replay and signed-URL refresh disabled.

Native regression failed before the fix with Reading network media: Input/output error. Real combined/split playback now survives dropped responses and socket timeout; persistent failures remain visible and retries are cancellable by seek, close, replacement and destruction.

Release, network, adaptive, media, reporting, runtime, relocated bundle and local playback checks passed. Packaged CLI/GUI playback started successfully; the full-suite keyboard smoke stopped because macOS refused foreground focus for its transparent check window.
This commit is contained in:
2026-10-12 02:06:06 +02:00
parent 31c4759c1d
commit e54cb2b549
4 changed files with 140 additions and 7 deletions
+9 -4
View File
@@ -181,7 +181,7 @@ class StreamingServer:
mode = parts[0]
token = None
if len(parts) == 3 and mode in ("stall", "stall-open", "chunked-media"):
if len(parts) == 3 and mode in ("stall", "stall-open", "chunked-media", "disconnect"):
token, name = parts[1:]
if not re.fullmatch(r"[A-Za-z0-9_-]+", token):
self._reply(request, 400, b"Invalid gate token", "text/plain")
@@ -229,14 +229,14 @@ class StreamingServer:
query = parse_qs(parsed.query)
try:
gate_at = (self._timestamp_offset(name, float(query["after_seconds"][0]))
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)
if mode in ("stall", "chunked-media", "disconnect") and "after_seconds" in query else
int(query.get("after", ["131072"])[0]) if mode in ("stall", "chunked-media", "disconnect") 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 in ("stall", "chunked-media") else {}
prefix = self._prefix_ends(name, gate_at) if mode in ("stall", "chunked-media", "disconnect") else {}
self.gates[token] = _Gate(size, prefix)
gate = self.gates[token]
gate.requests += 1
@@ -266,6 +266,9 @@ class StreamingServer:
request.end_headers()
request.wfile.flush()
request.close_connection = not persistent
if (mode == "disconnect" and not should_gate and start <= gate_at
and query.get("fail_reconnect") == ["1"]):
return # Every resumed response at the broken offset is also truncated.
cursor = start
with path.open("rb") as media:
media.seek(start)
@@ -280,6 +283,8 @@ class StreamingServer:
with self.lock:
gate.waiting -= 1
should_gate = False
if mode == "disconnect":
return # Advertise the complete range, then cut its body short.
amount = min(16384, end - cursor + 1)
if should_gate:
amount = min(amount, gate_at - cursor)