| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
- """Shared camera capture for AI failure detection providers."""
- import logging
- from backend.app.core.database import async_session
- from backend.app.models.printer import Printer
- logger = logging.getLogger(__name__)
- async def capture_detection_frame(printer_id: int, *, session_factory=async_session, timeout: int = 20) -> bytes | None:
- """Capture a JPEG, sharing live camera buffers to avoid competing readers."""
- # Late import to avoid cycles at module load time
- from backend.app.services.camera import capture_camera_frame_bytes
- from backend.app.services.external_camera import capture_frame as capture_external_frame
- async with session_factory() as db:
- printer = await db.get(Printer, printer_id)
- if printer is None:
- raise ValueError(f"Printer {printer_id} not found")
- if printer.external_camera_enabled and printer.external_camera_url:
- # Same rule as the built-in branch below, which this used to skip:
- # an external camera is single-reader too, so polling while a viewer
- # is attached just fails (#2707).
- from backend.app.api.routes.camera import live_frame_for_capture
- defer, buffered = live_frame_for_capture(printer_id)
- if defer:
- if buffered:
- return buffered
- logger.info(
- "AI failure detection: viewer attached for printer %s but buffer empty; "
- "skipping this poll to avoid competing camera handle (#2707)",
- printer_id,
- )
- return None
- return await capture_external_frame(
- printer.external_camera_url,
- printer.external_camera_type,
- timeout=timeout,
- snapshot_url=printer.external_camera_snapshot_url,
- )
- # Reuse the fan-out broadcaster's buffered frame when a viewer is
- # already watching — avoids opening a second concurrent RTSP socket
- # on printers that allow only one camera connection (e.g. X2D
- # firmware 01.01.00.00; see #1271). Buffered frame is <1s old while
- # a viewer is connected.
- #
- # When a viewer is attached but no frame is buffered yet (startup
- # race, mid-reconnect), we DELIBERATELY skip this poll cycle instead
- # of falling through to capture_camera_frame_bytes. Opening a fresh
- # RTSP/chamber socket would compete with the live viewer and kick
- # the fan-out connection on most firmwares — exactly the freeze
- # reported in #1348. The poll loop retries in ~10s.
- from backend.app.api.routes.camera import is_stream_active, try_get_active_buffered_frame
- if is_stream_active(printer_id):
- buffered = try_get_active_buffered_frame(printer_id)
- if buffered:
- return buffered
- logger.info(
- "AI failure detection: viewer attached for printer %s but buffer empty; skipping this poll to avoid competing camera socket (#1348)",
- printer_id,
- )
- return None
- return await capture_camera_frame_bytes(
- ip_address=printer.ip_address,
- access_code=printer.access_code,
- model=printer.model,
- timeout=timeout,
- )
|