"""A remux that stops producing bytes is killed promptly, not at the wall clock.
2026-09-03, No Other Choice (tmdb 639988): ffmpeg started at 21:07:50 and wrote
2,848,981,040 bytes by 21:16 -- then produced NOTHING for 36 minutes, until
`subprocess.run(timeout=MP4_REMUX_TIMEOUT)` killed it at 21:52:51. The job
reported "timed out after 2700 seconds", which reads as "too slow" and is wrong:
the copy ran at ~5.3 MB/s while it ran at all. It stalled.
The cost is not the wasted 36 minutes. It is that the wall-clock timeout is the
ONLY thing that ever ends a stall, so the failure always arrives 45 minutes
late, and `.part` -- the evidence and the resume point -- looks the same either
way. Watching the output grow distinguishes them at the only place the
difference shows.
ffmpeg's own -rw_timeout/-reconnect flags are set too, but they cannot be tested
here without a CDN that stalls on demand, and they only cover the stalls ffmpeg
itself notices. This watchdog is outside ffmpeg and covers all of them.
Run: .venv/bin/python3 -m pytest test_remux_stall.py -q
"""
import os
import time
import pytest
import addon
@pytest.fixture
def out_file(tmp_path):
return str(tmp_path / "out.part")
def test_a_command_that_never_writes_is_killed_at_the_stall_deadline(out_file):
started = time.time()
with pytest.raises(addon.RemuxStalled):
addon._run_remux(["sh", "-c", "sleep 30"], out_file,
stall_timeout=2, hard_timeout=30)
elapsed = time.time() - started
# The point of the whole change: it ends at the STALL deadline, nowhere near
# the hard one. Generous upper bound so a loaded machine cannot flake it.
assert elapsed < 10, f"took {elapsed:.1f}s — did not end at the stall deadline"
def test_a_command_that_keeps_writing_is_left_alone(out_file):
# Appends a byte every 0.2s for ~3s, i.e. across several stall windows.
cmd = ["sh", "-c", f"for i in $(seq 15); do printf x >> {out_file}; sleep 0.2; done"]
proc = addon._run_remux(cmd, out_file, stall_timeout=1, hard_timeout=30)
assert proc.returncode == 0
assert os.path.getsize(out_file) == 15
def test_the_hard_timeout_still_applies_to_a_command_that_writes_for_ever(out_file):
cmd = ["sh", "-c", f"while :; do printf x >> {out_file}; sleep 0.1; done"]
started = time.time()
with pytest.raises(addon.RemuxTimedOut):
addon._run_remux(cmd, out_file, stall_timeout=30, hard_timeout=2)
assert time.time() - started < 10
def test_a_stall_is_measured_from_the_last_byte_not_from_the_start(out_file):
# Writes for ~1s, then goes quiet. The stall deadline must be counted from
# the last write -- otherwise a copy that starts slowly is killed for it.
cmd = ["sh", "-c", f"for i in $(seq 5); do printf x >> {out_file}; sleep 0.2; done; sleep 30"]
started = time.time()
with pytest.raises(addon.RemuxStalled):
addon._run_remux(cmd, out_file, stall_timeout=2, hard_timeout=30)
elapsed = time.time() - started
assert elapsed > 2, f"ended in {elapsed:.1f}s — did not wait out the writing phase"
assert elapsed < 12, f"took {elapsed:.1f}s — did not end at the stall deadline"
def test_a_missing_output_file_counts_as_no_progress(out_file):
# ffmpeg has not created the .part yet. That is still no progress, and must
# not raise its own error on os.path.getsize.
assert not os.path.exists(out_file)
with pytest.raises(addon.RemuxStalled):
addon._run_remux(["sh", "-c", "sleep 30"], out_file,
stall_timeout=2, hard_timeout=30)
def test_the_reconnect_flags_are_actually_passed_to_ffmpeg(monkeypatch, tmp_path):
"""The flags are the half of the fix this file cannot exercise directly, so
at minimum assert they reach the command line."""
seen = {}
def fake_run(cmd, out_path, stall_timeout, hard_timeout):
seen["cmd"] = cmd
with open(out_path, "wb") as f:
f.write(b"x")
class P:
returncode = 0
stderr = ""
return P()
monkeypatch.setattr(addon, "_run_remux", fake_run)
monkeypatch.setattr(addon, "_playlist_duration", lambda url: None)
dest = str(tmp_path / "movie.mp4")
addon._remux_to_mp4("http://example.invalid/v.m3u8", dest)
cmd = seen["cmd"]
assert "-rw_timeout" in cmd
assert "-reconnect" in cmd
assert "-reconnect_streamed" in cmd
assert "-reconnect_on_network_error" in cmd
# The hls demuxer defaults seg_max_retry to 0 -- one bad segment is fatal.
assert "-seg_max_retry" in cmd
def test_stderr_is_captured_on_success(out_file):
cmd = ["sh", "-c", f"printf x >> {out_file}; echo 'a warning' >&2"]
proc = addon._run_remux(cmd, out_file, stall_timeout=10, hard_timeout=30)
assert proc.returncode == 0
assert "a warning" in proc.stderr
def test_a_chatty_stderr_does_not_look_like_a_stall(out_file):
"""The reason stderr is a temp file and not a pipe.
A pipe holds 64K. Past that ffmpeg blocks on the write, therefore stops
writing the OUTPUT, and the watchdog sees a stall -- killing a process that
was only waiting for us to read, in exactly the failing case whose stderr is
worth having. 400K is comfortably past any pipe buffer.
"""
cmd = ["sh", "-c",
f"yes 'ffmpeg noise on stderr, at length' | head -c 400000 >&2; "
f"printf x >> {out_file}"]
proc = addon._run_remux(cmd, out_file, stall_timeout=5, hard_timeout=30)
assert proc.returncode == 0
assert len(proc.stderr) > 100000, f"only captured {len(proc.stderr)} bytes"
def test_ffmpeg_s_last_words_survive_a_stall(out_file):
cmd = ["sh", "-c", "echo 'Server returned 403 Forbidden' >&2; sleep 30"]
with pytest.raises(addon.RemuxStalled) as e:
addon._run_remux(cmd, out_file, stall_timeout=2, hard_timeout=30)
assert "403 Forbidden" in str(e.value)