Most Bambu Lab printers only allow one concurrent camera connection, but
GET /printers/{id}/camera/stream opened a fresh upstream per viewer.
Two browser tabs → second viewer fails or kicks the first off.
New MjpegBroadcaster (services/camera_fanout.py) owns one upstream per
printer and fans MJPEG chunks out to N subscribers. 5 s grace window
absorbs tab refreshes without reconnecting. Bounded subscriber queues
drop frames for slow viewers rather than blocking the broadcaster.
Audit-pass fixes:
- _stream_start_times set with setdefault() so stream_uptime reflects
the shared upstream's age, not the most-recent viewer's
- subscribe() retried once on RuntimeError to close a tiny grace race
- unsubscribe() returns post-removal count atomically so the detach log
no longer races with concurrent leavers
Permission gates unchanged; broadcaster has no FastAPI surface.
Tests: 13 broadcaster unit tests + 2 integration tests on /camera/stop.
External-camera path untouched.
The periodic camera cleanup task scans /proc for ffmpeg processes and
kills any not in the active-streams registry. The Obico detection
service's capture_camera_frame_bytes() spawns short-lived ffmpeg for
snapshots but never registered the PID — so cleanup killed it as
"orphaned" mid-capture (SIGKILL, exit -9), producing false errors and
missed detection frames.
Track capture PIDs in _active_capture_pids and exclude them from the
cleanup kill list.
Two bugs surfaced while investigating camera reconnect behaviour in #925.
The camera page briefly displayed "Reconnecting attempt 6 of 5" before
giving up, because the attempt counter could be incremented to the
maximum while the reconnect banner was still rendering. The displayed
value is now clamped to the configured maximum.
Every failed ffmpeg spawn logged the full ~20-line ffmpeg version,
configuration, and lib* banner, producing hundreds of lines of noise
per failed camera click (one reported click produced 555 log lines
across 30 retries). A new _summarize_ffmpeg_stderr helper strips the
banner and caps output at the last 10 meaningful lines, applied at
all three stderr log sites (immediate-failure, stream-ended,
read-timeout). Covered by unit tests for empty input, banner
stripping, line cap, blank-line filtering, and banner-only input.
The underlying "camera service stops accepting connections after
prolonged uptime" behaviour in the X1C firmware is still under
investigation — these two fixes are independent of that root cause.
Camera snapshot, test, and plate detection endpoints created temporary
JPEG files with default 0644 permissions. Switch from NamedTemporaryFile
to mkstemp with explicit 0600 permissions.
Camera streams, snapshots, thumbnails, timelapse videos, photos, QR
codes, and cover images served via <img>/<video> tags were previously
unauthenticated because browser media elements cannot send Authorization
headers. When auth is enabled, these endpoints are now protected by a
reusable stream token (?token=xxx) obtained from POST
/printers/camera/stream-token (requires CAMERA_VIEW permission).
When a user closed the camera viewer, the stop endpoint killed the
ffmpeg process but never signaled the stream generator's disconnect
event. The generator saw "process died" as a dropped RTSP session
and respawned ffmpeg — up to 30 times per stream. The orphan cleanup
couldn't catch these because they were still tracked as active.
- Add per-stream disconnect events dict so stop endpoint can signal
generators to stop reconnecting before killing the process
- Check if stream_id was removed from _active_streams before
reconnecting (belt-and-suspenders with the event)
- Track frame timestamps per stream_id instead of per printer_id
so stale detection isn't fooled by newer streams for the same
printer
- Reduce stale thresholds from 120s+60s to 60s+30s
- Signal disconnect events from cleanup when killing stale streams
The Debian ffmpeg package uses GnuTLS, whose hardened defaults reject
TLS renegotiation and legacy ciphers that some Bambu printer firmwares
(notably P2S) rely on — causing RTSP sessions to drop after a few
seconds.
Add a local TLS termination proxy (Python ssl/OpenSSL) that handles
the TLS connection to the printer and exposes a plain RTSP port to
ffmpeg. The proxy rewrites RTSP request-line URLs (rtsp://proxy →
rtsps://printer) while preserving Authorization headers so Digest
auth hashes remain valid.
Also:
- Reduce RTSP reconnect delay from 1.0s to 0.2s
- Add ffmpeg fast-start flags (-probesize 32, -analyzeduration 0,
-fflags nobuffer, -flags low_delay)
- Fix external camera double rate-limiting causing choppy streams
- Apply TLS proxy to external camera rtsps:// URLs and snapshot capture
- Update orphan ffmpeg cleanup to match rtsp:// (proxied) URLs
- Add unit tests for RTSP URL rewriting and proxy lifecycle
Fix P2S camera stream dropping and snapshot capture race (#661)
P2S firmware's TLS renegotiation is rejected by Debian's hardened GnuTLS
defaults, causing ffmpeg RTSP sessions to drop after ~3 seconds. Add
GnuTLS config allowing unsafe renegotiation and legacy ciphers. Also add
ffmpeg fast-start flags, reduce reconnect delay from 1.0s to 0.2s,
remove double rate-limiting on external camera streams, and fix orphan
cleanup killing snapshot capture ffmpeg processes (exit code -9).
Or as a single combined commit:
Fix P2S camera streaming, snapshot race, and energy stats (#661, #695)
Camera: P2S firmware's TLS renegotiation rejected by Debian's hardened
GnuTLS defaults, dropping RTSP sessions after ~3s. Add GnuTLS compat
config, ffmpeg fast-start flags, reduce reconnect delay to 0.2s, remove
external camera double rate-limiting, and register snapshot ffmpeg PIDs
with the orphan tracker to prevent SIGKILL during capture.
The P2S firmware drops RTSP sessions after a few seconds with an I/O
error. The backend treated this as fatal, ending the MJPEG stream and
forcing the frontend through a full reconnection cycle. Added transparent
auto-reconnection: when ffmpeg's RTSP connection dies, it respawns
immediately and continues streaming MJPEG frames to the browser with
only a brief freeze (~1s). Up to 30 reconnections before giving up.
On Windows, process.terminate() on ffmpeg broadcasts CTRL_C_EVENT to
the entire process group, causing uvicorn to shut down. Spawn ffmpeg
with CREATE_NEW_PROCESS_GROUP so cleanup doesn't affect the server.
Fix camera button permissions & ffmpeg process leak (#550)
Camera button on printer card was clickable without camera:view
permission. ffmpeg processes (~240MB each) accumulated after closing
camera streams because: (1) stop endpoint called terminate() without
wait()/kill(), (2) HTTP disconnect detection only ran between frames
so was blocked when the generator was stuck on stdout read, and
(3) no mechanism caught processes orphaned by generator abandonment
or app restarts.
- Add camera:view permission check + tooltip to camera button
- Fix stop endpoint: terminate() → wait(2s) → kill() → wait()
- Add background disconnect monitor (polls every 2s, kills ffmpeg
directly on disconnect)
- Add periodic /proc scan (every 60s) that SIGKILLs any ffmpeg
with rtsps://bblp: not in an active stream
- Add noCamera i18n key to all 6 locales
- Fix camera API test mocks for async wait() and pid attribute
The snapshot endpoint always used the internal printer camera even when
an external camera was configured. Now checks for external camera first,
matching the stream endpoint pattern. Also added retry logic (3 attempts,
2s delay) to MJPEG and RTSP stream generators so they reconnect on
timeout instead of silently ending the stream.
- Add USB camera type to external camera service
- Auto-detect available V4L2 devices on Linux
- New API endpoint: GET /api/v1/printers/usb-cameras
- Use ffmpeg for USB camera capture and streaming
- Add "USB Camera (V4L2)" option in Settings UI
- Debounce camera URL input to avoid saving on every keystroke
Closes#143
Automatically detect if objects are on the build plate before printing
and pause the print immediately if detected.
Features:
- Per-printer toggle to enable/disable plate detection
- Multi-reference calibration: store up to 5 reference images per printer
for different plate types (textured, smooth, high-temp, etc.)
- Automatic print pause when objects detected at print start
- Push notification and WebSocket alert when print is paused
- ROI (Region of Interest) calibration UI with sliders to adjust
detection area
- Reference management: view thumbnails, add labels, delete references
- Works with both built-in and external cameras
- Uses buffered camera frames when stream is active (no blocking)
- Split button UI: main button opens modal, chevron toggles on/off
- Green visual indicator when plate detection is enabled
- Included in backup/restore
Add support for external network cameras (MJPEG, RTSP, HTTP snapshot)
that replace a printer's built-in camera when configured.
Features:
- Live streaming on printers page (replaces built-in camera)
- Finish photo capture from external camera on print complete
- Layer-based timelapse: captures frame on each layer change,
stitches to MP4 video on print completion
Backend changes:
- Add external_camera_url, external_camera_type, external_camera_enabled
fields to Printer model with database migration
- New external_camera.py service: MJPEG/RTSP/snapshot frame capture,
connection testing, MJPEG stream generation
- New layer_timelapse.py service: TimelapseSession management,
layer-by-layer frame capture, ffmpeg video stitching
- Add on_layer_change callback to MQTT client and printer manager
- Update camera routes with external camera streaming and tracking
- Update print lifecycle hooks for timelapse start/stitch/cancel
- Add external camera fields to backup/restore
- Rate limiting for external camera streams (prevents browser freeze)
Frontend changes:
- Add external camera configuration UI in Settings > Camera
- Per-printer enable toggle, URL input, type selector, test button
- Toast notification on save
Closes#143
Features:
- Resizable printer cards (S/M/L/XL) with toolbar controls on Printers page
- Queue Only mode for staging prints without automatic scheduling
- "Queue Only" option in add/edit queue modals
- Purple "Staged" badge and Play button to release to queue
- manual_start field in database with migration
Fixes:
- Improved camera stream stuck detection with automatic reconnection
Tests:
- Added 16 integration tests for print queue API endpoints
prints (not just completed), matching get_printer_total_hours behavior
- Add option to keep or delete archives when deleting a printer
- Custom maintenance types no longer auto-assign to all printers
- Add UI to manually assign/remove custom maintenance types per printer
- Add backend endpoints for assigning types to printers and removing items
- Exclude static/assets from large file pre-commit check
- Fix A1/P1 camera streaming with extended timeouts and lower FPS cap
Root cause: When a print completed with the camera stream popup open,
spawning a second ffmpeg process for finish photo capture caused a
conflict that froze the browser tab and video window.
Solution: Buffer the last frame from active camera streams. When
capturing finish photo, use the buffered frame if a stream is active
instead of spawning a new ffmpeg process.
Backend changes:
- camera.py: Added _last_frames buffer and get_buffered_frame() helper
- main.py: Photo capture uses buffered frame when stream is active
- main.py: Moved slow operations to background tasks (energy calc,
photo capture, smart plug, notifications, maintenance)
- archive.py: Fixed sync file write blocking event loop (asyncio.to_thread)
- printers.py: Added debug endpoint to simulate print completion
Frontend changes:
- useWebSocket.ts: Throttled printer status updates (100ms) to prevent
UI overload from rapid WebSocket messages
- useWebSocket.ts: Debounced archive invalidations (3s) to prevent
cascade of re-renders on print completion
- useWebSocket.test.ts: Updated tests for throttled/debounced handlers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Separate AMS and AMS-HT notification switches (one per device type)
- Fix notification variables not showing (duration, filament, estimated_time)
- Add fallback values for empty notification variables ("Unknown" instead of blank)
Settings:
- Fix API keys badge count only showing after visiting tab
- Move External Links card to third column above Updates
- Add Release Notes modal for viewing full notes before updating
Statistics:
- Fix filament usage trends not showing (wrong API parameters)
- Move dashboard controls (Hidden, Reset Layout) to header row
- Remove duplicate Reset Layout button
Camera:
- Fix ffmpeg processes not killed when closing webcam window
- Add /camera/stop endpoint with POST support for sendBeacon
- Track active streams and proper cleanup on disconnect