CAMERA
Camera: live snapshot + live MJPEG stream
Starts the "Edge AI bridge" line item in docs/ROADMAP.md, scoped
deliberately to its device-discovery and live-viewing half — local
inference has a separate scaffold (docs/EDGE_AI.md, --features edge-ai)
and still has no accelerator backend. Requires the daemon to be built with
--features camera (off by default, like hotplug/tpm2).
Safety contract¶
- disabled by default; the feature itself must be compiled in, and each
camera must be individually
enabled = true - explicit device allowlist only — never auto-opens an undeclared
/dev/video*, same posture as[industrial.can_capture]'s interface allowlist - acquisition only: no exposure/white-balance/other
VIDIOC_S_CTRLcontrol tuning, no daemon-side recording/storage - per-camera accepted-frame rate cap
- only the single newest frame per camera is cached — no video history is warehoused
- MJPG-capable cameras: the driver's own JPEG bytes are passed straight
through. Other cameras: software YUYV→RGB→JPEG fallback (pure Rust, no
libjpeg/ffmpeg/gstreamerdependency). A camera advertising neither format is not captured. - on error, the capture thread retries with a bounded backoff (unlike CAN capture's "fail once" policy) — USB webcam replug is common enough to be worth recovering from automatically
Config¶
[camera]
devices = []
# [[camera.devices]]
# id = "front-dock"
# path = "/dev/video0"
# enabled = true
# max_frames_per_second = 10
# jpeg_quality = 75 # 1-100, used only on the YUYV software-encode fallback path
# max_stream_clients = 4 # 0 = unlimited
# publish_to_nodra = true # health/presence only, see below
id is a stable identifier used in API paths — not the /dev/videoN path,
whose numbering isn't stable across reboots/replugs.
Endpoints¶
GET /api/v1/camera # status for every configured camera
GET /api/v1/camera/{id}/snapshot # latest frame, image/jpeg
GET /api/v1/camera/{id}/stream # multipart/x-mixed-replace MJPEG — works in a plain <img> tag
404:{id}isn't a configured camera.503: configured but no frame captured yet (not enabled, still starting, or erroring).429on/stream:max_stream_clientsexceeded.
<img src> can't set an Authorization header, so mint a short-lived
stream ticket with POST /api/v1/stream-tickets and pass ?ticket= —
never the long-lived bearer. See src/auth/tickets.rs.
Nodra hand-off¶
When camera.devices[].publish_to_nodra = true, only health/presence
is published — capturing state, frame/drop/encode-error counters, last
error — never frame bytes:
<topic-prefix>/<serial>/camera/<id>/status # retained
This is a deliberate boundary, not an oversight: raw video must never reach
Nodra in this increment. Device Agent's job stops at "here's a live JPEG
feed and camera health"; a future local-inference bridge (scaffold in
docs/EDGE_AI.md, --features edge-ai) or Nodra itself would own
actually consuming frames.
Permissions¶
The daemon opens /dev/video* directly (not a plugin subprocess) and runs
as root by default, so no group membership is needed today — see
docs/HARDWARE_PERMISSIONS.md's camera row for the video-group path if
the daemon is ever de-privileged.