mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Bambu firmware allows exactly one camera connection. The existing guards (is_stream_active / try_get_active_buffered_frame, #1271 and #1348) only stop a one-shot capturer from competing with the fan-out broadcaster. Nothing coordinated the capturers with each other, so with no viewer attached every consumer correctly concluded it was not competing with a viewer and then collided with the others. On the reporter's P2S an Obico poll and a snapshot opened two RTSP sockets 207 ms apart, which knocked over the fan-out stream feeding the camera wall; it was then reaped for having received no frames for 58s. capture_camera_frame_bytes() now coalesces: the first caller opens the connection, callers arriving while it is in flight await the same result. Eight paths reach that function independently - Obico polling, the snapshot route, the finish-photo moment and its disk-writing sibling, plate detection, the camera test and the diagnose tool - so the single-flight sits at the bottom of the stack and no call site changes. Keyed by IP, since that is what the firmware's limit applies to and the function never sees a printer_id. The key excludes the timeout on purpose: the call sites disagree about it, from 10s to 30s, so keying on it would mean the Obico-vs-snapshot pair from the report never coalesced at all. It coalesces, it does not cache. A call arriving after the previous capture finished still captures fresh, because plate detection and the finish-photo path judge a running print from these frames and a stale one there is worse than a slow one - #1397 was a finish photo taken seconds late showing the bed already lowered. Each caller waits on its own deadline rather than inheriting whichever one happened to open the connection, and shield() means giving up leaves the capture running for whoever else is still waiting. A follower whose leader fails takes a turn of its own instead of inheriting a failure it never had a chance to avoid; the leader has finished by then, so there is nothing left to compete with. Bounded at two rounds. That also covers the follower whose timeout is longer than the leader's, which coalescing alone cannot. Cancellation is disambiguated via leader.cancelled(), so a follower's own cancellation propagates while a cancelled leader is treated as a failed one. The leader is deliberately not wrapped in a second wait_for: the implementation already enforces the timeout internally, where it can also kill the ffmpeg process, and an outer deadline would abandon the subprocess instead of killing it. The diagnose tool now marks a stage whose frame came from a capture already in flight as coalesced_capture. The pass is real evidence the camera works, but duration_ms is then mostly time spent queueing, and a diagnostic must not report a connection it never opened - the same reason that file declares its live_stream_active shortcut instead of quietly passing. Failures are not annotated, since a follower whose leader fails goes on to capture on its own.