Files
KX-Bridge-Release/tests/test_camera_cache_url_rotation.py
viewit 16c1a8ee73 fix(camera): recover automatically after printer reboot rotates stream token (Issue #99)
The printer rotates its FLV/RTSP stream token on reboot. CameraCache.set_url()
was a bare assignment, so the three running ffmpeg loops never noticed - they
only re-read self._url at the top of their outer loop, which they never reach
while permanently blocked in a stdout read on the now-silent, stale-token
connection (TCP stays ESTABLISHED with no data and no FIN, so a passive
reader can't tell "peer is quiet" from "peer is gone").

Three-part fix, all per the excellent root-cause analysis and reproduction
in the issue (thanks @fmontagna):

1. set_url() now detects a URL change and calls reset() to tear down the
   stale ffmpeg loops, so the next ensure_running() respawns them against
   the new URL.
2. ffmpeg's -timeout option (10s, applies to both RTSP and HTTP-FLW since
   both share _input_args) as a second line of defense for a source that
   goes silent while the URL stays the same (network loss, camera hang).
3. handle_camera_stream now waits up to 5s for the first frame BEFORE
   calling resp.prepare() and returns 503 on timeout. Previously it had no
   first-frame timeout at all, and prepare() commits the response status
   to 200 - so a stalled source could never surface as an error to the
   client, only as an infinite hang.
2026-07-24 14:10:54 +02:00

52 lines
1.7 KiB
Python

"""Camera stream hangs forever after printer reboot (Issue #99).
The printer rotates its stream token on reboot, changing the camera URL.
CameraCache.set_url() used to be a bare assignment - the running ffmpeg loops
never noticed since they only re-read self._url at the top of their outer
loop, which they never reach while permanently blocked reading stdout from
the now-silent, stale-token connection. set_url() must detect the change and
tear the loops down so the next ensure_running() respawns them against the
new URL.
"""
from kobrax_moonraker_bridge import CameraCache
def test_set_url_first_time_does_not_reset():
"""No prior URL - nothing stale to tear down."""
c = CameraCache()
calls = []
c.reset = lambda: calls.append(True)
c.set_url("http://printer/live/tokenA")
assert c._url == "http://printer/live/tokenA"
assert not calls
def test_set_url_same_value_does_not_reset():
c = CameraCache()
calls = []
c.set_url("http://printer/live/tokenA")
c.reset = lambda: calls.append(True)
c.set_url("http://printer/live/tokenA")
assert not calls
def test_set_url_changed_triggers_reset():
"""The actual bug scenario: token rotates after a printer reboot."""
c = CameraCache()
calls = []
c.set_url("http://printer/live/tokenA")
c.reset = lambda: calls.append(True)
c.set_url("http://printer/live/tokenB")
assert c._url == "http://printer/live/tokenB"
assert calls == [True]
def test_set_url_empty_to_value_does_not_reset():
"""Startup case: no URL known yet, first status push sets it - nothing
running to tear down."""
c = CameraCache()
calls = []
c.reset = lambda: calls.append(True)
c.set_url("http://printer/live/tokenA")
assert not calls