feat(camera): optional snapshot URL override for external cameras (#1177)

go2rtc and several IP cameras still emit a warm-up / black frame on every
  fresh MJPEG connection — even with the v0.2.4b2 warm-up-skip fix it
  slipped through intermittently for @nkm8's setup. His own bisect named
  the clean solution: go2rtc exposes /api/frame.jpeg as a dedicated
  single-frame endpoint that never returns the encoder's stale keyframe.

  Adds an optional external_camera_snapshot_url column on printers. When
  set, every single-frame capture path (snapshot endpoint, [SNAPSHOT]
  notification thumbnails, [PHOTO-BG] finish photo, layer timelapse,
  Obico ML, plate-detect / calibrate-plate) routes through _capture_snapshot
  on the override URL via plain HTTP GET, bypassing the warm-up dance.

  Live view stays on the configured stream URL — only single-frame
  captures use the override. Override is camera-type-agnostic. SSRF guard
  applies (existing _sanitize_camera_url allowlist). Empty string treated
  as unset.

  Settings UI: new "Snapshot URL (optional)" input + Test button under
  External Cameras, hidden for camera_type=snapshot since the live URL is
  already a single-frame source. en + de fully translated; 6 other locales
  seeded with English copy.

  5 backend tests pin the routing contract; 3 frontend tests pin the
  input + debounced PATCH. Documented in
  bambuddy-wiki/docs/features/camera.md with the go2rtc example.
This commit is contained in:
maziggy
2026-05-03 07:59:02 +02:00
parent d6a3139397
commit abc8e97050
24 changed files with 517 additions and 133 deletions
+5
View File
@@ -28,6 +28,11 @@ class Printer(Base):
external_camera_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
external_camera_type: Mapped[str | None] = mapped_column(String(20), nullable=True) # mjpeg, rtsp, snapshot
external_camera_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
# Optional single-frame snapshot URL — when set, used for snapshot / finish-photo
# / timelapse / plate-detect captures instead of opening the live stream and
# skipping a warm-up frame. Bypasses MJPEG warm-up issues on sources that
# expose a dedicated frame endpoint (e.g. go2rtc's /api/frame.jpeg). #1177.
external_camera_snapshot_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
camera_rotation: Mapped[int] = mapped_column(default=0) # 0, 90, 180, 270 degrees
# Plate detection - check if build plate is empty before starting print
plate_detection_enabled: Mapped[bool] = mapped_column(Boolean, default=False)