forked from viewit/KX-Bridge-Release
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.
52 lines
1.7 KiB
Python
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
|