Camera stream hangs forever after printer reboot #99
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Description
After a printer reboot (or any interruption that drops the connection), all camera endpoints stop working permanently. MQTT reconnects on its own, but the camera never recovers and nothing is logged. The only fix is
POST /api/camera/resetor restarting the bridge.Root cause: the printer rotates its stream token on reboot (e.g.
/live/<old-token>becomes/live/<new-token>)._on_infopicks up the newrtspUrland callsCameraCache.set_url(), but that method only assigns the attribute. The three running ffmpeg processes still hold the old URL, and the loops only re-readself._urlat the top of eachwhile Trueiteration, which they never reach because they are blocked inawait proc.stdout.read().They stay blocked because the TCP connections remain
ESTABLISHEDwith no data and no FIN. A passive reader cannot tell "peer is silent" from "peer is gone" without an I/O timeout, and none is configured (ffmpeg's tcp-timeoutdefaults to-1, disabled).ensure_running()does not help: it checkstask.done(), and the tasks are alive, just permanently blocked. Nothing is logged because the exit/retry logging lives after thefinally, which is never reached.Steps to Reproduce
/api/camera/streamExpected Behavior
Camera streams recover automatically once the printer is back and a new stream URL has been received, in the same way MQTT reconnects on its own.
Actual Behavior
The stream page loads indefinitely and never renders a frame.
/api/camera/snapshotreturns a stale frame or 503.GET /api/cameraalready reports the new token, so the bridge knows the correct URL, it just never acts on it.All three ffmpeg processes still alive on the old token after 16h, never respawned:
Sockets alive but idle,
Recv-Q0 on all three:CPU time flat over 20s (
utime stimefrom/proc/<pid>/stat), confirming no I/O:POST /api/camera/resetrestores the stream immediately, the loops respawn and re-read the updated URL.Suggested fix
A changed URL is a reliable signal that the running processes are stale, so
set_url()can tear them down itself:This makes the recovery automatic and matches what the manual reset already does.
Optional hardening: the above covers a rotated token, but not a source that goes silent while the URL stays the same (transient network loss, camera hang). ffmpeg's tcp
-timeoutoption targets that case: it applies to socket I/O rather than just connect. Since it is a tcp-protocol option it applies to both branches of_input_args(the RTSP branch already forces-rtsp_transport tcp), so it can go before the split:With this, ffmpeg would exit on a stalled read and the existing retry loop takes over, logging normally.
Separately:
handle_camera_streamhas no timeout on the first frame (unlikehandle_api_camera_snapshot, which waits 5s). A client that never receives a frame hangs forever instead of getting a 503, which is why the failure presents as "the page loads forever" rather than an error.Environment
Logs
No camera-related log lines at all in this window, the ffmpeg loops never logged an exit or retry.
Thanks for the exceptionally thorough writeup, @fmontagna — root cause, reproduction, socket/process evidence, and a working fix proposal made this straightforward to verify and land.
Implemented all three parts, verified against current code:
CameraCache.set_url()now detects when the URL actually changed and callsreset()to tear down the stale ffmpeg loops, exactly as proposed. The nextensure_running()respawns them against the new URL.-timeout 10000000(10s, microseconds) added to_input_args()as a second line of defense — applies to both the RTSP and HTTP-FLV paths since they share this one method. Covers the case you flagged separately: a source going silent while the URL itself doesn't change./api/camera/stream— implemented with one adjustment from the suggested diff: the wait for the first frame (5s) now happens beforeresp.prepare(request)rather than wrapping the first loop iteration after streaming has already started.prepare()sends the response headers and commits the HTTP status to 200 — once that's called, a 503 can no longer actually be delivered to the client. Waiting on the queue first (with the subscriber already registered, so no frames are missed) lets a stalled source return a proper 503 instead of what would otherwise still be an infinite hang with the wrong status code already sent.All changes committed locally on
nightly, going out with the next nightly build. Added a small test suite for theset_url/resetchange-detection logic.